D3D12: Route texture load/unload through video thread when threaded video active

The D3D12 driver was partially thread-unsafe when used with the
threaded video wrapper.  Unlike gl2/gl3/d3d9 which route texture
operations through video_thread_texture_handle so that all GPU
work happens on the video thread, D3D12 relied on D3D12's
inherent free-threaded device API and executed load/unload on
the calling (main) thread.  This created several races when
threaded video was active:

1. texture->dirty flag
   Written on main thread (after Map/Unmap in d3d12_update_texture).
   Read and cleared on video thread (during d3d12_gfx_frame and
   d3d12_upload_texture).  No synchronisation.  On weak-memory-
   order CPUs the video thread could observe a stale `false` flag
   and skip the upload, producing missing textures.

2. Upload buffer data visibility
   Pixel data written via Map/memcpy on main thread.  Consumed
   via CopyTextureRegion on video thread during command list
   recording.  No CPU-side memory barrier ensures the main
   thread's writes are visible to the video thread's reads.

3. Release timing vs command list recording
   d3d12_gfx_unload_texture has a GPU fence wait that ensures
   submitted work has completed, but does not serialise against
   the video thread CURRENTLY building a command list that
   references the texture being released.  In D3D12, command
   lists do not hold references to resources they record, so
   releasing a texture while its use is being recorded can
   leave the recorded command list with a dangling pointer.

4. fenceValue counter race
   d3d12->queue.fenceValue is incremented via non-atomic ++
   from both the main thread (unload path at line 6273) and
   the video thread (frame submission, font free, shader
   reset paths).  Concurrent increments can skip or duplicate
   fence values, breaking synchronisation.

Fix: adopt the gl2/d3d9 pattern.  When threaded video is active,
dispatch both load_texture and unload_texture to the video
thread via video_thread_texture_handle, so all D3D12 command
queue operations are serialised on the queue-owning thread.
This eliminates all four races in one change: shared state is
no longer shared across threads.

Implementation:

- Split d3d12_gfx_load_texture into an internal function
  (d3d12_gfx_load_texture_internal) that does the actual GPU
  work, and an outer function that dispatches.  Same for unload.

- New d3d12_texture_cmd_t packages the d3d12 pointer, image,
  filter_type, and handle fields.  Used for both load (image
  is input, handle is output) and unload (handle is input).

- Return the uintptr_t handle via cmd->handle rather than the
  int return channel of custom_command_method_t, avoiding
  truncation on 64-bit Windows where uintptr_t is 64 bits but
  int is 32 bits.

- The outer d3d12_gfx_load_texture / d3d12_gfx_unload_texture
  check the `threaded` parameter.  By the time it reaches the
driver, `threaded` has already been validated against both
  VIDEO_DRIVER_IS_THREADED_INTERNAL and
  VIDEO_FLAG_THREAD_WRAPPER_ACTIVE by video_driver_texture_load
  and video_driver_texture_unload.  If threaded, dispatch via
  video_thread_texture_handle.  Otherwise, call the internal
  function directly on the current thread (single-threaded
  path unchanged).

- video_thread_texture_handle already handles the self-call
  case (invoked from video thread) by calling the wrap function
  directly, avoiding self-deadlock.  This means d3d12's internal
  paths that run on the video thread (shader reset, font init)
  can call d3d12_gfx_load_texture with threaded=true safely.

Cost: texture load now blocks for up to one frame duration
(~16ms) waiting for the video thread to process the dispatched
command.  Texture loads happen during menu navigation and
playlist icon population, not in frame-critical paths, so the
latency is imperceptible.

Depends on: the video_thread_wait_idle barrier commit
(8d55232), which also hardens video_thread_texture_handle to
fall back to direct invocation when the wrapper is inactive.
That defense-in-depth means this patch is safe even against
future callers that don't follow the full guard pattern.

No behavioural change on non-threaded video (`threaded == false`
path unchanged).  No behavioural change on HW-context cores
(threaded video is force-disabled for those via
VIDEO_DRIVER_IS_THREADED_INTERNAL).

Not addressed by this patch: d3d12_font_free still runs on the
main thread during mid-session font scale changes.  XMB's
pending_context_reset counter already mitigates this by
deferring the context reset until the GPU has drained, so it's
not a new issue.  Addressing the font free path would require
a separate refactor outside this patch's scope.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-04-16 13:29:04 +02:00
1 parent 8d55232dbb
commit a2d7b486be
1 file changed
+103 -8
+103 -8
View File
@@ -73,6 +73,9 @@
#ifdef HAVE_SLANG
#include "../drivers_shader/slang_process.h"
#endif
#ifdef HAVE_THREADS
#include "../video_thread_wrapper.h"
#endif
#ifdef __WINRT__
#include "../../uwp/uwp_func.h"
@@ -6208,13 +6211,25 @@ static void d3d12_gfx_set_osd_msg(
font_driver_render_msg(d3d12, msg, params, font);
}
static uintptr_t d3d12_gfx_load_texture(
void* video_data, void* data, bool threaded,
#ifdef HAVE_THREADS
typedef struct
{
d3d12_video_t *d3d12;
struct texture_image *image;
enum texture_filter_type filter_type;
uintptr_t handle; /* in for unload, out for load */
} d3d12_texture_cmd_t;
#endif
/* Inner load function -- performs all GPU-touching work.
* Must run on the same thread that owns the D3D12 command
* queue (the video thread when threaded video is active,
* otherwise the main thread). */
static uintptr_t d3d12_gfx_load_texture_internal(
d3d12_video_t* d3d12, struct texture_image* image,
enum texture_filter_type filter_type)
{
d3d12_texture_t* texture = NULL;
d3d12_video_t* d3d12 = (d3d12_video_t*)video_data;
struct texture_image* image = (struct texture_image*)data;
d3d12_texture_t* texture = NULL;
if (!d3d12)
return 0;
@@ -6258,11 +6273,13 @@ static uintptr_t d3d12_gfx_load_texture(
return (uintptr_t)texture;
}
static void d3d12_gfx_unload_texture(void* data,
bool threaded, uintptr_t handle)
/* Inner unload function -- performs the fence wait and
* resource release. Must run on the same thread that owns
* the D3D12 command queue. */
static void d3d12_gfx_unload_texture_internal(
d3d12_video_t* d3d12, uintptr_t handle)
{
d3d12_texture_t* texture = (d3d12_texture_t*)handle;
d3d12_video_t* d3d12 = (d3d12_video_t*)data;
if (!texture)
return;
@@ -6282,6 +6299,84 @@ static void d3d12_gfx_unload_texture(void* data,
free(texture);
}
#ifdef HAVE_THREADS
/* Wrapped entry points for video_thread_texture_handle.
* These are invoked on the video thread via CMD_CUSTOM_COMMAND.
* Return value is written to cmd->handle (not via the int
* return channel) because uintptr_t may be 64-bit while the
* command's return_value is int -- truncation would corrupt
* pointer-valued handles on 64-bit Windows. */
static int d3d12_texture_load_wrap(void *data)
{
d3d12_texture_cmd_t *cmd = (d3d12_texture_cmd_t*)data;
cmd->handle = d3d12_gfx_load_texture_internal(
cmd->d3d12, cmd->image, cmd->filter_type);
return 0;
}
static int d3d12_texture_unload_wrap(void *data)
{
d3d12_texture_cmd_t *cmd = (d3d12_texture_cmd_t*)data;
d3d12_gfx_unload_texture_internal(cmd->d3d12, cmd->handle);
return 0;
}
#endif
static uintptr_t d3d12_gfx_load_texture(
void* video_data, void* data, bool threaded,
enum texture_filter_type filter_type)
{
d3d12_video_t* d3d12 = (d3d12_video_t*)video_data;
struct texture_image* image = (struct texture_image*)data;
#ifdef HAVE_THREADS
/* When threaded video is active, dispatch to the video
* thread so that texture creation, upload-buffer writes,
* and command-queue signalling all happen on the same
* thread that owns the D3D12 command queue. This
* prevents races on texture->dirty, descriptor heap
* allocation, and upload buffer visibility. */
if (threaded)
{
d3d12_texture_cmd_t cmd;
cmd.d3d12 = d3d12;
cmd.image = image;
cmd.filter_type = filter_type;
cmd.handle = 0;
video_thread_texture_handle(&cmd, d3d12_texture_load_wrap);
return cmd.handle;
}
#endif
return d3d12_gfx_load_texture_internal(d3d12, image, filter_type);
}
static void d3d12_gfx_unload_texture(void* data,
bool threaded, uintptr_t handle)
{
d3d12_video_t* d3d12 = (d3d12_video_t*)data;
if (!handle)
return;
#ifdef HAVE_THREADS
/* Same reasoning as load: dispatch Release to the video
* thread so it is serialized with command-list recording. */
if (threaded)
{
d3d12_texture_cmd_t cmd;
cmd.d3d12 = d3d12;
cmd.image = NULL;
cmd.filter_type = TEXTURE_FILTER_LINEAR;
cmd.handle = handle;
video_thread_texture_handle(&cmd, d3d12_texture_unload_wrap);
return;
}
#endif
d3d12_gfx_unload_texture_internal(d3d12, handle);
}
static bool d3d12_get_hw_render_interface(
void* data, const struct retro_hw_render_interface** iface)
{