From 594d365060acfa56fa83bf1eb9f7911853cc2706 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:13:19 -0700 Subject: [PATCH] GPU: count the depth24 present hold down at guest VBlank GP1(07h) arms s_d24_present_hold to skip a few Swaps after a depth24 display-band retarget. The hold is in the GPU snap, but it counted down only at host present: a headless run (and any path that skips the present) left it stuck, and a rollback snap load zeroed it. In R4's 4-seat rollback self-test the replayed saves then differed from the live saves of the same ticks in the GPU section (hold 3 live, 0 replay; #636). The count now moves at guest VBlank (gpu_vblank_tick), which also latches whether the following present skips its Swap; the present only consumes that latch. A rollback snap load keeps the restored count. A disk savestate load still clears it so the restored VRAM shows at once. Co-Authored-By: Claude Opus 5.5 --- runtime/CMakeLists.txt | 3 ++ runtime/src/gpu.c | 17 ++++++++-- runtime/src/main.cpp | 9 +++--- .../tests/test_depth24_present_hold_vblank.py | 32 +++++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 runtime/tests/test_depth24_present_hold_vblank.py diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 517121c6..50197b26 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1810,6 +1810,9 @@ if(BUILD_TESTING) --cc ${CMAKE_C_COMPILER}) endif() add_test(NAME fntrace_c_linkage_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fntrace_c_linkage.py) + add_test(NAME depth24_present_hold_vblank_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_depth24_present_hold_vblank.py) add_test(NAME netplay_load_apply_flow_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_netplay_load_apply_flow.py) diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 0a9e12a8..7c2b0369 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3408,6 +3408,12 @@ static uint16_t vram_write_pixels[1024 * 512]; * gpu_depth24_rgb_limit — declared early so gpu_reset_state can clear it. */ static uint32_t s_d24_upload_x1 = 0; static int s_d24_present_hold = 0; /* vblanks to skip Swap after GP1(07h) */ +/* Host side of the hold: whether the present after the latest guest VBlank + * skips its Swap. The count itself is guest state (in the GPU snap) and moves + * only at guest VBlank, so a live run, a rollback replay and a headless run + * all hold the same span (#636: a present-ticked hold stuck at 3 in headless + * live play and was zeroed by a rollback load, so replay != live). */ +static int s_d24_present_skip = 0; static uint32_t s_d24_prev_disp_h = 0; /* last GP1(07h) band height */ static void depth24_note_upload(uint32_t x, uint32_t w); @@ -3880,6 +3886,7 @@ static void gpu_reset_state(int clear_vram) { ws_scene_hold_reset(&s_ws_scene_hold); s_d24_upload_x1 = 0; s_d24_present_hold = 0; + s_d24_present_skip = 0; s_d24_prev_disp_h = 0; } @@ -4145,6 +4152,9 @@ void gpu_vblank_tick(void) { * poll first became reachable in this runtime. */ if (vertical_interlace) interlace_field = (lcf ^ 1) & 1; + s_d24_present_skip = s_d24_present_hold > 0; + if (s_d24_present_hold > 0) + s_d24_present_hold--; /* snapshot per-frame draw-offset-Y range for the strobe instrumentation */ if (g_doff_cnt_this) { g_doff_min_last = g_doff_min_this; @@ -4232,15 +4242,16 @@ void gpu_depth24_upload_span_reset(void) { } int gpu_depth24_present_hold_tick(void) { - if (s_d24_present_hold <= 0) return 0; - s_d24_present_hold--; - return 1; + int skip = s_d24_present_skip; + s_d24_present_skip = 0; /* one skipped Swap per guest VBlank */ + return skip; } void gpu_depth24_on_savestate_loaded(void) { /* Hold skips Swap — after restore we want the restored VRAM visible now. * Upload span / prev_h were restored from the GPU snap. */ s_d24_present_hold = 0; + s_d24_present_skip = 0; } /* ---- Present-time screen-colour LUT (verified-enhancement, opt-in) ------- diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index db52ae0b..e1350d3f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1120,7 +1120,9 @@ extern "C" void psx_frontend_on_rb_snap_loaded(void) { const int media = gpu_display_is_depth24() || mdec_recently_active(8) || cdrom_fmv_stream_pending() || cdrom_xa_stream_active(); g_audio_cycle_resync = 1; - gpu_depth24_on_savestate_loaded(); + /* The depth24 present hold came back with the GPU snap and is guest state + * now (ticked at guest VBlank): keep it, so the replay matches the live + * run of the same ticks. */ s_d24_cutover_blank = 0; if (media) { s_d24_saw_gap = 0; @@ -8885,9 +8887,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { /* Depth24 GP1(07h) retarget (MotK intro→crawl): keep the prior Swap for a * few vblanks so stale trailing VRAM never flashes on the right edge. - * Must tick every present — gpu.c arms s_d24_present_hold and also - * freezes upload-span tracking while hold > 0; without this call the hold - * sticks and depth24_fix_trailing_margin blanks the whole FMV forever. */ + * gpu.c counts the hold down at guest VBlank (it also freezes upload-span + * tracking while > 0); this skips the Swap of each held VBlank. */ if (gpu_depth24_present_hold_tick()) return ep; diff --git a/runtime/tests/test_depth24_present_hold_vblank.py b/runtime/tests/test_depth24_present_hold_vblank.py new file mode 100644 index 00000000..53bff315 --- /dev/null +++ b/runtime/tests/test_depth24_present_hold_vblank.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""The depth24 present hold is guest state, counted down at guest VBlank. + +GP1(07h) arms s_d24_present_hold (skip a few Swaps after a depth24 band +retarget). It is in the GPU snap. It used to count down only at host present, +so a headless run never moved it, and a rollback snap load zeroed it: the +replayed save of a tick differed from the live save in the GPU section +(psxrecomp #636). Guards: gpu_vblank_tick counts it down, the present-side +query never touches it, and a rollback snap load keeps the restored value. +""" +from pathlib import Path + +from test_netplay_load_apply_flow import function_body + +SRC = Path(__file__).resolve().parents[1] / "src" + + +def main(): + gpu = (SRC / "gpu.c").read_text() + vblank = function_body(gpu, "void gpu_vblank_tick(void)") + assert "s_d24_present_hold--" in vblank + tick = function_body(gpu, "int gpu_depth24_present_hold_tick(void)") + assert "s_d24_present_hold" not in tick, "the hold must not move at host present" + + main_cpp = (SRC / "main.cpp").read_text() + rb_loaded = function_body(main_cpp, 'extern "C" void psx_frontend_on_rb_snap_loaded(void)') + assert "gpu_depth24_on_savestate_loaded" not in rb_loaded + print("depth24 present hold: ok") + + +if __name__ == "__main__": + main()