В прошлой статье я рассмотрел модель приложений Windows Runtime (WinRT) (msdn.microsoft.com/magazine/dn342867). Я показал, как написать приложение Windows Store или Windows Phone на стандартном C++ с применением классической COM, и На этот раз я покажу, как задействовать этот базовый скелет и добавить поддержку рендеринга. Модель приложений WinRT оптимизирована под рендеринг с помощью DirectX. Мы посмотрим, как использовать то, чему вы научились из моих предыдущих статей о рендеринге в Direct2D и Direct3D, и применим это к WinRT-приложению на основе CoreWindow, использующему Direct2Dспользуя только API-функции WinRT. При разработке таких приложений не предъявляется требование обязательно использовать какую-либо языковую проекцию вроде C++/CX или C#. Возможность обхода этих абстракций — важная особенность и отличный способ понять, как работает эта технология.
В статье из моей рубрики за май 2013 года я познакомил вас с Direct2D 1.1 и показал, как с его помощью осуществлять рендеринг в настольном приложении (msdn.microsoft.com/magazine/dn198239). А в следующей рубрике я описал библиотеку dx.h (доступна на dx.codeplex.com), которая радикально упрощает программирование DirectX на C++ (msdn.microsoft.com/magazine/dn201741).
Код, представленный в последней статье, был вполне достаточен, чтобы создать приложение на основе CoreWindow, но в нем не выполнялся никакой рендеринг.
На этот раз я покажу, как задействовать этот базовый скелет и добавить поддержку рендеринга. Модель приложений WinRT оптимизирована под рендеринг с помощью DirectX. Мы посмотрим, как использовать то, чему вы научились из моих предыдущих статей о рендеринге в Direct2D и Direct3D, и применим это к WinRT-приложению на основе CoreWindow, использующему Direct2D 1.1 через библиотеку dx.h. По большей части команды рисования Direct2D и Direct3D, которые вам понадобится писать, одинаковы независимо от того, ориентируетесь вы на настольную программу или на Windows Runtime. Однако есть некоторые небольшие различия, и уж, если на то пошло, все это по-разному соединяется друг с другом. Так что я начну с того места, на котором остановился в прошлый раз, и объясню, как отображать пиксели на экране.
Для корректной поддержки рендеринга окно должно распознавать определенные события — как минимум, изменения видимости и размера окна, а также изменения в логической конфигурации DPI экрана, выбранной пользователем. Как и в случае события Activated, о котором я рассказывал в прошлый раз, все эти новые события отправляются приложению через обратные вызовы COM-интерфейса. Интерфейс ICoreWindow предоставляет методы для регистрации на события VisibilityChanged и SizeChanged, но сначала нужно реализовать соответствующие обработчики. Эти два COM-интерфейса, которые я должен реализовать, во многом похожи на обработчик событий Activated с его шаблонами классов, генерируемых MIDL (Microsoft Interface Definition Language):
typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *>
IVisibilityChangedEventHandler;
typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *>
IWindowSizeChangedEventHandler;
Следующий COM-интерфейс, который нужно реализовать, — IDisplayPropertiesEventHandler, и, к счастью, он уже определен. Достаточно включить соответствующий заголовочный файл:
#include <Windows.Graphics.Display.h>
Кроме того, релевантные типы определены в следующем пространстве имен:
Кроме того, релевантные типы определены в следующем пространстве имен:
При наличии этих определений я могу обновить класс SampleWindow из своей прошлой статьи так, чтобы он наследовал и от этих трех интерфейсов:
struct SampleWindow :
...
IVisibilityChangedEventHandler,
IWindowSizeChangedEventHandler,
IDisplayPropertiesEventHandler
Кроме того, я должен обновить свою реализацию QueryInterface, чтобы сообщать о поддержке этих интерфейсов. Это упражнение я оставлю вам. Конечно, как я говорил в прошлый раз, Windows Runtime безразлично, где именно реализованы эти обратные вызовы COM-интерфейса. Из этого следует, что Windows Runtime не предполагает, что IFrameworkView моего приложения (основной интерфейс, реализуемый классом SampleWindow) также реализует интерфейсы этих обратных вызовов. Поэтому, хотя QueryInterface должным образом обрабатывает запросы к этим интерфейсам, Windows Runtime не будет запрашивать их. Вместо этого нужно зарегистрироваться на получение соответствующих событий, и лучшее место для этого — в моей реализации метода Load интерфейса IFrameworkView. Напомню, что Load — это место, в которое вы должны помещать весь код для подготовки вашего приложения к начальному отображению на экране. Таким образом, я регистрируюсь на события VisibilityChanged и SizeChanged в методе Load:
EventRegistrationToken token;
HR(m_window->add_VisibilityChanged(this, &token));
HR(m_window->add_SizeChanged(this, &token));
Это явным образом сообщает Windows Runtime, где найти реализации первых двух интерфейсов. Третий и последний интерфейс предназначен для события LogicalDpiChanged, но регистрация этого события обеспечивается интерфейсом IDisplayPropertiesStatics. Этот статический интерфейс реализуется WinRT-классом DisplayProperties. Чтобы получить его, я могу просто использовать шаблон функции GetActivationFactory (реализацию GetActivationFactory можно найти в моей прошлой статье):
ComPtr<IDisplayPropertiesStatics> m_displayProperties;
m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics>(
RuntimeClass_Windows_Graphics_Display_DisplayProperties);
Данная переменная-член сохраняет указатель на этот интерфейс, поскольку мне понадобится вызывать его в различные моменты жизненного цикла окна. А пока можно просто зарегистрироваться на событие LogicalDpiChanged в методе Load:
HR(m_displayProperties->add_LogicalDpiChanged(this, &token));
Вскоре я вернусь к реализации этих трех интерфейсов. Теперь следует подготовить инфраструктуру DirectX. Мне потребуется стандартный набор обработчиков аппаратных ресурсов (device resource handlers), которые неоднократно обсуждались в моих предыдущих статьях:
void CreateDeviceIndependentResources() {}
void CreateDeviceSizeResources() {}
void CreateDeviceResources() {}
void ReleaseDeviceResources() {}
В первом обработчике я могу создать или загрузить любые ресурсы, не специфичные для нижележащего Direct3D-устройства рендеринга. Следующие два предназначены для создания аппаратно-зависимых ресурсов. Лучше всего разделять ресурсы, специфичные и неспецифичные для размера окна. В конце все аппаратные ресурсы должны быть освобождены. Остальная инфраструктура DirectX полагается на то, что приложение корректно реализует эти четыре метода в соответствии со своими требованиями. Она предоставляет дискретные точки в приложении для управления ресурсами рендеринга, а также для эффективного создания и повторного использования этих ресурсов.
Теперь я могу включить файл dx.h, который берет на себя всю тяжелую работу, связанную с DirectX:
#include "dx.h"
И каждое Direct2D-приложение начинается с Direct2D-фабрики:
Factory1 m_factory;
Вы найдете ее в пространстве имен Direct2D, и обычно я включаю это пространство так:
using namespace KennyKerr;
using namespace KennyKerr::Direct2D;
В библиотеке dx.h имеются дискретные пространства имен для Direct2D, DirectWrite, Direct3D, Microsoft DirectX Graphics Infrastructure (DXGI) и т. д. Большинство моих приложений интенсивно использует Direct2D, поэтому для меня это имеет смысл. А вы, разумеется, можете управлять пространствами имен так, как это нужно в вашем приложении.
Переменная-член m_factory представляет фабрику Direct2D 1.1. С ее помощью создается мишень рендеринга (render target), а также множество других аппаратно-независимых ресурсов (по мере необходимости). Я создам Direct2D-фабрику, а затем позабочусь, чтобы любые аппаратно-независимые ресурсы создавались на последнем этапе метода Load:
m_factory = CreateFactory();
CreateDeviceIndependentResources();
После того как метод Load возвращает управление, WinRT-класс CoreApplication немедленно вызывает метод Run интерфейса IFrameworkView.
Реализация метода Run в SampleWindow из моей прошлой статьи просто блокировалась, вызвав метод ProcessEvents диспетчера CoreWindow. Блокирование в таком стиле вполне адекватно, если ваше приложение будет выполнять рендеринг нечасто — лишь на основе каких-то событий. Но, возможно, вы реализуете игру или вам просто нужна какая-то анимация высокого разрешения. В этом случае бросаются в другую крайность — использование непрерывного цикла анимации, но скорее всего вы предпочтете нечто более разумное. Я реализую компромиссный вариант между этими крайностями. Сначала я добавлю переменную-член, которая будет отслеживать, видимо ли окно. Это позволит мне убавить интенсивность рендеринга, когда окно физически не видимо пользователю:
bool m_visible;
SampleWindow() : m_visible(true) {}
После этого можно переписать метод Run, как показано на рис. 1.
Рис. 1. Цикл динамического рендеринга
auto __stdcall Run() -> HRESULT override
{
ComPtr<ICoreDispatcher> dispatcher;
HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
while (true)
{
if (m_visible)
{
Render();
HR(dispatcher->
ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
}
else
{
HR(dispatcher->
ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
}
}
return S_OK;
}
Как и раньше, метод Run получает диспетчер CoreWindow. Затем входит в бесконечный цикл, постоянно выполняя рендеринг и обрабатывая любые оконные сообщения (в Windows Runtime их называют событиями), которые могут находиться в очереди. Однако, если окно невидимо, он блокируется до тех пор, пока не появится сообщение. Как приложение узнает об изменении видимости окна? Для этого предназначен интерфейс IVisibilityChangedEventHandler. Теперь я могу реализовать его метод Invoke для обновления переменной-члена m_visible:
auto __stdcall Invoke(ICoreWindow *,
IVisibilityChangedEventArgs * args) -> HRESULT override
{
unsigned char visible;
HR(args->get_Visible(&visible));
m_visible = 0 != visible;
return S_OK;
}
Интерфейс, сгенерированный MIDL, использует unsigned char как портируемый булев тип данных. Я просто получаю текущее состояние видимости окна, используя предоставляемый указатель на интерфейс IVisibilityChangedEventArgs и соответственно обновляю переменную-член. Это событие генерируется всякий раз, когда окно скрывается или показывается, и такой вариант немного проще, чем в реализации для настольных приложений, поскольку он охватывает целый ряд сценариев, включая завершение приложения и управление электропитанием, не говоря уже о переключении между окнами.
Далее нужно реализовать метод Render, вызываемый методом Run на рис. 1. Именно здесь создается по запросу стек рендеринга (rendering stack) и формируются собственно команды рисования. Базовый скелет этого метода представлен на рис. 2.
Рис. 2. Скелет метода Render
void Render()
{
if (!m_target)
{
// Подготавливаем мишень рендеринга...
}
m_target.BeginDraw();
Draw();
m_target.EndDraw();
auto const hr = m_swapChain.Present();
if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
{
ReleaseDevice();
}
}
Метод Render должен выглядеть знакомым вам. У него та же базовая форма, которую я обрисовал ранее для Direct2D 1.1. Он начинает с создания мишени рендеринга. Сразу же за этим следуют команды рисования, отправляемые между вызовами BeginDraw и EndDraw. Поскольку мишенью рендеринга является контекст Direct2D-устройства, реальный вывод нарисованных пикселей на экран включает использование цепочки замен (swap chain). Попутно мне нужно добавить типы из dx.h, представляющие контекст устройства Direct2D 1.1, а также цепочку замен в DirectX версии 11.1. Последнюю можно найти в пространстве имен Dxgi:
DeviceContext m_target;
Dxgi::SwapChain1 m_swapChain;
Завершается метод Render вызовом ReleaseDevice, если отображение терпит неудачу:
void ReleaseDevice()
{
m_target.Reset();
m_swapChain.Reset();
ReleaseDeviceResources();
}
Этот код освобождает мишень рендеринга и цепочку замен. Он также вызывает ReleaseDeviceResources для освобождения любых аппаратно-зависимых ресурсов, таких как кисти, битовые карты или эффекты. Вызов метода ReleaseDevice в данном случае может показаться не существенным, но он крайне важен для надежной обработки потери устройства в DirectX-приложении. Без должного освобождения всех аппаратных ресурсов (любых ресурсов, так или иначе поддерживаемых графическим процессором) ваше приложение может не восстановиться после потери устройства и просто рухнет.
Затем я должен подготовить мишень рендера — эту часть я опустил в методе Render на рис. 2. Она начинается с создания Direct3D-устройства (библиотека dx.h по-настоящему упрощает и несколько последующих этапов):
auto device = Direct3D::CreateDevice();
Располагая Direct3D-устройством, я могу с помощью Direct2D-фабрики создать Direct2D-устройство и контекст этого устройства:
m_target = m_factory.CreateDevice(device).CreateDeviceContext();
Следующий шаг — создание цепочки замен окна. Сначала я получаю DXGI-фабрику от Direct3D-устройства:
auto dxgi = device.GetDxgiFactory();
Потом создаю цепочку замен для CoreWindow приложения:
m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
Здесь библиотека dx.h снова намного облегчает мне жизнь, автоматически заполняя за меня структуру DXGI_SWAP_CHAIN_DESC1. После этого я вызываю метод CreateDeviceSwapChainBitmap, чтобы создать битовую карту Direct2D, которая будет представлять буфер невидимых поверхностей (back buffer) цепочки замен:
void CreateDeviceSwapChainBitmap()
{
BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
auto bitmap =
m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
m_target.SetTarget(bitmap);
}
Этот метод должен сначала описать буфер невидимых поверхностей цепочки замен так, чтобы это было понятно Direct2D. BitmapProperties1 — версия Direct2D-структуры D2D1_BITMAP_PROPERTIES1 из dx.h. Константа BitmapOptions::Target указывает, что в качестве мишени контекста устройства будет использоваться битовая карта. Константа BitmapOptions::CannotDraw сообщает, что буфер невидимых поверхностей цепочки замен можно использовать только как вывод, но не ввод для других операций рисования. PixelFormat — это версия Direct2D-структуры D2D1_PIXEL_FORMAT из dx.h.
Определив свойства битовой карты, метод CreateBitmapFromDxgiSurface получает буфер невидимых поверхностей цепочки замен и создает для его представления битовую карту Direct2D. Тем самым контекст Direct2D-устройства может осуществлять рендеринг напрямую в цепочку замен, просто выбрав в качестве мишени битовую карту через метод SetTarget.
В методе Render мне нужно просто сообщить Direct2D, как масштабировать любые команды рисования согласно конфигурации DPI, установленной пользователем:
float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);
Далее я вызываю обработчики аппаратных ресурсов в приложении для создания любых необходимых ресурсов. На рис. 3 показана полная последовательность инициализации устройства для метода Render.
Рис. 3. Подготовка мишени рендера
void Render()
{
if (!m_target)
{
auto device = Direct3D::CreateDevice();
m_target = m_factory.CreateDevice(device).CreateDeviceContext();
auto dxgi = device.GetDxgiFactory();
m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
CreateDeviceSwapChainBitmap();
float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);
CreateDeviceResources();
CreateDeviceSizeResources();
}
// Рисование и представление... (см. рис. 2)
Хотя изменение DPI корректно применяется сразу после создания контекста Direct2D-устройства, его конфигурацию нужно обновлять всякий раз, когда эта настройка меняется пользователем. Тот факт, что конфигурация DPI может измениться для выполняемого приложения, является новшеством Windows 8. И здесь на сцену выходит интерфейс IDisplayPropertiesEventHandler. Теперь можно просто реализовать его метод Invoke и соответственно обновлять устройство. Вот обработчик событий LogicalDpiChanged:
auto __stdcall Invoke(IInspectable *) -> HRESULT override
{
if (m_target)
{
float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);
CreateDeviceSizeResources();
Render();
}
return S_OK;
}
Предполагая, что мишень (контекст устройства) уже создана, он получает текущее логическое значение DPI и просто пересылает его в Direct2D. Потом обращается к приложению для создания заново любых ресурсов, специфичных для размера устройства (device-size-specific resources), перед повторных рендерингом. Тем самым мое приложение может динамически реагировать на изменения в конфигурации DPI экрана. Наконец, окно должно динамически обрабатывать изменения в размере окна. Я уже подключил регистрацию события, поэтому просто добавляю реализацию метода Invoke в интерфейсе IWindowSizeChangedEventHandler, представляющем обработчик событий SizeChanged:
auto __stdcall Invoke(ICoreWindow *,
IWindowSizeChangedEventArgs *) -> HRESULT override
{
if (m_target)
{
ResizeSwapChainBitmap();
Render();
}
return S_OK;
}
Осталось лишь изменить размер битовой карты цепочки замен вызовом метода ResizeSwapChainBitmap. И вновь это нужно делать осторожно. Такое изменение может и должно быть эффективной операцией, но только при условии ее корректности. Чтобы эта операция прошла успешно, сначала нужно гарантировать освобождение всех ссылок на эти буферы. Такие ссылки приложение может хранить как в явном, так и неявном виде. В данном случае ссылка хранится контекстом Direct2D-устройства. Целевое изображение — это битовая карта Direct2D, которую я создал для обертывания буфера невидимых поверхностей. Освободить ее достаточно легко:
m_target.SetTarget();
Затем можно вызвать метод ResizeBuffers цепочки замен для выполнения остальной работы, а потом вызвать необходимые обработчики аппаратных ресурсов приложения. На рис. 4 показано, как все это делается.
Рис. 4. Изменение размеров цепочки замен
void ResizeSwapChainBitmap()
{
m_target.SetTarget();
if (S_OK == m_swapChain.ResizeBuffers())
{
CreateDeviceSwapChainBitmap();
CreateDeviceSizeResources();
}
else
{
ReleaseDevice();
}
}
Теперь можно добавить какие-либо команды рисования, и DirectX эффективно выполнит соответствующий рендеринг на мишени CoreWindow. В качестве простого примера вы могли бы создать кисть со сплошной закраской в обработчике CreateDeviceResources и присвоить ее какой-то переменной-члену:
SolidColorBrush m_brush;
m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));
В методе Draw окна можно начать с очистки фона окна белым цветом:
m_target.Clear(Color(1.0f, 1.0f, 1.0f));
Затем использовать кисть и нарисовать простой красный прямоугольник:
RectF rect (100.0f, 100.0f, 200.0f, 200.0f);
m_target.DrawRectangle(rect, m_brush);
Чтобы гарантировать корректное восстановление приложения после потери устройства, нужно обеспечить освобождение им кисти сразу после того, как необходимость в ней отпадает:
void ReleaseDeviceResources()
{
m_brush.Reset();
}
И это все, что требуется для рендеринга приложения на основе CoreWindow с помощью DirectX. Конечно, если вы сравните это с моей статьей за май 2013 года, то наверняка приятно удивитесь тому, насколько проще работать с кодом, относящимся к DirectX, благодаря библиотеке dx.h. Но пока что в приложении еще очень много стереотипного кода, в основном связанного с реализацией COM-интерфейсов. И здесь очень пригодится C++/CX, который упрощает использование WinRT API в ваших приложениях.