Windows и C++

Слоистые окна в Direct2D

Кенни Керр

В своей третьей статье по Direct2D я намерен продемонстрировать некоторые из его непревзойденных возможностей, касающихся взаимодействия. Вместо занудного детального описания всех вариантов взаимодействия, поддерживаемых Direct2D, я покажу эти возможности на практическом примере: слоистых окнах. Слоистые окна (layered windows) — один из тех механизмов Windows, которые были введены сравнительно давно, но развивались очень слабо и из-за этого требуют особых усилий для эффективного использования с современными технологиями графики.

В данной статье я исхожу из того, что вы знакомы с базовыми концепциями программирования с применением Direct2D. В ином случае советую прочесть две мои предыдущие статьи (msdn.microsoft.com/magazine/dd861344) и (msdn.microsoft.com/magazine/ee413543), в которых были изложены фундаментальные основы программирования и рисования с помощью Direct2D.

Изначально слоистые окна служили другим целям. В частности, с их помощью можно было легко и эффективно создавать визуальные эффекты и обеспечивать визуализацию без мерцаний. Во времена, когда GDI был доминирующим средством создания графики, это было реальным преимуществом. Однако в современных компьютерах, оборудованных видеокартами с ускорителями графики, этот вариант больше не конкурентоспособен, так как слоистые окна так и остались в мире User32/GDI и в них не вносилось никаких существенных обновлений для поддержки DirectX — платформы Microsoft для высокопроизводительной работы с высококачественной графикой.

Тем не менее слоистые окна предоставляют уникальную возможность формировать окно с использованием смешивания по альфа-каналу для каждого пиксела, чего нельзя добиться никакими другими средствами Windows SDK.

Здесь следует упомянуть, что на самом деле существует два типа слоистых окон. Разница между ними сводится к тому, какой вид контроля за прозрачностью вам нужен — индивидуально для каждого пиксела или окна в целом. В этой статье рассматривается первый тип слоистых окон, но если вам достаточно контроля прозрачности всего окна, просто вызывайте функцию SetLayeredWindowAttributes после создания окна для установки альфа-значения:

Verify(SetLayeredWindowAttributes(
  windowHandle,
  0, // no color key
  180, // alpha value
  LWA_ALPHA));

Это предполагает, что вы создаете окно с расширенным стилем WS_EX_LAYERED или применяете его после вызова функции SetWindowLong. Пример такого окна показан на рис. 1. Преимущество должно быть очевидно: вам не нужно ничего менять в том, как ваше приложение рисует окно, так как Desktop Window Manager (DWM) будет автоматически смешивать цвета окна соответствующим образом. С другой стороны, вы должны рисовать абсолютно все самостоятельно. Но это не проблема, если вы используете совершенно новую технологию рендеринга вроде Direct2D!

Окно с альфа-значением

Рис. 1. Окно с альфа-значением

Так что же нам потребуется? Ну, на фундаментальном уровне все достаточно прямолинейно. Во-первых, нужно будет заполнить структуру UPDATELAYEREDWINDOWINFO. Эта структура содержит позицию и размер слоистого окна, а также контекст устройства GDI (device context, DC), который определяет поверхность окна, — и вот тут-то кроется одна проблема. Контексты устройств относятся к старому миру GDI и далеки от мира DirectX и аппаратного ускорения графики. Подробнее об этом поговорим чуть позже.

Помимо того, что вам придется иметь дело с кучей указателей на создаваемые самостоятельно структуры, структура UPDATELAYEREDWINDOWINFO не полностью документирована в Windows SDK, что затрудняет ее применение. Всего вам потребуется создать пять структур. Одну — для задания позиции источника (source position), определяющей адрес битовой карты, которую нужно копировать из DC. Вторую — для задания позиции окна, идентифицирующей, где окно будет размещено на рабочем столе после обновления. Третью — для указания размера копируемой битовой карты; эта же структура определяет размер окна:

POINT sourcePosition = {};
POINT windowPosition = {};
SIZE size = { 600, 400 };

Далее создается и заполняется структура BLENDFUNCTION, которая определяет, как цвета слоистого окна будут смешиваться с цветами рабочего стола. Это удивительно гибкая структура, которую часто упускают из виду, но она может быть весьма полезна. Обычно она заполняется так:

BLENDFUNCTION blend = {};
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;

Константа AC_SRC_ALPHA просто указывает, что в исходной битовой карте имеется альфа-канал; это наиболее распространенный случай.

Но поле SourceConstantAlpha интересно тем, что вы можете использовать его во многом аналогично тому, как функция SetLayeredWindowAttributes управляет прозрачностью окна в целом. Когда ему присвоено значение 255, слоистое окно будет просто использовать альфа-значения, индивидуальные для каждого пиксела, но вы можете плавно уменьшать его до нулевого значения (полной прозрачности), чтобы создавать такие эффекты, как медленно проявление окна на экране или его постепенное исчезновение, без всякой перерисовки. Теперь должно быть очевидно, почему структура BLENDFUNCTION названа именно так: получаемое в итоге окно со смешением цветов по альфа-каналу является функцией этого значения структуры.

И последняя структура — UPDATELAYEREDWINDOWINFO — связывает все воедино:

UPDATELAYEREDWINDOWINFO info = {};
info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
info.pptSrc = &sourcePosition;
info.pptDst = &windowPosition;
info.psize = &size;
info.pblend = &blend;
info.dwFlags = ULW_ALPHA;

Теперь вам должно быть достаточно ясно предназначение всех полей, кроме единственного недокументированного поля dwFlags. Его значение ULW_ALPHA, которое наверняка покажется вам знакомым, если вы в свое время пользовались устаревшей функцией UpdateLayeredWindow, просто указывает, что следует задействовать функцию смешивания по альфа-каналу.

Наконец, чтобы обновить окно, вы должны вызвать функцию UpdateLayeredWindowIndirect и передать ей описатель DC источника:

info.hdcSrc = sourceDC;

Verify(UpdateLayeredWindowIndirect(
  windowHandle, &info));

Вот и все. Это окно не будет принимать никаких сообщений WM_PAINT. В любой момент, когда вам понадобится показать или обновить окно, просто вызывайте функцию UpdateLayeredWindowIndirect. Чтобы не путаться со всем этим стереотипным кодом, поместим его в класс-оболочку LayeredWindowInfo (рис. 2) и в дальнейшем будем использовать его.

Рис. 2. Класс-оболочка LayeredWindowInfo

class LayeredWindowInfo {
  const POINT m_sourcePosition;
  POINT m_windowPosition;
  CSize m_size;
  BLENDFUNCTION m_blend;
  UPDATELAYEREDWINDOWINFO m_info;

public:

  LayeredWindowInfo(
    __in UINT width,
    __in UINT height) :
    m_sourcePosition(),
    m_windowPosition(),
    m_size(width, height),
    m_blend(),
    m_info() {

      m_info.cbSize = sizeof(UPDATELAYEREDWINDOWINFO);
      m_info.pptSrc = &m_sourcePosition;
      m_info.pptDst = &m_windowPosition;
      m_info.psize = &m_size;
      m_info.pblend = &m_blend;
      m_info.dwFlags = ULW_ALPHA;

      m_blend.SourceConstantAlpha = 255;
      m_blend.AlphaFormat = AC_SRC_ALPHA;
    }

  void Update(
    __in HWND window,
    __in HDC source) {

    m_info.hdcSrc = source;

    Verify(UpdateLayeredWindowIndirect(window, &m_info));
  }

  UINT GetWidth() const { return m_size.cx; }

  UINT GetHeight() const { return m_size.cy; }
};

На рис. 3 показан базовый скелетный код для слоистого окна, где используются ATL/WTL и класс-оболочка LayeredWindowInfo с рис. 2. Первым делом отметим, что здесь нет нужды вызывать UpdateWindow, поскольку этот код не работает с сообщениями WM_PAINT. Вместо этого он немедленно вызывает метод Render, который в свою очередь требуется для выполнения операций рисования и для передачи DC методу Update класса LayeredWindowInfo. А вот как происходит рисование и откуда берется DC это уже вопрос, куда более интересный.

Рис. 3. Скелетный код слоистого окна

class LayeredWindow :
  public CWindowImpl<LayeredWindow, 
  CWindow, CWinTraits<WS_POPUP, WS_EX_LAYERED>> {

  LayeredWindowInfo m_info;

public:

  BEGIN_MSG_MAP(LayeredWindow)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()

  LayeredWindow() :
    m_info(600, 400) {

    Verify(0 != __super::Create(0)); // parent
    ShowWindow(SW_SHOW);
    Render();
  }

  void Render() {
    // Do some drawing here

    m_info.Update(m_hWnd,
      /* source DC goes here */);
  }

  void OnDestroy() {
    PostQuitMessage(1);
  }
};

Если идти по пути GDI/GDI+

Сначала рассмотрим, как это делается в GDI/GDI+. Во-первых, вам нужно создать битовую карту с 32 битами на пиксел с предварительным умножением, которая использует байтовый порядок цветового канала «blue-green-red-alpha» (BGRA). Предварительное умножение означает лишь то, что значения цветового канала заранее умножаются на альфа-значение. В целом это ускоряет процесс смешивания изображений по альфа-каналу, но требует от вас для получения реальных значений цветов делить эти значения на альфа-значение. В терминологии GDI такая битовая карта называется аппаратно независимой (device-independent bitmap, DIB) с 32 битами на пиксел (bpp) и создается заполнением структуры BITMAPINFO с последующей передачей функции CreateDIBSection (рис. 4).

Рис. 4. Создание DIB

BITMAPINFO bitmapInfo = {};
bitmapInfo.bmiHeader.biSize = 
  sizeof(bitmapInfo.bmiHeader);
bitmapInfo.bmiHeader.biWidth = 
  m_info.GetWidth();
bitmapInfo.bmiHeader.biHeight = 
  0 – m_info.GetHeight();
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = 
  BI_RGB;

void* bits = 0;

CBitmap bitmap(CreateDIBSection(
  0, // no DC palette
  &bitmapInfo,
  DIB_RGB_COLORS,
  &bits,
  0, // no file mapping object
  0)); // no file offset

Здесь масса всяких деталей, но они не относятся к нашему обсуждению. Эта API-функция уходит своими корнями в далекое прошлое. А вот на что стоит обратить внимание, так это на отрицательную высоту, указанную мной для битовой карты. Структура BITMAPINFOHEADER определяет битовую карту, размещаемую либо снизу вверх, либо сверху вниз. Если высота положительная, вы получите в итоге битовую карту первого вида, а если она отрицательная, — второго. У битовых карт первого вида начало координат находится в нижнем левом углу, тогда как у битовых карт второго вида — в верхнем левом.

Хотя в данном случае в этом нет особой нужды, я предпочитаю битовые карты, размещаемые сверху вниз, так как именно такой формат используется большинством современных Windows-компонентов, связанных с обработкой изображений, а значит, взаимодействие с ними упрощается. Это также позволяет использовать положительный шаг приращения, который можно вычислять так:

UINT stride = (width * 32 + 31) / 32 * 4;

К этому моменту у вас достаточно информации, чтобы начать рисовать в битовой карте, используя указатель на биты. Конечно, если вы еще не спятили, то наверняка предпочтете использовать какие-то функции рисования, но, увы, большинство функций, предоставляемых GDI, не поддерживает альфа-канал. Вот тут-то и поможет GDI+.

Хотя вы могли бы передавать данные битовой карты напрямую в GDI+, давайте вместо этого создадим DC для нее, поскольку он все равно понадобится для передачи в функцию UpdateLayeredWindowIndirect. Чтобы создать DC, вызовите функцию CreateCompatibleDC; она создает DC в памяти, который совместим с рабочим столом. Затем вы можете вызвать функцию SelectObject для выбора растровой карты в DC. Все эти и другие операции мы обернем в класс-оболочку GdiBitmap (рис. 5).

Рис. 5. Класс-оболочка GdiBitmap

class GdiBitmap {
  const UINT m_width;
  const UINT m_height;
  const UINT m_stride;
  void* m_bits;
  HBITMAP m_oldBitmap;

  CDC m_dc;
  CBitmap m_bitmap;

public:

  GdiBitmap(__in UINT width,
            __in UINT height) :
    m_width(width),
    m_height(height),
    m_stride((width * 32 + 31) / 32 * 4),
    m_bits(0),
    m_oldBitmap(0) {

    BITMAPINFO bitmapInfo = { };
    bitmapInfo.bmiHeader.biSize = 
      sizeof(bitmapInfo.bmiHeader);
    bitmapInfo.bmiHeader.biWidth = 
      width;
    bitmapInfo.bmiHeader.biHeight = 
      0 - height;
    bitmapInfo.bmiHeader.biPlanes = 1;
    bitmapInfo.bmiHeader.biBitCount = 32;
    bitmapInfo.bmiHeader.biCompression = 
      BI_RGB;

    m_bitmap.Attach(CreateDIBSection(
      0, // device context
      &bitmapInfo,
      DIB_RGB_COLORS,
      &m_bits,
      0, // file mapping object
      0)); // file offset
    if (0 == m_bits) {
      throw bad_alloc();
    }

    if (0 == m_dc.CreateCompatibleDC()) {
      throw bad_alloc();
    }

    m_oldBitmap = m_dc.SelectBitmap(m_bitmap);
  }

  ~GdiBitmap() {
    m_dc.SelectBitmap(m_oldBitmap);
  }

  UINT GetWidth() const {
    return m_width;
  }

  UINT GetHeight() const {
    return m_height;
  }

  UINT GetStride() const {
    return m_stride;
  }

  void* GetBits() const {
    return m_bits;
  }

  HDC GetDC() const {
    return m_dc;
  }
};

Класс Graphics в GDI+, который содержит методы для рисования в каком-либо устройстве, можно сконструировать с DC битовой карты. На рис. 6 показано, как обновить класс LayeredWindow с рис. 3 для поддержки рендеринга с помощью GDI+. Как только весь стереотипный GDI-код выносится в отдельный класс, остальное уже не столь сложно. Размер окна передается конструктору GdiBitmap, а DC битовой карты — конструктору Graphics и методу Update. Однако учтите, что ни GDI, ни GDI+ не ускоряются на аппаратном уровне (по большей части), а также что в них нет особо мощной функциональности рендеринга.

Рис. 6. Слоистое окно в GDI

class LayeredWindow :
  public CWindowImpl< ... {

  LayeredWindowInfo m_info;
  GdiBitmap m_bitmap;
  Graphics m_graphics;

public:
  LayeredWindow() :
    m_info(600, 400),
    m_bitmap(m_info.GetWidth(), m_info.GetHeight()),
    m_graphics(m_bitmap.GetDC()) {
    ...
  }

  void Render() {
    // Do some drawing with m_graphics object

    m_info.Update(m_hWnd,
      m_bitmap.GetDC());
  }
...

Проблема архитектуры

Для контраста взгляните, что требуется для создания слоистого окна в Windows Presentation Foundation (WPF):

class LayeredWindow : Window {
  public LayeredWindow() {
    WindowStyle = WindowStyle.None;
    AllowsTransparency = true;

    // Do some drawing here
  }
}

Здесь скрыты все сложности и архитектурные ограничения использования слоистых окон. Но прячь — не прячь, а слоистые окна подчиняются архитектурным принципам, уже обрисованным в этой статье. Хотя WPF позволяет задействовать аппаратное ускорение для рендеринга, результаты все равно нужно копировать в предварительно умноженную BGRA-битовую карту, выбираемую в совместимый DC до обновления экрана вызовом функции UpdateLayeredWindowIndirect. Так как WPF в данном случае не предоставляет ничего, кроме булевой переменной, эта инфраструктура должны принимать определенные решения, контроля над которыми у вас нет. Почему это важно? Из-за аппаратного обеспечения.

Графический процессор (GPU) для достижения максимальной производительности предпочитает работать со специализированной выделенной памятью. То есть, если вы хотите манипулировать существующей битовой картой, ее нужно скопировать из системной (оперативной) памяти в видеопамять, а этот процесс протекает гораздо медленнее, чем копирование между двумя участками в оперативной памяти. Верно и обратное: если вы создаете и выполняете рендеринг битовой карты с использованием GPU, а потом решаете скопировать ее в оперативную память, то такая операция окажется весьма дорогостоящей.

В обычных ситуациях этого происходить не должно, поскольку битовые карты, рендеринг которых выполняется GPU, как правило, посылаются напрямую устройству отображения. В случае слоистых окон битовая карта должна быть передана обратно в оперативную память, потому что User32/GDI манипулирует ресурсами как режима ядра, так и пользовательского режима, которым нужен доступ к этой битовой карте. Например, User32 нужно проверять слоистые окна на попадание в их границы курсора мыши (hit test). Такая проверка слоистого окна основывается на альфа-значениях битовой карты, при этом сообщения от мыши пропускаются, если пиксел в конкретной точке прозрачен. Из-за этого в оперативной памяти нужна копия битовой карты. После ее копирования с помощью UpdateLayeredWindowIndirect она посылается обратно GPU, чтобы DWM мог выполнить композицию рабочего стола.

Помимо издержек копирования памяти туда-сюда, синхронизация GPU с CPU обойдется тоже весьма дорого. В отличие от типичных операций, завязанных на CPU, все операции GPU обычно выполняются асинхронно, что обеспечивает высокую производительность за счет пакетирования потока команд рендеринга. Всякий раз, когда приходится пересекаться с CPU, тот вызывает сброс пакетных команд и ждет освобождения GPU, что ведет к падению производительности.

Все это означает, что нужно быть внимательным к передаче данных между разными типами памяти, учитывать частоту таких передач и издержки, которые они влекут за собой. Если сцены, подлежащие рендерингу, достаточно сложны, производительность за счет аппаратного ускорения может легко перевесить издержки копирования битовых карт. С другой стороны, если рендеринг не требует особых ресурсов и может быть выполнен обычным процессором (CPU), то вполне вероятно, что отказ от аппаратного ускорения в этом случае даст более высокую производительность. Принять решение в конкретной ситуации очень не просто. Некоторые GPU даже не имеют специализированной памяти и вместо нее используют часть системной памяти, что уменьшает издержки копирования.

Подвох в том, что ни GDI, ни WPF не дают вам выбора. В случае GDI вы привязаны к CPU, а в случае WPF вы вынуждены принимать то, что решает за вас WPF, а обычно эта инфраструктура выбирает аппаратное ускорение через Direct3D.

И тут на сцену выходит Direct2D.

Direct2D и GDI/DC

Direct2D рассчитан на рендеринг в любую выбранную вами мишень. Если это окно или Direct3D-текстура, Direct2D выполняет рендеринг напрямую с помощью GPU без всякого копирования. Если же это битовая карта Windows Imaging Component (WIC), Direct2D напрямую использует обычный процессор. Тогда как WPF всегда стремится переложить максимум работы на GPU и в крайнем случае использует программный растеризатор, Direct2D предоставляет вам лучшее из двух миров с применением прямого режима рендеринга на GPU, чтобы задействовать аппаратное ускорение, и высоко оптимизированный рендеринг на CPU, если GPU недоступен или если его использование нежелательно.

Как вы, видимо, догадываетесь, есть немало способов рендеринга слоистого окна с помощью Direct2D. Рассмотрим некоторые из них и обратим внимание на рекомендуемые подходы в зависимости от того, хотите ли вы использовать аппаратное ускорение.

Во-первых, вы могли бы просто вырезать GDI+-класс Graphics (рис. 3) и заменить его мишенью рендеринга Direct2D DC. Это имело бы смысл при наличии устаревшего приложения, интенсивно использующего GDI, но такое решение явно не самое лучшее. Direct2D — вместо рендеринга непосредственно в DC — сначала выполняет рендеринг во внутреннюю битовую карту WIC, а затем копирует результат в DC. Хотя этот вариант быстрее, чем при использовании GDI+, он все равно включает дополнительную операцию копирования, без которой можно было бы обойтись, если бы вам не требовался DC для рендеринга.

Чтобы применить этот подход, инициализируйте структуру D2D1_RENDER_TARGET_PROPERTIES. Она сообщает Direct2D формат битовой карты, который будет использоваться для мишени рендеринга. Вспомните, что это должен быть пиксельный формат BGRA с предварительным умножением. Он выражается структурой D2D1_PIXEL_FORMAT и может быть определен так:

const D2D1_PIXEL_FORMAT format = 
  D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM,
  D2D1_ALPHA_MODE_PREMULTIPLIED);

const D2D1_RENDER_TARGET_PROPERTIES properties = 
  D2D1::RenderTargetProperties(
  D2D1_RENDER_TARGET_TYPE_DEFAULT,
  format);

Теперь вы можете создать мишень рендеринга DC с применением Direct2D-объекта фабрики:

CComPtr<ID2D1DCRenderTarget> target;

Verify(factory->CreateDCRenderTarget(
  &properties,
  &target));

Наконец, вам нужно сообщить мишени рендеринга, в какой DC посылать команды рисования:

const RECT rect = {0, 0, bitmap.GetWidth(), bitmap.GetHeight()};

Verify(target->BindDC(bitmap.GetDC(), &rect));

В этот момент вы можете рисовать с помощью Direct2D как обычно — между вызовами методов BeginDraw и EndDraw, — а затем вызвать метод Update, как это делалось и раньше с DC битовой карты. Метод EndDraw гарантирует, что все результаты рисования переданы связанному DC.

Direct2D и WIC

Теперь, если вы можете полностью отказаться от GDI DC и напрямую использовать битовую карту WIC, то можете добиться максимальной производительности без аппаратного ускорения. Этот подход требует начать с создания предварительно умноженной битовой карты BGRA напрямую средствами WIC:

CComPtr<IWICImagingFactory> factory;
Verify(factory.CoCreateInstance(
  CLSID_WICImagingFactory));

CComPtr<IWICBitmap> bitmap;

Verify(factory->CreateBitmap(
  m_info.GetWidth(),
  m_info.GetHeight(),
  GUID_WICPixelFormat32bppPBGRA,
  WICBitmapCacheOnLoad,
  &bitmap));

Далее вам нужно вновь инициализировать структуру D2D1_RENDER_TARGET_PROPERTIES во многом по аналогии с тем, как вы делали это раньше, но теперь потребуется также сообщить Direct2D, что мишень рендеринга должна быть совместимой с GDI:

const D2D1_PIXEL_FORMAT format = 
  D2D1::PixelFormat(
  DXGI_FORMAT_B8G8R8A8_UNORM,
  D2D1_ALPHA_MODE_PREMULTIPLIED);

const D2D1_RENDER_TARGET_PROPERTIES properties = 
  D2D1::RenderTargetProperties(
  D2D1_RENDER_TARGET_TYPE_DEFAULT,
  format,
  0.0f, // default dpi
  0.0f, // default dpi
  D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE);

После этого вы можете создать мишень рендеринга WIC с использованием Direct2D-объекта фабрики:

CComPtr<ID2D1RenderTarget> target;

Verify(factory->CreateWicBitmapRenderTarget(
  bitmap,
  properties,
  &target));

Но что именно делает D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE? Она подсказывает Direct2D, что вы будете запрашивать мишень рендеринга для интерфейса ID2D1GdiInteropRenderTarget:

CComPtr<ID2D1GdiInteropRenderTarget> interopTarget;
Verify(target.QueryInterface(&interopTarget));

Для упрощения и большей эффективности реализации будем считать, что запрос к этому интерфейсу всегда проходит успешно. Однако неудачей заканчиваются лишь те попытки, когда вы заранее не указываете, чего именно хотите.

В интерфейсе ID2D1GdiInteropRenderTarget всего два метода: GetDC и ReleaseDC. Чтобы добиться оптимизации для случаев, где применяется аппаратное ускорение, эти методы разрешается вызывать только между вызовами методов BeginDraw и EndDraw мишени рендеринга. GetDC очищает мишень рендеринга перед возвратом DC. Так методы этого interop-интерфейса должны вызываться парно, имеет смысл обернуть их в C++-класс, показанный на рис. 7.

Рис. 7. Класс-оболочка для мишени рендеринга DC

class RenderTargetDC {
  ID2D1GdiInteropRenderTarget* m_renderTarget;
  HDC m_dc;

public:
  RenderTargetDC(ID2D1GdiInteropRenderTarget* renderTarget) :
    m_renderTarget(renderTarget),
    m_dc(0) {

    Verify(m_renderTarget->GetDC(
      D2D1_DC_INITIALIZE_MODE_COPY,
      &m_dc));

  }

  ~RenderTargetDC() {
    RECT rect = {};
    m_renderTarget->ReleaseDC(&rect);
  }

  operator HDC() const {
    return m_dc;
  }
};

Метод Render окна теперь можно обновить для использования RenderTargetDC, как на рис. 8. Приятная особенность этого подхода в том, что весь код, специфичный для создания мишени рендеринга WIC, убирается в метод CreateDeviceResources. Далее я покажу, как создать мишень рендеринга Direct3D, чтобы задействовать преимущества аппаратного ускорения, но в любом случае метод Render, приведенный на рис. 8, останется одним и тем же. Это делает возможным сравнительно легко переключать реализации мишеней рендеринга в вашем приложении, не модифицируя код рисования.

Рис. 8. GDI-совместимый метод Render

void Render() {
  CreateDeviceResources();
  m_target->BeginDraw();
  // Do some drawing here
  {
    RenderTargetDC dc(m_interopTarget);
    m_info.Update(m_hWnd, dc);
  }

  const HRESULT hr = m_target->EndDraw();

  if (D2DERR_RECREATE_TARGET == hr) {
    DiscardDeviceResources();
  }
  else {
    Verify(hr);
  }
}

Direct2D и Direct3D/DXGI

Для аппаратно-ускоренного рендеринга нужно использовать Direct3D. Поскольку вы не выполняете рендеринг напрямую по HWND через ID2D1HwndRenderTarget, который обеспечил бы автоматическое применение аппаратного ускорения, необходимо самостоятельно создать Direct3D-устройство и подключить нижележащую DirectX Graphics Infrastructure (DXGI) — это позволит получить GDI-совместимые результаты.

DXGI — сравнительно новая подсистема, которая размещается на уровне ниже Direct3D, абстрагируя Direct3D от нижележащего оборудования и предоставляя высокопроизводительный шлюз для взаимодействия с другими подсистемами. Direct2D тоже использует преимущества этого нового API, что позволит упростить переход на будущие версии Direct3D. Чтобы применить этот подход, начните с создания Direct3D-устройства. Это устройство представляет GPU, который будет выполнять рендеринг. Ниже я использую Direct3D 10.1 API, как того требует Direct2D в настоящее время:

CComPtr<ID3D10Device1> device;

Verify(D3D10CreateDevice1(
  0, // adapter
  D3D10_DRIVER_TYPE_HARDWARE,
  0, // reserved
  D3D10_CREATE_DEVICE_BGRA_SUPPORT,
  D3D10_FEATURE_LEVEL_10_0,
  D3D10_1_SDK_VERSION,
  &device));

Флаг D3D10_CREATE_DEVICE_BGRA_SUPPORT крайне важен для поддержки возможностей взаимодействия Direct2D, а пиксельный формат BGRA теперь должен быть вам знаком. В традиционном Direct3D-приложении вы могли бы создать цепочку обмена (swap chain) и извлекать содержимое ее буфера невидимых поверхностей (back buffer) как текстуру, в которую осуществляется рендеринг, до вывода готового окна. Так как вы используете Direct3D только для рендеринга, а не для отображения, вы можете напрямую создать ресурс текстуры. Текстура — это Direct3D-ресурс для хранения текселей (элементов текстуры), которые являются Direct3D-эквивалентами пикселей. Хотя Direct3D поддерживает одно-, двух- и трехмерные текстуры, вам нужна лишь двухмерная, которая наиболее близко соответствует двухмерной поверхности (рис. 9).

Рис. 9 Двухмерная текстура

D3D10_TEXTURE2D_DESC description = {};
description.ArraySize = 1;
description.BindFlags = 
  D3D10_BIND_RENDER_TARGET;
description.Format = 
  DXGI_FORMAT_B8G8R8A8_UNORM;
description.Width = GetWidth();
description.Height = GetHeight();
description.MipLevels = 1;
description.SampleDesc.Count = 1;
description.MiscFlags = 
  D3D10_RESOURCE_MISC_GDI_COMPATIBLE;

CComPtr<ID3D10Texture2D> texture;

Verify(device->CreateTexture2D(
  &description,
  0, // no initial data
  &texture));

Создаваемая текстура описывается структурой D3D10_TEXTURE2D_DESC. Константа D3D10_BIND_RENDER_TARGET указывает, что текстура связывается как выходной буфер, или мишень рендеринга, конвейера Direct3D. Константа DXGI_FORMAT_B8G8R8A8_UNORM гарантирует, что Direct3D будет использовать корректный пиксельный формат для GDI. Наконец, константа D3D10_RESOURCE_MISC_GDI_COMPATIBLE сообщает нижележащей DXGI-поверхности предоставить GDI DC, через который будут получены результаты рендеринга. Direct2D доступен через интерфейс ID2D1GdiInteropRenderTarget, о котором я рассказывал в предыдущем разделе.

Как я упоминал, Direct2D способен выполнять рендеринг на Direct3D-поверхности через DXGI API, чтобы избежать привязки API к конкретной версии Direct3D. Это означает, что вам нужно получить интерфейс DXGI-поверхности, принадлежащей Direct3D-текстуре и передать его в Direct2D:

CComPtr<IDXGISurface> surface;
Verify(texture.QueryInterface(&surface));

В этот момент можно использовать Direct2D-объект фабрики для создания мишени рендеринга «DXGI-поверхность»:

CComPtr<ID2D1RenderTarget> target;

Verify(factory->CreateDxgiSurfaceRenderTarget(
  surface,
  &properties,
  &target));

Свойства мишени рендеринга идентичны описанным в предыдущем разделе. Просто не забывайте использовать корректный пиксельный формат и запрашивать совместимость с GDI. Затем вы можете запросить получение интерфейса ID2D1GdiInteropRenderTarget и использовать тот же метод Render, который был показан на рис. 8.

И на этом все. Если вы хотите выполнить рендеринг слоистого окна с применением аппаратного ускорения, используйте Direct3D-текстуру. В ином случае — битовую карту WIC. Эти два подхода обеспечат максимальную производительность при минимальном объеме копируемых данных.

Советую почаще заглядывать в блог по DirectX (blogs.msdn.com/directx) и, в частности, прочесть в нем статью Бена Констебля (Ben Constable) за август 2009 г., посвященную разбиению на компоненты (componentization) и взаимодействию.

Кенни Керр (Kenny Kerr)— высококвалифицированный специалист в области разработки ПО для Windows. Автор программы Window Clippings (windowclippings.com). С ним можно связаться через блог weblogs.asp.net/kennykerr.

Вопросы и комментарии (на английском языке) присылайте по адресу mmwincpp@microsoft.com.

Выражаю благодарность за рецензирование данной статьи экспертамБену Констеблю (Ben Constable) и Марку Лоренсу (Mark Lawrence).