Рендеринг для Windows Runtime

OSzone.net » Microsoft » Разработка приложений » Windows (до Windows 10) » Рендеринг для Windows Runtime
Автор: Кенни Керр
Иcточник: MSDN Magazine
Опубликована: 15.05.2014

В прошлой статье я рассмотрел модель приложений 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 в ваших приложениях.


Ссылка: http://www.oszone.net/23988/