mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-09 03:29:43 -10:00
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:
1 parent
8d55232dbb
commit
a2d7b486be
1 file changed
+103
-8
+103
-8
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user