Integrate revised GPU hold snapshot and executable rendering regression

# Conflicts:
#	runtime/tests/test_depth24_present_hold_exec.c
This commit is contained in:
Matthew Stanley
2026-10-14 11:17:06 -07:00
5 changed files with 2520 additions and 2229 deletions
+2146 -2140
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -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). */
+50 -9
View File
@@ -3435,11 +3435,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);
@@ -4269,9 +4271,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) {
@@ -4420,6 +4420,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);
}
@@ -7850,7 +7886,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
@@ -7884,6 +7921,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
+8 -43
View File
@@ -7483,55 +7483,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 {
+306 -34
View File
@@ -1,49 +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 <assert.h>
#include <stdint.h>
#include <stdio.h>
#define GPU_EXEC_REAL_UI_GROUP
#include <stdlib.h>
#include <string.h>
#include "../src/gpu.c"
#include "gpu_exec_stubs.inc"
static void arm(void) {
gpu_reset_state(0);
display_depth = 1;
gp1_v_display_range((260u<<10)|20u);
gp1_v_display_range((244u<<10)|20u);
assert(s_d24_present_hold==3);
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;
}
int main(void) {
for (int host_present=0; host_present<2; ++host_present) {
arm();
for (int vb=0;vb<3;++vb) {
depth24_note_upload(0,480);
assert(s_d24_upload_x1==0);
gpu_vblank_tick();
assert(s_d24_present_hold==2-vb);
if (host_present) {
assert(gpu_depth24_present_hold_tick()==1);
assert(gpu_depth24_present_hold_tick()==0);
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);
}
}
depth24_note_upload(0,480);
assert(s_d24_upload_x1==480);
gpu_vblank_tick();
assert(gpu_depth24_present_hold_tick()==0);
}
arm(); gpu_vblank_tick();
unsigned char saved[4096];
uint32_t n=gpu_snapshot_bytes();
assert(n<=sizeof saved);
gpu_snapshot_write(saved);
gpu_vblank_tick(); gpu_vblank_tick();
assert(s_d24_present_hold==0);
assert(gpu_snapshot_read(saved,n));
assert(s_d24_present_hold==2);
gpu_vblank_tick(); assert(s_d24_present_hold==1);
/* 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_present_hold==0 && gpu_depth24_present_hold_tick()==0);
puts("depth24 hold: retarget, headless/present cadence, upload span and snapshot restore PASS");
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;
}