Search code examples
c++directxdirect2dwicdirect-composition

Directx draws images with lighter colors


I tried a lot of png images and all of them fade in color

Window on the left, original image on the right

Window on the left, original image on the right

#include <Windows.h>
#include <d3d11.h>
#include <d3d11_4.h>
#include <d2d1.h>
#include <d2d1_3.h>
#include <d2d1_3helper.h>
#include <dxgi.h>
#include <wincodec.h>
#include <dcomp.h>
#include <tchar.h>
#include <atlbase.h>

#pragma comment (lib, "d3dcompiler")
#pragma comment (lib, "D2d1")
#pragma comment (lib, "dxguid")
#pragma comment (lib, "d3d11")
#pragma comment (lib, "dxgi")
#pragma comment (lib, "windowscodecs")
#pragma comment (lib, "Dwrite")
#pragma comment (lib, "uuid")
#pragma comment (lib, "dcomp")
#pragma comment (lib, "ole32")

#define WINDOW_CLASS_NAME _T("mainWindow")
#define HR(result) if(SUCCEEDED(hr)){hr = result;}

class Tool
{
    Tool()
    {
        HRESULT hr = OleInitialize(NULL);
        int const d3dDeviceOptions = { D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG };
        D2D1_FACTORY_OPTIONS const d2dDeviceoptions = { D2D1_DEBUG_LEVEL_INFORMATION };
        D3D_FEATURE_LEVEL featureLevelSupported;
        HR(D3D11CreateDevice(
            nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL, d3dDeviceOptions, NULL, 0, D3D11_SDK_VERSION, &D3dDevice, &featureLevelSupported, &D3dDeviceContext
        ));
        HR(D3dDevice->QueryInterface(__uuidof(ID3D11Device5), reinterpret_cast<void**>(&D3dDevice5)));
        HR(D3dDevice5->QueryInterface(&D3d_DXGI_IDC_));
        HR(DCompositionCreateDevice3(D3d_DXGI_IDC_, __uuidof(IDCompositionDevice), reinterpret_cast<void**>(&IdcDevice)));


        HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, d2dDeviceoptions, &D2dFactory));
        HR(D2dFactory->QueryInterface(__uuidof(ID2D1Factory6), reinterpret_cast<void**>(&D2dFactory6)));
        HR(D3dDevice5->QueryInterface(&D3d_DXGI_D2d_));
        HR(D2dFactory6->CreateDevice(D3d_DXGI_D2d_, &D2dDevice5));
        HR(D2dDevice5->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &D2dDeviceContext5));

        HR(CoCreateInstance(CLSID_WICImagingFactory2, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&IWICFactory2)));
    }
    static Tool tool;

public:
    static CComPtr<ID3D11Device>            D3dDevice;
    static CComPtr<ID3D11Device5>           D3dDevice5;
    static CComPtr<ID3D11DeviceContext>     D3dDeviceContext;
    static CComPtr<ID2D1Device5>            D2dDevice5;
    static CComPtr<ID2D1Factory>            D2dFactory;
    static CComPtr<ID2D1Factory6>           D2dFactory6;
    static CComPtr<ID2D1DeviceContext5>     D2dDeviceContext5;
    static CComPtr<IDCompositionDevice>     IdcDevice;
    static CComPtr<IWICImagingFactory2>     IWICFactory2;
    static CComPtr<IDXGIDevice4>            D3d_DXGI_IDC_;
    static CComPtr<IDXGIDevice4>            D3d_DXGI_D2d_;
};
CComPtr<ID3D11Device>           Tool::D3dDevice;
CComPtr<ID3D11Device5>          Tool::D3dDevice5;
CComPtr<ID3D11DeviceContext>    Tool::D3dDeviceContext;
CComPtr<ID2D1Device5>           Tool::D2dDevice5;
CComPtr<ID2D1Factory>           Tool::D2dFactory;
CComPtr<ID2D1Factory6>          Tool::D2dFactory6;
CComPtr<ID2D1DeviceContext5>    Tool::D2dDeviceContext5;
CComPtr<IDCompositionDevice>    Tool::IdcDevice;
CComPtr<IWICImagingFactory2>    Tool::IWICFactory2;
CComPtr<IDXGIDevice4>           Tool::D3d_DXGI_IDC_;
CComPtr<IDXGIDevice4>           Tool::D3d_DXGI_D2d_;
Tool Tool::tool = Tool::Tool();

HRESULT LoadPngAsBitmap(_In_ PCWSTR uri, _Outptr_ ID2D1Bitmap** bitmap)
{
    HRESULT hr = S_OK;

    CComPtr<IWICStream> stream;
    HR(Tool::IWICFactory2->CreateStream(&stream));
    HR(stream->InitializeFromFilename(uri, GENERIC_READ));

    CComPtr<IWICBitmapDecoder> decoder;
    HR(Tool::IWICFactory2->CreateDecoderFromStream(stream, NULL, WICDecodeMetadataCacheOnLoad, &decoder));

    CComPtr<IWICBitmapFrameDecode> frameDecode;
    HR(decoder->GetFrame(0, &frameDecode));

    CComPtr<IWICFormatConverter> converter;
    HR(Tool::IWICFactory2->CreateFormatConverter(&converter));

    HR(converter->Initialize(frameDecode, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0.0f, WICBitmapPaletteTypeCustom));
    HR(Tool::D2dDeviceContext5->CreateBitmapFromWicBitmap(converter, NULL, bitmap));
    return hr;
}

LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    LRESULT hr = S_OK;
    switch (msg)
    {
    default:
        hr = DefWindowProc(hwnd, msg, wParam, lParam);
        break;
    }
    return hr;
}
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR pszCmdLine, _In_ int iCmdShow)
{
    HRESULT hr = S_OK;
    HWND mainWindow;
    CComPtr<IDCompositionTarget>  IdcTarget;
    CComPtr<IDCompositionVisual>  IdcVisual;
    CComPtr<IDCompositionSurface> IdcSurface;
    CComPtr<ID2D1Bitmap1>         ExchangeChainsBitmap;
    WNDCLASSEX wndClassEx = { };
    wndClassEx.cbSize = sizeof(WNDCLASSEX);
    wndClassEx.lpfnWndProc = wndProc;
    wndClassEx.hInstance = hInstance;
    wndClassEx.cbClsExtra = DLGWINDOWEXTRA;
    wndClassEx.style = CS_DBLCLKS;
    wndClassEx.lpszClassName = WINDOW_CLASS_NAME;
    if (!RegisterClassEx(&wndClassEx)) {
        return 0;
    }
    mainWindow = CreateWindowEx(
        WS_EX_ACCEPTFILES,
        WINDOW_CLASS_NAME, _T("text"),
        WS_VISIBLE | WS_POPUP,
        100, 100, 1000, 1000,
        NULL, NULL, hInstance, NULL
    );
    ShowWindow(mainWindow, SW_SHOWDEFAULT);


    HR(Tool::IdcDevice->CreateTargetForHwnd(mainWindow, TRUE, &IdcTarget));
    HR(Tool::IdcDevice->CreateVisual(&IdcVisual));

    RECT windowRect;
    GetClientRect(mainWindow, &windowRect);
    HR(Tool::IdcDevice->CreateSurface(
        windowRect.right - windowRect.left, windowRect.bottom - windowRect.top,
        DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_ALPHA_MODE_PREMULTIPLIED,
        &IdcSurface
    ));
    HR(IdcVisual->SetContent(IdcSurface));

    HR(IdcTarget->SetRoot(IdcVisual));
    HR(Tool::IdcDevice->Commit());

    POINT offset;
    CComPtr<IDXGISurface> _Surface_DXGI_Draw;
    HR(IdcSurface->BeginDraw(
        NULL, __uuidof(IDXGISurface), reinterpret_cast<void**>(&_Surface_DXGI_Draw), &offset
    ));

    D2D1_BITMAP_PROPERTIES1 properties = { };
    properties.pixelFormat = D2D1::PixelFormat(DXGI_FORMAT_R16G16B16A16_FLOAT, D2D1_ALPHA_MODE_PREMULTIPLIED);
    properties.dpiX = properties.dpiY = 96;
    properties.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW;
    properties.colorContext = NULL;
    HR(Tool::D2dDeviceContext5->CreateBitmapFromDxgiSurface(
        _Surface_DXGI_Draw, properties, &ExchangeChainsBitmap
    ));

    Tool::D2dDeviceContext5->SetTarget(ExchangeChainsBitmap);
    Tool::D2dDeviceContext5->BeginDraw();
    Tool::D2dDeviceContext5->Clear(NULL);
    /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
    CComPtr<ID2D1Bitmap> img;
    HR(LoadPngAsBitmap(_T("C:\\image2.png"), &img));
    Tool::D2dDeviceContext5->DrawBitmap(img);
    /*-------------------------------------------------------------------------------------------------------*/
    HR(Tool::D2dDeviceContext5->EndDraw());
    HR(IdcSurface->EndDraw());
    HR(Tool::IdcDevice->Commit());


    int i = 0;
    MSG msg = { };
    while ((i = GetMessage(&msg, NULL, 0, 0))) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return i;
}

I attempted to save the bitmap created by LoadPngAsBitmap to a file, and its colors appear correct. However, I suspect the issue is related to alpha premultiplication. I’m unsure how to resolve this problem or modify the premultiplied flags. Changing 'D2D1_ALPHA_MODE_PREMULTIPLIED' to 'D2D1_ALPHA_MODE_IGNORE' within CreateBitmapFromDxgiSurface did not yield any changes in the result.


Solution

  • This is because you use the DXGI_FORMAT_R16G16B16A16_FLOAT format, which is a 16bit floating point swap chain (indirectly created by Direct Composition), introduced for HDR rendering.

    As explained here Use DirectX with Advanced Color on high/standard dynamic range displays

    By default, a swap chain created with a floating point pixel format is treated as if it uses the DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 color space.

    DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 is the scRGB color space, so you need to either convert the WIC bitmap upfront, or as I'll show here, use the D2D1 Color Management builtin effect for that, which will use the GPU.

    First you need to determine what's the image colorspace (aka color context), if any. By default, it will be sRGB:

    HRESULT LoadPngAsBitmap(_In_ PCWSTR uri, _Outptr_ ID2D1Bitmap** bitmap, ID2D1ColorContext1** colorContext)
    {
        HRESULT hr = S_OK;
        CComPtr<IWICStream> stream;
        HR(Tool::IWICFactory2->CreateStream(&stream));
        HR(stream->InitializeFromFilename(uri, GENERIC_READ));
    
        CComPtr<IWICBitmapDecoder> decoder;
        HR(Tool::IWICFactory2->CreateDecoderFromStream(stream, NULL, WICDecodeMetadataCacheOnLoad, &decoder));
    
        CComPtr<IWICBitmapFrameDecode> frameDecode;
        HR(decoder->GetFrame(0, &frameDecode));
    
        UINT count;
        HR(frameDecode->GetColorContexts(0, nullptr, &count));
        if (count == 0)
        {
            // sRGB
            HR(Tool::D2dDeviceContext5->CreateColorContextFromDxgiColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, colorContext));
        }
        else
        {
            // we'll consider the first but you should choose one
            // more than one color space is kinda weird anyway
            CComPtr<IWICColorContext> wicColorContext;
            HR(frameDecode->GetColorContexts(1, &wicColorContext, &count));
            CComPtr<ID2D1ColorContext> cc;
            HR(Tool::D2dDeviceContext5->CreateColorContextFromWicColorContext(wicColorContext, &cc));
            HR(cc->QueryInterface(colorContext));
        }
    
        CComPtr<IWICFormatConverter> converter;
        HR(Tool::IWICFactory2->CreateFormatConverter(&converter));
        HR(converter->Initialize(frameDecode, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0, WICBitmapPaletteTypeCustom));
    
        HR(Tool::D2dDeviceContext5->CreateBitmapFromWicBitmap(converter, NULL, bitmap));
        return hr;
    }
    

    And then you can change your rendering code like this:

    // create DXGI surface (not need to specify bitmap properties here)
    HR(Tool::D2dDeviceContext5->CreateBitmapFromDxgiSurface(_Surface_DXGI_Draw, nullptr, &ExchangeChainsBitmap));
    
    // get WIC image & color context
    CComPtr<ID2D1Bitmap> img;
    CComPtr<ID2D1ColorContext1> imgColorContext;
    HR(LoadPngAsBitmap(L"C:\\image2.png", &img, &imgColorContext));
    
    // create DXGI scRGB color context
    CComPtr<ID2D1ColorContext1> colorContext;
    HR(Tool::D2dDeviceContext5->CreateColorContextFromDxgiColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709, &colorContext));
    
    // create D2D1 Color Management effect
    CComPtr<ID2D1Effect> colorManagement;
    HR(Tool::D2dDeviceContext5->CreateEffect(CLSID_D2D1ColorManagement, &colorManagement));
    
    // define conversion parameter
    HR(colorManagement->SetValue(D2D1_COLORMANAGEMENT_PROP_QUALITY, D2D1_COLORMANAGEMENT_QUALITY_BEST));
    HR(colorManagement->SetValue(D2D1_COLORMANAGEMENT_PROP_SOURCE_COLOR_CONTEXT, imgColorContext));
    HR(colorManagement->SetValue(D2D1_COLORMANAGEMENT_PROP_DESTINATION_COLOR_CONTEXT, colorContext));
    
    // set bitmap (from WIC) as input
    colorManagement->SetInput(0, img);
    
    Tool::D2dDeviceContext5->SetTarget(ExchangeChainsBitmap);
    Tool::D2dDeviceContext5->BeginDraw();
    Tool::D2dDeviceContext5->Clear(NULL);
    
    // "draw" effect
    Tool::D2dDeviceContext5->DrawImage(colorManagement);
    
    HR(Tool::D2dDeviceContext5->EndDraw());
    HR(IdcSurface->EndDraw());
    HR(Tool::IdcDevice->Commit());
    

    PS: of course, you can change the code to test if the conversion is needed or not.