c++asynchronousdirect2dstdthreadmute

Load bitmap asynchronously Direct2D C++


I have a class that represents an gui-element, that has method to set an image on it's background:

class Element
{
public:
ID2D1Bitmap *image;
ID2D1DeviceContext *target;
int x, y, width, height;

Element(ID2D1DeviceContext *target, int x, int y, int width, int height)
{
    image = nullptr; 
    this->target = target;
    this->x = x; this->y = y; this->width = width; this->height = height;
}

void Render()
{
    if(image)
       target->DrawBitmap(image, D2D1::RectF(x, y, x + width, y + height));
}

void setBackgroundImage(const wchar_t* path)
{
    if (!path || wcslen(path) == 0)
        return;
    
    IWICBitmapFrameDecode* d2dBmpSrc = nullptr;
    IWICBitmapDecoder* d2dDecoder = nullptr;

        d2dWICFactory->CreateDecoderFromFilename(path, NULL, GENERIC_READ,
            WICDecodeMetadataCacheOnLoad, &d2dDecoder);
        if (d2dDecoder)
        {
            d2dDecoder->GetFrame(0, &d2dBmpSrc);
            if (d2dBmpSrc)
            {
                d2dWICFactory->CreateFormatConverter(&d2dConverter2);
                d2dConverter2->Initialize(d2dBmpSrc, GUID_WICPixelFormat32bppPBGRA,
                    WICBitmapDitherTypeNone, NULL, 0.f, WICBitmapPaletteTypeMedianCut);

                ID2D1Bitmap *temp = nullptr;
                tar->CreateBitmapFromWicBitmap(d2dConverter2, NULL, &temp);

                if (temp)
                {
                    D2D1_SIZE_F si = temp->GetSize();

                    tar->CreateBitmap(D2D1::SizeU(si.width, si.height), 0, 0, D2D1::BitmapProperties(
                        D2D1::PixelFormat(DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE::D2D1_ALPHA_MODE_PREMULTIPLIED)
                    ), &image);

                    image->CopyFromBitmap(0, temp, 0);
                    SafeRelease(&temp);
                }
            }
        }

        SafeRelease(&d2dDecoder);
        SafeRelease(&d2dBmpSrc);
        SafeRelease(&d2dConverter2);
}
~Element(){SafeRelease(&image);}

}*object[100] = {NULL};

int main()
{
ID2D1Factory *factory = nullptr;
D2D1CreateFactory(D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_MULTI_THREADED, &factory);

ID2D1DeviceContext *target = ObtainDeviceContext(factory);
object[0] = new Element(target, 0, 0, 100, 100);
object[0]->setBackgroundImage(L"img.png");

for(;;) // But in reality here also is Windows message loop with Dispatch message
{
    target->BeginDraw();
    target->Clear(D2D1::ColorF(1, 1, 1));
    for(int i = 0; i < 100 && object[i]; i++)
        object[i]->Render();
    target->EndDraw();
}

return 0;
}

All works fine, but the problem, is that loading an image is obviously hangs the program.

Unfortunately, my asynchronous c++ skills are almost empty. I tried to just change method to this:

void Element::setBackgroundImage(const wchar_t*path)
{
    thread th(LoadImage(this, path)); th.detach();
}

And jsut bring all code from the method to global function, with additional first argument - LoadImage(Object*,const wchar_t*);

Unfortunately, it immediately crashes. Then I created global variable mutex mu and placed mu.lock() and mu.unlock() as first and last line in LoadImage correspond. Still crashes.

Maybe I also need to lock in Render, and probably on destructor? By the way, what will happen if destructor will try to release image variable at time when it is locked by another thread? It will not be free, and hence memory leak?

Can someone please explain at least general conception of using asynchronous c++ programming for my case? Not thread::join, I need the main thread be going.

Also I would appreciate if You explain how to properly make render loop running in asynchronous thread.


Solution

  • First off, your code is far from a minimal reproducible example. But then again, I've never touched Direct2D before, and I still managed to get it working.

    Anyway, it doesn't matter what you do, you'll have to load the image before you're able to render it. Since you used browsers as an example in the comments, what they do is indeed fetch and load files on separate threads, but before they're loaded and in memory, they're rendering placeholders or nothing at all.

    Loading the image in a separate thread and locking a mutex in the render function is the same as not using any threads at all (functionally, at least. The main thread will still block in render while the image is being loaded). Really, what you tried before, with this:

    void Element::setBackgroundImage(const wchar_t*path)
    {
        thread th(LoadImage(this, path)); th.detach();
    }
    

    should work, (in my testing even with a D2D1_FACTORY_TYPE_SINGLE_THREADED factory), since you're not rendering the element if image == nullptr, which acts a sort of lock. Using a proper bool loaded state variable would be even better, but it still works.

    You just misused the std::thread constructor syntax, since you're handing it the return value of LoadImage with the arguments this and path, which is almost certainly not a function pointer or lambda, given that your program is crashing (In simple words: You're calling the function instead of passing it in as an argument).

    Creating a std::thread calling Element::setBackgroundImage properly:

    class Element
    {
    public:
    ...
        void asyncSetBackgroundImage(const wchar_t* path)
        {
            std::thread thread(
                &Element::setBackgroundImage,
                this,
                path
            );
            thread.detach();
        }
    ...
    };
    

    Also I would appreciate if You explain how to properly make render loop running in asynchronous thread.

    It's really as simple as handling the Win32 window message loop on the main thread, while doing the rendering on a separate thread.

    bool render_state = true;
    std::thread render_thread(
        [&]()
        {
            while (render_state)
            {
                target->BeginDraw();
                target->Clear(D2D1::ColorF(1, 1, 1));
                for(int i = 0; i < 100 && object[i]; i++)
                    object[i]->Render();
                target->EndDraw();
            }
        }
    );
    render_thread.detach();
    
    while (window.win32_loop_iteration())
    { }
    
    render_state = false;
    

    This is where you might want to use a mutex, to synchronize the start and stop of the loop, but aside from that, this is enough.


    Also, a (mostly) minimally reproducible example; all that's missing is the WINDOW::Window class, which can create a Win32 window among other things:

    #include <d2d1.h>
    #include <d2d1_1.h>
    #include <wincodec.h>
    
    #include "window/window.hpp"
    
    #include <thread>
    
    template <class T> void SafeRelease(T **ppT)
    {
        if (*ppT)
        {
            (*ppT)->Release();
            *ppT = NULL;
        }
    }
    
    IWICImagingFactory* d2dWICFactory = nullptr;
    
    class Element
    {
    public:
        ID2D1Bitmap *image;
        ID2D1DeviceContext *target;
        int x, y, width, height;
    
        Element(ID2D1DeviceContext *target, int x, int y, int width, int height)
        {
            image = nullptr; 
            this->target = target;
            this->x = x; this->y = y; this->width = width; this->height = height;
        }
    
        void Render()
        {
            if(image)
               target->DrawBitmap(image, D2D1::RectF(x, y, x + width, y + height));
        }
    
        void setBackgroundImage(const wchar_t* path)
        {
            if (!path || wcslen(path) == 0)
                return;
    
            IWICBitmapFrameDecode* d2dBmpSrc = nullptr;
            IWICBitmapDecoder* d2dDecoder = nullptr;
            IWICFormatConverter* d2dConverter2 = nullptr;
    
            d2dWICFactory->CreateDecoderFromFilename(path, NULL, GENERIC_READ,
                WICDecodeMetadataCacheOnLoad, &d2dDecoder);
            if (d2dDecoder)
            {
                d2dDecoder->GetFrame(0, &d2dBmpSrc);
                if (d2dBmpSrc)
                {
                    d2dWICFactory->CreateFormatConverter(&d2dConverter2);
                    d2dConverter2->Initialize(d2dBmpSrc, GUID_WICPixelFormat32bppPBGRA,
                        WICBitmapDitherTypeNone, NULL, 0.f, WICBitmapPaletteTypeMedianCut);
    
                    ID2D1Bitmap *temp = nullptr;
                    target->CreateBitmapFromWicBitmap(d2dConverter2, NULL, &temp);
    
                    if (temp)
                    {
                        D2D1_SIZE_F si = temp->GetSize();
    
                        target->CreateBitmap(D2D1::SizeU(si.width, si.height), 0, 0, D2D1::BitmapProperties(
                            D2D1::PixelFormat(DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE::D2D1_ALPHA_MODE_PREMULTIPLIED)
                        ), &image);
    
                        image->CopyFromBitmap(0, temp, 0);
                        SafeRelease(&temp);
                    }
                }
            }
    
            SafeRelease(&d2dDecoder);
            SafeRelease(&d2dBmpSrc);
            SafeRelease(&d2dConverter2);
        }
    
        void asyncSetBackgroundImage(const wchar_t* path)
        {
            std::thread thread(
                &Element::setBackgroundImage,
                this,
                path
            );
            thread.detach();
        }
    
        ~Element()
        {
            SafeRelease(&image);
        }
    };
    
    Element* object[100] = { nullptr };
    
    int main(
        int argument_count,
        char** arguments
    )
    {
        CoInitialize(
            NULL
        );
    
        ID2D1Factory1 *factory = nullptr;
        D2D1CreateFactory(D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory);
    
        ID2D1HwndRenderTarget* render_target = nullptr;
    
        D2D1_RENDER_TARGET_PROPERTIES properties = {};
        properties.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
        properties.pixelFormat = D2D1::PixelFormat(DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE::D2D1_ALPHA_MODE_IGNORE);
        properties.dpiX = 0;
        properties.dpiY = 0;
        properties.usage = D2D1_RENDER_TARGET_USAGE::D2D1_RENDER_TARGET_USAGE_NONE;
        properties.minLevel = D2D1_FEATURE_LEVEL::D2D1_FEATURE_LEVEL_DEFAULT;
    
        WINDOW::Window window = {};
        window.create();
    
        window.show();
    
        D2D1_HWND_RENDER_TARGET_PROPERTIES hwnd_properties = {};
        hwnd_properties.hwnd = window.win32_handle_get();
        hwnd_properties.pixelSize.width = window.size_x_get();
        hwnd_properties.pixelSize.height = window.size_y_get();
        hwnd_properties.presentOptions = D2D1_PRESENT_OPTIONS::D2D1_PRESENT_OPTIONS_NONE;
    
        factory->CreateHwndRenderTarget(
            &properties,
            &hwnd_properties,
            &render_target
        );
    
        ID2D1DeviceContext *target = reinterpret_cast<ID2D1DeviceContext*>(render_target);
    
        CoCreateInstance(
            CLSID_WICImagingFactory,
            NULL,
            CLSCTX_INPROC_SERVER,
            __uuidof(IWICImagingFactory),
            reinterpret_cast<LPVOID*>(&d2dWICFactory)
        );
    
        object[0] = new Element(target, 0, 0, 100, 100);
        object[0]->asyncSetBackgroundImage(L"img.png");
    
        while (window.win32_loop_iteration())
        {
            target->BeginDraw();
            target->Clear(D2D1::ColorF(1, 1, 1));
            for(int i = 0; i < 100 && object[i]; i++)
                object[i]->Render();
            target->EndDraw();
        }
    
        return 0;
    }