-
-
Notifications
You must be signed in to change notification settings - Fork 11.4k
Description
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:
Dear ImGui 1.92.5 (19250)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=199711
define: _WIN32
define: _WIN64
define: _MSC_VER=1944
define: _MSVC_LANG=202002
IM_ASSERT: runs expression: OK. expand size: OK
--------------------------------
io.BackendPlatformName: imgui_impl_win32
io.BackendRendererName: imgui_impl_dx11
io.ConfigFlags: 0x00000001
NavEnableKeyboard
io.ConfigNavCaptureKeyboard
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x0000001F
HasGamepad
HasMouseCursors
HasSetMousePos
RendererHasVtxOffset
RendererHasTextures
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,128
io.Fonts->FontLoaderName: stb_truetype
io.DisplaySize: 2560.00,1369.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00Details:
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:
#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);
My problem
- I often (but not always) see blank/missing character glyphs.
- I sometimes get a crash in ImGui_ImplDX11_UpdateTexture, on this line:
bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0);
Callstack:
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++
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:
// ----------------------------------------------------------------------------
// - 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;
}