Skip to content

Request for feedback: multi-threaded update/render domain with dynamic fonts & texture updates (v1.92) #8597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ocornut opened this issue Apr 26, 2025 · 13 comments

Comments

@ocornut
Copy link
Owner

ocornut commented Apr 26, 2025

Version/Branch of Dear ImGui:

Branch: dynamic_fonts

Details:

Regarding the work-in-progress rewrite of textures & fonts system in dynamic_fonts.

This is a topic dedicated to discussing the issue mentioned by @DucaRii in #8465 (comment)

Lastly Id like to voice a concern regarding the ImGuiPlatformIO::Textures list.
From what ive seen its filled up and updated in the span from ImGui::NewFrame() -> ImGui::EndFrame() and then processed in the ImGui_ImplXXX_RenderDrawData() function.
I believe that this is gonna create problems for people who run ImGui in a thread separate from the rendering thread. For example, a lot of game engine devs i know (including myself) run ImGui inside of the games logic thread, call ImGui::Render and then store the resulting DrawData inside a mutex.
Now all that happens in the rendering thread (DirectX in my case) is it calls the necessary ImGui_ImplXXX_* functions, reusing the same DrawData until the logic thread updates it.
Reason for that being you will frequently run into race conditions if youre trying to output or modify anything about the actors from the rendering thread.
Im not sure if ImGui was ever supposed to even be compatible with that but it was because the ImGui_ImplXXX_* functions only acted upon the passed DrawData and not the actual imgui context, which now isnt the case anymore due to the Textures list.
I dont know how many people this will realistically affect but I know that sort of pattern is very common when working with game engines so it might be worth considering.

I don't have a proposal just yet but I'm opening an issue to gather feedback from people using custom engine who use staged rendering, e.g. Update thread vs Render thread.


Outline of the current design as of today (2025/04/26) is described below.
It is likely we would need to change it.

  • ImGui::EndFrame() updates a texture lists in platform_io.Textures[].
  • Backend rendering function e.g. ImGui_ImplDX11_RenderDrawData() are iterating this list to update dirty textures (see code below).
  • The list is also used in backend shutdown functions to destroy textures.

Note: backends are also their exposing the function that "updates" a texture, e.g. ImGui_ImplDX11_UpdateTexture() the reasoning being that user application may want to process this update before the _RenderDrawData() function is called.

void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
{
   ...
    // Catch up with texture updates. Most of the times, the list will have 1 element will an OK status, aka nothing to do.
    for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
        if (tex->Status != ImTextureStatus_OK)
            ImGui_ImplDX11_UpdateTexture(tex);
   ...
void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex)
{
    ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
    if (tex->Status == ImTextureStatus_WantCreate)
    {
        // Create and upload new texture to graphics system
        ....
    }
    else if (tex->Status == ImTextureStatus_WantUpdates)
    {
        // Update selected blocks. We only ever write to textures regions which have never been used before!
        // This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
    }
    else if (tex->Status == ImTextureStatus_WantDestroy)
    {
       ....
}

A problem arises for people who aim to perform rendering from another thread.

The general suggestion until now is that we suggest use to keep a copy of the ImDrawData instances after ImGui::Render(), which may be rendered later from another thread, while in the main/ui thread may already be processing another ImGui frame.
Helpers such as this ImDrawDataSnapshot (#1860 (comment)) allow for easily preserving the ImDrawData contents without needing a do a costly deep-copy. This was based on the premise that all data necessary to rendering was self-contained inside the ImDrawData structure.

This isn't the case anymore with that WIP design.
It is also the case that in the current WIP, ImTextureData is a input/output structure, as the backend is in charge of updating texture Status to acknowledge e.g. creation/update/destruction, and the backend is in charge of storing TexID and BackendUserData inside the texture.

What is guaranteed:

  • Textures are never resized. When a resize is needed, we destroy one texture and create another.
  • As a result of this: Format, Width, Height, BytesPerPixel, UniqueID, and the Pixels pointer are never changing from the point of the view of the user/backend.
  • Partial texture updates (using dirty rect) only ever involve writing to texels that were NEVER used before. In other words, we only notify of required updates in texture regions that previously contained zero-cleared texels.
  • As a result of this: there is no need to care about timing of reading from the CPU texture/buffer and writing to GPU texture/buffer. The dirty rect will always be valid in the CPU buffer (until destroying the textures), and always be unused in the GPU buffer. Since we are using dirty rectangle, it is possible that parts of the CPU buffer contained in a given dirty rectangle for frame N may during frame N+x be written with new data in the other thread, but they will always be texels that are not used in the render for frame N (their own update will be part of a subsequent dirty update for frame N+x).
  • Backends can defer destroying a texture by however long they want. So a typical backend will likely defer destroying by the amount of in-flight frames in their rendering pipeline (often 2 or 3).

Draft for implementing this

A theoretical solution below.

[ UI / Main Thread ]

  • calls ImGui::EndFrame(), ImGui::Render()
  • iterates the io.Textures[] lists, queue creates/update/destroy textures request into a format suitable for Render Task.
    • [1]. for textures in ImTextureStatus_WantCreate or ImTextureStatus_WantUpdate status:
      • [1.1]. store a copy of the ImTextureData struct somewhere for Render Task (*)
      • [1.2]. store a copy of the ImTextureData* pointer somewhere for Render Task. This will be used to write back TexID/BackendUserData during rendering. Because we control destroying textures, we now this pointer is going to be valid.
      • [1.3]. set tex->Status = ImTextureStatus_OK immediately, for next ImGui::NewFrame() to catch on it.
    • [2]. for textures in ImTextureStatus_WantDestroy status:
      • [2.1]. calculate if it is safe to destroy the texture (based on tex->UnusedFrames and your own rendering pipeline). if it is safe:
      • [2.2]. store a copy of the ImTextureData struct somewhere for Render Task (*)
      • [2.3]. set tex->Status = ImTextureStatus_Destroyed immediately, for next ImGui::NewFrame() to catch on it.
      • [2.4]. set tex->SetTexID(ImTextureID_Invalid) immediately.

(*) would need to deep-copy ImTextureData::Updates[] if using multi-dirty rects, but I suspect the single UpdateRect is enough.

[ Render Task ]

  • [3]. process copied requests in a similar way as a rendering backend would. we copied entire ImTextureData struct for simplicity (it's only 88 bytes), the stored copy is "read-only" in the sense there's no point in modifying it. But we will output/write to the original, imgui-owned ImTextureData:
    • [3.1]. create: sizes and pixels pointer are non-mutable and valid. when done, pull the pointer stored in [1.2] and call tex->SetTexID() and tex->BackendUserData = xxx. This is safe because:
      • tex->BackendUserData is only used by subsequent requests also done in Render Task.
      • tex->SetTexID() is only called from ImTextureStatus_WantCreate requests, and that stored value will be pulled via ImDrawCmd::GetTexID().
    • [3.2]. update: sizes and pixels pointer are non-mutable and valid. no problem.
    • [3.3]. destroy: you only need access to your own data (GPU/texture handle). no problem.

I think this should work and be safe.

I'm writing this down as a first step, then I will try to implement it, then I will decide if it's worth making changes to main lib to make it more obviously / easy to implement, or investigate helpers.

I don't have a framework for staged rendering but I can partly simulate this by calling NewFrame() for Frame N+1 before running rendering for Frame N.

It may also make sense that we decide to store the io.Textures[] list somewhere else. The reason it wasn't in ImDrawData is because:

  • each viewport has a ImDrawData and their rendering order is user controlled.
  • backend during shutdown need to destroy textures.

Screenshots/Video:

No response

Minimal, Complete and Verifiable Example code:

No response

@DucaRii
Copy link
Contributor

DucaRii commented Apr 26, 2025

I think this is a nice and elegant solution, given the guarantee that the TexID field will never be directly accessed from within the ImGui thread (at least while a texture has ImTextureStatus_OK status because thats the only time a race condition could happen?).
That should, in my opinion, be enforced by privating it and adding asserts to the GetTexID() function.
I believe right now the only places where TexID is accessed directly is in the font atlas updater (but only while status != ImTextureStatus_OK so that should be okay) and some of the debug functions which would need to be changed as they directly access it no matter the status.

An edge case I could think of for this approach would be if 2 ImGui::Render happen without a ImGui_ImplDX11_RenderDrawData call (i.e. imgui thread runs every frame on high framerate while render thread is synced to 60fps).
What could happen is that the first ImGui::Render call adds a texture to create to the list for the backend to update, and then since the texture is assumed to be ImTextureStatus_OK for the next frame it wouldnt add that request again for the next constructed ImDrawData and if that is then rendered in the render thread it would completely miss the texture creation request.
Heres a rough outline of what im trying to explain:

Frame 1:

  • ImGui::Render();
    • Texture has ImTextureStatus_WantCreate status so is added to ImDrawData to be created in render thread and set to ImTextureStatus_OK
  • Swap ImDrawData Buffer in render thread with the new one
  • Render thread is dormant for whatever reason (i.e. locked to 60 fps)

Frame 2:

  • ImGui::Render();
    • No textures need creating or updating so the texture queue in ImDrawData is empty
  • Swap ImDrawData Buffer in render thread with the new one
  • Render thread runs and processes the current ImDrawData buffer but since the texture queue is empty the texture from Frame 1 is never created but still assumed to be okay to use

I dont think this is necessarily the responsibility of ImGui to handle but i do think that in order for the user to be able to nicely handle this, texture updates shouldnt be put into ImDrawData but a separate texture queue list which is then fed as an argument to ImGui_ImplDX11_RenderDrawData.

@ocornut
Copy link
Owner Author

ocornut commented Apr 27, 2025

and some of the debug functions which would need to be changed as they directly access it no matter the status.

They are only printing out the value so it doesn't really matter.

I dont think this is necessarily the responsibility of ImGui to handle but i do think that in order for the user to be able to nicely handle this, texture updates shouldnt be put into ImDrawData but a separate texture queue list which is then fed as an argument to ImGui_ImplDX11_RenderDrawData.

Yes it should definitively be a queue because in this proposed scheme the main scheme writes to ->Status immediately.

It would be tempting to formalize a backend design where an extra function needs to be called:

ImGui::Render();
ImGui_ImplDX11_UpdateTextures(ImGui::GetTextures());
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());

Tho it niggles me a bit to add an extra call in the main loop.
It also means the if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) { return; } early-out of _RenderDrawData() functions is not fully exercised.

PS.1: I genuinely don't know if stock backend _RenderDrawData() function have been ever used for threaded rendering. (note than in this wip branch, a majority of the xxx_NewFrame() functions are now emptier than ever, or near empty, so it's becoming more likely). My intuition is that nobody used stock backends for this. Therefore an extra hypothetical ImGui_ImplDX11_UpdateTextures() like call would be mostly useful for documentation purpose, to convey a concept, rather than a strict need. But it would make things nice and obvious and it would be an idiomatic pattern people can copy.

PS.2: An alternative would be that backend could optionally set a function pointer e.g. io.Renderer_UpdateTextures(), which would avoid the explicit call. The legacy design from 1.30 to 1.60 was that backend would set a io.RenderDrawListsFn function pointer, which would be automatically called during ImGui::Render(). Function pointers aligns with how multi-viewports are implemented by backends today. But we strayed away from this design in 1.60 for rendering because the explicit function calls were more flexible for all kinds of main loops and render loops design.

@DucaRii
Copy link
Contributor

DucaRii commented Apr 27, 2025

Tho it niggles me a bit to add an extra call in the main loop.

I agree, though I do prefer it for the sake of being explicit.

PS.1: I genuinely don't know if stock backend _RenderDrawData() function have been ever used for threaded rendering.

I cant speak for everyone but people I know and myself have used stock ImGui to perform threaded rendering, it doesnt really need any modifications if you use a double buffered ImDrawData instance that you swap out from the main ImGui thread

@ocornut
Copy link
Owner Author

ocornut commented Apr 27, 2025

I tried a basic implementation, but I haven't formally tested it for threading issues.

I cant speak for everyone but people I know and myself have used stock ImGui to perform threaded rendering, it doesnt really need any modifications if you use a double buffered ImDrawData instance that you swap out from the main ImGui thread

  • There's no ImGui::GetTextures() function yet, you have to pass &ImGui::GetPlatformIO().Textures.
  • This assume that the backend does not update textures in _RenderDrawData()
  • I think GetTextures() + ImGui_ImplDX11_UpdateTextures(xxx) seems sane but I'd need a type name for it, as I don't think it's right to carry around a ImVector<>*.

If you have a threaded rendering setup I'd be interested if you can try it:

// Usage for multi-threaded rendering.
// [Storage]
//  - ImTextureQueue  tex_queue;
// [Main Thread]
//  - after ImGui::EndFrame()/Render()
//  - call tex_queue.QueueRequests(ImGui::GetTextures());
// [Render Thread]
//  - call tex_queue.ProcessRequests(ImGui_ImplDX11_UpdateTexture, 3);  // 3 is number of maximum frames in flight
struct ImTextureRenderQueueRequest
{
    ImTextureData   TexCopy;
    ImTextureData*  TexInMainThread;

    ~ImTextureRenderQueueRequest() { TexCopy.Pixels = NULL; }
};

struct ImTextureRenderQueue
{
    ImVector<ImTextureRenderQueueRequest>  Requests;

    // Main/Update thread queue requests.
    void    QueueRequests(ImVector<ImTextureData*>* textures);

    // Render thread process requests.
    template<typename Backend_UpdateTextureFunc>
    void    ProcessRequests(Backend_UpdateTextureFunc update_texture_func, int in_flight_frames);
};

void ImTextureRenderQueue::QueueRequests(ImVector<ImTextureData*>* textures)
{
    for (ImTextureData* tex : *textures)
    {
        if (tex->Status == ImTextureStatus_OK)
            continue;

        // Store a copy of the ImTextureData
        // (code is a bit fragile/awkward because our helpers are not fulfilling constructor/destructor in a standard way)
        Requests.resize(Requests.Size + 1, ImTextureRenderQueueRequest());
        ImTextureRenderQueueRequest& new_req = Requests.back();
        new_req.TexCopy = *tex;
        new_req.TexInMainThread = tex;

        // Acknowledge as created from Main Thread POV
        if (tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates)
            tex->Status = ImTextureStatus_OK;
    }
}

template<typename Backend_UpdateTextureFunc>
void ImTextureRenderQueue::ProcessRequests(Backend_UpdateTextureFunc update_texture_func, int in_flight_frames)
{
    for (ImTextureRenderQueueRequest& req : Requests)
    {
        ImTextureData* tex_copy = &req.TexCopy;
        if (tex_copy->Status == ImTextureStatus_OK)
            continue;
        if (tex_copy->Status == ImTextureStatus_WantDestroy && tex_copy->UnusedFrames <= in_flight_frames)
            continue;
        ImTextureStatus prev_status = tex_copy->Status;

        // Call backend function
        update_texture_func(tex_copy);

        // Backend must honor immediately
        if (prev_status == ImTextureStatus_WantCreate || prev_status == ImTextureStatus_WantUpdates)
            IM_ASSERT(tex_copy->Status == ImTextureStatus_OK);
        if (prev_status == ImTextureStatus_WantDestroy)
            IM_ASSERT(tex_copy->Status == ImTextureStatus_Destroyed);

        // Write back to main thread
        if (prev_status == ImTextureStatus_WantCreate)
        {
            req.TexInMainThread->SetTexID(tex_copy->TexID);
            req.TexInMainThread->BackendUserData = tex_copy->BackendUserData;
        }
        if (prev_status == ImTextureStatus_WantDestroy)
        {
            req.TexInMainThread->Status = ImTextureStatus_Destroyed;
            req.TexInMainThread->SetTexID(ImTextureID_Invalid);
            req.TexInMainThread->BackendUserData = NULL;
        }
    }
    Requests.resize(0);
}

@ocornut
Copy link
Owner Author

ocornut commented Apr 27, 2025

I think GetTextures() + ImGui_ImplDX11_UpdateTextures(xxx) seems sane but I'd need a type name for it, as I don't think it's right to carry around a ImVector<>*.

The reason a name like ImGui::GetTextureRequests is not right:

struct ImTextureRequests
{
    ImVector<ImTextureData*>    Textures;
};
ImGui_ImplDX11_UpdateTextures(ImGui::GetTextureRequests());

Is because the backend also this needs a list during shutdown. Current code being:

// Destroy all textures
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
    if (tex->RefCount == 1)
    {
        tex->Status = ImTextureStatus_WantDestroy;
        ImGui_ImplDX11_UpdateTexture(tex);
    }

Because the shutdown order is:

ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();

We can't mark WantDestroy and check RefCount in ImGui::DestroyContext(). (RefCount is used for multiple contexts sharing a font atlas).


If the function name is ImGui::GetTextures() then what is the structure called? ImTextures seems too general.

Alternative

struct ImTextureList
{
    ImVector<ImTextureData*>    Textures;
};
ImGui_ImplDX11_UpdateTextures(ImGui::GetTextureList());

@DucaRii
Copy link
Contributor

DucaRii commented Apr 28, 2025

I tried a basic implementation, but I haven't formally tested it for threading issues.

Okay so I've been testing it on a barebones multithreaded setup using your ImDrawDataSnapshot class (#1860 (comment)) in the DX11 imgui example

ImTextureRenderQueue tex_queue;

ImDrawDataSnapshot active_snapshot;
ImDrawDataSnapshot pending_snapshot;
std::mutex snapshot_mutex;

// moved up here from the main function 
bool done = false;

void ImGuiThread()
{
    const auto tick = std::chrono::duration<float>( 1.f / 32.f );
    while ( !done )
    {
        const auto start = std::chrono::steady_clock::now();

        // Start the Dear ImGui frame
        ImGui_ImplDX11_NewFrame();
        ImGui_ImplWin32_NewFrame();
        ImGui::NewFrame();

        ImGui::ShowDemoWindow( &show_demo_window );

        // Rendering
        ImGui::Render();
        tex_queue.QueueRequests( &ImGui::GetPlatformIO().Textures );

        snapshot_mutex.lock();
        pending_snapshot.SnapUsingSwap( ImGui::GetDrawData(), ImGui::GetTime() );
        snapshot_mutex.unlock();

        const auto time_elapsed = std::chrono::steady_clock::now() - start;
        if ( time_elapsed < tick )
            std::this_thread::sleep_for( tick - time_elapsed );
    }
}

// Main code
int main( int, char** )
{
    // Omitted ImGui setup code

    std::thread( ImGuiThread ).detach();

    // Main loop
    while ( !done )
    {
        // Omitted loop code

        if ( snapshot_mutex.try_lock() )
        {
            if ( pending_snapshot.DrawData.Valid )
            {
                // reason we process the requests here is to make sure that "active_snapshot" cant have references to deleted textures
                tex_queue.ProcessRequests( ImGui_ImplDX11_UpdateTexture, 3 );

                active_snapshot.SnapUsingSwap( &pending_snapshot.DrawData, ImGui::GetTime() );

                // mark as invalid so we wait for the next one and dont 
                // keep swapping back and forth in case the imgui thread is running slow
                pending_snapshot.DrawData.Valid = false;
            }

            snapshot_mutex.unlock();
        }

        if ( active_snapshot.DrawData.Valid ) {
            ImGui_ImplDX11_RenderDrawData( &active_snapshot.DrawData );
        }
        // Present
        HRESULT hr = g_pSwapChain->Present( 1, 0 );   // Present with vsync
        //HRESULT hr = g_pSwapChain->Present(0, 0); // Present without vsync
        g_SwapChainOccluded = ( hr == DXGI_STATUS_OCCLUDED );
    }

Turning vsync on/off and increasing or decreasing the ImGuiThread tickrate can create the necessary scenarios for either a much faster rendering thread or a much faster logic thread.

To create texture updates i decided to just slide around the io.FontGlobalScale slider.

The 2 issues ive come across are:

  • race condition on the vector when both QueueRequests and ProcessRequests are called at the same time
  • in the case of the logic thread running at much higher speed than the rendering thread its possible for ImGui to queue up multiple requests that would normally rely on eachother

The race condition can be fixed by just adding lock guards to both of the functions, but the other one is a little trickier.
The different scenarios that can happen from my understanding are:

  • ImGui sends a ImTextureStatus_WantCreate request followed directly by a ImTextureStatus_WantUpdates request, with the latters BackendUserData being NULL
  • ImGui sends 2 ImTextureStatus_WantCreate requests and then internally removes the former because of it never reaching the backend in the first place and thus invalidating all the memory inside (including the Pixels field), that creates an access violation when the graphics api tries to read the pixels for texture creation
  • ImGui sends multiple ImTextureStatus_WantDestroy requests which leads to the first one being handled but then all the subsequent ones trying to remove an already destroyed texture
  • ImGui sends a ImTextureStatus_WantUpdates request followed by a ImTextureStatus_WantDestroy request, causing the graphics api to cause an access violation when trying to access invalidated pixels

Here is my (dirty) modified version which addresses all these issues

void ImTextureRenderQueue::QueueRequests( ImVector<ImTextureData*>* textures )
{
    RequestsMutex.lock();

    for ( ImTextureData* tex : *textures )
    {
        if ( tex->Status == ImTextureStatus_OK )
            continue;

        // Store a copy of the ImTextureData
        // (code is a bit fragile/awkward because our helpers are not fulfilling constructor/destructor in a standard way)
        Requests.resize( Requests.Size + 1, ImTextureRenderQueueRequest() );
        ImTextureRenderQueueRequest& new_req = Requests.back();
        new_req.TexCopy = *tex;
        new_req.TexInMainThread = tex;
        new_req.OriginalStatus = tex->Status;

        // Acknowledge as created from Main Thread POV
        if ( tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates )
            tex->Status = ImTextureStatus_OK;

        // Need to set this immediately to prevent ImGui from dispatching more requests
        if ( tex->Status == ImTextureStatus_WantDestroy )
        {
            tex->Status = ImTextureStatus_Destroyed;
            tex->SetTexID( ImTextureID_Invalid );
            tex->BackendUserData = NULL;

            // In addition, we need to also invalidate all prior requests that attempted to modify this texture (excluding this one)
            for ( auto i = 0; i < Requests.size() - 1; i++ )
            {
                auto& req = Requests[ i ];
                if ( req.TexCopy.UniqueID == tex->UniqueID )
                    req.TexCopy.Status = ImTextureStatus_Destroyed;
            }
        }
    }

    // FIXME: is there no better way to do this?
    // Validate that all requests are still for valid textures, this is to account for ImGui sometimes creating 2 texture in a row without it ever reaching the backend and thus just removing it from the list
    for ( auto i = 0; i < Requests.size(); i++ )
    {
        auto& req = Requests[ i ];
        if ( req.TexCopy.Status != ImTextureStatus_WantCreate && req.TexCopy.Status != ImTextureStatus_WantUpdates )
            continue;

        bool does_texture_exist = false;

        for ( ImTextureData* tex : *textures )
        {
            if ( tex->UniqueID == req.TexCopy.UniqueID )
            {
                does_texture_exist = true;
                break;
            }
        }

        if ( does_texture_exist )
            continue;

        // Reaching this point means this texture was already removed internally, so its no longer safe to access the pixels
        req.TexCopy.Status = ImTextureStatus_Destroyed;
    }

    RequestsMutex.unlock();
}

template<typename Backend_UpdateTextureFunc>
void ImTextureRenderQueue::ProcessRequests( Backend_UpdateTextureFunc update_texture_func, int in_flight_frames )
{
    RequestsMutex.lock();

    for ( auto i = 0; i < Requests.size(); i++ )
    {
        auto& req = Requests[ i ];

        ImTextureData* tex_copy = &req.TexCopy;
        if ( tex_copy->Status == ImTextureStatus_OK || tex_copy->Status == ImTextureStatus_Destroyed )
            continue;
        if ( tex_copy->Status == ImTextureStatus_WantDestroy && tex_copy->UnusedFrames <= in_flight_frames )
            continue;
        ImTextureStatus prev_status = tex_copy->Status;

        // Call backend function
        update_texture_func( tex_copy );

        // Backend must honor immediately
        if ( prev_status == ImTextureStatus_WantCreate || prev_status == ImTextureStatus_WantUpdates )
            IM_ASSERT( tex_copy->Status == ImTextureStatus_OK );
        if ( prev_status == ImTextureStatus_WantDestroy )
            IM_ASSERT( tex_copy->Status == ImTextureStatus_Destroyed );

        // Write back to main thread
        if ( prev_status == ImTextureStatus_WantCreate )
        {
            // Make sure future requests will access the correct texture
            for ( auto j = i + 1; j < Requests.size(); j++ )
            {
                auto& subsequent_req = Requests[ j ];
                if ( subsequent_req.TexCopy.UniqueID == req.TexCopy.UniqueID )
                {
                    subsequent_req.TexCopy.SetTexID( tex_copy->TexID );
                    subsequent_req.TexCopy.BackendUserData = tex_copy->BackendUserData;
                }
            }

            req.TexInMainThread->SetTexID( tex_copy->TexID );
            req.TexInMainThread->BackendUserData = tex_copy->BackendUserData;
        }
    }
    Requests.resize( 0 );

    RequestsMutex.unlock();
}

Theoretically, since the status is immediately set without waiting for the render thread this would also remove the need for a separate ImGui_ImplDX11_UpdateTextures function, because if ImTextureRenderQueue::ProcessRequests is called before ImGui_ImplDX11_RenderDrawData, all textures would already be marked as either ImTextureStatus_OK or ImTextureStatus_Destroyed. I still think having it be separate function is better for clarity because its not really part of rendering the draw data as the function name would imply.


If the function name is ImGui::GetTextures() then what is the structure called? ImTextures seems too general.

Alternative

struct ImTextureList
{
ImVector<ImTextureData*> Textures;
};

ImGui_ImplDX11_UpdateTextures(ImGui::GetTextureList());

ImTextureList seems like a nice and sane name for whats its representing

@ocornut
Copy link
Owner Author

ocornut commented Apr 30, 2025

Thank you for your feedback. I'll first try to get myself a proper multi-threaded rendering test (without even considering texture update).

I'm struggling to understand your code above. The render loop seems to be swapping continuously regardless of anything being rendering. It seems to be missing part. I'm not sure I understand the pending/active logic. Shouldn't it be modeled with a small array representing a round-robin queue?

@DucaRii
Copy link
Contributor

DucaRii commented Apr 30, 2025

The goal of this setup is to keep both (but especially the rendering thread) going with minimal slowdowns. If the pending & active snapshots were merged into just 1 snapshot variable then the try_lock() in the render thread would need to be replaced with a lock() and would create slowdowns in both threads.

The render thread only ever wants to render the most recent frame so theres no point in storing any past imgui drawdata frames in a queue.

Also regarding:

The render loop seems to be swapping continuously regardless of anything being rendering

Thats what setting pending_snapshot.DrawData.Valid = false; after swapping to the active snapshot prevents, itll wait for the next time the logic thread updates

@ocornut
Copy link
Owner Author

ocornut commented Apr 30, 2025

But it's not clearing framebuffer and it's always swapping:

        if ( active_snapshot.DrawData.Valid ) {
            ImGui_ImplDX11_RenderDrawData( &active_snapshot.DrawData );
        }
        // Present
        HRESULT hr = g_pSwapChain->Present( 1, 0 );   // Present with vsync
        //HRESULT hr = g_pSwapChain->Present(0, 0); // Present without vsync
        g_SwapChainOccluded = ( hr == DXGI_STATUS_OCCLUDED );

@DucaRii
Copy link
Contributor

DucaRii commented Apr 30, 2025

Oh yea sorry that was part of the // Omitted loop code i mentioned, thats done before checking if theres a new snapshot ready.

const float clear_color_with_alpha[ 4 ] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
g_pd3dDeviceContext->OMSetRenderTargets( 1, &g_mainRenderTargetView, nullptr );
g_pd3dDeviceContext->ClearRenderTargetView( g_mainRenderTargetView, clear_color_with_alpha );

if ( snapshot_mutex.try_lock() )
{
    if ( pending_snapshot.DrawData.Valid )
    {
        // reason we process the requests here is to make sure that "active_snapshot" cant have references to deleted textures
        tex_queue.ProcessRequests( ImGui_ImplDX11_UpdateTexture, 3 );

        active_snapshot.SnapUsingSwap( &pending_snapshot.DrawData, ImGui::GetTime() );

        // mark as invalid so we wait for the next one and dont keep swapping back and forth in case the imgui thread is running slow
        pending_snapshot.DrawData.Valid = false;
    }

    snapshot_mutex.unlock();
}

if ( active_snapshot.DrawData.Valid ) {
    ImGui_ImplDX11_RenderDrawData( &active_snapshot.DrawData );
}

// Present
HRESULT hr = g_pSwapChain->Present( 1, 0 );   // Present with vsync
//HRESULT hr = g_pSwapChain->Present(0, 0); // Present without vsync
g_SwapChainOccluded = ( hr == DXGI_STATUS_OCCLUDED );

But im not sure what you mean by

it's always swapping

Are you saying it shouldn't always call into ImGui_ImplDX11_RenderDrawData and only when a new snapshot from the logic thread is ready?

@ocornut
Copy link
Owner Author

ocornut commented Apr 30, 2025

Sorry I realize the terminology overlap, by "It's always swapping" I meant

  • It's always calling swapChain->Present() which seems bizarre.
  • If active_snapshot.DrawData.Valid == false then you are clearing but not rendering imgui. Therefore I don't think this path is actually exercised.

@DucaRii
Copy link
Contributor

DucaRii commented Apr 30, 2025

  • It's always calling swapChain->Present() which seems bizarre.

Yea calling Present every time doesnt make sense in the scenario of a standalone imgui application. The way i use this code is in my game engine though, the game needs to be rendered every frame so maintaining the framebuffer until the next imgui update is not feasible. For example: for my game i have an imgui overlay which lists all entities and allows you to modify any field (health, speed etc.). In order for that to not cause race conditions imgui needs to run in the games logic thread.

  • If active_snapshot.DrawData.Valid == false then you are clearing but not rendering imgui. Therefore I don't think this path is actually exercised.

active_snapshot.DrawData.Valid can only ever be false if there hasnt been a single pending snapshot yet, its basically just a safeguard so we dont try to render drawdata when just starting the application and for some reason the render thread runs before the logic thread has a chance to update the pending snapshot.
So basically as soon as the logic thread has run once active_snapshot.DrawData.Valid will always be true.

@ocornut
Copy link
Owner Author

ocornut commented May 12, 2025

ImGui_ImplDX11_UpdateTextures(ImGui::GetTextureList());
ImTextureList seems like a nice and sane name for whats its representing

I have been quite agonizing over this, as it would be a breaking change for 100% of apps updating backends, and in the name of supporting multi-threaded staged rendering for standard backends, which I haven't really expected would happen.

I have instead added a pointer to the texture array in ImDrawData d16f151 tho it still rubs me off a little bit as it means rendering uses ImDrawData->Textures[] while backend shutdown uses GetPlatformIO()->Textures[].

I also had to go through a tangent verifying and making fixes/changes to ensure that both multi-atlas was supported in a same context and that it was possible for end-user/app/third-party extension to submit their own texture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants