Direct 2D #15.1 Пользовательский эффект. Вычислительный шейдер. язык HLSL. Про шейдеры

от автора

Здравствуйте! Очень важная и интересная тема, но чтобы это осознать, важно знать, что такое вообще шейдер, точнее — шейдеры. Так как меня попросили, да и я сам об этом думал, теперь буду заливать код ещё и на GitHub. Позже — и код прошлых статей.

На Хабре есть статьи про шейдеры, я просмотрел их все и, скажем так, техническая реализация довольно сумбурная, какие-то упрощения в виде картинок, без более-менее подробного объяснения. Поэтому начну с шейдеров.

Вы уже писали под-системы (программы) на C++, например:

под-система CONSOLE: используется для консольных приложений. При запуске такого .exe-файла операционная система всегда создаёт консольное окно. Точкой входа обычно служит функция main.

под-система WINDOWS: используется для приложений с собственным графическим интерфейсом (GUI). При запуске консольное окно не создаётся. Точкой входа обычно служит функция WinMain или wWinMain.

Шейдеры — точно такая же под-система, но в зависимости от типа шейдера выполняются видеокартой на разных этапах обработки вашей графической команды либо вообще отдельно (например, расчёт физики на мощностях видеокарты).

Чтобы более подробно объяснить, важно понимать, что происходит под капотом при выполнении подобного кода:

  m_d2dContext->BeginDraw();  m_d2dContext->Clear(D2D1::ColorF(D2D1::ColorF::CornflowerBlue));  m_d2dContext->EndDraw();

Важно понимать, что будет цепочка действий в трёх пространствах:

Этап UMD (User-Mode Driver)

Этап KMD (Kernel-Mode Driver)

Исполнение на GPU и шейдеры

Собственно, первый этап — подготовка команд.

BeginDraw() – этот вызов не отправляет команды на GPU. Он лишь сообщает Direct2D, что начинается новая серия команд. Внутри драйвера UMD подготавливается командный буфер (Command Buffer). Это область памяти, куда ваше приложение (через UMD) будет записывать понятные для драйвера инструкции. Формат этого буфера определяется производителем видеокарты и непонятен операционной системе.

m_d2dContext->Clear(…) – UMD преобразует высокоуровневый запрос на очистку в набор низкоуровневых аппаратных инструкций для GPU и записывает их в подготовленный командный буфер.

m_d2dContext->EndDraw() – этот вызов сигнализирует, что пакет команд готов. UMD инициирует отправку командного буфера в ядро системы.

Второй этап — трансляция и подготовка для GPU.

Теперь управление переходит в режим ядра. Здесь работает диспетчер видеопамяти и сам KMD.

Получение буфера: DirectX Graphics Kernel Subsystem (Dxgkrnl) получает командный буфер от UMD.

Валидация и трансляция: KMD (kernel-mode display miniport driver) проверяет командный буфер на корректность. Самая важная задача на этом этапе – трансформировать команды в формат, который понимает конкретная видеокарта. KMD записывает их в новый буфер – DMA-буфер (Direct Memory Access buffer).

Создание списка распределения (Allocation List): попутно KMD создаёт список распределения. Это список всех ресурсов (поверхностей, текстур), которые используются в командах. В вашем случае это будет целевая поверхность (render target), которую нужно очистить.

Управление памятью: важно отметить, что DMA-буфер на этом этапе ещё не исправлен (not patched). Это означает, что в нём пока содержатся не физические адреса видеопамяти, а логические ссылки на ресурсы из списка распределения. Задача по преобразованию этих ссылок в реальные физические адреса лежит на следующем этапе.

Создание страничных буферов: диспетчер видеопамяти вызывает функцию KMD DxgkDdiBuildPagingBuffer для создания специальных DMA-буферов – страничных буферов (paging buffers). Их задача – переместить все необходимые данные (например, саму целевую поверхность) в память, доступную GPU (видеопамять), если они там ещё не находятся.

Третий этап — исполнение на GPU и шейдеры.

Теперь команда попадает в исполнение на GPU. Здесь следующие подэтапы:

Input Assembler (IA) – GPU считывает вершины из буфера вершин и индексный буфер, собирает примитивы (треугольники, линии).

И вот результат передаётся в вершинный шейдер: Vertex Shader – это подпрограмма, которая выполняется для каждой вершины. Собственно, при помощи него трёхмерный объект на экране имеет глубину.

Hull Shader, Tessellator, Domain Shader (если включена тесселяция) – для стандартной фигуры Direct2D тесселяция обычно не используется, поэтому мы пропустим, и подобное будем рассматривать уже в Direct3D, сейчас это избыточно.

Geometry Shader (геометрический шейдер) – опционально, может создать новые примитивы.

Растеризатор (генератор развёртки) – преобразует треугольники в набор фрагментов (пикселей) на экране. Вычисляет, какие пиксели покрывает геометрия, и генерирует для каждого фрагмента координаты, которые будут переданы в пиксельный шейдер.

Pixel Shader (пиксельный шейдер) – может читать соседние пиксели из текстур и выполнять сложные вычисления. В общем, его задача – заниматься цветовой обработкой.

Output Merger (OM) – смешивает (блендинг) новый цвет пикселя с тем, что уже есть в целевом буфере (например, с учётом прозрачности). Записывает финальный цвет в буфер кадра (back buffer).

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

Собственно, в этой статье рассмотрим вычислительный шейдер.

Шейдеры в данной статье и последующих будут написаны на языке HLSL. Важно понимать:

Создавая HLSL, разработчики из Microsoft и NVIDIA не стали изобретать язык с нуля. За основу был взят язык C, который уже был стандартом де-факто для системного программирования. Вместе с NVIDIA они разработали язык Cg (C for Graphics), чей синтаксис и семантика были унаследованы HLSL.

Любая подпрограмма начинается с точки входа. Выглядит она следующим образом:

// Точка входа по умолчанию - функция main[numthreads(8, 8, 1)]void main(uint3 id : SV_DispatchThreadID){    // ... код шейдера ...}

Стоит сказать что в данном языке есть примитивные типы векторы, uint3 один из них, это вектор который хранит три типа uint.

а как же вы наверное заметили [numthreads(8, 8, 1)] это атрибут (attribute) в языке HLSL, который применяется к точки входа вычислительного шейдера. Он задаёт количество потоков, которые будут выполняться вместе в одной группе, организованных в трёхмерную сетку.

В этом примере каждая группа содержит 8 × 8 × 1 = 64 потока. Они выполняются одновременно на одном вычислительном блоке (CU/SM) GPU.

Каждый поток(при этом счёт с нуля), вызывает функцию. И каждый поток получает свой SV_DispatchThreadID , если поток (0,0,0), то и ид такой же, собственно поэтому и uint3.

И сразу объясню, почему (8,8,1) — главная причина кроется в том, как GPU физически выполняет потоки. Это делается группами, которые называются Warp (у NVIDIA, по 32 потока) или Wavefront (у AMD/Intel, по 64 потока).

А 8*8*1=64, то есть главное чтобы была кратность, ну иначе ресурсы будут простаивать.

Так же в зависимости от архитектуры, на блок есть некоторое количество регистров , сколько их всего надо уточнять в документации.

На каждый примитив тратятся регистр, на каждые 32 бита, 1 регистр. Если меньше, то так же 1, или 2 примитива в один регистр.

Собственно узнаём сколько регистров занимает функция и умножаем на кол-во потоков, и получаем то, сколько они займут. Но это одна группа, снова прибегнув к математике, поймём сколько все групп может быть. В шейдерной модели 5.0 не может быть больше1024 потоков.

Собственно, теперь про системные переменные:

  1. uint3 SV_GroupID — номер группы

  2. uint3 SV_GroupThreadID — позиция внутри группы

  3. uint3 SV_DispatchThreadID — ID потока

  4. uint SV_GroupIndex — плоский индекс, например ID потока (7,7,0) будет 63, то есть просто счёт потока, можно и так сказать.

Кроме этого есть спец. регистры:

t# (в таких записях, # это id, счёт с нуля) — Входные ресурсы: текстуры и буферы данных. В шейдерной модели 5.0 лимит 128 элементов.

s# — сэмплер — определяет правила чтения текстелей из текстуры , лимит 16 штук в модели 5.0

u# — Выходные/произвольные буферы (UAV), максимум 8 элементов

b# — Константные буферы(на самом деле данные, которые редко меняются), максимальный размер одного одного буфера, это 4096 векторов типа float4. Всего таких буферов может быть 15 элементов

Для локальных переменных регистр r# — но ним занимается компилятор.

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

Собственно пришло время написать вычислительный шейдер, который на вход примет то, на сколько увеличить яркость и текстуру:

Texture2D<float4> InputTexture : register(t0);RWTexture2D<float4> OutputTexture : register(u0);cbuffer constantBuffer : register(b0){    float magnitudeScale;}[numthreads(16,16,1)]void main(uint3 id : SV_DispatchThreadID){  uint width, height;  InputTexture.GetDimensions(width, height);  if (id.x >= width || id.y >= height) return; // Лишние потоки просто завершаются    float4 colorRGBA = InputTexture[id.xy];colorRGBA *= magnitudeScale;    OutputTexture[id.xy] = colorRGBA;}

Входное изображение у нас RGBA 50 на 50 пикселей. Выбрал такое количество потоков, по итогу потоков у нас 256. При помощи графических отладчиков можно мониторить нагрузку, определять, больше или меньше потоков нужно, но об этом потом. Если начинать объяснение сверху вниз, то:

Texture2D – двумерная текстура, каждый пиксель – 4-компонентный вектор float4. : register(t0) – это, так сказать, откуда брать указатель на текстуру.

RWTexture2D – текстура с произвольным доступом на чтение и запись. Тут мы явно привязываем к слоту 0 для ресурсов типа u#.

cbuffer constantBuffer – блок констант, передаваемых из CPU. : register(b0) – привязывает к слоту 0 для константных буферов (b#). float magnitudeScale – единственная переменная в буфере, скалярный множитель для всех цветовых каналов.

Ну а дальше, так сказать, всё понятно. GetDimensions возвращает границы изображения, так как вообще-то, если говорить про Direct3D, то там кроме потоков мы ещё сами определяем, сколько раз запустить группу потоков, и приходится округлять, но сейчас этим занимается Direct2D, поэтому нам достаточно проверить выход за границы.

Грубо говоря, теперь осталось самое лёгкое – создать пользовательский эффект, загрузить шейдер и воспользоваться эффектом.

Для начала я наглядно опишу логику:

В Direct2D создание пользовательского эффекта состоит из двух частей: создаём узел обработки (один узел – один шейдер) и менеджер узлов. В узлах обработки мы загружаем шейдер и т.п. А вот менеджер узлов настраивает, в каком порядке применять узлы обработки к нашим данным.

То есть задача ясна: создать один узел и менеджер узлов. Создаётся это благодаря наследованию от интерфейсов: для узла – это ID2D1ComputeTransform, для менеджера узлов – ID2D1EffectImpl. Ну, начнём с узла. Вот готовый класс (HabrNodeHandler.h):

#pragma once#include <d2d1_1.h>#include <d2d1effectauthor.h>#include <d3d11.h>#include <d3dcommon.h>DEFINE_GUID(GUID_HABR_COMPUTE_SHADER, 0x895136D1, 0x1511, 0x3850, 0xAF, 0x3B, 0x7A, 0xA3, 0xB0, 0x6C, 0x5D, 0xE1);class HabrNodeHandler : public ID2D1ComputeTransform{public:    HabrNodeHandler(_In_ ID2D1EffectContext* context, UINT32 width, UINT32 height);    IFACEMETHODIMP_(UINT32) GetInputCount() const override { return 1; }    IFACEMETHODIMP SetComputeInfo(_In_ ID2D1ComputeInfo* pComputeInfo) override;    IFACEMETHODIMP CalculateThreadgroups(        _In_ const D2D1_RECT_L* pOutputRect,        _Out_ UINT32* pDimensionX,        _Out_ UINT32* pDimensionY,        _Out_ UINT32* pDimensionZ    ) override;       IFACEMETHODIMP MapOutputRectToInputRects(        _In_ const D2D1_RECT_L* pOutputRect,        _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects,        UINT32 inputRectCount    ) const override;    IFACEMETHODIMP MapInputRectsToOutputRect(        _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects,        _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects,        UINT32 inputRectCount,        _Out_ D2D1_RECT_L* pOutputRect,        _Out_ D2D1_RECT_L* pOutputOpaqueSubRect    ) override;    IFACEMETHODIMP MapInvalidRect(        UINT32 inputIndex,        D2D1_RECT_L invalidInputRect,        _Out_ D2D1_RECT_L* pInvalidOutputRect    ) const override;    IFACEMETHODIMP_(ULONG) AddRef() override;    IFACEMETHODIMP_(ULONG) Release() override;    IFACEMETHODIMP QueryInterface(REFIID riid, _Outptr_ void** ppOutput) override;private:    ~HabrNodeHandler() = default;    LONG m_cRef;    ID2D1ResourceTexture* m_outputResource;    struct ConstantBuffer    {        float magnitudeScale;    } m_constantBuffer;};

Начнём:

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

  2. GetInputCount — Возвращает количество входных изображений, которые принимает эффект. В нашем случаем 1 текстура.

  3. SetComputeInfo — метод, где происходит настройка узла, привязываем буфер и так далее.

  4. CalculateThreadgroups — Direct2D вызывает этот метод, чтобы узнать, сколько групп потоков нужно запустить для обработки заданного выходного прямоугольника.

  5. MapOutputRectToInputRects — Direct2D запрашивает, какая область входных изображений соответствует заданному выходному прямоугольнику. Поскольку эффект — попиксельное масштабирование без смещения, отображение один-к-одному

  6. MapInputRectsToOutputRect — Обратное отображение — вычисляет, какой выходной прямоугольник соответствует заданным входным областям. Поскольку отображение идентичное, выходной прямоугольник будет равен объединению всех входных прямоугольников (для одного входа — просто равен ему).

  7. MapInvalidRect — Когда входной прямоугольник помечен как “недействительный” (изменился), Direct2D запрашивает, какая часть выходного изображения должна быть пересчитана. Поскольку эффект попиксельный, выходной недействительный прямоугольник будет соответствовать входному

  8. AddRef, Release, QueryInterface (COM-методы) — Стандартная COM-реализация для управления временем жизни объекта. AddRef увеличивает счётчик ссылок, Release уменьшает и уничтожает объект, когда счётчик становится 0. QueryInterface позволяет запрашивать другие интерфейсы (например, IUnknown, ID2D1ComputeTransform).

  9. Деконструктор(деструктор) — закрытый, чтобы объект удалялся только через Release

  10. поле LONG m_cRef; — счётчик ссылок COM.

  11. поле ID2D1ResourceTexture* m_outputResource; — это поле содержит указатель на выходную текстуру, да нам надо это указывать вручную. Привязав выходную текстуру.

  12. struct ConstantBuffer { float magnitudeScale; } m_constantBuffer; — хранит значение множителя, которое будет передано в GPU через константный буфер. Если хотите обновлять, то необходимо добавить функции , но об этом потом.

Теперь о реализации всех этих функций:

#include "pch.h"#include "HabrNodeHandler.h"HabrNodeHandler::HabrNodeHandler(_In_ ID2D1EffectContext* context, UINT32 width, UINT32 height) :    m_cRef(0){    // 1. Массив размеров для 2D-текстуры (ширина, высота)    UINT32 extents[2] = { width, height };    D2D1_EXTEND_MODE extendModes[2] = { D2D1_EXTEND_MODE_CLAMP, D2D1_EXTEND_MODE_CLAMP };    // 2. Заполняем структуру свойств    D2D1_RESOURCE_TEXTURE_PROPERTIES props = {};    props.extents = extents;                          // Указатель на массив    props.dimensions = 2;                             // 2D-текстура    props.bufferPrecision = D2D1_BUFFER_PRECISION_32BPC_FLOAT; // float4    props.channelDepth = D2D1_CHANNEL_DEPTH_4;        // 4 канала (RGBA)    props.filter = D2D1_FILTER_MIN_MAG_MIP_LINEAR;    // Фильтр (можно D2D1_FILTER_MIN_MAG_MIP_POINT)    props.extendModes = extendModes;   //Указывает способ выборки значений пикселей, превышающих экстент текстуры, в каждом измерении. (Выход за рамки).    // GUID для идентификации ресурса (может быть NULL, если не нужно разделять)    // Передаём nullptr, чтобы создать новый уникальный ресурс.    context->CreateResourceTexture(        nullptr,              // GUID (nullptr = создаём новый)        &props,        nullptr,              // initialData (не нужно для выхода)        nullptr,              // strides        0,                    // initialDataSize        &m_outputResource    );    HANDLE hFile = CreateFileW(        L"HABR_SHADER.cso",        GENERIC_READ,        FILE_SHARE_READ,        nullptr,        OPEN_EXISTING,        FILE_ATTRIBUTE_NORMAL,        nullptr    );    DWORD fileSize = GetFileSize(hFile, nullptr);    std::vector<BYTE> shaderData(fileSize);        DWORD bytesRead = 0;    ReadFile(hFile, shaderData.data(), fileSize, &bytesRead, nullptr);      CloseHandle(hFile);    context->LoadComputeShader(        GUID_HABR_COMPUTE_SHADER,        shaderData.data(),        static_cast<UINT32>(shaderData.size())    );}IFACEMETHODIMP HabrNodeHandler::MapInvalidRect(    UINT32 inputIndex,    D2D1_RECT_L invalidInputRect,    _Out_ D2D1_RECT_L* pInvalidOutputRect) const{    *pInvalidOutputRect = invalidInputRect;    return S_OK;}IFACEMETHODIMP HabrNodeHandler::MapOutputRectToInputRects(    _In_ const D2D1_RECT_L* pOutputRect,    _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects,    UINT32 inputRectCount) const{    if (inputRectCount != 1)        return E_INVALIDARG;    pInputRects[0] = *pOutputRect;    return S_OK;}IFACEMETHODIMP HabrNodeHandler::MapInputRectsToOutputRect(    _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects,    _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects,    UINT32 inputRectCount,    _Out_ D2D1_RECT_L* pOutputRect,    _Out_ D2D1_RECT_L* pOutputOpaqueSubRect){    if (inputRectCount < 1 || pInputRects == nullptr || pOutputRect == nullptr)    {        return E_INVALIDARG;    }    *pOutputRect = pInputRects[0];    if (pInputOpaqueSubRects != nullptr && pOutputOpaqueSubRect != nullptr)    {        *pOutputOpaqueSubRect = pInputOpaqueSubRects[0];    }    else    {        *pOutputOpaqueSubRect = pInputRects[0];    }    return S_OK;}IFACEMETHODIMP HabrNodeHandler::CalculateThreadgroups(    _In_ const D2D1_RECT_L* pOutputRect,    _Out_ UINT32* pDimensionX,    _Out_ UINT32* pDimensionY,    _Out_ UINT32* pDimensionZ){    // Вычисляем ширину и высоту выходного прямоугольника в пикселях    UINT32 width = pOutputRect->right - pOutputRect->left;    UINT32 height = pOutputRect->bottom - pOutputRect->top;    // Размер группы из шейдера (16, 16, 1)    const UINT32 groupSizeX = 16;    const UINT32 groupSizeY = 16;    // Округление вверх: (размер + группа - 1) / группа    *pDimensionX = (width + groupSizeX - 1) / groupSizeX;    *pDimensionY = (height + groupSizeY - 1) / groupSizeY;    *pDimensionZ = 1;    return S_OK;}IFACEMETHODIMP HabrNodeHandler::SetComputeInfo(_In_ ID2D1ComputeInfo* pComputeInfo){        pComputeInfo->SetOutputBuffer(D2D1_BUFFER_PRECISION_32BPC_FLOAT, D2D1_CHANNEL_DEPTH_4);                pComputeInfo->SetComputeShader(GUID_HABR_COMPUTE_SHADER);               m_constantBuffer.magnitudeScale = 2.0f;         pComputeInfo->SetComputeShaderConstantBuffer(            reinterpret_cast<const BYTE*>(&m_constantBuffer),            sizeof(m_constantBuffer)        );         pComputeInfo->SetResourceTexture(0, m_outputResource);    return S_OK;}IFACEMETHODIMP_(ULONG) HabrNodeHandler::AddRef(){    return ++m_cRef;}IFACEMETHODIMP_(ULONG) HabrNodeHandler::Release(){    ULONG cRef = --m_cRef;    if (cRef == 0)        delete this;    return cRef;}IFACEMETHODIMP HabrNodeHandler::QueryInterface(REFIID riid, _Outptr_ void** ppOutput){    *ppOutput = nullptr;    HRESULT hr = S_OK;    if (riid == __uuidof(ID2D1ComputeTransform))        *ppOutput = static_cast<ID2D1ComputeTransform*>(this);    else if (riid == __uuidof(ID2D1Transform))        *ppOutput = static_cast<ID2D1Transform*>(this);    else if (riid == __uuidof(ID2D1TransformNode))        *ppOutput = static_cast<ID2D1TransformNode*>(this);    else if (riid == __uuidof(IUnknown))        *ppOutput = this;    else        hr = E_NOINTERFACE;    if (*ppOutput != nullptr)        AddRef();    return hr;}

Собственно, начинаем описание:

  1. Конструктор, по аргументам я уже объяснил, логика:

    1. Инициализирует счётчик ссылок COM (m_cRef = 0).

    2. Определяет массив extents размером 2 (width и height) для 2D-текстуры.

    3. Заполняет структуру D2D1_RESOURCE_TEXTURE_PROPERTIES:

      1. extents – указатель на массив размеров.

      2. dimensions = 2 – двумерная текстура.

      3. bufferPrecision = D2D1_BUFFER_PRECISION_32BPC_FLOAT – 32 бита на канал с плавающей точкой (соответствует float4).

      4. channelDepth = D2D1_CHANNEL_DEPTH_4 – 4 канала (RGBA).

      5. filter = D2D1_FILTER_MIN_MAG_MIP_LINEAR – режим фильтрации (для чтения, если потребуется).

      6. extendModes — стандартные настройки передали, что делать с тем, что вне указанных рамок(обрезать).

    4. И вызов функции — CreateResourceTexture

      1. Первый аргумент GUID для идентификации ресурса, передаём nullptr , чтобы создать новый уникальный ресурс.

      2. Второй аргумент ссылка на заполненную структуру.

      3. Третий аргумент указатель на исходные данные, которыми нужно инициализировать текстуру. Если текстура будет использоваться только для записи (как в вашем случае), этот параметр должен быть nullptr.

      4. Четвёртый аргумент указатель на массив, определяющий шаг в байтах для каждого измерения. Это позволяет загружать данные, которые не являются непрерывными в памяти (например, с паддингом). Если шаг не требуется, передайте nullptr.

      5. Пятый аргумент размер буфера в байтах.

      6. И последний аргумент ссылка на указатель на текстуру.

    5. Ну а дальше не знаю, стандартный Win32 , загружаем файл, получаем размер, и в конце LoadComputeShader:

      1. Первый аргумент это GUID шейдера

      2. Второй аргумент это указатель на данные

      3. Третий аргумент это размер данных

  2. Функция MapInvalidRect, по недействительной области на входе определяет, какая область на выходе должна быть пересчитана.

    1. Аргументы:

      1. Первый аргумент inputIndex – индекс входа (всегда 0, т.к. вход один).

      2. Второй аргумент — invalidInputRect – прямоугольник на входе, помеченный как недействительный.

      3. Третий аргумент pInvalidOutputRect – выходной прямоугольник, который нужно пересчитать.

    2. Логика: Копирует входной прямоугольник в выходной, т.к. преобразование попиксельное без смещения.

  3. Функция MapOutputRectToInputRects. Определяет, какая часть входного изображения нужна для генерации заданного выходного прямоугольника.

    1. Аргументы:

      1. pOutputRect – интересующий прямоугольник на выходе.

      2. pInputRects – массив входных прямоугольников (размер равен inputRectCount).

      3. inputRectCount – количество входов (должно быть 1).

    2. Логика: E_INVALIDARG если inputRectCount != 1, иначе S_OK. Копирует pOutputRect в pInputRects[0] (преобразование 1:1).

  4. Функция MapInputRectsToOutputRect. Определяет выходной прямоугольник и его непрозрачную часть на основе заданных входных прямоугольников.

    1. Аргументы:

      1. pInputRects – массив прямоугольников на входе.

      2. pInputOpaqueSubRects – массив непрозрачных подпрямоугольников на входе (области с альфа=1).

      3. inputRectCount – количество входов (≥1).

      4. pOutputRect – вычисленный выходной прямоугольник.

      5. pOutputOpaqueSubRect – вычисленная непрозрачная часть выхода.

    2. Логика: возвращает E_INVALIDARG при некорректных аргументах, иначе S_OK. Проверяет inputRectCount >= 1, pInputRects != nullptr, pOutputRect != nullptr. Копирует pInputRects[0] в pOutputRect. Если pInputOpaqueSubRects и pOutputOpaqueSubRect валидны, копирует непрозрачный подпрямоугольник первого входа в выходной; иначе использует весь выходной прямоугольник как непрозрачный.

  5. Функция CalculateThreadgroups. Вычисляет количество групп потоков (Dispatch), которые нужно запустить для покрытия выходного прямоугольника.

    1. Аргументы:

      1. pOutputRect – прямоугольник, который должен быть обработан.

      2. pDimensionX, pDimensionY, pDimensionZ – выходные размеры Dispatch (количество групп по каждой оси).

    2. Логика: Вычисляет ширину и высоту выходного прямоугольника. Задаёт размер группы из HLSL (groupSizeX = 16, groupSizeY = 16). Округляет вверх: pDimensionX = (width + 15) / 16, pDimensionY = (height + 15) / 16, pDimensionZ = 1. Это гарантирует, что все пиксели будут покрыты, а лишние потоки отсекутся проверкой границ в шейдере.

  6. Функция SetComputeInfo. Настраивает вычислительный шейдер: привязывает шейдер, константный буфер и выходной ресурс (UAV). Этот метод вызывается Direct2D перед рендерингом.

    1. Аргументы:

      1. pComputeInfo – указатель на интерфейс для настройки вычислений.

    2. Логика:

      1. pComputeInfo->SetOutputBuffer(D2D1_BUFFER_PRECISION_32BPC_FLOAT, D2D1_CHANNEL_DEPTH_4); – сообщает Direct2D формат выходного буфера (32-битный float RGBA). Это нужно для композиции и оптимизации.

      2. pComputeInfo->SetComputeShader(GUID_HABR_COMPUTE_SHADER); – активирует загруженный шейдер по GUID.

      3. pComputeInfo->SetComputeShaderConstantBuffer(reinterpret_cast<const BYTE*>(&m_constantBuffer), sizeof(m_constantBuffer)); – передаёт константный буфер в GPU. Индекс по умолчанию – 0 (регистр b0), что соответствует объявлению register(b0) в HLSL.

      4. pComputeInfo->SetResourceTexture(0, m_outputResource); – привязывает созданный выходной ресурс к регистру u0 (индекс 0), что соответствует RWTexture2D OutputTexture : register(u0) в HLSL.

      5. Возвращает S_OK.

  7. Функция AddRef() — Увеличивает счётчик ссылок COM на 1.

  8. Функция Release(). Уменьшает счётчик ссылок на 1. Если счётчик становится 0, уничтожает объект через delete this.

  9. Функция QueryInterface. Реализует запрос интерфейса. Возвращает указатель на поддерживаемый интерфейс.

С узлом закончили, переходим к менеджеру.

Сразу говорю, он проще:

#pragma once#include <d2d1_1.h>#include <d2d1effectauthor.h>DEFINE_GUID(HabrEffect, 0x88F0F871, 0x18A3, 0x4C55, 0xB5, 0x28, 0xE7, 0x38, 0xA7, 0x81, 0x35, 0x1F);class HabrEffect : public ID2D1EffectImpl{public:        static HRESULT Register(_In_ ID2D1Factory1* pFactory);    static HRESULT __stdcall Create(_Outptr_ IUnknown** ppEffectImpl);    // ID2D1EffectImpl    IFACEMETHODIMP Initialize(        _In_ ID2D1EffectContext* pEffectContext,        _In_ ID2D1TransformGraph* pTransformGraph    ) override;    IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType) override;    IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pTransformGraph) override;    // IUnknown    IFACEMETHODIMP_(ULONG) AddRef() override;    IFACEMETHODIMP_(ULONG) Release() override;    IFACEMETHODIMP QueryInterface(REFIID riid, _Outptr_ void** ppOutput) override;private:    HabrEffect();    ~HabrEffect() = default;    LONG m_cRef;};

Начнём объяснение:

  1. Функция Register. Принимает указатель на фабрику Direct2D (ID2D1Factory1), полученную из приложения. Внутри регистрирует эффект.

  2. Функция Create. Статический метод создания экземпляра эффекта, который будет вызван Direct2D при запросе создания нового объекта эффекта.

  3. Функция Initialize. Вызывается Direct2D сразу после создания эффекта.

  4. Функция PrepareForRender. Вызывается перед каждым рендерингом.

  5. Функция SetGraph. Вызывается когда Direct2D запрашивает установку или обновление графа трансформаций(Дальше поймёте что это значит). Обычно вызывается после Initialize или если граф изменился.

  6. Функция AddRef. увеличивающий счётчик ссылок COM на 1.

  7. Функция Release. уменьшающий счётчик ссылок COM на 1. Если счётчик становится 0, уничтожает объект.

  8. Функция QueryInterface. Реализует запрос интерфейса.

Собственно как и сказал — по проще. Ну а теперь реализация функций:

#include "pch.h"#include <initguid.h>#include "HabrEffect.h"#include "HabrNodeHandler.h"#define XML(X) TEXT(#X)HabrEffect::HabrEffect() :    m_cRef(0){}HRESULT HabrEffect::Register(_In_ ID2D1Factory1* pFactory){    PCWSTR pszXml =        XML(            <?xml version='1.0'?>            <Effect>            <Property name='DisplayName' type='string' value='Sample Discrete Fourier Transform (DFT) Compute Effect'/>            <Property name='Author' type='string' value='Microsoft Corporation'/>            <Property name='Category' type='string' value='Transform'/>            <Property name='Description' type='string' value='A sample compute shader effect that computes the Discrete Fourier Transform of a given input.'/>            <Inputs>            <Input name='Source'/>            </Inputs>            </Effect>        );    return pFactory->RegisterEffectFromString(HABR_EFFECT, pszXml, nullptr, 0, Create);}HRESULT __stdcall HabrEffect::Create(_Outptr_ IUnknown** ppEffectImpl){    *ppEffectImpl = static_cast<ID2D1EffectImpl*>(new (std::nothrow) HabrEffect());    if (*ppEffectImpl == nullptr)        return E_OUTOFMEMORY;    (*ppEffectImpl)->AddRef();    return S_OK;}IFACEMETHODIMP HabrEffect::Initialize(    _In_ ID2D1EffectContext* pEffectContext,    _In_ ID2D1TransformGraph* pTransformGraph){    Microsoft::WRL::ComPtr<HabrNodeHandler> dftHorizontalTransform = new (std::nothrow) HabrNodeHandler(pEffectContext,50,50);    pTransformGraph->AddNode(dftHorizontalTransform.Get());      pTransformGraph->ConnectToEffectInput(0, dftHorizontalTransform.Get(), 0);    pTransformGraph->SetOutputNode(dftHorizontalTransform.Get());    return S_OK;}IFACEMETHODIMP HabrEffect::SetGraph(_In_ ID2D1TransformGraph* pTransformGraph){    return E_NOTIMPL;}IFACEMETHODIMP HabrEffect::PrepareForRender(D2D1_CHANGE_TYPE changeType){    return S_OK;}IFACEMETHODIMP_(ULONG) HabrEffect::AddRef(){    return ++m_cRef;}IFACEMETHODIMP_(ULONG) HabrEffect::Release(){    ULONG cRef = --m_cRef;    if (cRef == 0)        delete this;    return cRef;}IFACEMETHODIMP HabrEffect::QueryInterface(REFIID riid, _Outptr_ void** ppOutput){    *ppOutput = nullptr;    HRESULT hr = S_OK;    if (riid == __uuidof(ID2D1EffectImpl))        *ppOutput = static_cast<ID2D1EffectImpl*>(this);    else if (riid == __uuidof(IUnknown))        *ppOutput = this;    else        hr = E_NOINTERFACE;    if (*ppOutput != nullptr)        AddRef();    return hr;}

Описание:

  1. Конструктор инициализирует счётчик ссылок COM (m_cRef) нулём. Других действий не требуется, так как всё управление ресурсами будет через COM-интерфейсы.

  2. Функция Register. Регистрация эффекта в системе Direct2D.

    1. Аргументы:

      1. Указатель на фабрику

    2. Логика:

      1. Определяет XML-строку с метаданными эффекта:

        1. DisplayName — отображаемое имя.

        2. Author — автор.

        3. Category — категория (здесь “Transform”).

        4. Description — описание.

        5. — объявляет один вход (с именем “Source”).

      2. Вызывает pFactory->RegisterEffectFromString для регистрации эффекта

        1. Первый аргумент — GUID эффекта

        2. Второй аргумент — XML-строка.

        3. Третий аргумент (nullptr) — массив свойств (по умолчанию).

        4. Четвёртый аргумент (0) — количество свойств.

        5. Пятый аргумент — указатель на статическую функцию Create, которая создаёт экземпляр эффекта.

  3. Функция Create. Создаёт новый экземпляр HabrEffect. Эта функция вызывается Direct2D, когда требуется объект эффекта.

    1. Аргументы:

      1. ppEffectImpl — выходной указатель на IUnknown, куда будет записан созданный объект.

    2. Логика:

      1. Создаёт новый объект HabrEffect через new (std::nothrow), чтобы избежать исключений при нехватке памяти.

      2. Приводит его к ID2D1EffectImpl* (и одновременно к IUnknown*, так как ID2D1EffectImpl наследует IUnknown).

      3. Проверяет, не вернулся ли nullptr — тогда возвращает E_OUTOFMEMORY.

      4. Увеличивает счётчик ссылок на созданном объекте (AddRef()) — согласно правилам COM, фабричный метод должен вернуть объект с уже увеличенным счётчиком, чтобы вызывающий не удерживал указатель без ссылки.

      5. Возвращает S_OK.

  4. Функция Initialize. Инициализирует эффект, создавая и настраивая граф трансформаций. Вызывается Direct2D сразу после создания эффекта.

    1. Аргументы:

      1. pEffectContext — контекст эффекта, предоставляет доступ к ресурсам и сервисам.

      2. pTransformGraph — граф трансформаций, который нужно заполнить узлами.

    2. Логика:

      1. Создаёт экземпляр узла-обработчика HabrNodeHandler с помощью new (std::nothrow) и передаёт ему контекст и размеры выходной текстуры — захардкоженные 50×50. Сделал специально, вы же можете, добавить функций , чтобы получать актуальные значения, или ещё что-то придумать

      2. Обёртываем указатель в Microsoft::WRL::ComPtr для автоматического управления временем жизни.

      3. Добавляет узел в граф: pTransformGraph->AddNode(dftHorizontalTransform.Get()).

      4. Соединяет вход эффекта (индекс 0) с входом узла (индекс 0) через pTransformGraph->ConnectToEffectInput(0, dftHorizontalTransform.Get(), 0). Это означает, что данные, подаваемые на вход эффекта, будут переданы в узел.

      5. Устанавливает этот узел как выходной (pTransformGraph->SetOutputNode(dftHorizontalTransform.Get())), то есть результат обработки узла будет выходом всего эффекта.

      6. Возвращает S_OK.

  5. Функция SetGraph. Этот метод должен обновлять граф трансформаций (например, при изменении свойств эффекта). (То что делали в прошлой функции), однако у нас он не реализован и возвращает E_NOTIMPL (не реализовано). Это допустимо, если граф не меняется после инициализации.

  6. Функция PrepareForRender. Вызывается перед каждым рендерингом. Позволяет обновить данные (например, передать новые значения констант в шейдер). В текущей реализации ничего не делает и возвращает S_OK. Если в будущем потребуется передавать динамические параметры (например, magnitudeScale), здесь нужно будет обновлять узел. Ну или ещё в каком-то месте.

  7. Функции: AddRef, Release, QueryInterface. С ними всё понятно.

Теперь реализуем финальный крупный кусок(дальше то что будет написано, можно реализовать отредактировав пару функций):

#include <Windows.h>#include "pch.h"#include "HabrEffect.h"// Глобальные переменныеHWND                                        m_hwnd = nullptr;bool                                        m_windowClosed = false;bool                                        m_windowVisible = false;// Direct3D 11Microsoft::WRL::ComPtr<ID3D11Device>        device;Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;Microsoft::WRL::ComPtr<IDXGISwapChain1>     m_swapChain;// Direct2DMicrosoft::WRL::ComPtr<ID2D1Factory3>           m_d2dFactory;Microsoft::WRL::ComPtr<ID2D1Device2>            m_d2dDevice;Microsoft::WRL::ComPtr<ID2D1DeviceContext2>     m_d2dContext;Microsoft::WRL::ComPtr<ID2D1Bitmap1>            m_d2dTargetBitmap;// Эффект и вспомогательныеMicrosoft::WRL::ComPtr<ID2D1Effect>             m_HabrEffect;Microsoft::WRL::ComPtr<IDXGIDevice3>            dxgiDevice;Microsoft::WRL::ComPtr<IDXGIFactory2>           dxgiFactory;Microsoft::WRL::ComPtr<IDXGIAdapter>            dxgiAdapter;Microsoft::WRL::ComPtr<ID2D1Bitmap1>            blackBitmap;D2D1_SIZE_F                                     m_d3dRenderTargetSize = { 0, 0 };float                                           m_dpi = 96.0f;D3D_FEATURE_LEVEL                               m_d3dFeatureLevel = D3D_FEATURE_LEVEL_11_0;// Прототипы функцийvoid Render1();void Shutdown();LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);void Run();void CreateIndependentResources();void CreateDependentResources();HRESULT LoadTextureFromFile(const std::wstring& filename, Microsoft::WRL::ComPtr<ID2D1Bitmap1>& bitmap);bool Initialize(HINSTANCE hInstance, int nCmdShow);void OnResize();// Точка входаint WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE /*hPrevInstance*/,    _In_ LPSTR /*lpCmdLine*/, _In_ int nCmdShow){    CoInitializeEx(nullptr, COINIT_MULTITHREADED);    if (!Initialize(hInstance, nCmdShow))        return 0;    Run();    Shutdown();    CoUninitialize();    return 0;}// Инициализацияbool Initialize(HINSTANCE hInstance, int nCmdShow){    // Регистрация класса окна    WNDCLASSEX wcex = {};    wcex.cbSize = sizeof(WNDCLASSEX);    wcex.style = CS_HREDRAW | CS_VREDRAW;    wcex.lpfnWndProc = WndProc;    wcex.hInstance = hInstance;    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);    wcex.lpszClassName = L"CustomComputeShaderWindowClass";    if (!RegisterClassEx(&wcex))        return false;    // Размер окна    RECT rc = { 0, 0, 1920, 1080 };    AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);    // Создание окна    m_hwnd = CreateWindowEx(        0,        L"CustomComputeShaderWindowClass",        L"Direct2D Custom Compute Shader Effect",        WS_OVERLAPPEDWINDOW,        CW_USEDEFAULT, CW_USEDEFAULT,        rc.right - rc.left, rc.bottom - rc.top,        nullptr,        nullptr,        hInstance,        nullptr);    if (!m_hwnd)        return false;    // Задаём размеры рендеринга    RECT clientRect;    GetClientRect(m_hwnd, &clientRect);    m_d3dRenderTargetSize.width = static_cast<FLOAT>(clientRect.right - clientRect.left);    m_d3dRenderTargetSize.height = static_cast<FLOAT>(clientRect.bottom - clientRect.top);    // Получение DPI (современный способ)    HMODULE hUser32 = GetModuleHandleW(L"user32.dll");    if (hUser32)    {        auto pGetDpiForWindow = (UINT(WINAPI*)(HWND))GetProcAddress(hUser32, "GetDpiForWindow");        if (pGetDpiForWindow)            m_dpi = (float)pGetDpiForWindow(m_hwnd);        else            m_dpi = 96.0f;    }    else        m_dpi = 96.0f;    // Создание фабрики D2D    D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,        __uuidof(ID2D1Factory3),        &m_d2dFactory);    CreateIndependentResources();    CreateDependentResources();    // Регистрация и создание эффекта    HabrEffect::Register(m_d2dFactory.Get());    m_d2dContext->CreateEffect(HABR_EFFECT, m_HabrEffect.GetAddressOf());    // Загрузка текстуры    LoadTextureFromFile(L"image.png", blackBitmap);        m_HabrEffect->SetInput(0, blackBitmap.Get());    // Показываем окно    ShowWindow(m_hwnd, nCmdShow);    UpdateWindow(m_hwnd);    return true;}// Создание независимых ресурсов (D3D11, связь с D2D)void CreateIndependentResources(){    D3D11CreateDevice(        nullptr,        D3D_DRIVER_TYPE_HARDWARE,        0,        D3D11_CREATE_DEVICE_BGRA_SUPPORT,        nullptr, 0,        D3D11_SDK_VERSION,        &device,        &m_d3dFeatureLevel,        &context    );    device.As(&dxgiDevice);    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice);    m_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_d2dContext);    dxgiDevice->GetAdapter(&dxgiAdapter);    dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));}// Создание зависимых ресурсов (цепочка обмена, целевой буфер)void CreateDependentResources(){    m_d2dTargetBitmap.Reset();    m_swapChain.Reset();    DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};    swapChainDesc.Width = static_cast<UINT>(m_d3dRenderTargetSize.width);    swapChainDesc.Height = static_cast<UINT>(m_d3dRenderTargetSize.height);    swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;    swapChainDesc.Stereo = FALSE;    swapChainDesc.SampleDesc.Count = 1;    swapChainDesc.SampleDesc.Quality = 0;    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;    swapChainDesc.BufferCount = 2;    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;    swapChainDesc.Flags = 0;    swapChainDesc.Scaling = DXGI_SCALING_NONE;    swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;    dxgiFactory->CreateSwapChainForHwnd(        device.Get(),        m_hwnd,        &swapChainDesc,        nullptr,        nullptr,        &m_swapChain    );    dxgiDevice->SetMaximumFrameLatency(1);    Microsoft::WRL::ComPtr<IDXGISurface2> dxgiBackBuffer;    m_swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer));    D2D1_BITMAP_PROPERTIES1 bitmapProperties =        D2D1::BitmapProperties1(            D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,            D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),            m_dpi, m_dpi        );    m_d2dContext->CreateBitmapFromDxgiSurface(dxgiBackBuffer.Get(),        &bitmapProperties, &m_d2dTargetBitmap);    m_d2dContext->SetTarget(m_d2dTargetBitmap.Get());}// Загрузка текстуры из файлаHRESULT LoadTextureFromFile(const std::wstring& filename, Microsoft::WRL::ComPtr<ID2D1Bitmap1>& bitmap){    Microsoft::WRL::ComPtr<IWICImagingFactory> wicFactory;    HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr,        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(wicFactory.GetAddressOf()));    if (FAILED(hr)) return hr;    Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;    hr = wicFactory->CreateDecoderFromFilename(filename.c_str(), nullptr,        GENERIC_READ, WICDecodeMetadataCacheOnLoad, decoder.GetAddressOf());    if (FAILED(hr)) return hr;    Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;    hr = decoder->GetFrame(0, frame.GetAddressOf());    if (FAILED(hr)) return hr;    Microsoft::WRL::ComPtr<IWICFormatConverter> converter;    hr = wicFactory->CreateFormatConverter(converter.GetAddressOf());    if (FAILED(hr)) return hr;    hr = converter->Initialize(frame.Get(), GUID_WICPixelFormat32bppPBGRA,        WICBitmapDitherTypeNone, nullptr, 0.0f, WICBitmapPaletteTypeCustom);    if (FAILED(hr)) return hr;    return m_d2dContext->CreateBitmapFromWicBitmap(converter.Get(), nullptr, bitmap.GetAddressOf());}// Отрисовка одного кадраvoid Render1(){    m_d2dContext->BeginDraw();    m_d2dContext->Clear(D2D1::ColorF(D2D1::ColorF::CornflowerBlue));    m_d2dContext->DrawImage(        m_HabrEffect.Get(),        D2D1_INTERPOLATION_MODE_LINEAR,        D2D1_COMPOSITE_MODE_SOURCE_OVER    );    HRESULT hr = m_d2dContext->EndDraw();    if (hr == D2DERR_RECREATE_TARGET)    {        OnResize();        // Повторный вызов для перерисовки после восстановления        Render1();        return;    }    m_swapChain->Present(1, 0);}// Обработка изменения размераvoid OnResize(){    if (m_hwnd)    {        RECT clientRect;        GetClientRect(m_hwnd, &clientRect);        m_d3dRenderTargetSize.width = static_cast<FLOAT>(clientRect.right - clientRect.left);        m_d3dRenderTargetSize.height = static_cast<FLOAT>(clientRect.bottom - clientRect.top);        if (m_swapChain)        {            m_d2dContext->SetTarget(nullptr);            m_d2dTargetBitmap.Reset();            if (m_d3dRenderTargetSize.width > 0 && m_d3dRenderTargetSize.height > 0)            {                m_swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, 0);                CreateDependentResources();            }        }    }}// Главный циклvoid Run(){    MSG msg = {};    while (!m_windowClosed)    {        while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))        {            TranslateMessage(&msg);            DispatchMessage(&msg);        }        // Рендерим каждый кадр        if (!m_windowClosed)            Render1();    }}// Завершение работыvoid Shutdown(){    m_windowClosed = true;    // Освобождаем COM-объекты явно (хотя умные указатели сделают это сами)    m_HabrEffect.Reset();    blackBitmap.Reset();    m_d2dTargetBitmap.Reset();    m_d2dContext.Reset();    m_d2dDevice.Reset();    m_d2dFactory.Reset();    m_swapChain.Reset();    context.Reset();    device.Reset();    dxgiDevice.Reset();    dxgiAdapter.Reset();    dxgiFactory.Reset();    if (m_hwnd)    {        DestroyWindow(m_hwnd);        m_hwnd = nullptr;    }}// Оконная процедураLRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){    switch (message)    {    case WM_SIZE:        if (wParam != SIZE_MINIMIZED)        {            OnResize();        }        return 0;    case WM_DESTROY:        m_windowClosed = true;        PostQuitMessage(0);        return 0;    case WM_CLOSE:        DestroyWindow(hWnd);        return 0;    }    return DefWindowProc(hWnd, message, wParam, lParam);}

В общем почти всё что в коде уже было, так что объяснение будет не длинным:

  1. Функция WinMain. Точка входа.

    1. Логика:

      1. Инициализирует COM (CoInitializeEx).

      2. Вызывает Initialize для создания окна и ресурсов.

      3. Запускает главный цикл Run.

      4. После завершения цикла вызывает Shutdown для очистки.

      5. Завершает COM (CoUninitialize).

  2. Функция Initialize. Создаёт окно, инициализирует Direct3D, Direct2D, регистрирует и создаёт эффект, загружает тестовую текстуру.

    1. Логика:

      1. Регистрирует класс окна (WNDCLASSEX).

      2. Создаёт окно (CreateWindowEx) с размерами 1920×1080.

      3. Получает клиентскую область окна и сохраняет размеры в m_d3dRenderTargetSize.

      4. Определяет DPI через GetDpiForWindow (или использует 96).

      5. Создаёт фабрику Direct2D (D2D1CreateFactory).

      6. Вызывает CreateIndependentResources() для создания D3D11-устройства и связывания с D2D.

      7. Вызывает CreateDependentResources() для создания цепочки обмена и целевого буфера.

      8. Регистрирует эффект (HabrEffect::Register) и создаёт его экземпляр (CreateEffect).

      9. Загружает текстуру image.png через LoadTextureFromFile (если файл отсутствует, создаётся тестовый градиент – в вашем коде нет fallback, но он есть в некоторых версиях).

      10. Устанавливает загруженную текстуру как вход эффекта (SetInput).

      11. Показывает окно (ShowWindow, UpdateWindow).

  3. Функция CreateIndependentResources. Создаёт независимые ресурсы, общие для всего приложения: устройство D3D11, контекст, связь с Direct2D. Это уже было ни раз, так что не задерживаюсь.

  4. Функция CreateDependentResources. Создаёт зависимые ресурсы, которые зависят от размера окна (цепочка обмена, целевой битмап).

  5. Функция LoadTextureFromFile. Загружает изображение из файла и создаёт ID2D1Bitmap1.

  6. Функция Render1. Выполняет рендеринг одного кадра.

    1. Логика:

      1. Начинает рисование (m_d2dContext->BeginDraw()).

      2. Очищает фон синим цветом (Clear).

      3. Рисует эффект (DrawImage) с интерполяцией LINEAR и композицией D2D1_COMPOSITE_MODE_SOURCE_OVER (Надо чтобы всё что вне текстуры, не затрагивалось) Полный список режимов.

      4. Завершает рисование (EndDraw).

      5. Если результат D2DERR_RECREATE_TARGET, вызывает OnResize() и рекурсивно перерисовывает.

      6. Отображает цепочку обмена (m_swapChain->Present(1, 0)) с вертикальной синхронизацией.

  7. Функция OnResize. Обрабатывает изменение размера окна: обновляет размеры и пересоздаёт цепочку обмена.

  8. Функция Run. Главный цикл обработки сообщений и рендеринга.

  9. Функция Shutdown. Освобождает все ресурсы и закрывает окно.

  10. Функция WndProc. Оконная процедура, обрабатывает системные сообщения.

И последний файл(pch.h):

#pragma once#ifndef _WIN32_WINNT#define _WIN32_WINNT 0x0A00#endif#ifndef NOMINMAX#define NOMINMAX#endif#define WIN32_LEAN_AND_MEAN// Основные заголовки Windows#include <windows.h>#include <windowsx.h>// Стандартная библиотека C++#include <memory>#include <string>#include <vector>#include <algorithm>#include <cmath>#include <cstdint>#include <cassert>#include <exception>#include <stdexcept>// COM и WRL#include <objbase.h>#include <wrl/client.h>          // для ComPtr// Direct3D 11 (все версии)#include <d3d11.h>#include <d3d11_1.h>#include <d3d11_2.h>             // ID3D11Device2#include <d3d11_3.h>             // ID3D11DeviceContext3// DXGI#include <dxgi.h>#include <dxgi1_2.h>// Direct2D (все версии)#include <d2d1.h>#include <d2d1_1.h>#include <d2d1_2.h>              // дополнительные интерфейсы#include <d2d1_3.h>              // ID2D1Factory3, ID2D1Device2, ID2D1DeviceContext2// DirectWrite (все версии)#include <dwrite.h>#include <dwrite_1.h>#include <dwrite_2.h>            // IDWriteFactory2// WIC#include <wincodec.h>// DirectXMath (опционально)#include <DirectXMath.h>

По идее, в этом файле нечего объяснять.

Собственно, теперь можете собрать, запустить и увидеть, что текстура стала ярче.

Да, вот такая незамысловатая текстура. В оригинале выглядит так:

И самое последнее действие — это скомпилировать шейдер. Собственно, у меня файл носит название HABR_SHADER.hlsl. Компилировать я буду при помощи fxc, через VS (её консоль Developer Command Prompt for VS 2022):

fxc.exe /T cs_5_0 /E main /Fo HABR_SHADER.hlsl HABR_SHADER.hlsl

/T — это версия шейдерной модели. Я выбрал 5.0 — видеокарты с её поддержкой появились в 2009–2010 годах. Сейчас же современной считается 6.0.

/E — название функции точки входа.

/Fo — входной файл и выходной.

Дополнение. Работа с произвольными данными через буферы.

Вообще да, передавать можно только текстуры, но ничто не мешает нам интерпретировать текстуру как одномерный буфер данных.

Самый простой способ — создать текстуру с высотой 1 пиксель и шириной, равной количеству элементов.

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

Пишем новый шейдер BufferShader.hlsl:

Texture2D<float4> InputTexture : register(t0);RWTexture2D<float4> OutputTexture : register(u0);cbuffer Constants : register(b0){    float multiplier;};[numthreads(16, 1, 1)]   // 16 потоков вдоль оси X, по одному по Y и Zvoid main(uint3 id : SV_DispatchThreadID){    uint width, height;    InputTexture.GetDimensions(width, height);    if (id.x >= width || id.y >= height) return;    float4 pixel = InputTexture[id.xy];    pixel.r *= multiplier;   // умножаем только красный канал    // Остальные каналы (g,b,a) оставляем без изменений или обнуляем    OutputTexture[id.xy] = pixel;}

В этом шейдере каждый поток обрабатывает один пиксель (элемент данных). Группа потоков имеет размер 16×1×1, что даёт 16 потоков на группу — кратность 16 удобна для большинства GPU.

В приложении мы создаём битмап (текстуру) размером 256×1 из массива значений float. Для простоты используем формат DXGI_FORMAT_B8G8R8A8_UNORM, где значения должны быть в диапазоне [0,1] — это удобно для визуализации. Если вам нужны числа с плавающей точкой за пределами [0,1], можно использовать формат DXGI_FORMAT_R32G32B32A32_FLOAT, но тогда нужно убедиться, что устройство и контекст поддерживают такой формат (в большинстве современных систем поддерживается).

Добавим функцию, которая создаёт битмап из вектора чисел:

HRESULT CreateInputBitmapFromArray(    ID2D1DeviceContext2* d2dContext,    const std::vector<float>& data,    ID2D1Bitmap1** outBitmap){    if (data.empty()) return E_INVALIDARG;    // Создаём массив пикселей типа D2D1::ColorF (RGBA)    std::vector<D2D1::ColorF> pixels(data.size());    for (size_t i = 0; i < data.size(); ++i)    {        float v = data[i];        // Ограничим значения в [0,1] для UNORM формата (если данные выходят за пределы, лучше использовать float формат)        pixels[i] = D2D1::ColorF(v, 0.0f, 0.0f, 1.0f);    }    D2D1_SIZE_U size = D2D1::SizeU(static_cast<UINT32>(data.size()), 1);    D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1(        D2D1_BITMAP_OPTIONS_NONE,        D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)    );    HRESULT hr = d2dContext->CreateBitmap(        size,        pixels.data(),        size.width * sizeof(D2D1::ColorF),        &props,        outBitmap    );    return hr;}

Изменения в коде эффекта.

В функции Initialize мы загружаем массив данных, создаём битмап и устанавливаем его как вход эффекта вместо загрузки изображения:

// Вместо LoadTextureFromFilestd::vector<float> inputData(256);for (int i = 0; i < 256; ++i)    inputData[i] = (float)i / 255.0f;  // градиент от 0 до 1Microsoft::WRL::ComPtr<ID2D1Bitmap1> inputBitmap;CreateInputBitmapFromArray(m_d2dContext.Get(), inputData, &inputBitmap);m_HabrEffect->SetInput(0, inputBitmap.Get());

Теперь при рендеринге эффект обработает одномерную текстуру, умножит красный канал каждого пикселя на заданный коэффициент (например, 2.0) и выведет результат на экран. Мы увидим полосу, яркость которой возрастает слева направо и усилена вдвое.

Настройка размеров группы потоков. В нашем узле (HabrNodeHandler) метод CalculateThreadgroups должен быть адаптирован под ширину текстуры. Если мы знаем, что высота всегда равна 1, то:

IFACEMETHODIMP HabrNodeHandler::CalculateThreadgroups(    _In_ const D2D1_RECT_L* pOutputRect,    _Out_ UINT32* pDimensionX,    _Out_ UINT32* pDimensionY,    _Out_ UINT32* pDimensionZ){    UINT32 width = pOutputRect->right - pOutputRect->left;    UINT32 height = pOutputRect->bottom - pOutputRect->top; // всегда 1    const UINT32 groupSizeX = 16;    const UINT32 groupSizeY = 1;    *pDimensionX = (width + groupSizeX - 1) / groupSizeX;    *pDimensionY = (height + groupSizeY - 1) / groupSizeY;    *pDimensionZ = 1;    return S_OK;}

Получение результата обратно на CPU:

Если вам нужно прочитать обработанные данные обратно в оперативную память, задача усложняется, поскольку RWTexture2D, используемая как выход, является ресурсом только для GPU. В контексте Direct2D эффектов прямой доступ к данным через ID2D1ResourceTexture не предусмотрен. Однако есть несколько путей:

Использовать буферы (ID2D1Buffer) — они поддерживают Map для чтения/записи с CPU. Но тогда придётся переписать узел на работу с буферами, а не с текстурами, что выходит за рамки данной статьи (это тема для отдельного материала).

Создать отдельный системный битмап и скопировать результат — после рендеринга можно скопировать содержимое целевого битмапа (который является целью рендеринга) в системный битмап с флагом D2D1_BITMAP_OPTIONS_CPU_READ через метод ID2D1DeviceContext::CopyFromBitmap. Однако в нашем случае выходной ресурс — это ID2D1ResourceTexture, а не ID2D1Bitmap, поэтому скопировать напрямую нельзя. Можно изменить архитектуру: сделать два прохода — сначала вычислительный шейдер пишет во временный ресурс, затем через стандартный DrawBitmap скопировать его на целевой битмап, который уже можно прочитать. Это неэффективно, но возможно.

Интегрировать Direct3D 11 напрямую — выйти из обёртки Direct2D и использовать чистый D3D11 Compute Shader с буферами, где чтение данных на CPU тривиально через staging-буферы. Это рекомендуемый подход для серьёзных расчётов. Но мы ещё не дошли до этого.

Собственно, в следующей статье рассмотрим вариант с буфером.

ссылка на оригинал статьи https://habr.com/ru/articles/1072742/