gl, glcore: take the hardware ring's fence ahead of the swap

Under threaded video a hardware rendered GL core and the video thread
wait on each other's fences. The video thread's was created after the
frame, so behind the buffer swap. A driver that queues swaps completes
such a fence only when the swap is released, at the display's rate,
which fits the report: a core fast-forwarding at 500 fps that settles
at the display's 120 once the queue has filled, where software cores,
which never wait on it, stay uncapped.

The fence is now created just before the swap, after everything that
reads the core's framebuffer, and handed over as before. Still one
fence a frame.

tools/gl_ring_fence_check.py keeps both drivers doing it.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-05 12:50:53 +02:00
1 parent 8c94b8f9f2
commit cf12b75b70
5 files changed
+146 -3

No files matched your search

@@ -189,6 +189,17 @@ jobs:
python3 tools/input_state_grab_check.py --selftest
python3 tools/input_state_grab_check.py
- name: Check the GL drivers take the hardware ring's fence ahead of the swap
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# Taken after the swap, the fence completes only when a driver
# that queues swaps releases it, and a hardware rendered core
# fast-forwards at the display's rate.
python3 tools/gl_ring_fence_check.py --selftest
python3 tools/gl_ring_fence_check.py
- name: Check a setting's label exists wherever its translations name it
shell: bash
working-directory: ${{ github.workspace }}
+1
View File
@@ -145,6 +145,7 @@ struct gl2
* this thread to wait before reading it. Sync objects are shared
* between the two contexts. */
void *hw_ring_sync[3];
void *hw_ring_done_sync; /* taken ahead of the swap */
uint32_t flags;
+34 -1
View File
@@ -3441,6 +3441,23 @@ static bool gl2_hw_ring_present_slot(void *data, unsigned slot)
* again. */
typedef struct { void *sync; } gl2_ring_fence_t;
/* The ring's fence for this frame, taken ahead of the swap: a driver
* that queues swaps completes a fence behind one only once the swap is
* released, at the display's rate. */
static void gl2_hw_ring_drawn(gl2_t *gl)
{
#ifdef GL2_HW_RING_SYNC
if ( !gl2_core_context_is_mains(gl)
|| !(gl->flags & GL2_FLAG_HAVE_SYNC))
return;
if (gl->hw_ring_done_sync)
glDeleteSync((GLsync)gl->hw_ring_done_sync);
gl->hw_ring_done_sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
#else
(void)gl;
#endif
}
static bool gl2_hw_ring_fence_new(void *data, void **fence)
{
gl2_ring_fence_t *f;
@@ -3475,7 +3492,13 @@ static void gl2_hw_ring_fence_signal(void *data, void *fence)
{
if (f->sync)
glDeleteSync((GLsync)f->sync);
f->sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (gl->hw_ring_done_sync)
{
f->sync = gl->hw_ring_done_sync;
gl->hw_ring_done_sync = NULL;
}
else
f->sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
return;
}
@@ -4398,6 +4421,15 @@ static bool gl2_frame(void *data, const void *frame,
if (!gl)
return false;
#ifdef GL2_HW_RING_SYNC
/* One a frame without the ring's hand-over left behind */
if (gl->hw_ring_done_sync)
{
glDeleteSync((GLsync)gl->hw_ring_done_sync);
gl->hw_ring_done_sync = NULL;
}
#endif
/* These travel with the frame, so this thread does not read what the
* main thread writes: the scRGB encode below and gl2_encode_pq_to_sdr()
* read the latched copies. */
@@ -4848,6 +4880,7 @@ static bool gl2_frame(void *data, const void *frame,
gl->retained_dark = 0;
}
gl2_hw_ring_drawn(gl);
if (gl->ctx_driver->swap_buffers)
gl->ctx_driver->swap_buffers(gl->ctx_data);
+29 -2
View File
@@ -247,6 +247,7 @@ typedef struct gl3
GLuint hw_ring_texture[3];
GLuint hw_ring_fbo[3];
void *hw_ring_sync[3];
void *hw_ring_done_sync; /* taken ahead of the swap */
float menu_texture_alpha;
math_matrix_4x4 mvp; /* float alignment */
@@ -5011,6 +5012,18 @@ static void gl3_encode_pq_to_sdr(gl3_t *gl, unsigned width, unsigned height)
#endif
}
/* The ring's fence for this frame, taken ahead of the swap: a driver
* that queues swaps completes a fence behind one only once the swap is
* released, at the display's rate. */
static void gl3_hw_ring_drawn(gl3_t *gl)
{
if (!gl3_core_context_is_mains(gl))
return;
if (gl->hw_ring_done_sync)
glDeleteSync((GLsync)gl->hw_ring_done_sync);
gl->hw_ring_done_sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
static bool gl3_frame(void *data, const void *frame,
unsigned dims,
uint64_t frame_count,
@@ -5051,6 +5064,13 @@ static bool gl3_frame(void *data, const void *frame,
if (!gl)
return false;
/* One a frame without the ring's hand-over left behind */
if (gl->hw_ring_done_sync)
{
glDeleteSync((GLsync)gl->hw_ring_done_sync);
gl->hw_ring_done_sync = NULL;
}
/* Travels with the frame, for set_texture_frame() to read rather
* than the setting the menu writes */
gl->frame_menu_linear_filter = video_info->menu_linear_filter;
@@ -5652,6 +5672,7 @@ static bool gl3_frame(void *data, const void *frame,
}
}
gl3_hw_ring_drawn(gl);
if (gl->ctx_driver->swap_buffers)
gl->ctx_driver->swap_buffers(gl->ctx_data);
@@ -6239,13 +6260,19 @@ static void gl3_hw_ring_fence_free(void *data, void *fence)
static void gl3_hw_ring_fence_signal(void *data, void *fence)
{
gl3_t *gl = (gl3_t*)data;
gl3_ring_fence_t *f = (gl3_ring_fence_t*)fence;
(void)data;
if (!f)
return;
if (f->sync)
glDeleteSync((GLsync)f->sync);
f->sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (gl && gl->hw_ring_done_sync)
{
f->sync = gl->hw_ring_done_sync;
gl->hw_ring_done_sync = NULL;
}
else
f->sync = (void*)glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
}
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""The GL drivers hand the threaded wrapper's hardware ring a fence taken
ahead of the buffer swap. A fence taken after it completes only when a
driver that queues swaps releases that swap, which holds a hardware
rendered core to the display's rate in fast-forward.
Fails if a driver's frame function swaps before taking the fence, or if
its hand-over (hw_ring_fence_signal) stops using the one taken.
"""
import re
import sys
DRIVERS = (("gfx/drivers/gl2.c", "gl2"), ("gfx/drivers/gl3.c", "gl3"))
def body(src, name):
m = re.search(r"^static \w+ %s\(" % re.escape(name), src, re.M)
if not m:
return None
end = src.find("\n}\n", m.start())
return src[m.start():end] if end >= 0 else None
def check(src, prefix):
errs = []
frame = body(src, prefix + "_frame")
signal = body(src, prefix + "_hw_ring_fence_signal")
if frame is None or signal is None:
return ["%s_frame or %s_hw_ring_fence_signal not found" % (prefix, prefix)]
swap = frame.find("swap_buffers(")
drawn = frame.find("%s_hw_ring_drawn(gl);" % prefix)
if swap < 0:
errs.append("%s_frame: no swap_buffers call" % prefix)
elif drawn < 0 or drawn > swap:
errs.append("%s_frame: swaps before %s_hw_ring_drawn(gl)" % (prefix, prefix))
if "hw_ring_done_sync" not in signal:
errs.append("%s_hw_ring_fence_signal: does not hand over hw_ring_done_sync" % prefix)
return errs
def selftest():
good = ("static bool glx_frame(void *d)\n{\n glx_hw_ring_drawn(gl);\n"
" gl->ctx_driver->swap_buffers(gl->ctx_data);\n return true;\n}\n"
"static void glx_hw_ring_fence_signal(void *d, void *f)\n{\n"
" f->sync = gl->hw_ring_done_sync;\n}\n")
late = good.replace(" glx_hw_ring_drawn(gl);\n", "").replace(
" return true;", " glx_hw_ring_drawn(gl);\n return true;")
unused = good.replace("gl->hw_ring_done_sync", "glFenceSync(0, 0)")
assert check(good, "glx") == []
assert len(check(late, "glx")) == 1
assert len(check(unused, "glx")) == 1
print("gl_ring_fence_check: selftest ok")
def main():
if "--selftest" in sys.argv:
selftest()
return 0
errs = []
for path, prefix in DRIVERS:
with open(path, encoding="utf-8", errors="replace") as f:
errs += ["%s: %s" % (path, e) for e in check(f.read(), prefix)]
for e in errs:
print("FAIL " + e)
if not errs:
print("gl_ring_fence_check: ok")
return 1 if errs else 0
if __name__ == "__main__":
sys.exit(main())