From 0d096e2858f746ce801b507c9b2753db2f5f9989 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:53:21 -0700 Subject: [PATCH] GPU: carry the depth24 held-VBlank flag in the snap; executable hold regression Review follow-up on #661 (#636): - test_depth24_present_hold_vblank.py reads sources as UTF-8 (cp1252 on native Windows Python raised UnicodeDecodeError). - The present skip is now guest state: set at guest VBlank, read (not consumed) by the present and packed into the GPU snap's hold word (bit 8; older snaps derive it from the count). The executable test showed the host-side latch made the present of a restored tick differ from live on the last held VBlank (hold 0, skip 1), exposing stale trailing FMV columns after a rollback load. - Depth24 scanout staging and trailing-column blanking move from main.cpp into gpu_depth24_stage_scanout (gpu.c), so the test renders through the same code every CPU depth24 present runs. - New depth24_present_hold_exec_test: drives real gpu.c through a 15-bit to 24-bit FMV, GP1(07h) retargets and mode transitions; compares hold, skip, upload span and snapshot bytes across windowed/headless/deferred presents, rollback replay from every tick and disk loads, and checks staged frames never show stale movie pixels while the new movie stays visible. Co-Authored-By: Claude Opus 5.5 --- runtime/CMakeLists.txt | 16 + runtime/include/gpu.h | 13 +- runtime/src/gpu.c | 59 +++- runtime/src/main.cpp | 51 +-- .../tests/test_depth24_present_hold_exec.c | 321 ++++++++++++++++++ .../tests/test_depth24_present_hold_vblank.py | 4 +- 6 files changed, 407 insertions(+), 57 deletions(-) create mode 100644 runtime/tests/test_depth24_present_hold_exec.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 50197b26..83a2e28d 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -972,6 +972,22 @@ if(BUILD_TESTING) endif() add_test(NAME ws_local_view_margin_test COMMAND ws_local_view_margin_test) + # Depth24 GP1(07h) present hold (#636): hold count, upload span and the + # staged frame agree across windowed/headless/deferred presents, rollback + # replay and disk loads; stale trailing FMV pixels never reach a frame. + add_executable(depth24_present_hold_exec_test + tests/test_depth24_present_hold_exec.c + src/psx_video_timing.c + src/gpu_timeline.c) + target_include_directories(depth24_present_hold_exec_test PRIVATE include src) + if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(depth24_present_hold_exec_test PRIVATE + -ffunction-sections -fdata-sections) + target_link_options(depth24_present_hold_exec_test PRIVATE + $,-Wl$-dead_strip,-Wl$--gc-sections>) + endif() + add_test(NAME depth24_present_hold_exec_test COMMAND depth24_present_hold_exec_test) + add_executable(ws_view_anchor_test tests/test_ws_view_anchor.c) add_executable(ws_view_anchor_gpu_test tests/test_ws_view_anchor_gpu.c src/psx_video_timing.c diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 868b8506..fa51c73d 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -75,15 +75,22 @@ uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y * overhead. `out` must hold at least `count` uint32_t ARGB entries. */ void gpu_depth24_present_row(const GpuDisplayInfo* di, uint32_t y, uint32_t* out, uint32_t count); +/* Stage a depth24 frame into a w x di->screen_height ARGB canvas and + * black-fill trailing columns no upload covered (see gpu.c). Returns the + * source rows staged. */ +uint32_t gpu_depth24_stage_scanout(const GpuDisplayInfo* di, uint32_t* buf, + uint32_t w, uint32_t margin); /* Depth24: RGB columns covered by CPU→VRAM uploads since the last reset. * Returns 0 when no uploads yet, crtc_w when coverage is full/unknown-beyond, * else the covered RGB width. Present black-fills [limit, crtc_w) without * shrinking the CRTC-derived draw width (avoids a flickering black pillar). */ uint32_t gpu_depth24_rgb_limit(uint32_t display_x, uint32_t crtc_w); void gpu_depth24_upload_span_reset(void); -/* MotK intro cuts retarget GP1(07h). While hold > 0, present should skip - * Swap (keep prior frame) so stale trailing VRAM never flashes. One tick - * per vblank; returns non-zero while the hold is still active after tick. */ +/* MotK intro cuts retarget GP1(07h): the guest-state hold counts down at + * guest VBlank (gpu_vblank_tick). Returns non-zero while the latest guest + * VBlank was held (read-only; guest state in the GPU snap): presents should + * skip Swap (keep the prior frame) so stale trailing + * VRAM never flashes. */ int gpu_depth24_present_hold_tick(void); /* After savestate restore: clear ephemeral hold so the restored depth24 * frame can present immediately (span/prev_h come from the GPU snap). */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 7c2b0369..3403841f 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3408,11 +3408,13 @@ 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). */ +/* Whether the latest guest VBlank was held: presents after it skip their + * Swap. Both the count and this flag are guest state, set only at guest + * VBlank and carried in the GPU snap (packed into the hold word), and a + * present only reads them, so a windowed, headless or frame-skipping run and + * a rollback replay hold the same span and restore the same output (#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); @@ -4242,9 +4244,7 @@ void gpu_depth24_upload_span_reset(void) { } int gpu_depth24_present_hold_tick(void) { - int skip = s_d24_present_skip; - s_d24_present_skip = 0; /* one skipped Swap per guest VBlank */ - return skip; + return s_d24_present_skip; } void gpu_depth24_on_savestate_loaded(void) { @@ -4393,6 +4393,42 @@ void gpu_depth24_present_row(const GpuDisplayInfo* di, uint32_t y, uint32_t* out out[x] = 0xFF000000u; } +/* Depth24 present staging, shared by every CPU present path: compose the + * GP1(07h) intersection with active video into the PAL/NTSC canvas (rows + * off-screen are clipping, not a shift), then black-fill the trailing RGB + * columns no CPU->VRAM upload has covered since the last span reset, so stale + * VRAM from a prior movie never shows past the new blit. With no span yet + * (lim == 0) only the last `margin` columns are blanked: blanking [0..w) left + * MotK's FMV permanently black after the GP1(07h) hold reset the span. + * Returns the source rows staged. */ +uint32_t gpu_depth24_stage_scanout(const GpuDisplayInfo *di, uint32_t *buf, + uint32_t w, uint32_t margin) { + if (!di || !buf || w == 0u || di->screen_height == 0u) + return 0u; + const size_t count = (size_t)w * di->screen_height; + for (size_t i = 0; i < count; i++) + buf[i] = 0xFF000000u; + if (di->screen_origin_y >= di->screen_height) + return 0u; + uint32_t rows = di->height; + if (rows > di->screen_height - di->screen_origin_y) + rows = di->screen_height - di->screen_origin_y; + uint32_t *source = buf + (size_t)di->screen_origin_y * w; + for (uint32_t y = 0; y < rows; y++) + gpu_depth24_present_row(di, di->screen_source_skip_y + y, + source + (size_t)y * w, w); + if (rows == 0u || w < 8u || margin == 0u || margin >= w) + return rows; + uint32_t start = w - margin; + uint32_t lim = gpu_depth24_rgb_limit(di->display_x, w); + if (lim > 0u && lim < w && lim < start) + start = lim; + for (uint32_t y = 0; y < rows; y++) + for (uint32_t x = start; x < w; x++) + source[(size_t)y * w + x] = 0xFF000000u; + return rows; +} + int gpu_display_is_depth24(void) { return (int)(display_depth & 1u); } @@ -7823,7 +7859,8 @@ static int gpu_snap_emit(PstW *w) { WI(vram_read_active); WH(vram_read_x); WH(vram_read_y); WH(vram_read_w); WH(vram_read_h); WH(vram_read_col); WH(vram_read_row); /* Depth24 present helpers (MotK FMV) — must resume with upload span. */ - WU(s_d24_upload_x1); WI(s_d24_present_hold); WU(s_d24_prev_disp_h); + WU(s_d24_upload_x1); WI(s_d24_present_hold | (s_d24_present_skip ? 0x100 : 0)); + WU(s_d24_prev_disp_h); #undef WU #undef WI #undef WH @@ -7857,6 +7894,10 @@ static int gpu_snap_parse(PstR *r) { RI(vram_read_active); RH(vram_read_x); RH(vram_read_y); RH(vram_read_w); RH(vram_read_h); RH(vram_read_col); RH(vram_read_row); RU(s_d24_upload_x1); RI(s_d24_present_hold); RU(s_d24_prev_disp_h); + /* Bit 8: the latest VBlank was held. Older snaps lack it; a positive + * count after a VBlank implies the VBlank was held. */ + s_d24_present_skip = (s_d24_present_hold & 0x100) || (s_d24_present_hold & 0xFF) > 0; + s_d24_present_hold &= 0xFF; #undef RU #undef RI #undef RH diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index e1350d3f..fe049601 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -7457,55 +7457,20 @@ static void depth24_cutover_tick(int depth24) { * * Optional short cutover blank: full-frame black for 1–2 presents on movie * start / long idle resume only (see depth24_cutover_tick). */ -static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, - uint32_t display_x) { - if (!buf || w < 8u || h == 0u) return; - - if (s_d24_cutover_blank > 0) { - s_d24_cutover_blank--; - const uint32_t n = w * h; - for (uint32_t i = 0; i < n; i++) - buf[i] = 0xFF000000u; - return; - } - - /* Default: last 8 columns. If the upload span is known and ends earlier - * inside that margin, start blanking from the span edge instead. */ - const uint32_t margin = (uint32_t)g_video_depth24_trailing_margin; - if (margin == 0u || margin >= w) return; - uint32_t start = w - margin; - uint32_t lim = gpu_depth24_rgb_limit(display_x, w); - if (lim > 0u && lim < w && lim < start) - start = lim; - for (uint32_t y = 0; y < h; y++) { - for (uint32_t x = start; x < w; x++) - buf[y * w + x] = 0xFF000000u; - } -} - -/* Compose visible depth24 rows into the PAL/NTSC active canvas. Decode only - * the GP1(07h) intersection with active video. This preserves every visible - * source row and represents off-screen rows as clipping, not as an in-place - * shift that discards additional FMV content. */ static void depth24_stage_scanout(const GpuDisplayInfo *di, uint32_t *buf, uint32_t w) { - if (!di || !buf || w == 0u || di->screen_height == 0u) + if (!di || !buf || w == 0u) return; - + /* gpu.c stages the visible rows and black-fills the uncovered trailing + * columns (the same code depth24_present_hold_exec_test renders). */ + const uint32_t rows = gpu_depth24_stage_scanout( + di, buf, w, (uint32_t)g_video_depth24_trailing_margin); + if (rows == 0u || w < 8u || s_d24_cutover_blank <= 0) + return; + s_d24_cutover_blank--; const size_t count = (size_t)w * di->screen_height; for (size_t i = 0; i < count; i++) buf[i] = 0xFF000000u; - - if (di->screen_origin_y >= di->screen_height) - return; - uint32_t rows = di->height; - if (rows > di->screen_height - di->screen_origin_y) - rows = di->screen_height - di->screen_origin_y; - uint32_t *source = buf + (size_t)di->screen_origin_y * w; - for (uint32_t y = 0; y < rows; y++) - gpu_depth24_present_row(di, di->screen_source_skip_y + y, - source + (size_t)y * w, w); - depth24_fix_trailing_margin(source, w, rows, di->display_x); } enum { diff --git a/runtime/tests/test_depth24_present_hold_exec.c b/runtime/tests/test_depth24_present_hold_exec.c new file mode 100644 index 00000000..f8cc4b40 --- /dev/null +++ b/runtime/tests/test_depth24_present_hold_exec.c @@ -0,0 +1,321 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif + +/* Executable regression for the depth24 GP1(07h) present hold (#636). + * + * Drives the real gpu.c through a scripted depth24 FMV: 15-bit setup, a + * switch to 24-bit, a full-width movie A, a GP1(07h) band retarget to a + * narrower movie B (the hold), display-mode transitions and a second + * retarget. Each frame is: guest GP0/GP1 writes, gpu_vblank_tick(), then + * (per present policy) the frontend present, modelled exactly as + * sdl_vblank_present_body does it: skip the Swap when + * gpu_depth24_present_hold_tick() says so, else stage the frame through + * gpu_depth24_stage_scanout() (the code every CPU depth24 present runs). + * + * Checks: + * - the hold count, its latched skips and the upload span are identical + * per guest tick whether presents run every VBlank (windowed), never + * (headless) or every other VBlank (deferred/skipped presents), and so + * are the GPU snapshot bytes; + * - the hold spans exactly three VBlanks (3 -> 2 -> 1 -> 0) and skips + * exactly those presents; uploads during it do not grow the span; + * - GP1(07h) in 15-bit mode, or an unchanged band, does not arm it; + * - rendering: no staged frame after a retarget shows a single pixel of + * the stale trailing movie-A columns, movie B is visible (not a black + * screen), and staging that ignored the hold would expose the stale + * columns (so the check has teeth); + * - rollback: loading the snapshot of any tick (hold and held-VBlank flag + * restored from the snap, whatever the pre-load flag was) and + * replaying reproduces the live run tick for tick, including the present + * of the restored tick and every staged frame; + * - disk load: the hold and skip clear, the span comes back from the + * snapshot and the restored frame presents at once. + */ +#include +#include +#include +#include +#include + +#include "../src/gpu.c" + +#include "gpu_exec_stubs.inc" + +enum { FRAMES = 30, PRESENT_W = 320, MAX_ROWS = 600 }; +enum { POLICY_EVERY, POLICY_NONE, POLICY_EVERY_OTHER, POLICY_COUNT }; + +#define MOVIE_A 0xAAu /* stale full-width movie bytes */ +#define MOVIE_B 0x33u /* narrower movie after the retarget */ +#define B_HALFWORDS 384u /* 256 RGB pixels */ +#define A_HALFWORDS 480u /* 320 RGB pixels */ +#define RETARGET_1 7 +#define MODE_15_FRAME 16 +#define MODE_24_FRAME 17 +#define RETARGET_2 21 + +typedef struct { + int hold; + int skip_latch; + uint32_t span; + uint64_t snap_hash; + int presented; /* -1: no present ran, 0: Swap skipped, 1: staged */ + uint64_t frame_hash; +} Tick; + +static uint32_t canvas[PRESENT_W * MAX_ROWS]; + +static uint64_t fnv(const void *p, size_t n) { + const uint8_t *b = (const uint8_t *)p; + uint64_t h = 1469598103934665603ull; + for (size_t i = 0; i < n; i++) { h ^= b[i]; h *= 1099511628211ull; } + return h; +} + +static uint64_t snap_hash(void) { + uint32_t n = gpu_snapshot_bytes(); + uint8_t *buf = (uint8_t *)malloc(n); + assert(buf); + gpu_snapshot_write(buf); + uint64_t h = fnv(buf, n); + free(buf); + return h; +} + +static void gp1_vrange(uint32_t h) { + gpu_write_gp1(0x07000000u | 0x10u | ((0x10u + h) << 10)); +} + +static void upload(uint8_t byte, uint32_t halfwords, uint32_t rows) { + const uint32_t hw = (uint32_t)byte | ((uint32_t)byte << 8); + gpu_write_gp0(0xA0000000u); + gpu_write_gp0(0u); /* x = 0, y = 0 */ + gpu_write_gp0((rows << 16) | halfwords); + for (uint32_t i = 0; i < halfwords * rows / 2u; i++) + gpu_write_gp0(hw | (hw << 16)); + assert(gp0_state == GP0_IDLE); +} + +/* Guest work for frame f (identical in every run and every replay). */ +static void guest_frame(int f) { + switch (f) { + case 0: + gpu_write_gp1(0x08000001u); /* 15-bit, 320 */ + gpu_write_gp1(0x05000000u); /* display area 0,0 */ + gpu_write_gp1(0x06000000u | 0x260u | (0xC60u << 12)); + gp1_vrange(64); + break; + case 1: gp1_vrange(48); break; /* 15-bit: no hold */ + case 2: gp1_vrange(64); break; + case 3: gpu_write_gp1(0x08000011u); break; /* -> 24-bit */ + case 4: gp1_vrange(64); break; /* same band: no hold */ + case RETARGET_1: gp1_vrange(48); break; /* MotK-style retarget */ + case MODE_15_FRAME: gpu_write_gp1(0x08000001u); break; + case MODE_24_FRAME: gpu_write_gp1(0x08000011u); break; + case RETARGET_2: gp1_vrange(40); break; + default: break; + } + if (f >= 3 && f < RETARGET_1) + upload(MOVIE_A, A_HALFWORDS, 64); + else if (f >= RETARGET_1) + upload(MOVIE_B, B_HALFWORDS, 48); +} + +static int present(uint64_t *frame_hash, int honor_hold) { + int skip = gpu_depth24_present_hold_tick(); + if (skip && honor_hold) + return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + if (!di.depth24) { *frame_hash = 0; return 1; } + assert(di.screen_height <= MAX_ROWS); + uint32_t rows = gpu_depth24_stage_scanout(&di, canvas, PRESENT_W, 8u); + assert(rows > 0u); + *frame_hash = fnv(canvas, sizeof(uint32_t) * PRESENT_W * di.screen_height); + /* Rendering check on the staged frame. */ + const uint32_t stale = 0xFF000000u | (MOVIE_A << 16) | (MOVIE_A << 8) | MOVIE_A; + const uint32_t fresh = 0xFF000000u | (MOVIE_B << 16) | (MOVIE_B << 8) | MOVIE_B; + uint32_t n_stale = 0, n_fresh = 0; + for (uint32_t i = 0; i < PRESENT_W * di.screen_height; i++) { + n_stale += canvas[i] == stale; + n_fresh += canvas[i] == fresh; + } + return 1 + (int)(n_stale > 0) * 2 + (int)(n_fresh > 0) * 4; +} + +static void reset_gpu(void) { + gpu_reset_state(1); + memset(test_ram, 0, sizeof(test_ram)); +} + +static void record(Tick *t) { + t->hold = s_d24_present_hold; + t->skip_latch = s_d24_present_skip; + t->span = s_d24_upload_x1; + t->snap_hash = snap_hash(); +} + +/* Present step; returns -1/0/1 and checks the staged image. */ +static int present_checked(int f, uint64_t *frame_hash) { + int r = present(frame_hash, 1); + if (r == 0) return 0; + if (f >= RETARGET_1) { + if (r & 2) { + fprintf(stderr, "frame %d: stale movie-A pixels exposed\n", f); + abort(); + } + if (s_d24_upload_x1 != 0u && display_depth) + assert(r & 4); /* movie B on screen */ + } + return 1; +} + +static void run(int policy, Tick out[FRAMES]) { + reset_gpu(); + for (int f = 0; f < FRAMES; f++) { + guest_frame(f); + gpu_vblank_tick(); + record(&out[f]); + out[f].presented = -1; + out[f].frame_hash = 0; + int do_present = policy == POLICY_EVERY || + (policy == POLICY_EVERY_OTHER && (f & 1)); + if (do_present) + out[f].presented = present_checked(f, &out[f].frame_hash); + } +} + +static void check_policies_agree(void) { + static Tick t[POLICY_COUNT][FRAMES]; + for (int p = 0; p < POLICY_COUNT; p++) + run(p, t[p]); + for (int p = 1; p < POLICY_COUNT; p++) + for (int f = 0; f < FRAMES; f++) { + assert(t[p][f].hold == t[0][f].hold); + assert(t[p][f].skip_latch == t[0][f].skip_latch); + assert(t[p][f].span == t[0][f].span); + assert(t[p][f].snap_hash == t[0][f].snap_hash); + } + + const Tick *w = t[POLICY_EVERY]; + /* 15-bit retargets and an unchanged band never arm the hold. */ + for (int f = 0; f < RETARGET_1; f++) assert(w[f].hold == 0 && !w[f].skip_latch); + /* All three VBlanks of each hold: 2, 1, 0 after the ticks, three skips. */ + const int starts[2] = { RETARGET_1, RETARGET_2 }; + for (int k = 0; k < 2; k++) { + int s = starts[k]; + assert(w[s].hold == 2 && w[s + 1].hold == 1 && w[s + 2].hold == 0); + assert(w[s + 3].hold == 0); + for (int i = 0; i < 3; i++) { + assert(w[s + i].presented == 0); /* Swap skipped */ + assert(w[s + i].span == 0u); /* held uploads ignored */ + } + assert(w[s + 3].presented == 1); /* first staged frame */ + assert(w[s + 3].span == B_HALFWORDS); + } + int skipped = 0; + for (int f = 0; f < FRAMES; f++) skipped += w[f].presented == 0; + assert(skipped == 6); + /* Display-mode transitions: leaving 24-bit drops the span; coming back + * gets it from that frame's upload. */ + assert(w[MODE_15_FRAME].span == 0u); + assert(w[MODE_24_FRAME].span == B_HALFWORDS); + /* Before the retarget movie A is legitimately full width. */ + assert(w[RETARGET_1 - 1].presented == 1); + /* Deferred presents (odd ticks only): a held tick's present skips, the + * first present after the hold stages a clean frame. */ + const Tick *d = t[POLICY_EVERY_OTHER]; + assert(d[RETARGET_1].presented == 0 && d[RETARGET_1 + 1].presented == -1); + assert(d[RETARGET_1 + 2].presented == 0 && d[RETARGET_1 + 4].presented == 1); +} + +/* Teeth: a present that ignored the hold right after the retarget would + * show stale movie-A columns (the span was reset, lim == 0). */ +static void check_hold_is_needed(void) { + reset_gpu(); + for (int f = 0; f <= RETARGET_1; f++) { guest_frame(f); gpu_vblank_tick(); } + uint64_t h; + int r = present(&h, 0); + assert(r & 2); +} + +static void check_rollback_and_disk(void) { + static Tick live[FRAMES]; + static uint8_t snaps[FRAMES][4096]; + static uint16_t vrams[FRAMES][1024 * 512]; + uint32_t n = gpu_snapshot_bytes(); + assert(n <= sizeof snaps[0]); + + reset_gpu(); + for (int f = 0; f < FRAMES; f++) { + guest_frame(f); + gpu_vblank_tick(); + record(&live[f]); + gpu_snapshot_write(snaps[f]); + memcpy(vrams[f], vram, sizeof vram); + live[f].presented = present_checked(f, &live[f].frame_hash); + } + + for (int k = RETARGET_1 - 2; k < FRAMES - 1; k++) { + for (int latch = 0; latch < 2; latch++) { + s_d24_present_skip = latch; /* whatever the tip had */ + memcpy(vram, vrams[k], sizeof vram); + assert(gpu_snapshot_read(snaps[k], n)); + /* psx_frontend_on_rb_snap_loaded keeps the hold. */ + assert(s_d24_present_hold == live[k].hold); + assert(s_d24_upload_x1 == live[k].span); + /* Restored output/skip: the present of the restored tick does + * what the live present of that tick did. */ + uint64_t h = 0; + int r = present_checked(k, &h); + assert(r == live[k].presented); + if (r == 1) assert(h == live[k].frame_hash); + for (int f = k + 1; f < FRAMES; f++) { + Tick t; + guest_frame(f); + gpu_vblank_tick(); + record(&t); + assert(t.hold == live[f].hold); + assert(t.skip_latch == live[f].skip_latch); + assert(t.span == live[f].span); + assert(t.snap_hash == live[f].snap_hash); + t.frame_hash = 0; + t.presented = present_checked(f, &t.frame_hash); + assert(t.presented == live[f].presented); + assert(t.frame_hash == live[f].frame_hash); + } + } + } + + /* Disk savestate load mid-hold: hold and skip clear, span from the snap, + * the restored frame presents at once. */ + for (int k = RETARGET_2; k < RETARGET_2 + 3; k++) { + s_d24_present_skip = 1; + memcpy(vram, vrams[k], sizeof vram); + assert(gpu_snapshot_read(snaps[k], n)); + gpu_depth24_on_savestate_loaded(); + assert(s_d24_present_hold == 0); + assert(s_d24_upload_x1 == live[k].span); + uint64_t h; + assert(present(&h, 1) != 0); + /* The next VBlank latches no skip: the cleared hold stays cleared. */ + gpu_vblank_tick(); + assert(gpu_depth24_present_hold_tick() == 0); + } + /* A disk load after the hold keeps the restored span. */ + memcpy(vram, vrams[RETARGET_1 + 3], sizeof vram); + assert(gpu_snapshot_read(snaps[RETARGET_1 + 3], n)); + gpu_depth24_on_savestate_loaded(); + assert(s_d24_upload_x1 == B_HALFWORDS); + uint64_t h = 0; + int r = present_checked(RETARGET_1 + 3, &h); + assert(r == live[RETARGET_1 + 3].presented && h == live[RETARGET_1 + 3].frame_hash); +} + +int main(void) { + check_policies_agree(); + check_hold_is_needed(); + check_rollback_and_disk(); + puts("depth24_present_hold_exec_test: ok"); + return 0; +} diff --git a/runtime/tests/test_depth24_present_hold_vblank.py b/runtime/tests/test_depth24_present_hold_vblank.py index 53bff315..957df479 100644 --- a/runtime/tests/test_depth24_present_hold_vblank.py +++ b/runtime/tests/test_depth24_present_hold_vblank.py @@ -16,13 +16,13 @@ SRC = Path(__file__).resolve().parents[1] / "src" def main(): - gpu = (SRC / "gpu.c").read_text() + gpu = (SRC / "gpu.c").read_text(encoding="utf-8") 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() + main_cpp = (SRC / "main.cpp").read_text(encoding="utf-8") 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")