Merge pull request #654 from RetroPortingToolKit/fix/gl-present-ring-restart

Debug present ring: forget GL names when the renderer shuts down
This commit is contained in:
Matt Stanley
2026-10-13 01:31:20 -07:00
committed by GitHub
4 changed files with 161 additions and 2 deletions
+12
View File
@@ -411,6 +411,18 @@ if(BUILD_TESTING)
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-fmv-chroma-results")
set_tests_properties(gl_fmv_chroma_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
# Debug present ring: GL names do not outlive a renderer restart.
add_test(NAME gl_present_ring_restart_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
--debug-tools
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_present_ring_restart.c"
--cc "${CMAKE_C_COMPILER}"
--sdl-include "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>"
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-present-ring-restart-results")
set_tests_properties(gl_present_ring_restart_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
endif()
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
+20 -1
View File
@@ -6351,8 +6351,12 @@ int gl_renderer_get_swap_interval(void) {
static void pass_resources_release(void);
static void present_image_ring_gl_forget(void);
void gl_renderer_shutdown(void) {
gl_renderer_render_thread_stop(); /* the context comes back to this thread */
gl_renderer_render_thread_stop(); /* drains; the context comes back to this thread */
/* After the worker has stopped (it presents, and captures, on its own
* thread) and before the context dies: forget the ring's GL names. */
present_image_ring_gl_forget();
s_native_surface_enabled=s_native_surface_pending=0;
gl_renderer_clear_hd_texture_cache();
pass_resources_release();
@@ -9069,6 +9073,18 @@ static GLuint s_pir_fbo, s_pir_tex, s_pir_pbo[2];
static int s_pir_w, s_pir_pending[2], s_pir_pending_w[2], s_pir_idx;
static uint32_t s_pir_frame[2];
static int s_pir_disabled;
/* The GL objects belong to the context: a renderer restart (lobby rematch,
* in-game menu -> match) makes new ones, so forget the old names. Reading
* back into a stale pack-buffer name reads into client memory instead. */
static int s_pir_forget_rth_on = -1; /* tests: was a render thread still running? */
static void present_image_ring_gl_forget(void) {
s_pir_forget_rth_on = s_rth_on;
s_pir_fbo = s_pir_tex = 0;
s_pir_pbo[0] = s_pir_pbo[1] = 0;
s_pir_pending[0] = s_pir_pending[1] = 0;
s_pir_idx = 0;
s_pir_disabled = 0;
}
static void present_image_ring_capture_gl(void) {
if (s_pir_disabled || !p_glMapBuffer || !p_glUnmapBuffer) return;
@@ -9136,6 +9152,9 @@ static void present_image_ring_capture_gl(void) {
s_pir_w = tw;
}
#endif
#ifdef PSX_NO_DEBUG_TOOLS
static void present_image_ring_gl_forget(void) {} /* no ring in this build */
#endif
/* Host overlays (OSD text, volume bar, rewind strip, savestate menu) are
* rasterized by the emulation thread. A recorded present carries a copy taken
+2 -1
View File
@@ -13,6 +13,7 @@ def main():
parser.add_argument('--sdl-include',required=True)
parser.add_argument('--sdl-library',required=True)
parser.add_argument('--output',required=True)
parser.add_argument('--debug-tools',action='store_true',help='build with the debug tools (present ring) compiled in')
parser.add_argument('--fixture',type=Path)
parser.add_argument('--build-only',action='store_true',
help='build the probe without starting a GL context')
@@ -35,7 +36,7 @@ def main():
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8')
print(r.stdout,r.stderr[-4000:],flush=True)
if r.returncode: raise subprocess.CalledProcessError(r.returncode,command)
flags=['-std=gnu11','-O'+args.optimization,'-DPSX_SDL3=1','-DPSX_NO_DEBUG_TOOLS=1',
flags=['-std=gnu11','-O'+args.optimization,'-DPSX_SDL3=1',*([] if args.debug_tools else ['-DPSX_NO_DEBUG_TOOLS=1']),
'-ffunction-sections','-fdata-sections','-I',fw/'runtime/include','-I',fw/'runtime/src']
for include in args.sdl_include.split(';'):
if include: flags+=['-I',include]
@@ -0,0 +1,127 @@
/* The debug present ring across a renderer restart, on a real hidden GL
* context (from the fmv_chroma fixture header).
*
* A synthetic 4:2:0-style 24-bit frame (constant luma, chroma constant per
* 2x2 block and flipping between saturated hues block to block, plus a sharp
* luma edge on the right) is presented through gl_renderer_present, the
* CPU/24-bit present path:
* - off, and on for a frame that is not a 24-bit scanout, are byte-identical;
* - on, chroma steps between neighbouring output pixels shrink a lot while
* luma stays (the flat-luma area keeps its luma within a few levels, and
* the sharp luma edge stays as sharp);
* - the smoothed-present counter counts only 24-bit presents.
* Original source-owned scene; no retail payload. */
#define GL_PRESENT_TEST_HOOK(gen) cap_hook(gen)
static void cap_hook(int gen);
#define main scale_fixture_main
#include "test_gl_scale_invariance.c"
#undef main
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;(void)w;(void)h;(void)p;}
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_begin_copy(int sx,int sy,int x,int y,int w,int h){(void)sx;(void)sy;(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_end_copy(void){}
#ifndef PSX_NO_DEBUG_TOOLS
int present_image_ring_accepting(void){return 1;}
int present_image_ring_thumb_w(int w,int h){(void)h;return w > 0 ? 64 : 0;}
void present_image_ring_push(uint32_t f,const uint8_t* p,int w,int pitch,int flip){(void)f;(void)p;(void)w;(void)pitch;(void)flip;}
void present_image_ring_push_argb(uint32_t f,const uint32_t* p,int w,int h,int pitch){(void)f;(void)p;(void)w;(void)h;(void)pitch;}
#endif
void present_shot_done(int ok) { (void)ok; }
int host_osd_needs_present(void) { return 0; }
int psx_present_vsync_owns_cadence(void) { return 0; }
void latency_ring_mark(LatencyStage stage) { (void)stage; }
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
void psx_host_sleep_micros(unsigned us) { SDL_Delay((us + 999u) / 1000u); }
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
void host_osd_present_done(void) {}
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
float psx_rewind_slide(void) { return 0; }
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
static void cap_hook(int gen) { (void)gen; }
#define SW 64
#define SH 48
static uint32_t src[SW * SH];
/* Gpu tests run without PSX_NO_DEBUG_TOOLS, so the ring capture is live. */
static SDL_Window *open_ctx(void) {
SDL_Window *win = SDL_CreateWindow("present ring restart test", 0, 0, 320, 240,
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
if (!win) return NULL;
glb_init(vram); glb_set_scale(1); gl_renderer_set_swap_interval(0);
if (!gl_renderer_init_context(win)) { SDL_DestroyWindow(win); return NULL; }
return win;
}
static void check_forgotten(const char* when) {
char l[160];
snprintf(l, sizeof l, "%s: ring names forgotten at shutdown", when);
check(s_pir_fbo == 0 && s_pir_tex == 0 && s_pir_pbo[0] == 0 && s_pir_pbo[1] == 0, l);
snprintf(l, sizeof l, "%s: no readback pending across contexts", when);
check(s_pir_pending[0] == 0 && s_pir_pending[1] == 0, l);
snprintf(l, sizeof l, "%s: forgotten after the render thread stopped", when);
check(s_pir_forget_rth_on == 0, l);
}
int main(void) {
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#if defined(__APPLE__)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif
for (int i = 0; i < SW * SH; i++) src[i] = 0xFF336699u;
/* 1. Direct presents (no render thread), then shutdown. */
SDL_Window *win = open_ctx();
if (!win) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
for (int i = 0; i < 3; i++) gl_renderer_present(src, SW, SH, 0, 0, 0);
check(s_pir_fbo != 0 && s_pir_pbo[0] != 0, "ring objects made in the first context");
gl_renderer_shutdown();
/* A stale pack-buffer name made the next readback write through a null
* client pointer (glReadPixels crash after a lobby rematch). */
check_forgotten("direct");
SDL_DestroyWindow(win);
/* 2. Render thread: presents (and ring captures) run on the worker.
* Queued frames are drained before the check, and shutdown must stop
* the worker before the names are forgotten. */
win = open_ctx();
if (!win) { fprintf(stderr, "SKIP no second GL context\n"); return 77; }
if (gl_renderer_render_thread_start(2) == 1) {
for (int i = 0; i < 6; i++) {
gl_renderer_present(src, SW, SH, 0, 0, 0);
gl_renderer_render_thread_frame_boundary();
}
gl_renderer_render_thread_sync("ring test drain");
check(s_pir_fbo != 0 && s_pir_pbo[0] != 0, "ring objects made by the render thread");
for (int i = 0; i < 3; i++) { /* leave presents queued at shutdown */
gl_renderer_present(src, SW, SH, 0, 0, 0);
gl_renderer_render_thread_frame_boundary();
}
gl_renderer_shutdown();
check(!gl_renderer_render_thread_active(), "render thread stopped by shutdown");
check_forgotten("render thread");
} else {
printf("render thread not eligible here: threaded case skipped\n");
gl_renderer_shutdown();
}
SDL_DestroyWindow(win);
/* 3. A fresh context rebuilds live objects. */
win = open_ctx();
if (!win) { fprintf(stderr, "SKIP no third GL context\n"); return 77; }
for (int i = 0; i < 3; i++) gl_renderer_present(src, SW, SH, 0, 0, 0);
check(s_pir_fbo != 0 && s_pir_pbo[0] != 0, "ring rebuilt in the new context");
check(glIsTexture(s_pir_tex) == GL_TRUE, "ring texture is a live object of the new context");
check(glGetError() == GL_NO_ERROR, "GL errors");
printf("checks=%d failures=%d\n", checks, failures);
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
return failures ? 1 : 0;
}