DirectX с появлением Windows Vista стал базовым графическим API для платформы Windows, обеспечивающим аппаратное ускорение всех операций рисования на экране, выполняемых операционной системой (ОС). Однако до Windows 8 разработчикам, использующим DirectX, приходилось либо создавать с нуля собственные UI-инфраструктуры на неуправляемом C++ и COM, либо лицензировать промежуточное ПО для разработки UI, такое как Scaleform.
В Windows 8 есть возможность навести мосты между неуправляемым DirectX и подходящей UI-инфраструктурой с помощью механизма DirectX-XAML interop, предоставляемого Windows Runtime (WinRT). Чтобы задействовать преимущества API-поддержки XAML в DirectX, нужно использовать «неуправляемый» C++ (впрочем, доступ к смарт-указателям и расширениям компонентов C++ у вас сохранится). Также поможет базовое знание COM, хотя я буду обстоятельно пояснять все специфические процедуры взаимодействия, необходимые для совместной работы с инфраструктурой XAML и DirectX.
В этой статье из двух частей я рассмотрю два подхода к взаимодействию DirectX-XAML: один из них — тот, где я размещаю поверхности на элементах XAML-инфраструктуры с помощью графических API-функций DirectX, а в другом случае я рисую иерархию элементов управления XAML на поверхности цепочки буферов обмена DirectX (swap chain surface).
В этой части обсуждается первый сценарий, где осуществляется рендеринг изображений или примитивов, отображаемых в вашей инфраструктуре XAML.
Но сначала я дам краткий обзор доступных вам вариантов API. В настоящее время в Windows Runtime имеются три XAML-типа, которые поддерживают взаимодействие с DirectX.
- Windows::UI::Xaml::Media::Imaging::SurfaceImageSource (далее SurfaceImageSource) Этот тип позволяет рисовать относительно статический контент на общей поверхности XAML, используя графические API-функции DirectX. Представление (view) полностью управляется XAML-инфраструктурой WinRT, а значит, все презентационные элементы тоже контролируются ею. Это делает его идеальным для рисования сложного контента, который не изменяется в каждом кадре, но не столь подходящим для сложных двух- или трехмерных игры, где обновление происходит с высокой частотой.
- Windows::UI::Xaml::Media::Imaging::VirtualSurfaceImageSource (далее VirtualSurfaceImageSource) Подобно SurfaceImageSource этот тип использует графические ресурсы, определенные для инфраструктуры XAML. Но VirtualSurfaceImageSource в отличие от SurfaceImageSource поддерживает логические большие поверхности с оптимизацией на основе регионов, так чтобы DirectX рисовал между обновлениями лишь измененные регионы поверхности. Выбирайте этот элемент, если вы создаете, например, элемент управления «карта» (map control) или средство просмотра документов с крупными изображениями. И вновь, как и SurfaceImageSource, этот вариант не пригоден для сложных двух- или трехмерных игр, особенно тех, где требуется отображение и обратная связь в режиме реального времени.
- Windows::UI::Xaml::Controls::SwapChainBackgroundPanel (далее SwapChainBackgroundPanel) Этот элемент управления XAML и тип позволяют вашему приложению использовать собственный провайдер представления (view provider) DirectX (или цепочку обменов), поверх которого можно рисовать XAML-элементы и которое обеспечивает более высокую производительность в ситуациях, где требуется отображение с очень малой задержкой или обратная связь с высокой частотой (например, в современных играх). Ваше приложение будет управлять контекстом устройства DirectX для SwapChainBackgroundPanel отдельно от инфраструктуры XAML. Конечно, это означает, что кадры SwapChainBackgroundPanel и XAML не будут синхронизироваться друг с другом при обновлении. Кроме того, вы можете осуществлять рендеринг в SwapChainBackgroundPanel из фонового потока.
В этой статье я рассмотрю SurfaceImageSource и VirtualSurfaceImageSource и то, как их можно включить в медийные элементы управления XAML (SwapChainBackgroundPanel является особым случаем и будет обсуждаться в другой статье).
Примечание: SurfaceImageSource и VirtualSurfaceImageSource можно использовать из C# или Visual Basic .NET, но компоненты рендеринга DirectX должны быть написаны на C++ и скомпилированы в отдельную DLL, к которой вы будете обращаться из проекта на C#. Существуют также сторонние управляемые инфраструктуры WinRT DirectX, например SharpDX (sharpdx.org) и MonoGame (monogame.net), которые можно использовать вместо SurfaceImageSource или VirtualSurfaceImageSource.
Итак, приступим. В этой статье предполагается, что вы знаете основы DirectX, в частности Direct2D, Direct3D и Microsoft DirectX Graphics Infrastructure (DXGI). Конечно же, вы должны знать XAML и C++. А теперь вперед!
SurfaceImageSource и композиция изображений в DirectX
Пространство имен Windows::UI::Xaml::Media::Imaging содержит тип SurfaceImageSource наряду со множеством других XAML-типов визуализации (imaging types). По сути, тип SurfaceImageSource предоставляет способ динамического рисования на общих поверхностях (shared surfaces) многих графических и визуализирующих XAML-примитивов, в конечном счете заполняя их контентом, рендеринг которого осуществляется вызовами графических API в DirectX, и их применения в качестве кисти. (В частности, в качестве ImageBrush вы используете именно ImageSource.) Считайте это чем-то вроде битовой карты, которую вы генерируете «на лету» средствами DirectX, и этот тип можно применять во многих местах, где вы могли бы задействовать битовую карту или другой ресурс визуализации.
В этом разделе я буду рисовать в XAML-элементе <Image>, который содержит пустое PNG-изображение в качестве поля для заполнения. Я указываю высоту и ширину элемента <Image>, так как эта информация передается конструктору SurfaceImageSource в моем коде (если бы я не задал высоту и ширину, визуализируемый контент был бы растянут под значения параметров тега <Image>):
<Image x:Name="MyDxImage" Width="300" Height="200" Source="blank-image.png" />
В этом примере мишенью является тег <Image> — он будет отображать поверхность, на которой я рисую. Я мог бы использовать какой-нибудь XAML-примитив, скажем, <Rectangle> или <Ellipse>; оба эти примитива можно заполнять кисть SurfaceImageSource. Это возможно потому, что операции рисования для этих примитивов и изображений выполняются Windows Runtime через DirectX; все, что я делаю, — подключаю «за кулисами», так сказать, другой источник.
В свой код я включаю следующее:
#include <wrl.h>
#include <wrl\client.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <d2d1_1.h>
#include <d3d11_1.h>
#include "windows.ui.xaml.media.dxinterop.h"
Это заголовочные файлы для Windows Runtime Library (WRL), некоторых ключевых компонентов DirectX и, самое важное, неуправляемых интерфейсов взаимодействия с DirectX. Необходимость во включении последних станет понятной чуть позже.
Я также импортирую соответствующие библиотеки: dxgi.lib, d2d1.lib and d3d11.lib.
И для удобства я включаю следующие пространства имен:
using namespace Platform;
using namespace Microsoft::WRL;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Media::Imaging;
Теперь в коде я создаю тип MyImageSourceType, производный от базового типа SurfaceImageSource, и он вызывает свой конструктор, как показано на рис. 1.
Рис. 1. Наследование от SurfaceImageSource
public ref class MyImageSourceType sealed : Windows::UI::Xaml::
Media::Imaging::SurfaceImageSource
{
// ...
MyImageSourceType::MyImageSourceType(
int pixelWidth,
int pixelHeight,
bool isOpaque
) : SurfaceImageSource(pixelWidth, pixelHeight, isOpaque)
{
// Глобальная переменная, содержащая ширину
// SurfaceImageSource в пикселях
m_width = pixelWidth;
// Глобальная переменная, содержащая высоту
// SurfaceImageSource в пикселях
m_height = pixelHeight;
CreateDeviceIndependentResources();
CreateDeviceResources();
}
// ...
}
Примечание: наследовать от SurfaceImageSource не обязательно, хотя это немного упрощает организацию кода. Вы можете просто создать экземпляр объекта SurfaceImageSource как член и использовать его. В примерах кода просто подставляйте в уме имя своего члена вместо ссылки объекта на самого себя (this).
Методы CreateDeviceResources и CreateDeviceIndependentResources являются пользовательскими реализациями и удобны для логического разделения настройки, специфичной для аппаратного графического интерфейса DirectX, и более универсальной настройки DirectX, специфичной для приложения. Оба метода выполняют важные операции. Однако их операции обязательно нужно разделять, так как в какой-то момент вам может потребоваться заново создать аппаратные ресурсы, не влияя на аппаратно-независимые ресурсы, и наоборот.
CreateDeviceResources должен выглядеть аналогично тому, что показано на рис. 2, по крайней мере в своей базовой части.
Рис. 2. Создание аппаратно-зависимых ресурсов DirectX
// Где-то в заголовочном файле должно быть определено следующее
Microsoft::WRL::ComPtr<ISurfaceImageSourceNative> m_sisNative;
// Direct3D-устройство
Microsoft::WRL::ComPtr<ID3D11Device> m_d3dDevice;
// Direct2D-объекты
Microsoft::WRL::ComPtr<ID2D1Device> m_d2dDevice;
Microsoft::WRL::ComPtr<ID2D1DeviceContext> m_d2dContext;
// ...
void MyImageSourceType::CreateDeviceResources()
{
// Этот флаг добавляет поддержку для поверхностей с порядком
// цветовых каналов, отличным от используемого в API
// по умолчанию. Необходим для совместимости с Direct2D.
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if defined(_DEBUG)
// Если проект собирается в режиме отладки,
// включает поддержку отладки через SDK Layers
creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// Этот массив определяет набор уровней аппаратных
// возможностей DirectX, которые будут поддерживаться данным
// приложением. Заметьте, что порядок должен быть сохранен.
// Не забывайте объявлять в описании своего приложения
// минимально необходимый уровень аппаратных возможностей.
// Считается, что все приложения поддерживают 9.1, если
// не указано иное.
const D3D_FEATURE_LEVEL featureLevels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1,
};
// Создаем объект устройства Direct3D 11 API
D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
creationFlags,
featureLevels,
ARRAYSIZE(featureLevels),
// Для приложений Windows Store
// этот параметр равен D3D_SDK_VERSION
D3D11_SDK_VERSION,
// Возвращает Direct3D-устройство,
// созданное в глобальной переменной
&m_d3dDevice,
nullptr,
nullptr);
// Получаем устройство Direct3D API
ComPtr<IDXGIDevice> dxgiDevice;
m_d3dDevice.As(&dxgiDevice);
// Создаем объект Direct2D-устройства
// и соответствующий контекст устройства
D2D1CreateDevice(
dxgiDevice.Get(),
nullptr,
&m_d2dDevice);
m_d2dDevice->CreateDeviceContext(
D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
&m_d2dContext);
// Сопоставляем DXGI-устройство с SurfaceImageSource
m_sisNative->SetDevice(dxgiDevice.Get());
}
К этому моменту я создал контекст устройства и связал его с…, постойте-ка, а что такое ISurfaceImageSourceNative? Это же не WinRT-тип! Что здесь происходит?
Все дело в механизме взаимодействия. Здесь я ныряю в «трубу Джеффриза» («Jeffries tube»)1, чтобы попасть в WRL и «переключить кое-какие проводки». Здесь же я попадаю в COM, которая стоит за большей частью WRL.
1 Это отсылка к знаменитому сериалу Star Trek, где такие трубы использовались для ускоренного перемещения по звездолету. —
Прим. ред.
Чтобы разрешить нужное поведение механизма interop, мне потребуется «за кулисами» подключиться к тому DirectX-источнику. Для этого надо подключить мой тип к реализации методов, определенных в WRL-специфичном COM-интерфейсе ISurfaceImageSourceNative. Далее я присоединю этот тип к элементу <Image> (в данном примере), и, когда приложение отправит обновление инфраструктуре XAML, оно будет использовать мои DirectX-реализации вызовов для рисования вместо предлагаемых по умолчанию.
ISurfaceImageSourceNative определен в заголовочном файле interop, указанном мной ранее. Видите, что здесь происходит?
Теперь в своем методе CreateDeviceIndependentResources, специфичном для приложения, я выхожу из COM и запрашиваю неуправляемые методы, определенные в SurfaceImageSource. Поскольку эти методы не предоставляются напрямую, их нужно получать вызовом IUnknown::QueryInterface в типе, производном от SurfaceImageSource или SurfaceImageSource. Для этого я заново привожу свой тип, производный от SurfaceImageSource, к IUnknown — базовому интерфейсу любого COM-интерфейса (я мог бы также приводить его к IInspectable, «базовому» интерфейсу любого WinRT-типа, который наследует от IUnknown). Чтобы получить список методов ISurfaceImageSourceNative, я запрашиваю этот интерфейс так:
void MyImageSourceType::CreateDeviceIndependentResources()
{
// Запрос интерфейса ISurfaceImageSourceNative
reinterpret_cast<IUnknown*>(this)->QueryInterface(
IID_PPV_ARGS(&m_sisNative));
}
(IID_PPV_ARGS — вспомогательный макрос для WRL, который возвращает указатель на интерфейс. Очень удобно! Если вы не наследуете от SurfaceImageSource, замените имя своего члена объекта SurfaceImageSource на this.)
Наконец, стоит обратить внимание на эту часть метода CreateDeviceResources:
m_sisNative->SetDevice(dxgiDevice.Get());
ISurfaceImageSourceNative::SetDevice принимает сконфигурированный графический интерфейс и связывает его с поверхностью для любых операций рисования. Но заметьте: это также означает, что я должен вызвать CreateDeviceResources после вызова CreateDeviceIndependentResources минимум один раз, а иначе у меня не будет сконфигурированного устройства, к которому можно было бы подключиться.
Теперь я открыл доступ к нижележащей реализации ISurfaceImageSourceNative типа SurfaceImageSource, от которого наследует мой тип MyImageSourceType. То есть, по сути, я открыл капот и протянул проводки к типу SurfaceImageSource, пусть даже к базовой реализации вызовов для рисования, а не к моей. После этого я реализую свои вызовы.
С этой целью я реализую следующие методы:
- BeginDraw — открывает контекст устройства для рисования;
- EndDraw — закрывает контекст устройства.
Примечание: я выбрал имена методов BeginDraw и EndDraw для согласованности с методами ISurfaceImageSourceNative. Этот шаблон введен для удобства и не является обязательным.
Мой метод BeginDraw (или другой метод инициализации рисования, определенный в производном типе) должен в некий момент вызывать ISurfaceImageSourceNative::BeginDraw. (Для оптимизации можно добавить параметр для подпрямоугольника с регионом обновляемого изображения.) Аналогично метод EndDraw должен вызывать ISurfaceImageSourceNative::EndDraw.
В данном случае методы BeginDraw и EndDraw могли бы выглядеть примерно так, как показано на рис. 3.
Рис. 3. Рисование на DirectX-поверхности
void MyImageSourceType::BeginDraw(Windows::Foundation::Rect updateRect)
{
POINT offset;
ComPtr<IDXGISurface> surface;
// Выражаем целевую область неуправляемым типом RECT
RECT updateRectNative;
updateRectNative.left = static_cast<LONG>(updateRect.Left);
updateRectNative.top = static_cast<LONG>(updateRect.Top);
updateRectNative.right = static_cast<LONG>(updateRect.Right);
updateRectNative.bottom = static_cast<LONG>(updateRect.Bottom);
// Начинаем рисование – возвращаем целевую поверхность
// и смещение для использования в качестве начала координат
// вверху слева
HRESULT beginDrawHR = m_sisNative->BeginDraw(
updateRectNative, &surface, &offset);
if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED ||
beginDrawHR == DXGI_ERROR_DEVICE_RESET)
{
// Если устройство удалено или сброшено, пытаемся
// заново создать его и продолжить рисование
CreateDeviceResources();
BeginDraw(updateRect);
}
// Создаем мишень прорисовки
ComPtr<ID2D1Bitmap1> bitmap;
m_d2dContext->CreateBitmapFromDxgiSurface(
surface.Get(),
nullptr,
&bitmap);
// Задаем мишень прорисовки для контекста
m_d2dContext->SetTarget(bitmap.Get());
// Начинаем рисование, используя D2D-контекст
m_d2dContext->BeginDraw();
// Применяем усечение и преобразование для ограничения
// обновлений целевой областью. Это нужно, чтобы координаты
// внутри целевой области гарантированно оставались
// согласованными с учетом смещения, возвращаемого BeginDraw,
// и чтобы можно было повысить производительность за счет
// оптимизации области, в которой выполняется рисование
// средствами D2D. Приложения всегда должны учитывать
// выходной параметр offset, возвращаемый BeginDraw, так как
// он может не совпадать с координатами,
// переданными через updateRect.
m_d2dContext->PushAxisAlignedClip(
D2D1::RectF(
static_cast<float>(offset.x),
static_cast<float>(offset.y),
static_cast<float>(offset.x + updateRect.Width),
static_cast<float>(offset.y + updateRect.Height)),
D2D1_ANTIALIAS_MODE_ALIASED);
m_d2dContext->SetTransform(
D2D1::Matrix3x2F::Translation(
static_cast<float>(offset.x),
static_cast<float>(offset.y)
)
);
}
// Заканчиваем рисование (обновления),
// начатые предыдущим вызовом BeginDraw
void MyImageSourceType::EndDraw()
{
// Удаляем усечение и преобразование, примененное
// в BeginDraw, так как целевая область может изменяться
// при каждом обновлении
m_d2dContext->SetTransform(D2D1::IdentityMatrix());
m_d2dContext->PopAxisAlignedClip();
// Удаляем мишень прорисовки и завершаем рисование
m_d2dContext->EndDraw();
m_d2dContext->SetTarget(nullptr);
m_sisNative->EndDraw();
}
Заметьте, что мой метод BeginDraw принимает примитив Rect как входной параметр, который сопоставляется с неуправляемым типом RECT. Этот RECT определяет область экрана, где я намерен рисовать, используя соответствующий SurfaceImageSource. Однако BeginDraw можно вызывать единовременно только раз, поэтому я вынужден ставить вызовы BeginDraw для каждого SurfaceImageSource в очередь — один за другим.
Также заметьте, что я инициализирую ссылку на IDXGISurface и что структура POINT содержит координаты смещения RECT по осям x и y, который я буду рисовать в IDXGISurface относительно правого левого угла. Этот указатель на поверхность и смещение возвращаются ISurfaceImageSourceNative::BeginDraw, чтобы предоставить вам IDXGISurface для рисования. Последующие вызовы в этом примере создают битовую карту на основе полученного указателя на поверхность и рисуют на ней с помощью Direct2D-вызовов. Когда ISurfaceImageSourceNative::EndDraw вызывается в перегруженном методе EndDraw, конечным результатом является полное изображение, которое можно будет рисовать в XAML-элементе <Image> или примитиве.
Давайте посмотрим, что я получил:
- тип, наследуемый от SurfaceImageSource;
- методы в моем производном типе, определяющие его поведение при рисовании в переданном RECT на экране;
- графические ресурсы DirectX, необходимые для рисования;
- сопоставление между графическим устройством DirectX и SurfaceImageSource.
Что еще мне нужно:
- код, который будет осуществлять рендеринг изображения в RECT;
- связь между конкретным экземпляром <Image> (или примитива) в XAML и экземпляром SurfaceImageSource, который будет вызываться приложением.
Написание кода, отвечающего за поведение при рисовании, — целиком моя задача, и, вероятно, проще всего реализовать его в моем типе SurfaceImageSource как специфический открытый метод, который можно вызывать из отделенного кода (codebehind).
Остальное несложно. В отделенном коде для XAML я добавляю в свой конструктор следующий код:
// Источник изображения, производный от SurfaceImageSource
// и используемый для рисования DirectX-контента
MyImageSourceType^ _SISDXsource = ref new
MyImageSourceType((int)MyDxImage->Width, (int)MyDxImage->Height, true);
// Используем MyImageSourceType как источник
// для элемента управления Image
MyDxImage->Source = _SISDXsource;
И включаю в тот же отделенный код обработчик событий, который выглядит примерно так:
private void MainPage::MyCodeBehindObject_Click(
Object^ sender, RoutedEventArgs^ e)
{
// Начинаем обновление SurfaceImageSource
SISDXsource->BeginDraw();
// Здесь размещаются ваши DirectX-вызовы для рисования
// или анимации, например _SISDXsource->
// DrawTheMostAmazingSpinning3DShadedCubeEver();
// ...
// Прекращаем обновление SurfaceImageSource
// и рисуем его контент
SISDXsource->EndDraw();
}
(В качестве альтернативы, если бы я не наследовал от SurfaceImageSource, то мог бы поместить вызовы BeginDraw и EndDraw в какой-нибудь метод, например в DrawTheMostAmazingSpinning3DShadedCubeEver из предыдущего фрагмента кода.)
Теперь, если я использую XAML-примитив, скажем Rect или Ellipse, я создаю ImageBrush и подключаю к нему SurfaceImageSource (где MySISPrimitive — графический примитив XAML):
// Создаем новую кисть-изображение помещаем в свойство
// ImageSource ваш экземпляр SurfaceImageSource
ImageBrush^ myBrush = new ImageBrush();
myBrush->ImageSource = _SISDXsource;
MySISPrimitive->Fill = myBrush;
И это все! Подводя итог, процесс в моем примере выглядит следующим образом.
- Выбираем XAML-элемент вроде Image, ImageBrush или графического примитива (Rect, Ellipse и др.), на котором будет происходить визуализация. Кроме того, определяем, поддерживает ли данная поверхность анимированное изображение. Помещаем в XAML.
- Создаем DirectX-устройство и контексты устройств (обычно Direct2D или Direct3D, или оба), которые будут использоваться для операций рисований. Также, используя COM, получаем ссылку на интерфейс ISurfaceImageSourceNative, который поддерживает тип SurfaceImageSource, и сопоставляем с ним графическое устройство.
- Создаем тип, производный от SurfaceImageSource; в нем есть код, вызывающий ISurfaceImageSource::BeginDraw и ISurfaceImageSource::EndDraw.
- Добавляем любые специфические операции рисования как методы в типе SurfaceImageSource.
- В случае поверхностей Image связываем свойство Source с экземпляром типа SurfaceImageSource. А в случае поверхностей графических примитивов создаем ImageBrush присваиваем экземпляр SurfaceImageSource свойству ImageSource, а затем используем эту кисть в сочетании со свойством Fill примитива (или любым свойством, которое принимает ImageSource либо ImageBrush).
- Вызываем операции рисования в экземплярах SurfaceImageSource из обработчиков событий. В случае анимированных изображений убедитесь, что операции прорисовки кадров можно прерывать.
Я могу использовать SurfaceImageSource для двух- и трехмерных игр, если сцена и шейдеры достаточно просты. Например, рендеринг визуальных элементов на SurfaceImageSource вполне годится для стратегий со средней графической нагрузкой (вспомните Civilization 4).
Кроме того, заметьте, что можно создать производный тип SurfaceImageSource на C++, поместить его в отдельную DLL, а затем использовать из другого языка. В этом случае я мог бы написать на C++ свой рендер и методы, а прикладную инфраструктуру и файлы отделенного кода — на C#. Здесь очень удобен шаблон Model-View-ViewModel (MVVM)!
А теперь перейдем к ограничениям:
- элемент управления, отображающий SurfaceImageSource, предназначен для поверхностей фиксированного размера;
- элемент управления, отображающий SurfaceImageSource, не оптимизирован по скорости работы для произвольных больших поверхностей, особенно тех, которые могут динамически панорамироваться или масштабироваться;
- обновление элемента управления обрабатывается WinRT-провайдером представления инфраструктуры XAML (WinRT XAML framework view provider), что происходит при обновлении инфраструктуры. Для операций в реальном времени и графики высокого разрешения это может заметно повлиять на производительность (так что этот вариант не слишком хорошо подходит для вашей новой игрушки с межгалактическими боями, интенсивно использующей шейдеры).
И это приводит нас к VirtualSurfaceImageSource (и в конечном счете к SwapChainBackgroundPanel). Давайте рассмотрим первый из них.
VirtualSurfaceImageSource и рендеринг интерактивных элементов управления
VirtualSurfaceImageSource — это расширение SurfaceImageSource, но предназначено для поверхностей изображений, которые могут масштабироваться пользователем, особенно если их размер может превысить размер экрана и они частично будут не видны или же их могут частично или полностью закрывать другие изображения или XAML-элементы. VirtualSurfaceImageSource очень хорошо работает в приложениях, где пользователь регулярно панорамирует или масштабирует изображение, размеры которого потенциально превышают размеры экрана, например элемент управления «карта» или средство просмотра изображений.
Процесс для VirtualSurfaceImageSource идентичен представленному ранее для SurfaceImageSource — вы лишь используете тип VirtualSurfaceImageSource вместо SurfaceImageSource и реализацию интерфейса IVirtualImageSourceNative вместо ISurfaceImageSourceNative.
Графический интерфейс позволяет выполнять единовременно только одну операцию в UI-потоке.
Таким образом, я изменяю код из предыдущего примера следующим образом:
- использую VirtualSurfaceImageSource вместо SurfaceImageSource (рис. 4). В последующих примерах кода я буду наследовать свой базовый класс типа источника изображения MyImageSourceType от VirtualSurfaceImageSource;
- запрашиваю реализацию метода в нижележащем интерфейсе IVirtualSurfaceImageSourceNative.
Рис. 4. Наследование от VirtualSurfaceImageSource
public ref class MyImageSourceType sealed : Windows::UI::Xaml::Media::Imaging::VirtualSurfaceImageSource
{
// ...
MyImageSourceType::MyImageSourceType(
int pixelWidth,
int pixelHeight,
bool isOpaque
) : VirtualSurfaceImageSource(pixelWidth, pixelHeight, isOpaque)
{
// Глобальная переменная, содержащая ширину
// SurfaceImageSource в пикселях
m_width = pixelWidth;
// Глобальная переменная, содержащая высоту
// SurfaceImageSource в пикселях
m_height = pixelHeight;
CreateDeviceIndependentResources(); // см. ниже
CreateDeviceResources(); // подготовка DXGI-ресурсов
}
// ...
void MyImageSourceType::CreateDeviceIndependentResources()
{
// Запрос интерфейса IVirtualSurfaceImageSourceNative
reinterpret_cast<IUnknown*>(this)->QueryInterface(
IID_PPV_ARGS(&m_vsisNative));
}
// ...
}
О, и есть еще одно очень важное отличие: я должен реализовать обратный вызов, который запускается всякий раз, когда «плитка» поверхности (определенный прямоугольный регион, не путать с плитками в Windows 8 UI) становится видимой и нуждается в прорисовке. Эти плитки управляются инфраструктурой, когда приложение создает экземпляр VirtualSurfaceImageSource, а вы не контролируете их параметры. На внутреннем уровне крупное изображение делится на такие плитки, и обратный вызов запускается, когда часть одной из этих плиток становится видимой пользователю и требует обновления.
Чтобы задействовать этот механизм, мне нужно сначала реализовать тип, позволяющий создавать экземпляры, который наследуется от интерфейса IVirtualSurfaceUpdatesCallbackNative, и зарегистрировать экземпляр этого типа, передав его в IVirtualSurfaceImageSource::RegisterForUpdatesNeeded, как показано на рис. 5.
Рис. 5. Подготовка обратного вызова для VirtualSurfaceImageSource
class MyVisibleSurfaceDrawingType :
public IVirtualSurfaceUpdatesCallbackNative
{
// ...
private:
virtual HRESULT STDMETHODCALLTYPE UpdatesNeeded() override;
}
// ...
HRESULT STDMETHODCALLTYPE MyVisibleSurfaceDrawingType::UpdatesNeeded()
{
// ...здесь выполняется рисование...
}
void MyVisibleSurfaceDrawingType::Initialize()
{
// ...
m_vsisNative->RegisterForUpdatesNeeded(this);
// ...
}
Операция рисования реализуется как метод UpdatesNeeded интерфейса IVirtualSurfaceUpdatesCallbackNative. Если стал видимым какой-то регион, я должен определить, какие плитки следует обновлять. Для этого я вызываю IVirtualSurfaceImageSourceNative::GetRectCount и, если счетчик обновляемых плиток больше нуля, получаю конкретные прямоугольники для этих плиток с помощью IVirtualSurfaceImageSourceNative::GetUpdateRects и обновляю каждую из них:
HRESULT STDMETHODCALLTYPE MyVisibleSurfaceDrawingType::UpdatesNeeded()
{
HRESULT hr = S_OK;
ULONG drawingBoundsCount = 0;
m_vsisNative->GetUpdateRectCount(&drawingBoundsCount);
std::unique_ptr<RECT[]> drawingBounds(new RECT[drawingBoundsCount]);
m_vsisNative->GetUpdateRects(drawingBounds.get(), drawingBoundsCount);
for (ULONG i = 0; i < drawingBoundsCount; ++i)
{
// ...здесь размещается код для прорисовки каждой плитки...
}
}
Я могу получить параметры, определенные VirtualSurfaceImageSource для этих плиток как объекты RECT. В предыдущем примере я получаю массив объектов RECT для всех плиток, которые нужно обновить. Затем использую значения для этих RECT, чтобы перерисовать плитки, передав в VirtualSurfaceImageSource::BeginDraw.
И вновь, как и в случае SurfaceImageSource, я инициализирую указатель на IDXGISurface и вызываю метод BeginDraw в IVirtualSurfaceImageSourceNative (нижележащей неуправляемой реализации интерфейса), чтобы получить текущую поверхность для рисования. Однако смещение по осям x, y относится к целевому RECT, а не элементу изображения в целом.
Для каждого обновляемого RECT я вызываю код, который выглядит, как показано на рис. 6.
Рис. 6. Обработка обновлений для управления размером или видимостью
POINT offset;
ComPtr<IDXGISurface> dynamicSurface;
// Задаем смещение
// Вызываем следующий код по разу для каждого RECT плитки,
// которую нужно обновить
HRESULT beginDrawHR = m_vsisNative->
BeginDraw(updateRect, &dynamicSurface, &offset);
if (beginDrawHR == DXGI_ERROR_DEVICE_REMOVED ||
beginDrawHR == DXGI_ERROR_DEVICE_RESET)
{
// Обрабатываем изменение в графическом интерфейсе
}
else
{
// Рисуем на IDXGISurface для обновляемого RECT
// с указанным смещением
}
И опять я не могу распараллелить эти вызовы, так как графический интерфейс допускает выполнение единовременно лишь одной операции в UI-потоке. Я могу последовательно обрабатывать каждый RECT плитки или вызвать IVirtualSurfaceImageSourceNative::BeginDraw с передачей объединенной области всех RECT для выполнения всех обновлений как единой операции. Это выбор разработчика.
Наконец, я вызываю IVirtualSurfaceImageSourceNative::EndDraw после обновления каждого измененного RECT плитки. По окончании обработки последней обновляемой плитки я располагаю полностью обновленной битовой картой, которую передаю соответствующему XAML-изображению или примитиву, как это делалось в примере с SurfaceImageSource.
Вот и все! Эта форма механизма взаимодействия DirectX-XAML отлично подходит, когда пользователей не волнует ввод с малой задержкой при обработке трехмерной графики в реальном времени, что было бы необходимо в динамичных играх. Этот вариант также великолепен для приложений и элементов управления с богатой графикой и более асинхронных игр (читай: пошаговых стратегий).
В следующей статье я рассмотрю противоположный подход: рисование XAML поверх цепочки обменов DirectX и то, что нужно сделать, чтобы инфраструктура XAML корректно работала с пользовательским провайдером DirectX-представлений.