(XMB) Fix some major concurrency issues that could happen with nearly

every video driver, but D3D12 especially got hit hard with these issues.
It manifested itself most badly when trying to change Font Scale, it would
crash D3D12 for years

1. Struct field addition (after line 415):
2. Deferred reset handler at top of xmb_render (after if (!xmb) return;):
3. Replace direct call with deferred counter (in the scale factor change detection block):
4. Initialize margin values in xmb_init (after last_scale_factor initialization):

The D3D12 backend uses a deferred rendering model where GPU commands are recorded into a command list during the frame, then submitted all at once at the end. The frame lifecycle is:

Open command list (cmd->Reset)
Record draw calls (textures, fonts, sprites are referenced by the GPU command list)
Close command list (cmd->Close)
Execute (ExecuteCommandLists)
Sync (signal fence, WaitForSingleObject until GPU completes)

The problem was that xmb_render() and d3d12_gfx_frame() run in the same runloop_iterate() call, in this order:
runloop_check_state()
  ├─ xmb_render()                    ← menu logic, detects scale change
  │    └─ xmb_context_reset_internal()
  │         └─ d3d12_gfx_unload_texture()
  │              └─ D3D12_GFX_SYNC()  ← signals fence + WaitForSingleObject(INFINITE)
  │                                      but command list hasn't been submitted yet
  │                                      → DEADLOCK (or TDR if sync is skipped,
  │                                        because textures get freed while GPU
  │                                        still references them from prior frame)
  │
  └─ video_driver_cached_frame()
       └─ d3d12_gfx_frame()          ← opens, records, closes, executes, syncs
xmb_render was freeing GPU resources (textures, fonts) before d3d12_gfx_frame had a chance to submit and complete the previous frame's command list. The GPU was still reading those resources from the in-flight command buffer.
The fix defers the resource-freeing context reset by 2 frames using a countdown counter, ensuring the GPU has fully completed all prior command lists before any textures or fonts are released.

Any video driver that uses deferred command submission has the same structural vulnerability — xmb_render runs before the video driver's frame function, so freeing GPU resources during xmb_render can race with the GPU still consuming those resources from the previous frame.
Drivers that likely benefit:

D3D11 — uses a similar deferred context model, though its Map/Unmap calls are less strict about synchronization than D3D12, so it may have been silently corrupting rather than crashing
Vulkan — command buffers are explicitly recorded and submitted, same pattern as D3D12. Freeing a VkImage while a command buffer referencing it is still in flight is undefined behavior per the Vulkan spec

Drivers that are unaffected:

OpenGL — immediate mode, all commands execute inline on the CPU thread. By the time glDeleteTextures returns, the GPU is done with it (or the driver handles it internally)
Software renderers — no GPU pipeline at all

The fix is in xmb.c (the menu driver), not in any specific video backend, so it protects all backends equally. The 2-frame deferral is conservative enough that even triple-buffered GPU pipelines should be safe. The cost is a barely perceptible 2-frame delay (~33ms at 60fps) when changing the scale factor — which is a settings operation, not something that affects gameplay or normal menu navigation.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-04-15 01:38:47 +02:00
1 parent 55f6f6d3ac
commit 45f8bf4ac0
1 file changed
+26 -2
+26 -2
View File
@@ -413,6 +413,7 @@ typedef struct xmb_handle
float font_size;
float font2_size;
float last_scale_factor;
unsigned pending_context_reset;
float margins_screen_left;
float margins_screen_top;
@@ -6874,6 +6875,25 @@ static void xmb_render(void *data,
if (!xmb)
return;
/* Handle deferred context reset from a previous scale factor /
* layout change. We must wait two xmb_render() calls (not one)
* because xmb_render() is called from runloop_check_state()
* BEFORE video_driver_cached_frame() in the same iteration.
* The sequence per iteration is:
* xmb_render() -> video_driver_cached_frame() -> gfx_frame()
* So on the frame that detects the change (N), we set the counter
* to 2. On frame N+1 xmb_render runs first (counter becomes 1),
* then gfx_frame completes frame N's GPU work. On frame N+2
* xmb_render runs (counter becomes 0), and now the GPU has finished
* all prior command lists, so it is safe to release textures and
* fonts via xmb_context_reset_internal(). */
if (xmb->pending_context_reset > 0)
{
if (--xmb->pending_context_reset == 0)
xmb_context_reset_internal(xmb, video_driver_is_threaded(), false,
settings->uints.menu_xmb_theme);
}
xmb->use_ps3_layout = xmb_use_ps3_layout(settings->uints.menu_xmb_layout, width, height);
scale_factor = xmb_get_scale_factor(settings->floats.menu_scale_factor,
xmb->use_ps3_layout, width);
@@ -6888,8 +6908,8 @@ static void xmb_render(void *data,
xmb->last_margins_title_horizontal_offset = xmb->margins_title_horizontal_offset;
xmb->last_scale_factor = scale_factor;
xmb_context_reset_internal(xmb, video_driver_is_threaded(), false,
settings->uints.menu_xmb_theme);
/* Defer context reset by 2 frames — see comment above */
xmb->pending_context_reset = 2;
}
/* This must be set every frame when using a pointer,
@@ -9258,6 +9278,10 @@ static void *xmb_init(void **userdata, bool video_is_threaded)
xmb->last_scale_factor = xmb_get_scale_factor(
settings->floats.menu_scale_factor,
xmb->use_ps3_layout, width);
xmb->margins_title = (float)settings->ints.menu_xmb_title_margin * 10.0f;
xmb->last_margins_title = xmb->margins_title;
xmb->margins_title_horizontal_offset = (float)settings->ints.menu_xmb_title_margin_horizontal_offset * 10.0f;
xmb->last_margins_title_horizontal_offset = xmb->margins_title_horizontal_offset;
p_anim->updatetime_cb = xmb_menu_animation_update_time;