Windows и C++

Введение в Direct2D 1.1

Кенни Керр

Kenny KerrВ Windows 8 появилась новая основная версия Direct2D. После этого Direct2D попал в Windows RT (для устройств на базе ARM) и Windows Phone 8, обе из которых основаны на этой новейшей версии Windows. Поддержка Direct2D на ОС для смартфонов пока не официальная, но это лишь дело времени. А как насчет Windows 7? Готовится обновление этой платформы, чтобы ввести в Windows 7 самую новую версию DirectX-семейства API. Оно включает последние версии Direct2D, Direct3D, DirectWrite, Windows Imaging Component, Windows Animation Manager и т. д. Главный стимул для всего этого, конечно, — Internet Explorer. Везде, где есть Internet Explorer версий 9 и выше, вы найдете Direct2D. К тому времени, когда вы будете читать эту статью, для Windows 7 уже будет доступен Internet Explorer 10, и он тоже потребует установки Direct2D 1.1. Учитывая его распространенность, нет никаких причин не использовать Direct2D 1.1. Но что такое Direct2D 1.1 и как приступить к работе с ним? Это и есть тема данной статьи.

Direct2D 1.1 может показаться незначительным обновлением, и в каких-то отношениях это так. Никаких фундаментальных изменений в API не внесено. Все, что вы знаете о Direct2D, по-прежнему верно и сегодня. Он все так же базируется на аппаратно-специфических и аппаратно-независимых ресурсах, мишенях прорисовки (render targets), геометрических элементах, кистях и прочем. Но Direct2D в версии 1.1 стал совершеннее. Исходная версия Direct2D, выпущенная вместе с Windows 7, была в некоторых отношениях аутсайдером по сравнению с DirectX. Он отставал от нее, будучи привязанным к DirectX 10, а не 11, т. е. к той версии DirectX, с которой он был когда-то выпущен. Хотя он предоставлял отличный уровень совместимости с GDI и Windows Imaging Component, он не позволял максимально задействовать все возможности самого DirectX. Это не было столь страшно, но в Direct2D 1.1 многое стало гораздо эффективнее. Direct3D и Direct2D теперь во многом родственны и равноправны в семействе DirectX. Благодаря этому разработчик, использующий Direct2D, может задействовать большую часть графического процессора (GPU), не отказываясь от абстракции двухмерности. Более того, когда вам всем потребуется выйти за рамки этой абстракции, вы сможете сделать это проще и эффективнее.

В прошлой статье (msdn.microsoft.com/magazine/jj991972) я показал, как можно было бы использовать ID2D1HwndRenderTarget (HWND-мишень прорисовки Direct2D) в окне рабочего стола. Этот механизм по-прежнему поддерживается Direct2D 1.1 и по-прежнему является самым простым способом приступить к работе с Direct2D, так как он скрывает всю специфику нижележащей инфраструктуры Direct3D и DirectX Graphics Infrastructure (DXGI). Однако, чтобы задействовать преимущества усовершенствований в Direct2D 1.1, нужно отказаться от этой мишени прорисовки в пользу ID2D1DeviceContext — новой мишени прорисовки в контексте устройства. Поначалу это может показаться заимствованием из Direct3D, и в некоторых отношениях так и есть. Как и HWND-мишень прорисовки, контекст Direct2D-устройства наследует от интерфейса ID2D1RenderTarget и тем самым во многом похож на традиционную мишень прорисовки, но на самом деле он гораздо мощнее. Его создание несколько сложнее, но с лихвой окупает дополнительные усилия, поскольку он предоставляет множество новых средств и вдобавок является единственным способом использования Direct2D с Windows Runtime (WinRT). Так что, если вы хотите применять Direct2D в своих приложениях Windows Store или Windows Phone, вам потребуется контекст Direct2D-устройства. Сегодня я покажу, как использовать новую мишень прорисовки в настольном приложении, а в следующей статье мы обсудим, как это делается при работе с Windows Runtime, причем здесь вы будете иметь дело больше с Windows Runtime, чем с Direct2D. Вам понадобится знать, как управлять Direct3D-устройством, а также понимать, что такое цепочка замен (swap chain), буферы и ресурсы.

Удобство использования исходной HWND-мишени прорисовки в Direct2D заключалось в том, что на деле от вас не требовалось никаких познаний в области Direct3D или DirectX. Теперь это не так. К счастью, вам не придется изучать DirectX в полном объеме — это определенно устрашающая задача. DirectX фактически является семейством тесно связанных API, из которых наиболее известен Direct3D, тогда как Direct2D только начинает привлекать к себе внимание благодаря своей относительной простоте в использовании и невероятной мощи. Наряду с этими API появлялись и исчезали другие части DirectX. Один из сравнительно новых членов семейства — DXGI, дебют которого состоялся вместе с Direct3D 10. DXGI предоставляет общие средства управления ресурсами графического процессора в различных DirectX API. DXGI участвует в наведении мостов между Direct2D и Direct3D. То же самое относится и к наведению мостов между Direct3D и настольным HWND или WinRT CoreWindow. DXGI — это своего рода клей, связывающий их всех вместе.

Заголовочные файлы и другие «клейкие материалы»

Как я уже делал раньше, я продолжу использовать Active Template Library (ATL) в настольных приложениях просто для большей четкости примеров. Вы можете применять свою библиотеку или вообще обойтись без таковых. На самом деле это не имеет никакого значения. Все эти варианты я описывал в своей статье за февраль 2013 г. (msdn.microsoft.com/magazine/jj891018). Для начала включите обязательные библиотеки Visual C++:

#include <wrl.h>
#include <atlbase.h>
#include <atlwin.h>

Windows Runtime C++ Template Library (WRL) предоставляет удобный смарт-указатель ComPtr, а заголовочные файлы ATL обеспечивают управление окном рабочего стола. Далее вам понадобятся новейшие заголовочные файлы DirectX:

#include <d2d1_1.h>
#include <d3d11_1.h>

Первый — это новый заголовочный файл Direct2D 1.1. Заголовочный файл Direct3D 11.1 необходим для управления устройством. Чтобы не усложнять, я исхожу из того, что пространства имен WRL и Direct2D таковы:

using namespace Microsoft::WRL;
using namespace D2D1;

Затем вы должны сообщить компоновщику (linker), как разрешать функции фабрики, которыми вы будете пользоваться:

#pragma comment(lib, "d2d1")
#pragma comment(lib, "d3d11")

Я стараюсь избегать обсуждения обработки ошибок. Как и во многих других областях, у разработчиков на C++ широкий выбор того, как обрабатывать ошибки. Такая гибкость — как раз то, что привлекает меня да и многих других в C++, но может вызывать разногласия. Тем не менее, меня часто спрашивают об обработке ошибок, и поэтому, чтобы избежать недопонимания, на рис. 1 я показал на то, что именно я полагаюсь при обработке ошибок в настольных приложениях.

Рис. 1. Обработка ошибок

#define ASSERT ATLASSERT
#define VERIFY ATLVERIFY
#ifdef _DEBUG
#define HR(expression) ASSERT(S_OK == (expression))
#else
struct ComException
{
  HRESULT const hr;
  ComException(HRESULT const value) : hr(value) {}
};
inline void HR(HRESULT const hr)
{
  if (S_OK != hr) throw ComException(hr);
}
#endif

Макросы ASSERT и VERIFY определены в терминах соответствующих ATL-макросов. Если вы не используете ATL, то могли бы просто взять из C Run-Time Library (CRT) макрос _ASSERTE. В любом случае проверочные выражения жизненно важны при отладке. VERIFY проверяет результат выражения, но вызывает ASSERT только в отладочных сборках. В рабочих сборках (release builds) выражение ASSERT полностью удаляется, тогда как VERIFY остается. Последнее выражение полезно для проверки функций, которые должны выполняться, но не должны терпеть неудачу из-за какого-нибудь апокалиптического события. Наконец, HR — макрос, который гарантирует, что выражение (обычно функция или метод интерфейса в стиле COM) либо выполняется успешно, либо вообще не выполняется. В отладочных сборках он вызывает ASSERT так, чтобы быстро найти проблемную строку кода в отладчике, а в рабочих сборках — генерирует исключение, чтобы вызвать немедленный крах приложения. Это особенно удобно, если ваше приложение использует Windows Error Reporting (WER), так как автономный аварийный дамп подскажет вам причину сбоя.

Окно рабочего стола

Теперь пора заняться классом DesktopWindow. Сначала я определю базовый класс, обертывающий большую часть стереотипного кода, связанного с окнами. Исходная структура показана на рис. 2.

Рис. 2. Скелет окна рабочего стола

template <typename T>
struct DesktopWindow :
  CWindowImpl<DesktopWindow<T>,
              CWindow,
              CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  ComPtr<ID2D1DeviceContext> m_target;
  ComPtr<IDXGISwapChain1> m_swapChain;
  ComPtr<ID2D1Factory1> m_factory;
  DECLARE_WND_CLASS_EX(nullptr, 0, -1);
  BEGIN_MSG_MAP(c)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_SIZE, SizeHandler)
    MESSAGE_HANDLER(WM_DISPLAYCHANGE, DisplayChangeHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    Render();
    EndPaint(&ps);
    return 0;
  }
  LRESULT DisplayChangeHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    Render();
    return 0;
  }
  LRESULT SizeHandler(UINT, WPARAM wparam, LPARAM, BOOL &)
  {
    if (m_target && SIZE_MINIMIZED != wparam)
    {
      ResizeSwapChainBitmap();
      Render();
    }
    return 0;
  }
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
  void Run()
  {
    D2D1_FACTORY_OPTIONS fo = {};
    #ifdef _DEBUG
    fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
    #endif
    HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                         fo,
                         m_factory.GetAddressOf()));
    static_cast<T *>(this)->CreateDeviceIndependentResources();
    VERIFY(__super::Create(nullptr, nullptr, L"Introducing Direct2D 1.1"));
    MSG message;
    BOOL result;
    while (result = GetMessage(&message, 0, 0, 0))
    {
      if (-1 != result)
      {
        DispatchMessage(&message);
      }
    }
  }
};

DesktopWindow — это шаблон класса, так как я полагаюсь на статический полиморфизм или полиморфизм этапа компиляции в вызове оконного класса приложения для рисования и управления ресурсами в подходящие моменты. Поскольку я уже описывал механику ATL и окон рабочего стола в своей статье за февраль 2013 г., я не буду повторять ее здесь. Главное, что нужно отметить, — сообщения WM_PAINT, WM_SIZE и WM_DISPLAYCHANGE обрабатываются вызовом метода Render. Сообщение WM_SIZE также приводит к вызову метода ResizeSwapChainBitmap. Эти точки подключения (hooks) необходимы, чтобы DirectX был в курсе того, что происходит с вашим окном. Что они делают, я вскоре расскажу. Наконец, метод Run создает стандартный Direct2D-объект фабрики, в данном случае получая новый интерфейс ID2D1Factory1, и (не обязательно) позволяет оконному классу приложения создать аппаратно-независимые ресурсы. Затем он создает сам HWND и входит в цикл обработки сообщений. Функция wWinMain приложения состоит всего из двух строк кода:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  window.Run();
}

Создание устройства

До сих пор большая часть из рассказанного мной здесь была фактически кратким повторением показанного в предыдущих статьях управления окнами. Теперь я подошел к той точке, где все меняется. HWND-мишень прорисовки проделывала за вас много работы, чтобы скрыть нижележащую инфраструктуру DirectX. Контекст Direct2D-устройства по-прежнему доставляет мишени результаты команд рисования, но сама мишень больше не является HWND — это Direct2D-изображение. Данное изображение — абстракция, которая на самом деле может быть битовой картой Direct2D, DXGI-поверхностью или даже списком команд, которые должны воспроизводиться в каком-то другом контексте.

Первым делом нужно создать объект Direct3D-устройства. Direct3D определяет устройство как нечто, что выделяет ресурсы, выполняет рендеринг примитивов и взаимодействует с нижележащим аппаратным обеспечением графики. Это устройство состоит из объекта устройства для управления ресурсами и объекта контекста устройства для рендеринга этих ресурсов. Функция D3D11CreateDevice создает устройство, дополнительно возвращая указатели на объект устройства и объект контекста устройства. Как это может выглядеть, показано на рис. 3. Я не хочу задерживать ваше внимание на мелких деталях Direct3D, поэтому не стану подробно описывать каждый параметр, а сосредоточусь на том, что относится к Direct2D.

Рис. 3. Создание Direct3D-устройства

HRESULT CreateDevice(D3D_DRIVER_TYPE const type, 
  ComPtr<ID3D11Device> & device)
{
  UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
  #ifdef _DEBUG
  flags |= D3D11_CREATE_DEVICE_DEBUG;
  #endif
  return D3D11CreateDevice(nullptr,
                           type,
                           nullptr,
                           flags,
                           nullptr, 0,
                           D3D11_SDK_VERSION,
                           device.GetAddressOf(),
                           nullptr,
                           nullptr);
}

Второй параметр этой функции указывает тип устройства, который вы хотите создать. Константа D3D_DRIVER_TYPE_HARDWARE сообщает, что устройство должно поддерживаться GPU для аппаратного ускорения рендеринга. Если GPU недоступен, можно использовать константу D3D_DRIVER_TYPE_WARP. WARP — это высокопроизводительное программное средство прорисовки и отличный механизм на случай отсутствия аппаратного ускорения. Вы не должны полагаться на доступность GPU, особенно если собираетесь выполнять или тестировать свое программное обеспечение в ограниченных средах вроде виртуальных машин Hyper-V. Вот как можно было бы использовать функцию с рис. 3 для создания Direct3D-устройства:

ComPtr<ID3D11Device> d3device;
auto hr = CreateDevice(D3D_DRIVER_TYPE_HARDWARE, d3device);
if (DXGI_ERROR_UNSUPPORTED == hr)
{
  hr = CreateDevice(D3D_DRIVER_TYPE_WARP, d3device);
}
HR(hr);

Рис. 3 также иллюстрирует, как можно включить отладочный уровень Direct3D для отладочных сборок. Одна из проблем, которой стоит опасаться, заключается в том, что функция D3D11CreateDevice будет загадочным образом терпеть неудачу, если отладочный уровень на самом деле не установлен. Это не будет проблемой на вашем компьютере для разработки, так как Visual Studio установит его вместе с Windows SDK. Но, если вы скопируете отладочную сборку на тестовый компьютер, вы можете столкнуться с этой проблемой. Поведение этой функции отличается от такового у функции D2D1CreateFactory, которая будет завершаться успешно, даже если отладочный уровень Direct2D отсутствует.

Создание цепочки замен

Следующий этап — создание цепочки замен DXGI для HWND приложения. Цепочка замен (swap chain) — это набор буферов, используемых для отображения кадров пользователю. Обычно в этой цепочке два буфера, часто называемых буфером видимого кадра (front buffer) и буфером невидимого кадра (back buffer) соответственно. GPU показывает изображение, хранящееся в буфере видимого кадра, пока приложение осуществляет рендеринг в буфер невидимого кадра. Когда этот рендеринг завершается, приложение сообщает DXGI вывести буфер невидимого кадра, и эта процедура в основном сводится к замене указателей на буферы видимого и невидимого кадров. Тем самым GPU может вывести новый кадр, а приложение заняться рендерингом следующего кадра. Это, конечно, существенное упрощение, но на данном этапе это все, что вам нужно знать.

Чтобы создать цепочку замен, сначала надо получить фабрику DXGI и ее интерфейс IDXGIFactory2. Для этого можно вызвать функцию CreateDXGIFactory1, но, учитывая, что вы только что создали объект Direct3D-устройства, то же самое можно сделать, используя объектную модель DirectX. Перед этим вы должны запросить интерфейс IDXGIDevice объекта устройства:

ComPtr<IDXGIDevice> dxdevice;
HR(d3device.As(&dxdevice));

Далее нужно получить видеоадаптер для устройства — виртуальный или какой-то другой:

ComPtr<IDXGIAdapter> adapter;
HR(dxdevice->GetAdapter(adapter.GetAddressOf()));

Родительским объектом этого адаптера является фабрика DXGI:

ComPtr<IDXGIFactory2> factory;
HR(adapter->GetParent(__uuidof(factory),
                      reinterpret_cast<void **>(factory.GetAddressOf())));

Это может показаться куда большей работой по сравнению с простым вызовом функции CreateDXGIFactory1, но через мгновение вам потребуется интерфейс IDXGIDevice, так что на самом деле это всего один дополнительный вызов.

Интерфейс IDXGIFactory2 предоставляет метод CreateSwapChainForHwnd для создания цепочки замен для данного HWND. Прежде чем его вызывать, нужно подготовить структуру DXGI_SWAP_CHAIN_DESC1, описывающую структуру конкретной цепочки замен и желательное поведение. При инициализации этой структуры необходимо соблюдать некоторую осторожность, так как именно этим в наибольшей степени различаются платформы, где есть Direct2D. Вот как это могло бы выглядеть:

DXGI_SWAP_CHAIN_DESC1 props = {};
props.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
props.SampleDesc.Count = 1;
props.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
props.BufferCount = 2;

Член Format описывает нужный формат для буферов цепочки замен. Я выбрал 32-битный формат, где на каждый цветовой канал и альфа-компонент отводится по 8 бит. В целом, это обеспечивает максимальную производительность и совместимость с различными устройствами и API. Член SampleDesc влияет на качество Direct3D-изображения, поскольку он относится к сглаживанию (antialiasing). Как правило, вы предпочтете, чтобы сглаживание обрабатывалось Direct2D, поэтому данная конфигурация просто сообщает Direct3D ничего не делать. Член BufferUsage описывает, как будут использоваться буферы цепочки замен, и позволяет DirectX оптимизировать управление памятью. В нашем случае я указываю, что буфер будет использоваться только для рендеринга вывода на экран. То есть буфер будет недоступен процессору (CPU), но в результате производительность резко увеличится. Член BufferCount сообщает, сколько буферов будет содержаться в цепочке замен. Обычно для экономии памяти используют не более двух буферов, но их может быть больше при чрезвычайно быстром рендеринге (хотя это редкость). Например, для экономии памяти Windows Phone 8 позволяет использовать лишь один буфер. Существуют и другие члены структуры цепочки замен, но для настольного окна требуются только перечисленные мной. Теперь создадим цепочку замен:

HR(factory->CreateSwapChainForHwnd(d3device.Get(),
                                   m_hWnd,
                                   &props,
                                   nullptr,
                                   nullptr,
                                   m_swapChain.GetAddressOf()));

Первый параметр указывает Direct3D-устройству, где будут выделены ресурсы для цепочки замен, а второй — это целевой HWND, где будет отображаться буфер видимого кадра. Если все проходит успешно, создается цепочка замен. Следует учитывать, что с данным HWND можно сопоставить только одну цепочку замен. Кому-то это может показаться очевидным, а кому-то — нет, но помнить об этом нужно. Если данный метод завершается неудачей, то скорее всего вы освободили аппаратно-зависимые ресурсы перед повторным созданием устройства после события его потери.

Создание Direct2D-устройства

Следующий шаг — создание Direct2D-устройства. Модель Direct2D 1.1 ближе к Direct3D в том плане, что вместо простой мишени прорисовки теперь имеется как устройство, так и его контекст. Устройство является объектом Direct2D-ресурса, связанным с конкретным Direct3D-устройством. Как и Direct3D-устройство, он действует в роли диспетчера ресурсов. В отличие от контекста Direct3D-устройства, который вы можете спокойно игнорировать, контекст Direct2D-устройства — мишени прорисовки, которая предоставляет необходимые вам команды рисования. Первый делом используйте фабрику Direct2D для создания устройства, которое связывается с нижележащим Direct3D-устройством через DXGI-интерфейс:

ComPtr<ID2D1Device> device;
HR(m_factory->CreateDevice(dxdevice.Get(),
                           device.GetAddressOf()));

Это устройство представляет видеоадаптер в Direct2D. Обычно я не храню указатель на ID2D1Device, так как это значительно упрощает обработку потери устройства и изменение размеров буферов в цепочке замен. Реально указатель требуется только для создания контекста Direct2D-устройства:

HR(device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
                               m_target.GetAddressOf()));

Теперь у вас есть Direct2D-мишень прорисовки, с помощью которой можно, как обычно, собирать команды рисования в пакеты, но, прежде чем вы сможете это делать, предстоит сделать еще один шаг. В отличие от HWND-мишени прорисовки, позволяющей работать только с окном, для которого она была создана, контекст устройства может переключать мишени в период выполнения и изначально вообще не имеет назначенной мишени.

Подключение контекста устройства и цепочки замен

На следующем этапе буфер невидимого кадра цепочки замен назначается в качестве мишени для контекста Direct2D-устройства. Метод GetBuffer цепочки замен вернет буфер невидимого кадра как DXGI-поверхность:

ComPtr<IDXGISurface> surface;
HR(m_swapChain->GetBuffer(0, // buffer index
             __uuidof(surface),
             reinterpret_cast<void **>(surface.GetAddressOf())));

Теперь вы можете использовать метод CreateBitmapFromDxgiSurface контекста устройства для создания битовой карты Direct2D, представляющей DXGI-поверхность, но сначала нужно описать формат и предполагаемое применение этой битовой карты. Вы определяете свойства битовой карты следующим образом:

auto props = BitmapProperties1(
  D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
  PixelFormat(
    DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE));

Константа D2D1_BITMAP_OPTIONS_TARGET указывает, что битовая карта будет использоваться как мишень контекста устройства. Константа D2D1_BITMAP_OPTIONS_CANNOT_DRAW относится к атрибуту DXGI_USAGE_RENDER_TARGET_OUTPUT цепочки замен, сообщающему, что мишень может применяться только для вывода, но не для ввода в другие операции рисования. PixelFormat просто описывает для Direct2D, что представляет собой нижележащий буфер цепочки замен. Теперь вы можете создать битовую карту Direct2D так, чтобы она указывала на буфер невидимого кадра цепочки замен, а контекст устройства указывал на эту битовую карту:

ComPtr<ID2D1Bitmap1> bitmap;
HR(m_target->CreateBitmapFromDxgiSurface(surface.Get(),
                                         props,
                                         bitmap.GetAddressOf()));
m_target->SetTarget(bitmap.Get());

Наконец, надо сообщить Direct2D, как масштабировать его логическую систему координат на физическом экране, охватываемом DXGI-поверхностью:

float dpiX, dpiY;
m_factory->GetDesktopDpi(&dpiX, &dpiY);
m_target->SetDpi(dpiX, dpiY);

Рендеринг

Метод Render класса DesktopWindow действует как катализатор при управлении Direct2D-устройством. На рис. 4 показан базовый код метода Render.

Рис. 4. Метод Render класса DesktopWindow

void Render()
{
  if (!m_target)
  {
    CreateDevice();
    CreateDeviceSwapChainBitmap();
  }
  m_target->BeginDraw();
  static_cast<T *>(this)->Draw();
  m_target->EndDraw();
  auto const hr = m_swapChain->Present(1, 0);
  if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
  {
    ReleaseDevice();
  }
}

Как и в случае HWND-мишени прорисовки, метод Render сначала проверяет, нужно ли создать мишень прорисовки. Метод CreateDevice включает создание Direct3D-устройства, цепочки замен DXGI и контекста Direct2D-устройства. Метод CreateDeviceSwapChainBitmap содержит код для подключения цепочки замен к контексту устройства посредством битовой карты Direct2D, поддерживаемой DXGI-поверхностью. Битовая карта хранится отдельно, так как она нужна при изменении размеров окна.

Затем метод Render следует обычному шаблону, согласно которому команды рисования заключаются в блок из парных методов BeginDraw и EndDraw. Заметьте, что я не проверяю результат метода EndDraw. Метод EndDraw контекста устройства — в отличие от HWND-мишени прорисовки — не представляет только что прорисованный кадр на экране. Вместо этого он просто завершает рендеринг целевой битовой карты. Ее вывод на экран возлагается на метод Present цепочки замен, и именно в этот момент можно обрабатывать любые проблемы с рендерингом и отображением.

Поскольку для этого окна я использую простую модель рендеринга, управляемую событиями, отображение осуществляется весьма прямолинейно. Если бы я применял цикл анимации для рендеринга с синхронизацией с частотой обновления экрана, все стало бы гораздо сложнее, но об этом варианте мы поговорим в одной из будущих статей. В данном случае мы имеем дело с тремя сценариями. В идеале, Present возвращает S_OK, и все в порядке. В качестве альтернативы Present может вернуть DXGI_STATUS_OCCLUDED, указывая, что окно перекрыто другими окнами, т. е. оно не видимо. Это происходит все реже, так как композиция рабочего стола полагается на то, что представление окна остается активным. Однако одной из причин перекрытия может быть переключение активного рабочего стола. Чаще всего это бывает, если на экране появляется приглашение User Account Control (UAC) или если вы нажимаете Ctlr+Alt+Del для переключения пользователей или блокировки компьютера. В любом случае перекрытие не означает, что возникает сбой, поэтому вам не нужно ничего делать, кроме, возможно, исключения лишних вызовов для визуализации. Если Present завершается неудачей по какой-либо причине, тогда логично предположить, что нижележащее Direct3D-устройство было потеряно и его надо создать заново. Метод ReleaseDevice класса DesktopWindow мог бы выглядеть так:

void ReleaseDevice()
{
  m_target.Reset();
  m_swapChain.Reset();
  static_cast<T *>(this)->ReleaseDeviceResources();
}

Здесь вы наверняка начали понимать, почему я избегаю хранения любых ненужных указателей на интерфейсы. Каждый указатель на ресурс представляет прямую или косвенную ссылку на нижележащее устройство. Чтобы корректно заново создать устройство, каждый из этих указателей должен быть освобожден. Как минимум, вы должны располагать указателями на мишень прорисовки (чтобы можно было выдавать команды рисования) и на цепочку замен (чтобы выводить кадр на экран). С этим связана последняя часть головоломки — метод ResizeSwapChainBitmap, на который я ссылаюсь в обработчике сообщения WM_SIZE.

В случае HWND-мишени прорисовки это было просто благодаря ее методу Resize. Поскольку теперь вы имеете дело с цепочкой замен, на вас возлагается обязанность соответствующего изменения размеров буферов этой цепочки. Конечно, это не удастся сделать, пока не будут освобождены все ссылки на эти буферы. И если вы не удерживаете никаких прямых ссылок на них, все достаточно просто. Как это делается, показано на рис. 5.

Рис. 5. Изменение размеров буферов в цепочке замен

void ResizeSwapChainBitmap()
{
  m_target->SetTarget(nullptr);
  if (S_OK == m_swapChain->ResizeBuffers(0,
                                         0, 0,
                                         DXGI_FORMAT_UNKNOWN,
                                         0))
  {
    CreateDeviceSwapChainBitmap();
  }
  else
  {
    ReleaseDevice();
  }
}

Здесь единственная ссылка на буфер невидимого кадра цепочки замен, хранящаяся DesktopWindow, является косвенной ссылкой, удерживаемой мишенью прорисовки контекста устройства. Устанавливая ее в nullptr, вы освобождаете ссылку, и метод ResizeBuffers успешно выполняется. Другие параметры просто сообщают цепочке замен изменить размеры буферов в соответствии с новым размером окна и сохранить остальное в том виде, в каком оно было. Предполагая, что ResizeBuffers будет выполнен успешно, я просто вызываю метод CreateDeviceSwapChainBitmap для создания новой битовой карты Direct2D для цепочки замен и подключаю ее к контексту Direct2D-устройства. Если что-то пойдет не так, я освобожу устройство и все его ресурсы; метод Render позаботится о повторном создании всего необходимого, когда это потребуется.

Теперь у вас есть все, что нужно для рендеринга в окне рабочего стола с помощью Direct2D 1.1! И это все, что я хотел сказать в этой статье. Присоединяйтесь ко мне в следующий раз для дальнейшего исследования Direct2D.


Кенни Керр (Kenny Kerr) — высококвалифицированный программист. Живет в Канаде. Автор учебных курсов для Pluralsight, обладатель звания Microsoft MVP. Ведет блог kennykerr.ca. Кроме того, читайте его заметки в twitter.com/kennykerr.

Выражаю благодарность за рецензирование статьи эксперту Ворачаи Чаовеерапраситу (Worachai Chaoweeraprasit).