gl: keep threaded hardware handoff nonblocking

Select an unqueued, completed target before publishing queried GL frames.
If none is available, retain the producer's target and account for a
missed presentation instead of waiting for the presenter or GPU.

Return source fences through atomic exchange and retire them on target
reuse, rather than deleting them immediately after a cross-context wait.
Only successful completion permits reuse. Superseded disposable GL
completion fences do not need a blocking wait before replacement.

Preserve cached-framebuffer compatibility and other hardware APIs.
This commit is contained in:
Codex authored and libretroadmin committed 2026-10-17 04:08:23 +02:00
1 parent b3dcba0388
commit 7fa57eaf2b
10 files changed
+242 -84

No files matched your search

+3 -4
View File
@@ -35,10 +35,8 @@
#include "../video_driver.h"
#include "gl_common.h"
/* The hardware ring's per-slot sync, handed from the core's thread to
* the video thread by exchange so that one thread waits and deletes
* each. A backend without pointer atomics only serves single-core
* targets, where a plain swap is the same. */
/* Source fences pass to the video thread and back to the producer
* by exchange. Without pointer atomics, only single-core targets run. */
#ifndef GL_HW_RING_SYNC_T
#if defined(RETRO_ATOMIC_HAS_PTR)
#define GL_HW_RING_SYNC_T retro_atomic_ptr_t
@@ -189,6 +187,7 @@ struct gl2
* placed after rendering into each slot's texture, for the frame on
* this thread to wait before reading it. Sync objects are shared
* between the two contexts (GL_HW_RING_SYNC_SWAP). */
GL_HW_RING_SYNC_T hw_ring_retired_sync[3];
GL_HW_RING_SYNC_T hw_ring_sync[3];
void *hw_ring_done_sync; /* taken ahead of the swap */
/* Placed after each PBO readback: a slot is read only once its
+32 -6
View File
@@ -1987,6 +1987,21 @@ static void gl2_bind_core_context(gl2_t *gl, bool enable)
static void gl2_renderchain_deinit_hw_render(gl2_t *gl, gl2_renderchain_data_t *chain)
{
gl2_bind_core_context(gl, true);
#ifdef GL2_HW_RING_SYNC
{
unsigned i;
for (i = 0; i < 3; i++)
{
void *sync;
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[i], NULL, sync);
if (sync)
glDeleteSync((GLsync)sync);
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[i], NULL, sync);
if (sync)
glDeleteSync((GLsync)sync);
}
}
#endif
if (gl->flags & GL2_FLAG_HW_RENDER_FBO_INIT)
gl2_delete_fb(gl->textures, gl->hw_render_fbo);
if (chain->flags & GL2_CHAIN_FLAG_HW_RENDER_DEPTH_INIT)
@@ -3700,9 +3715,14 @@ static bool gl2_hw_ring_capture(void *data, unsigned slot,
/* A sync the video thread never took belongs to a frame it
* dropped; deleting it does not disturb a wait already placed. */
void *old;
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot],
(void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), old);
void *sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!sync)
return false;
glFlush();
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[slot], NULL, old);
if (old)
glDeleteSync((GLsync)old);
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot], sync, old);
if (old)
glDeleteSync((GLsync)old);
return true;
@@ -3725,8 +3745,11 @@ static bool gl2_hw_ring_present_slot(void *data, unsigned slot)
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot], NULL, sync);
if (sync)
{
void *old;
glWaitSync((GLsync)sync, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)sync);
/* The producer retires this after the slot's reads complete. */
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[slot], sync, old);
(void)old;
}
}
#endif
@@ -3813,12 +3836,15 @@ static bool gl2_hw_ring_fence_wait(void *data, void *fence, unsigned timeout_us)
if (!f)
return true;
#ifdef GL2_HW_RING_SYNC
if (f->sync)
if (((gl2_t*)data)->flags & GL2_FLAG_HAVE_SYNC)
{
GLenum r = glClientWaitSync((GLsync)f->sync, GL_SYNC_FLUSH_COMMANDS_BIT,
GLenum r;
if (!f->sync)
return false;
r = glClientWaitSync((GLsync)f->sync, 0,
timeout_us == HW_RING_WAIT_FOREVER
? GL_TIMEOUT_IGNORED : (GLuint64)timeout_us * 1000);
if (r == GL_TIMEOUT_EXPIRED)
if (r != GL_ALREADY_SIGNALED && r != GL_CONDITION_SATISFIED)
return false;
/* Not deleted here: the video thread made it and may still be
* in the swap that follows it. It goes when the cell is
+33 -11
View File
@@ -52,10 +52,8 @@
#include "../../retroarch.h"
#include "../../verbosity.h"
/* The hardware ring's per-slot sync, handed from the core's thread to
* the video thread by exchange so that one thread waits and deletes
* each. A backend without pointer atomics only serves single-core
* targets, where a plain swap is the same. */
/* Source fences pass to the video thread and back to the producer
* by exchange. Without pointer atomics, only single-core targets run. */
#ifndef GL_HW_RING_SYNC_T
#if defined(RETRO_ATOMIC_HAS_PTR)
#define GL_HW_RING_SYNC_T retro_atomic_ptr_t
@@ -390,6 +388,7 @@ typedef struct gl3
* the two contexts. */
GLuint hw_ring_texture[3];
GLuint hw_ring_fbo[3];
GL_HW_RING_SYNC_T hw_ring_retired_sync[3];
GL_HW_RING_SYNC_T hw_ring_sync[3]; /* GL_HW_RING_SYNC_SWAP */
void *hw_ring_done_sync; /* taken ahead of the swap */
@@ -2357,6 +2356,16 @@ static void gl3_deinit_hw_render(gl3_t *gl)
{
unsigned i;
for (i = 0; i < 3; i++)
{
void *sync;
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[i], NULL, sync);
if (sync)
glDeleteSync((GLsync)sync);
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[i], NULL, sync);
if (sync)
glDeleteSync((GLsync)sync);
}
for (i = 1; i < 3; i++)
{
if (gl->hw_ring_fbo[i])
@@ -8556,9 +8565,14 @@ static bool gl3_hw_ring_capture(void *data, unsigned slot,
/* A sync the video thread never took belongs to a frame it
* dropped; deleting it does not disturb a wait already placed. */
void *old;
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot],
(void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), old);
void *sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!sync)
return false;
glFlush();
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[slot], NULL, old);
if (old)
glDeleteSync((GLsync)old);
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot], sync, old);
if (old)
glDeleteSync((GLsync)old);
}
@@ -8576,8 +8590,11 @@ static bool gl3_hw_ring_present_slot(void *data, unsigned slot)
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_sync[slot], NULL, sync);
if (sync)
{
void *old;
glWaitSync((GLsync)sync, 0, GL_TIMEOUT_IGNORED);
glDeleteSync((GLsync)sync);
/* The producer retires this after the slot's reads complete. */
GL_HW_RING_SYNC_SWAP(&gl->hw_ring_retired_sync[slot], sync, old);
(void)old;
}
gl->hw_render_texture = gl->hw_ring_texture[slot];
return true;
@@ -8628,12 +8645,17 @@ static bool gl3_hw_ring_fence_wait(void *data, void *fence, unsigned timeout_us)
{
gl3_ring_fence_t *f = (gl3_ring_fence_t*)fence;
(void)data;
if (!f || !f->sync)
if (!f)
return true;
if (glClientWaitSync((GLsync)f->sync, GL_SYNC_FLUSH_COMMANDS_BIT,
timeout_us == HW_RING_WAIT_FOREVER
? GL_TIMEOUT_IGNORED : (GLuint64)timeout_us * 1000) == GL_TIMEOUT_EXPIRED)
if (!f->sync)
return false;
{
GLenum ready = glClientWaitSync((GLsync)f->sync, 0,
timeout_us == HW_RING_WAIT_FOREVER
? GL_TIMEOUT_IGNORED : (GLuint64)timeout_us * 1000);
if (ready != GL_ALREADY_SIGNALED && ready != GL_CONDITION_SATISFIED)
return false;
}
/* Not deleted here: the video thread made it and may still be in
* the swap that follows it. It goes when the cell is signalled again */
return true;
+23 -2
View File
@@ -1061,6 +1061,27 @@ int video_thread_hw_publish(thread_video_t *thr, unsigned *taken_back)
return -1;
}
#endif
if (ring->api == HW_API_GL && !stay)
{
unsigned n;
for (n = 1; n < VIDEO_THREAD_HW_RING; n++)
{
unsigned next = (published + n) % VIDEO_THREAD_HW_RING;
int st = retro_atomic_load_acquire_int(&thr->frame.state);
if ((int)next == retro_atomic_load_acquire_int(&ring->last_presented)
|| hw_slot_queued(thr, next, st))
continue;
if (!hw_fence_wait(&ring->slot[next].fence,
thr->poke->hw_ring_fence_wait, thr->driver_data, 0))
continue;
if (!thr->poke->hw_ring_capture(thr->driver_data, published, NULL, 0))
return VIDEO_THREAD_HW_BUSY;
ring->index = next;
return (int)published;
}
/* Retain the producer's slot when every other slot has a reader. */
return VIDEO_THREAD_HW_BUSY;
}
if (ring->api == HW_API_GL)
{
/* Fence the core's rendering into the slot, on its context. */
@@ -1221,8 +1242,8 @@ void video_thread_hw_after_frame(thread_video_t *thr, int hw_slot)
/* Signalled after the submission the frame call just made, on a
* fence nobody else holds: a dupe arms a slot that is armed already. */
hw_fence_arm(&s->fence, thr->poke->hw_ring_fence_signal,
thr->poke->hw_ring_fence_wait, thr->driver_data,
HW_RING_WAIT_FOREVER);
ring->api == HW_API_GL ? NULL : thr->poke->hw_ring_fence_wait,
thr->driver_data, HW_RING_WAIT_FOREVER);
}
#ifdef HAVE_VULKAN
+4 -1
View File
@@ -56,6 +56,7 @@ RETRO_BEGIN_DECLS
#ifdef HAVE_THREADS
#define VIDEO_THREAD_HW_RING 3
#define VIDEO_THREAD_HW_BUSY (-2)
struct thread_video;
@@ -67,7 +68,9 @@ bool video_thread_get_hw_render_interface(void *data,
/* Main thread, at a push with RETRO_HW_FRAME_BUFFER_VALID: returns the
* HW slot to publish, advancing the core's index and waiting the slot
* the core will fill next; -1 if there is no ring. *taken_back counts
* the core will fill next; -1 if there is no ring. GL returns
* VIDEO_THREAD_HW_BUSY without publishing when no slot is ready.
* *taken_back counts
* the unclaimed frames taken off the frame ring for it. */
int video_thread_hw_publish(struct thread_video *thr, unsigned *taken_back);
+4 -2
View File
@@ -88,7 +88,8 @@ static INLINE void hw_fence_init(hw_fence_t *f)
/* Video thread, after a frame drawn from the slot; or the core's
* thread, for a frame taken back (see above). `forever` is the
* driver's unbounded timeout. */
* driver's unbounded timeout. NULL wait permits disposable syncs to
* be superseded without waiting. */
static INLINE void hw_fence_arm(hw_fence_t *f, hw_fence_signal_fn signal,
hw_fence_wait_fn wait, void *data, unsigned forever)
{
@@ -105,7 +106,8 @@ static INLINE void hw_fence_arm(hw_fence_t *f, hw_fence_signal_fn signal,
prev = retro_atomic_exchange_int(&f->armed, c + 1);
if (prev)
{
wait(data, f->cell[prev - 1], forever);
if (wait)
wait(data, f->cell[prev - 1], forever);
retro_atomic_store_release_int(&f->busy[prev - 1], 0);
}
}
+15
View File
@@ -3684,6 +3684,21 @@ static bool video_thread_frame(void *data, const void *frame_,
if (frame_ == RETRO_HW_FRAME_BUFFER_VALID)
{
hw_slot = video_thread_hw_publish(thr, &taken_back);
if (hw_slot == VIDEO_THREAD_HW_BUSY)
{
thr->miss_count++;
if (timed)
video_thread_handoff_account(thr,
(uint64_t)cpu_features_get_perf_counter() - c_in - c_wait,
0, c_wait, 0, true, false, waited, true);
thr->last_time = cpu_features_get_time_usec();
thr->run_start = thr->last_time;
if (timed)
video_thread_handoff_latch(thr,
(uint64_t)cpu_features_get_perf_counter() - c_now,
(uint64_t)(thr->last_time - t_now));
return true;
}
frame_ = NULL;
/* No ring: the driver cannot take a hardware frame from this
* thread. frame_ is NULL now, which this function treats as a
@@ -35,7 +35,51 @@ static int run_chain(void)
return chain.fbo_pass;
}
static unsigned ring_waits, ring_deletes;
static bool ring_alloc_fails;
static void APIENTRY ring_wait(GLsync sync, GLbitfield flags, GLuint64 timeout)
{
CHECK(sync == (GLsync)(uintptr_t)1 && !flags && timeout == GL_TIMEOUT_IGNORED,
"HW ring queues the source dependency");
ring_waits++;
}
static GLsync APIENTRY ring_fence(GLenum condition, GLbitfield flags)
{
(void)condition; (void)flags;
return ring_alloc_fails ? NULL : (GLsync)(uintptr_t)2;
}
static void APIENTRY ring_delete(GLsync sync)
{
CHECK(sync == (GLsync)(uintptr_t)1, "producer retires the consumed source fence");
ring_deletes++;
}
static void run_ring_cases(void)
{
gl2_t gl;
void *old;
memset(&gl, 0, sizeof(gl));
gl.textures = 3; gl.flags = GL2_FLAG_HAVE_SYNC;
__rglgen_glWaitSync = ring_wait;
__rglgen_glFenceSync = ring_fence;
__rglgen_glDeleteSync = ring_delete;
GL_HW_RING_SYNC_SWAP(&gl.hw_ring_sync[1], (void*)(uintptr_t)1, old);
(void)old;
CHECK(gl2_hw_ring_present_slot(&gl, 1), "HW frame presented");
CHECK(ring_waits == 1 && !ring_deletes, "video thread never deletes the source fence");
CHECK(gl2_hw_ring_present_slot(&gl, 1), "duplicate reuses HW frame");
CHECK(ring_waits == 1 && !ring_deletes, "duplicate does not consume the fence twice");
ring_alloc_fails = true;
CHECK(!gl2_hw_ring_capture(&gl, 1, NULL, 0), "failed source fence refuses publication");
CHECK(!ring_deletes, "failed capture retains returned fence");
ring_alloc_fails = false;
CHECK(gl2_hw_ring_capture(&gl, 1, NULL, 0), "producer captures the reused slot");
CHECK(ring_deletes == 1, "producer retires returned fence once");
}
int main(void)
{
return run_cases(run_chain, -1);
int result = run_cases(run_chain, -1);
run_ring_cases();
return result || fails;
}
@@ -304,11 +304,54 @@ static void run_resize_cases(void)
CHECK(sync_created == sync_deleted, "resize teardown retires every recording fence");
}
static unsigned ring_waits, ring_deletes;
static bool ring_alloc_fails;
static void APIENTRY ring_wait(GLsync sync, GLbitfield flags, GLuint64 timeout)
{
CHECK(sync == (GLsync)(uintptr_t)1 && !flags && timeout == GL_TIMEOUT_IGNORED,
"HW ring queues the source dependency");
ring_waits++;
}
static GLsync APIENTRY ring_fence(GLenum condition, GLbitfield flags)
{
(void)condition; (void)flags;
return ring_alloc_fails ? NULL : (GLsync)(uintptr_t)2;
}
static void APIENTRY ring_delete(GLsync sync)
{
CHECK(sync == (GLsync)(uintptr_t)1, "producer retires the consumed source fence");
ring_deletes++;
}
static void run_ring_cases(void)
{
gl3_t gl;
void *old;
memset(&gl, 0, sizeof(gl));
gl.hw_ring_texture[1] = 99;
__rglgen_glWaitSync = ring_wait;
__rglgen_glFenceSync = ring_fence;
__rglgen_glDeleteSync = ring_delete;
GL_HW_RING_SYNC_SWAP(&gl.hw_ring_sync[1], (void*)(uintptr_t)1, old);
(void)old;
CHECK(gl3_hw_ring_present_slot(&gl, 1), "HW frame presented");
CHECK(ring_waits == 1 && !ring_deletes, "video thread never deletes the source fence");
CHECK(gl3_hw_ring_present_slot(&gl, 1), "duplicate reuses HW frame");
CHECK(ring_waits == 1 && !ring_deletes, "duplicate does not consume the fence twice");
ring_alloc_fails = true;
CHECK(!gl3_hw_ring_capture(&gl, 1, NULL, 0), "failed source fence refuses publication");
CHECK(!ring_deletes, "failed capture retains returned fence");
ring_alloc_fails = false;
CHECK(gl3_hw_ring_capture(&gl, 1, NULL, 0), "producer captures the reused slot");
CHECK(ring_deletes == 1, "producer retires returned fence once");
}
int main(void)
{
int result = run_cases(run_chain, 0);
run_readback_cases();
run_fence_cases();
run_resize_cases();
run_ring_cases();
return result || fails;
}
+40 -57
View File
@@ -25,77 +25,60 @@ static bool wait_fence(void *data, void *fence, unsigned us)
static bool capture(void *data, unsigned slot, const void *context, unsigned format)
{ (void)data; (void)slot; (void)context; (void)format; captures++; return true; }
typedef struct test_run
{
thread_video_t *thr;
retro_atomic_int_t done;
unsigned taken;
int published;
} test_run_t;
static void publish(void *data)
static void signal_fence(void *data, void *fence) { (void)data; (void)fence; }
static bool gpu_ready = true;
static unsigned polls;
static bool poll_fence(void *data, void *fence, unsigned us)
{
test_run_t *t = (test_run_t*)data;
t->published = video_thread_hw_publish(t->thr, &t->taken);
retro_atomic_store_release_int(&t->done, 1);
(void)data; (void)fence;
CHECK(us == 0, "slot readiness never blocks");
polls++;
return gpu_ready;
}
static void await_flag(retro_atomic_int_t *flag)
{
retro_time_t start = cpu_features_get_time_usec();
while (!retro_atomic_load_acquire_int(flag))
{
if (cpu_features_get_time_usec() - start > 5000000)
{
fprintf(stderr, "FAIL: queue watchdog\n");
exit(1);
}
retro_sleep(1);
}
}
static void run_case(int state, int last, bool blocked)
static void run_case(int state, int last, int expected)
{
static thread_video_t thr;
video_poke_interface_t poke;
hw_ring_t *ring;
test_run_t t;
sthread_t *thread;
unsigned taken = 99;
int published;
memset(&thr, 0, sizeof(thr));
memset(&poke, 0, sizeof(poke));
memset(&t, 0, sizeof(t));
created = freed = captures = 0;
created = freed = captures = polls = 0;
poke.hw_ring_fence_new = new_fence;
poke.hw_ring_fence_free = free_fence;
poke.hw_ring_fence_wait = wait_fence;
poke.hw_ring_fence_wait = poll_fence;
poke.hw_ring_capture = capture;
thr.poke = &poke;
if (!retro_eventcount_init(&thr.frame.ring) || !hw_ring_setup(&thr, &ring))
exit(1);
ring->api = HW_API_GL;
ring->index = VIDEO_THREAD_HW_RING - 1;
ring->index = 2;
ring->gl_queried = true;
retro_atomic_store_release_int(&ring->last_presented, last);
thr.frame.slot[0].hw_slot = thr.frame.slot[1].hw_slot = -1;
thr.frame.slot[0].dupe = true;
thr.frame.slot[0].hw_slot = 0;
thr.frame.slot[1].hw_slot = 1;
retro_atomic_store_release_int(&thr.frame.state, state);
t.thr = &thr;
if (!(thread = sthread_create(publish, &t)))
exit(1);
if (blocked)
{
await_flag(&thr.frame.ring.waiters);
retro_sleep(80);
CHECK(!retro_atomic_load_acquire_int(&t.done),
"duplicate retains GL slot beyond the short wait");
retro_atomic_store_release_int(&thr.frame.state, VIDEO_THREAD_RING_MAKE(0, 0, false));
retro_eventcount_notify(&thr.frame.ring);
}
await_flag(&t.done);
sthread_join(thread);
CHECK(t.published == VIDEO_THREAD_HW_RING - 1 && !t.taken
&& ring->index == 0 && captures == 1, "publication preserves slot rotation");
CHECK(!retro_atomic_load_acquire_int(&thr.frame.ring.waiters), "waiter retired");
/* Both candidates have GPU work left from an earlier presentation. */
hw_fence_arm(&ring->slot[0].fence, signal_fence, NULL, NULL, 0);
hw_fence_arm(&ring->slot[0].fence, signal_fence, NULL, NULL, 0);
hw_fence_arm(&ring->slot[1].fence, signal_fence, NULL, NULL, 0);
CHECK(!polls, "superseded disposable fences never wait");
published = video_thread_hw_publish(&thr, &taken);
CHECK(!taken, "publication never takes queued slots back");
CHECK(!retro_atomic_load_acquire_int(&thr.frame.ring.waiters), "publication never parks");
if (expected < 0)
CHECK(published == VIDEO_THREAD_HW_BUSY && ring->index == 2 && !captures,
"busy ring retains the producer's slot without publishing");
else
CHECK(published == 2 && ring->index == (unsigned)expected && captures == 1,
"publication advances only to a completed unqueued slot");
CHECK(polls <= 2, "bounded readiness checks");
gpu_ready = true;
poke.hw_ring_fence_wait = wait_fence;
video_thread_hw_free(&thr);
CHECK(!thr.frame.hw_ring && created == freed, "all scripted fences retired");
retro_eventcount_free(&thr.frame.ring);
@@ -103,12 +86,12 @@ static void run_case(int state, int last, bool blocked)
int main(void)
{
run_case(VIDEO_THREAD_RING_MAKE(0, 1, false), 0, true);
run_case(VIDEO_THREAD_RING_MAKE(1, 0, true), 0, true);
run_case(VIDEO_THREAD_RING_MAKE(0, 2, false), 0, true);
run_case(VIDEO_THREAD_RING_MAKE(1, 2, false), 0, true);
run_case(VIDEO_THREAD_RING_MAKE(0, 0, false), 0, false);
run_case(VIDEO_THREAD_RING_MAKE(0, 1, false), 1, false);
run_case(VIDEO_THREAD_RING_MAKE(0, 1, false), 0, 1);
run_case(VIDEO_THREAD_RING_MAKE(1, 0, true), 0, 1);
run_case(VIDEO_THREAD_RING_MAKE(0, 2, false), -1, -1);
run_case(VIDEO_THREAD_RING_MAKE(0, 0, false), 1, 0);
gpu_ready = false;
run_case(VIDEO_THREAD_RING_MAKE(0, 0, false), -1, -1);
printf("%s hw_ring_queue_test (%u failures)\n", failures ? "FAIL" : "PASS", failures);
return failures ? 1 : 0;
}