isagsadvb
isagsadvb

Reputation: 33

Get current Direct2D render bitmap

I want to make UI control blur-behind effect, like css backdrop-filter:blur(). Logic is seems to be simple:

  1. Get current ID2D1DeviceContext* render bitmap;
  2. Crop to the area, that needed;
  3. Create ID2D1Effect blur, and pass there bitmap;
  4. Draw effect to current context using DrawImage.

I am facing issues on the first step.

I found two ways to get bitmap from ID2D1DeviceContext*: ID2D1DeviceContext::GetTarget(ID2D1Image**) and ID2D1Bitmap::CopyFromRenderTarget().

Seems, that calling that functions between BeginDraw() and EndDraw() returns white bitmap from calling Clear before BeginDraw(), as I assume due to double-buffering. If in the middle of render loop, add another EndDraw() and BeginDraw() pair, then, for some reason forward draw calls until the next EndDraw() will not do anything (perhaps because middle EndDraw() returned D2DERR_RECREATE_TARGET, and I currently didn't handle it.

Anyway, rendering the whole window in the middle of render loop for every blurred ui element seems to be bad idea, and also, probably it will cause flickering.

I there way to obtain current-state (compatible) bitmap from ID2D1DeviceContext* without bliting it to target? Or maybe there is better way to achieve effect I want?

Another example of blur effect in case of uwp in-app blur (not blur-behind window effect):

Minimal example with ID2D1DeviceContext::GetTarget(ID2D1Image**), that just fills target white, instead of drawing blurred red rectangle on blue background:

#include <Windows.h>

HDC hdcDevice = GetDC(NULL);
int xw = GetDeviceCaps(hdcDevice, HORZRES);
int yw = GetDeviceCaps(hdcDevice, VERTRES);

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp);

HWND hwnd;

#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <wchar.h>
#include <math.h>

#include <d2d1_1.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <wincodec.h>

#pragma comment(lib, "d2d1")
#pragma comment(lib, "dxguid.lib")

template<class Interface>
inline void SafeRelease(
    Interface** ppInterfaceToRelease)
{
    if (*ppInterfaceToRelease != NULL)
    {
        (*ppInterfaceToRelease)->Release();
        (*ppInterfaceToRelease) = NULL;
    }
}

#ifndef Assert
#if defined( DEBUG ) || defined( _DEBUG )
#define Assert(b) do {if (!(b)) {OutputDebugStringA("Assert: " #b "\n");}} while(0)
#else
#define Assert(b)
#endif //DEBUG || _DEBUG
#endif

#ifndef HINST_THISCOMPONENT
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
#endif

ID2D1Factory* m_pDirect2dFactory;
ID2D1HwndRenderTarget* m_pRenderTarget;
ID2D1DeviceContext* target;
ID2D1SolidColorBrush* brush;

void Release()
{
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&target);
    SafeRelease(&brush);
}


void Init()
{
    Release();

    m_pDirect2dFactory = nullptr;
    m_pRenderTarget = nullptr;

    SUCCEEDED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory));

    RECT rc;
    GetClientRect(hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(
        rc.right - rc.left,
        rc.bottom - rc.top);

    // Create a Direct2D render target.
    SUCCEEDED(m_pDirect2dFactory->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(),
        D2D1::HwndRenderTargetProperties(hwnd, size),
        &m_pRenderTarget));

    m_pRenderTarget->QueryInterface(&target);

    m_pRenderTarget->CreateSolidColorBrush(
        D2D1::ColorF(255,0,0),
        &brush
    );
}

void Render()
{
    target->BeginDraw();
    target->Clear(D2D1::ColorF(D2D1::ColorF::Blue));
    target->SetTransform(D2D1::Matrix3x2F::Identity());

    D2D1_SIZE_F rtSize = target->GetSize();

    target->FillRectangle(D2D1::RectF(30, 30, 100, 100), brush);

    ID2D1Effect *blur = nullptr;
    target->CreateEffect(CLSID_D2D1GaussianBlur, &blur);
    if (blur)
        blur->SetValue(D2D1_GAUSSIANBLUR_PROP::D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, 10);

    ID2D1Image* img = nullptr;
    target->GetTarget(&img); // Checked, img is not nullptr, after call

    blur->SetInput(0, img);

    target->DrawImage(blur); // DrawImage(img) also draws white
    // If to remove DrawImage call, red rectangle on blue background will be displayed,
    // yet, of course, not blurred

    SafeRelease(&blur);

    auto hr = target->EndDraw();

    if (hr == D2DERR_RECREATE_TARGET)
    {
        hr = S_OK;
        Init();
    }
}



int WINAPI WinMain(HINSTANCE hin, HINSTANCE, LPSTR, int)
{
    ReleaseDC(NULL, hdcDevice);

    WNDCLASS c = { NULL };
    c.lpszClassName = L"GROKEN";
    c.lpfnWndProc = WndProc;
    c.hInstance = hin;
    c.style = CS_VREDRAW | CS_HREDRAW;
    c.hCursor = LoadCursor(NULL, IDC_ARROW);
    c.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));
    RegisterClass(&c);

    int cx = 500, cy = 500;
    int x = xw / 2 - cx / 2, y = yw / 2 - cy / 2;

    hwnd = CreateWindowEx(NULL, L"GROKEN", L"asd", WS_POPUP | WS_VISIBLE, x, y, cx, cy, NULL, NULL, hin, 0);

    HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
    CoInitialize(NULL);

    Init();

    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0))
    {
        Render();
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    CoUninitialize();

    return 0;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
{
    switch (message)
    {
    default:
        return DefWindowProc(hwnd, message, wp, lp);
    }
    return NULL;
}

More particulary, in my case I have array of pointers to ui controll class objects, each has own Render(ID2D1DeviceContext*), so window's render loop looks like this:

inline void WINDOW::Render()
{
Init(); // (re)Initialize target as ID2D1DeviceContext*, if needed;

target->BeginDraw();
target->Clear(D2D1::ColorF(D2D1::ColorF::Blue));
target->SetTransform(D2D1::Matrix3x2F::Identity());

for(int i = 0; i < ui_elements_count; i++)
     ui_element[i]->Draw(target);

this->hr = target->EndDraw();
}

...

inline void UI_ELEMENT::Draw(ID2D1DeviceContext *target)
{
    ...
    if(this->blurRadius > 0)
    {
         BlurRectangle(this->x, this->y, this->cx, this->cy, this->blurRadius);
    }
}

Update

Using Simon's Mourier answer I tried to create the solution I need, but stuck, trying to SetInput backbuffer' bitmap to ID2D1Effect*. For some reasone, it either does not set in SetInput method correctly, or in DrawImage call. The saddest thing is that backbuffer bitmap is actually valid, it could be drawn by same DrawImage call. Maybe I should specify another bitmap options when create it?

// other code is same
ID2D1DeviceContext* target;

void Init()
{
    Release();
    m_pRenderTarget = NULL;


    RECT rc;
    GetClientRect(hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(
        rc.right - rc.left,
        rc.bottom - rc.top);

    // Create a Direct2D render target.
    SUCCEEDED(m_pDirect2dFactory->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(),
        D2D1::HwndRenderTargetProperties(hwnd, size),
        &m_pRenderTarget));

    m_pRenderTarget->QueryInterface(&target);
}


inline void Blur(ID2D1DeviceContext* backTarget, int rad, RECT r)
// r is not used, should contain element bound box
{
    RECT rc;
    GetClientRect(hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(
        rc.right - rc.left,
        rc.bottom - rc.top);

    // Draw rectangle for test
    backTarget->FillRectangle(D2D1::RectF(30, 30, 100, 100), brush);

    ID2D1Bitmap1* bb = nullptr;

    // Create bitmap
    backTarget->CreateBitmap(size, 0, 0, D2D1::BitmapProperties1(
        D2D1_BITMAP_OPTIONS_TARGET,
        D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)
    ), &bb);

    // Copy current taget's state to created bitmap
    bb->CopyFromRenderTarget(0, backTarget, 0);


    ID2D1Effect* blur = nullptr;
    target->CreateEffect(CLSID_D2D1GaussianBlur, &blur);
    blur->SetValue(D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, rad);
    blur->SetInput(0, bb);

    // Draw blurred result. Does nothing
    backTarget->DrawImage(blur);

    // Just test if bb is valid, draw
    // it with some offset. 
    // Draws correctly
    auto a = D2D1::Point2F(100, 0);
    backTarget->DrawImage(bb, a);

    SafeRelease(&blur);
}

inline void Render()
{
    RECT rc;
    GetClientRect(hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(
        rc.right - rc.left,
        rc.bottom - rc.top);

    ID2D1BitmapRenderTarget* tar = nullptr; // Create back buffer
    target->CreateCompatibleRenderTarget(&tar);

    ID2D1DeviceContext* tt = nullptr; 
    // Get exactly back buffer as ID2D1DeviceContext*, 
    // because it has more draw calls, such as DrawImage()
    tar->QueryInterface(&tt);
    tt->CreateSolidColorBrush(
        D2D1::ColorF(255, 0, 0),
        &brush
    );


    tt->BeginDraw();
    tt->Clear(D2D1::ColorF(D2D1::ColorF::Blue));
    tt->SetTransform(D2D1::Matrix3x2F::Identity());

    // loop through ui elements should here, 
    // assume we have an element with blur needed
    Blur(tt, 10, RECT());

    tt->EndDraw();

    target->BeginDraw();

    ID2D1Bitmap* bmp = nullptr;
    tar->GetBitmap(&bmp);

    target->DrawImage(bmp); // Draw back buffer to target

    target->EndDraw();

    SafeRelease(&tar);
    SafeRelease(&tt);
    SafeRelease(&bmp);
    SafeRelease(&brush);
}

Upvotes: 1

Views: 1333

Answers (1)

Simon Mourier
Simon Mourier

Reputation: 139177

There are multiple issues in your code, but the main reason it doesn't work is because you can't use the device context's target bitmap (GPU resource) as a source (since it's a target).

Your code doesn't check for errors (you should) so for example you don't see the error from this call:

auto hr = target->EndDraw();

which returns error D2DERR_INVALID_GRAPH_CONFIGURATION:

enter image description here

The solution is therefore to create an intermediary bitmap (render target), render on it, and draw that bitmap on the target device context, something like this:

// at device context init time
target->CreateCompatibleRenderTarget(&bitmapTarget);

// create blur & set bitmap as input
deviceContext->CreateEffect(CLSID_D2D1GaussianBlur, &blur);

//get bitmap from bitmap rt
bitmapTarget->GetBitmap(&bitmap);
blur->SetInput(0, bitmap);

// at render time
void Render()
{
    // draw to bitmap rt
    bitmapTarget->BeginDraw();
    bitmapTarget->Clear(D2D1::ColorF(D2D1::ColorF::Blue));
    D2D1_SIZE_F rtSize = deviceContext->GetSize();
    bitmapTarget->FillRectangle(D2D1::RectF(30, 30, 100, 100), brush);
    bitmapTarget->EndDraw();

    // draw to dc
    deviceContext->BeginDraw();

    // draw bitmap + effect
    deviceContext->DrawImage(blur);

    deviceContext->EndDraw();
}

And here is the result:

enter image description here

FWIW, I have put a complete correct code here:

#include <Windows.h>
#include <stdlib.h>
#include <d2d1_1.h>
#include <d2d1helper.h>

#pragma comment(lib, "d2d1")
#pragma comment(lib, "dxguid.lib")

template<class Interface>
inline void SafeRelease(Interface** ppInterfaceToRelease)
{
    if (*ppInterfaceToRelease)
    {
        (*ppInterfaceToRelease)->Release();
        (*ppInterfaceToRelease) = NULL;
    }
}

ID2D1DeviceContext* deviceContext;
ID2D1SolidColorBrush* brush;
ID2D1BitmapRenderTarget* bitmapTarget;
ID2D1Effect* blur;
ID2D1Bitmap* bitmap;

void Release()
{
    SafeRelease(&bitmapTarget);
    SafeRelease(&deviceContext);
    SafeRelease(&brush);
    SafeRelease(&blur);
    SafeRelease(&bitmap);
}

void Init(HWND hwnd)
{
    Release();

    ID2D1Factory* factory;
    D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory);

    RECT rc;
    GetClientRect(hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);

    // Create a Direct2D render deviceContext.
    ID2D1HwndRenderTarget* renderTarget;
    factory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(hwnd, size), &renderTarget);

    renderTarget->QueryInterface(&deviceContext);
    renderTarget->CreateSolidColorBrush(D2D1::ColorF(255, 0, 0), &brush);
    renderTarget->CreateCompatibleRenderTarget(&bitmapTarget);
    // create blur & set bitmap as input
    deviceContext->CreateEffect(CLSID_D2D1GaussianBlur, &blur);

    //get bitmap from bitmap rt
    bitmapTarget->GetBitmap(&bitmap);
    blur->SetInput(0, bitmap);

    SafeRelease(&renderTarget);
    SafeRelease(&factory);
}

void Render()
{
    // draw to bitmap rt
    bitmapTarget->BeginDraw();
    bitmapTarget->Clear(D2D1::ColorF(D2D1::ColorF::Blue));
    D2D1_SIZE_F rtSize = deviceContext->GetSize();
    bitmapTarget->FillRectangle(D2D1::RectF(30, 30, 100, 100), brush);
    bitmapTarget->EndDraw();

    // draw to dc
    deviceContext->BeginDraw();

    // draw bitmap + effect
    deviceContext->DrawImage(blur);

    deviceContext->EndDraw();
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
{
    switch (message)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hwnd, message, wp, lp);
    }
    return NULL;
}

int WINAPI WinMain(HINSTANCE hin, HINSTANCE, LPSTR, int)
{
    WNDCLASS c = { NULL };
    c.lpszClassName = L"GROKEN";
    c.lpfnWndProc = WndProc;
    c.hInstance = hin;
    c.style = CS_VREDRAW | CS_HREDRAW;
    c.hCursor = LoadCursor(NULL, IDC_ARROW);
    c.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));
    RegisterClass(&c);

    HDC hdcDevice = GetDC(NULL);
    int xw = GetDeviceCaps(hdcDevice, HORZRES);
    int yw = GetDeviceCaps(hdcDevice, VERTRES);
    int cx = 500, cy = 500;
    int x = xw / 2 - cx / 2, y = yw / 2 - cy / 2;

    HWND hwnd = CreateWindowEx(NULL, L"GROKEN", L"asd", WS_OVERLAPPEDWINDOW | WS_VISIBLE, x, y, cx, cy, NULL, NULL, hin, 0);
    ShowWindow(hwnd, SW_SHOW);
    Init(hwnd);

    MSG msg;
    BOOL bRet;
    while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
    {
        if (bRet == -1)
        {
            // handle the error and possibly exit
        }
        else
        {
            Render();
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    return 0;
}

Upvotes: 3

Related Questions