mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
338 lines
13 KiB
C
338 lines
13 KiB
C
#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>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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);
|
|
}
|
|
|
|
static void check_pre_vblank_snapshot(void) {
|
|
reset_gpu();
|
|
for (int f = 0; f < RETARGET_1; f++) { guest_frame(f); gpu_vblank_tick(); }
|
|
guest_frame(RETARGET_1); /* GP1 arms a count before the first held VBlank. */
|
|
assert(s_d24_present_hold == 3 && s_d24_present_skip == 0);
|
|
const uint32_t n = gpu_snapshot_bytes();
|
|
uint8_t *snap = (uint8_t *)malloc(n);
|
|
assert(snap);
|
|
gpu_snapshot_write(snap);
|
|
s_d24_present_hold = s_d24_present_skip = 0;
|
|
assert(gpu_snapshot_read(snap, n));
|
|
assert(s_d24_present_hold == 3 && s_d24_present_skip == 0);
|
|
free(snap);
|
|
}
|
|
|
|
int main(void) {
|
|
check_pre_vblank_snapshot();
|
|
check_policies_agree();
|
|
check_hold_is_needed();
|
|
check_rollback_and_disk();
|
|
puts("depth24_present_hold_exec_test: ok");
|
|
return 0;
|
|
}
|