Hello, I am getting some visual text bugs and crashes with a multi-threaded setup. I am fairly new to ImGui, so it is very possible I am missing something obvious or doing something dumb.
My project has a dedicated rendering thread. I am using imgui_threaded_rendering.h to take snapshots of the ImDrawData on the main thread and pass them to the rendering thread. Like this:
#define SNAPSHOT_COUNT 3
static CRITICAL_SECTION g_snapshotCS;
static int g_currentSnapshotIdx = -1;
static ImDrawDataSnapshot g_snapshots[SNAPSHOT_COUNT] {};
// ----------
// Create Snapshot - Run in MainThread update loop
EnterCriticalSection(&g_snapshotCS);
int nextIndex = (g_currentSnapshotIdx + 1) % SNAPSHOT_COUNT;
ImDrawDataSnapshot* snapshot = &g_snapshots[nextIndex];
snapshot->SnapUsingSwap(ImGui::GetDrawData(), ImGui::GetTime());
g_currentSnapshotIdx = nextIndex;
LeaveCriticalSection(&g_snapshotCS);
// ----------
// Run in RenderThread at end of frame
// Apply latest snapshot
EnterCriticalSection(&g_snapshotCS);
if (g_currentSnapshotIdx >= 0)
{
ImDrawData* drawData = &g_snapshots[g_currentSnapshotIdx].DrawData;
ImGui_ImplDX11_RenderDrawData(drawData);
}
LeaveCriticalSection(&g_snapshotCS);
d3d11_3SDKLayers.dll!IsBadReadPtr_OK(void const *,unsigned __int64) Unknown
d3d11_3SDKLayers.dll!NDebug::CContext::UpdateSubresourcePreValidation<struct ID3D11Resource>(class NDebug::CInterfaceSentinel::CFunctionSentinel &,struct ID3D11Resource *,unsigned int,struct D3D11_BOX const *,void const *,unsigned int,unsigned int) Unknown
d3d11_3SDKLayers.dll!NDebug::CContext::UpdateSubresource(struct ID3D11Resource *,unsigned int,struct D3D11_BOX const *,void const *,unsigned int,unsigned int) Unknown
myproj.exe!ImGui_ImplDX11_UpdateTexture(ImTextureData * tex) Line 415 C++
myproj.exe!ImGui_ImplDX11_RenderDrawData(ImDrawData * draw_data) Line 177 C++
I believe this happens because ImDrawDataSnapshot does not copy texture data. So the render thread can update textures while the main thread is modifying ImTextureData::Updates (and maybe other fields?). In my specific repro, ImFontAtlasTextureBlockQueueUpload is adding entries to ImTextureData::Updates on the main thread, which can cause the vector to grow and reallocate its internal buffer. And when this happens while the render thread is updating textures, it reads the old now-garbage buffer and crashes.
I have included code for a small MCVE program that I've been using to investigate the issue.
// ----------------------------------------------------------------------------
// - Example program demonstrating a thread syncronization issue with ImGui/Win32/DX11
// while running a dedicated render thread.
// - Press F1 to open the ImGui Demo Window
// - Sometimes there are missing/blank font glyphs
// - Sometimes there is a crash in ImGui_ImplDX11_UpdateTexture
// - Messing around with the UPDATE_FREQ_MS, RENDER_FREQ_MS, and g_vsync variables can
// help repro. For me, setting them all to zero repros the visual issues pretty
// consistently. The crash is more rare.
// ----------------------------------------------------------------------------
#pragma warning (disable: 4127) // conditional expression is constant
#include <windows.h>
#include <assert.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <imgui.h>
#include <imgui_threaded_rendering.h>
#include <imgui_memory_editor.h>
#include <backends/imgui_impl_dx11.h>
#include <backends/imgui_impl_win32.h>
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "imgui.lib")
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
namespace RenderThreadExample
{
#define UPDATE_FREQ_MS 0
#define RENDER_FREQ_MS 0
static HINSTANCE g_hInstance = NULL;
static HWND g_hWnd = NULL;
static int g_windowWidth = 0;
static int g_windowHeight = 0;
static BOOL g_exitRequested = FALSE;
static BOOL g_imGuiDemoOpen = FALSE;
// ----- Render Thread --------------------------------------------------------
static ID3D11Device* g_device = NULL;
static ID3D11DeviceContext* g_context = NULL;
static IDXGISwapChain1* g_swapChain = NULL;
static D3D_FEATURE_LEVEL g_featureLevel;
static ID3D11SamplerState* g_samplerState = NULL;
static ID3D11BlendState* g_blendState = NULL;
static ID3D11RasterizerState* g_rasterizerState = NULL;
static ID3D11RenderTargetView* g_renderTargetView = NULL;
static int g_backbufferWidth = 0;
static int g_backbufferHeight = 0;
static int g_vsync = 0; // 0 = no vsync, 1 = vsync
static HANDLE g_renderThreadHandle = NULL;
static DWORD g_renderThreadId = 0;
static BOOL g_renderThreadPendingQuit = FALSE;
static BOOL g_renderThreadPendingResize = FALSE;
#define SNAPSHOT_COUNT 3
static CRITICAL_SECTION g_snapshotCS;
static int g_currentSnapshotIdx = -1;
static ImDrawDataSnapshot g_snapshots[SNAPSHOT_COUNT] {};
// DX11 initialization - Might not be the simplest possible for this example, and
// has some pretty draconian asserts for brevity, but its
// a pretty close copy/paste of my actual project's setup.
static void InitDx11(HWND hWnd, int width, int height)
{
HRESULT hr = S_OK;
hr = D3D11CreateDevice(
NULL,
D3D_DRIVER_TYPE_HARDWARE,
NULL,
0,
NULL,
0,
D3D11_SDK_VERSION,
&g_device,
&g_featureLevel,
&g_context);
assert(!FAILED(hr));
IDXGIDevice* dxgiDevice = NULL;
hr = g_device->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice);
assert(!FAILED(hr));
IDXGIAdapter* adapter = NULL;
hr = dxgiDevice->GetAdapter(&adapter);
dxgiDevice->Release();
assert(!FAILED(hr));
IDXGIFactory2* factory = NULL;
hr = adapter->GetParent(__uuidof(IDXGIFactory2), (void**)&factory);
adapter->Release();
assert(!FAILED(hr));
DXGI_SWAP_CHAIN_DESC1 scDesc = {};
scDesc.Width = width;
scDesc.Height = height;
scDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
scDesc.Stereo = FALSE;
scDesc.SampleDesc.Count = 1;
scDesc.SampleDesc.Quality = 0;
scDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
scDesc.BufferCount = 2;
scDesc.Scaling = DXGI_SCALING_NONE;
scDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
scDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
scDesc.Flags = 0;
hr = factory->CreateSwapChainForHwnd(
g_device,
hWnd,
&scDesc,
NULL,
NULL,
(IDXGISwapChain1**)&g_swapChain);
assert(!FAILED(hr) && g_swapChain);
factory->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER);
factory->Release();
ID3D11Texture2D* backBuffer = NULL;
hr = g_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
assert(!FAILED(hr) && backBuffer);
D3D11_TEXTURE2D_DESC bbDesc{};
backBuffer->GetDesc(&bbDesc);
g_backbufferWidth = bbDesc.Width;
g_backbufferHeight = bbDesc.Height;
hr = g_device->CreateRenderTargetView(backBuffer, NULL, &g_renderTargetView);
assert(!FAILED(hr) && g_renderTargetView);
backBuffer->Release();
g_context->OMSetRenderTargets(1, &g_renderTargetView, NULL);
D3D11_SAMPLER_DESC samplerDesc{};
samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;
hr = g_device->CreateSamplerState(&samplerDesc, &g_samplerState);
assert(!FAILED(hr));
g_context->PSSetSamplers(0, 1, &g_samplerState);
D3D11_BLEND_DESC blendDesc{};
blendDesc.RenderTarget[0].BlendEnable = TRUE;
blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
hr = g_device->CreateBlendState(&blendDesc, &g_blendState);
assert(!FAILED(hr));
float blendFactor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
g_context->OMSetBlendState(g_blendState, blendFactor, 0xFFFFFFFF);
D3D11_RASTERIZER_DESC rasterizerDesc{};
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_NONE;
rasterizerDesc.DepthClipEnable = TRUE;
hr = g_device->CreateRasterizerState(&rasterizerDesc, &g_rasterizerState);
assert(!FAILED(hr));
g_context->RSSetState(g_rasterizerState);
D3D11_VIEWPORT viewport = { 0 };
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = (float)width;
viewport.Height = (float)height;
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
g_context->RSSetViewports(1, &viewport);
}
void DeinitDx11()
{
if (g_renderTargetView) { g_renderTargetView->Release(); g_renderTargetView = NULL; }
if (g_rasterizerState) { g_rasterizerState->Release(); g_rasterizerState = NULL; }
if (g_blendState) { g_blendState->Release(); g_blendState = NULL; }
if (g_samplerState) { g_samplerState->Release(); g_samplerState = NULL; }
if (g_swapChain) { g_swapChain->Release(); g_swapChain = NULL; }
if (g_context) { g_context->Release(); g_context = NULL; }
if (g_device) { g_device->Release(); g_device = NULL; }
}
static void ResizeDx11(int newWidth, int newHeight)
{
if (!g_context) return;
if (newWidth == 0 || newHeight == 0) return;
if (newWidth == g_backbufferWidth && newHeight == g_backbufferHeight) return;
g_context->OMSetRenderTargets(0, NULL, NULL);
g_context->Flush();
if (g_renderTargetView)
{
g_renderTargetView->Release();
g_renderTargetView = NULL;
}
HRESULT hr;
hr = g_swapChain->ResizeBuffers(0, newWidth, newHeight, DXGI_FORMAT_UNKNOWN, 0);
assert(!FAILED(hr));
ID3D11Texture2D* backBuffer = NULL;
hr = g_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
assert(!FAILED(hr));
hr = g_device->CreateRenderTargetView(backBuffer, NULL, &g_renderTargetView);
backBuffer->Release();
assert(!FAILED(hr) && g_renderTargetView);
g_context->OMSetRenderTargets(1, &g_renderTargetView, NULL);
g_backbufferWidth = newWidth;
g_backbufferHeight = newHeight;
D3D11_VIEWPORT viewport = { 0 };
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = (float)newWidth;
viewport.Height = (float)newHeight;
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
g_context->RSSetViewports(1, &viewport);
}
static DWORD WINAPI RenderThreadProc(LPVOID param)
{
(void)param;
InitDx11(g_hWnd, g_windowWidth, g_windowHeight);
while (!g_renderThreadPendingQuit)
{
if (g_renderThreadPendingResize)
{
ResizeDx11(g_windowWidth, g_windowHeight);
g_renderThreadPendingResize = FALSE;
}
if (g_renderTargetView)
{
g_context->OMSetRenderTargets(1, &g_renderTargetView, NULL);
}
float clearColor[4] = { 0.3f, 0.2f, 0.1f, 1.0f };
g_context->ClearRenderTargetView(g_renderTargetView, clearColor);
// Apply latest snapshot
EnterCriticalSection(&g_snapshotCS);
if (g_currentSnapshotIdx >= 0)
{
ImDrawData* drawData = &g_snapshots[g_currentSnapshotIdx].DrawData;
ImGui_ImplDX11_RenderDrawData(drawData);
}
LeaveCriticalSection(&g_snapshotCS);
DXGI_PRESENT_PARAMETERS presentParams = {};
HRESULT presHr = g_swapChain->Present1(g_vsync, 0, &presentParams);
if (FAILED(presHr))
{
printf("[RenderThread] SwapChain Present1 failed: 0x%08X\n", presHr);
}
if (RENDER_FREQ_MS > 0)
{
Sleep(RENDER_FREQ_MS);
}
}
DeinitDx11();
return 0;
}
// ----- Main Thread ----------------------------------------------------------
static LRESULT CALLBACK
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (g_imGuiDemoOpen)
{
ImGui_ImplWin32_WndProcHandler(hWnd, message, wParam, lParam);
}
switch (message)
{
case WM_DESTROY:
g_exitRequested = true;
break;
case WM_WINDOWPOSCHANGED:
{
RECT rect;
GetClientRect(hWnd, &rect);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
EnterCriticalSection(&g_snapshotCS);
g_renderThreadPendingResize = TRUE;
g_windowWidth = width;
g_windowHeight = height;
LeaveCriticalSection(&g_snapshotCS);
break;
}
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
{
if (wParam == VK_F1)
{
g_imGuiDemoOpen = !g_imGuiDemoOpen;
}
break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
void run()
{
g_hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcex = { 0 };
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = g_hInstance;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = L"ImGuiExample";
BOOL result = RegisterClassEx(&wcex);
if (!result)
return;
InitializeCriticalSection(&g_snapshotCS);
g_windowWidth = 1200;
g_windowHeight = 800;
g_hWnd = CreateWindow(
L"ImGuiExample",
L"ImGuiExample",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
g_windowWidth, g_windowHeight,
NULL, NULL, g_hInstance, NULL);
ShowWindow(g_hWnd, SW_SHOW);
UpdateWindow(g_hWnd);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
// Create render thread and wait for device/context to be ready
g_renderThreadHandle = CreateThread(NULL, 0, RenderThreadProc, NULL, 0, &g_renderThreadId);
while(g_context == NULL || g_device == NULL)
{
Sleep(1);
}
ImGui_ImplDX11_Init(g_device, g_context);
ImGui_ImplWin32_Init(g_hWnd);
while (!g_exitRequested)
{
MSG msg = { 0 };
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGui::Text("Press F1 to toggle ImGui demo window.");
if (g_imGuiDemoOpen)
{
ImGui::ShowDemoWindow();
}
ImGui::Render();
// Create Snapshot for RenderThread
EnterCriticalSection(&g_snapshotCS);
int nextIndex = (g_currentSnapshotIdx + 1) % SNAPSHOT_COUNT;
ImDrawDataSnapshot* snapshot = &g_snapshots[nextIndex];
snapshot->SnapUsingSwap(ImGui::GetDrawData(), ImGui::GetTime());
g_currentSnapshotIdx = nextIndex;
LeaveCriticalSection(&g_snapshotCS);
if (UPDATE_FREQ_MS > 0)
{
Sleep(UPDATE_FREQ_MS);
}
}
if (g_renderThreadHandle)
{
g_renderThreadPendingQuit = 1;
WaitForSingleObject(g_renderThreadHandle, INFINITE);
CloseHandle(g_renderThreadHandle);
g_renderThreadHandle = NULL;
g_renderThreadId = 0;
}
ImGui_ImplWin32_Shutdown();
ImGui_ImplDX11_Shutdown();
}
} // namespace RenderThreadExample
int main()
{
RenderThreadExample::run();
return 0;
}
Version/Branch of Dear ImGui:
v1.92.5
Back-ends:
imgui_impl_dx11 + imgui_impl_win32
Compiler, OS:
Windows 11 + MSVC 2022
Full config/build information:
Details:
Hello, I am getting some visual text bugs and crashes with a multi-threaded setup. I am fairly new to ImGui, so it is very possible I am missing something obvious or doing something dumb.
My set-up
My project has a dedicated rendering thread. I am using imgui_threaded_rendering.h to take snapshots of the ImDrawData on the main thread and pass them to the rendering thread. Like this:
My problem
bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0);Callstack:
The cause (my best guess)
I believe this happens because ImDrawDataSnapshot does not copy texture data. So the render thread can update textures while the main thread is modifying ImTextureData::Updates (and maybe other fields?). In my specific repro, ImFontAtlasTextureBlockQueueUpload is adding entries to ImTextureData::Updates on the main thread, which can cause the vector to grow and reallocate its internal buffer. And when this happens while the render thread is updating textures, it reads the old now-garbage buffer and crashes.
I read through these related issues, 8597 8465, but didn't come up with a solution.
I have included code for a small MCVE program that I've been using to investigate the issue.
Screenshots/Video:
Minimal, Complete and Verifiable Example code: