diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 62b8dd11..eb18f791 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1821,6 +1821,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 37b6494f..1c8f014d 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1122,7 +1122,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; @@ -9034,9 +9036,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()