Restore GPU presentation provenance after render passes

This commit is contained in:
Matthew Stanley
2026-10-06 18:34:39 -07:00
parent 7b2539415a
commit 58ccacc228
4 changed files with 369 additions and 4 deletions
+9
View File
@@ -221,6 +221,15 @@ mod; `psx_memory.h`), scratchpad, I-cache tags, I_STAT/I_MASK, timers, DMA
and GPU registers (without the widescreen side effects of a savestate load), the
VRAM rect (hr colour, mask stencil, raw 16-bit mirror, native-wide band, CPU
VRAM rows), the renderer's coherency bookkeeping, and every clock value.
RAM-keyed GPU presentation provenance is restored too: sprite/HUD/background
tags, primitive roles, reveal-clear/repeat/mask proofs, and the automatic UI
prepass. A synthetic draw can rewrite and retag the same packet addresses;
restoring only their RAM would leave the canonical draw comparing its original
words against a synthetic proof. Packet freshness, the full-composite latch,
backdrop phase, and scene classification evidence return to their pre-pass
values. Diagnostic counters and rings still include synthetic activity.
A watchdog abort leaves by longjmp from inside guest code, skipping the
exits of the frames it leaves, so the host nesting those frames own is put
back from the checkpoint too: the cycle-deferral depth, the native overlay
+7 -1
View File
@@ -586,7 +586,12 @@ if(BUILD_TESTING)
add_executable(native_wide_projection_test tests/test_native_wide_projection.c
src/psx_video_timing.c
src/gpu_timeline.c)
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection)
add_executable(gpu_render_pass_provenance_test tests/test_gpu_render_pass_provenance.c
src/ws_ui_group.c
src/psx_video_timing.c
src/gpu_timeline.c)
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection
gpu_render_pass_provenance)
target_include_directories(${_anchor_test}_test PRIVATE include src)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${_anchor_test}_test PRIVATE -ffunction-sections -fdata-sections)
@@ -601,6 +606,7 @@ if(BUILD_TESTING)
add_test(NAME ws_view_anchor_test COMMAND ws_view_anchor_test)
add_test(NAME ws_view_anchor_gpu_test COMMAND ws_view_anchor_gpu_test)
add_test(NAME native_wide_projection_test COMMAND native_wide_projection_test)
add_test(NAME gpu_render_pass_provenance_test COMMAND gpu_render_pass_provenance_test)
add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c)
target_include_directories(ws_repeat_rect_test PRIVATE include)
+110 -3
View File
@@ -7464,10 +7464,110 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
}
/* ---- Render-pass GPU checkpoint (render_pass.c) ---------------------------
* The same register set as a savestate (gpu_snap_emit), restored WITHOUT
* gpu_snapshot_read's side effects (widescreen scene history and HUD anchor
* tags belong to the live frame, which a pass must not reset). After the
* restore the renderer's mirrored draw state (area, offset, texture window,
* gpu_snapshot_read's side effects. Packet provenance belongs to the RAM it
* describes: a synthetic builder can rewrite and retag a canonical packet,
* so rolling its RAM back must also restore its original tags, not clear them.
* The renderer's mirrored draw state (area, offset, texture window,
* mask bits, native-wide target) is re-synced exactly as GP0(E2..E6) would. */
typedef struct {
WsTag ws_tags[WS_TAG_BUCKETS];
WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_background_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsHudAnchorTag ws_screen_mask_tags[WS_HUD_ANCHOR_TABLE_SIZE];
WsRadialScreenMaskTag ws_radial_screen_mask_tags[WS_RADIAL_MASK_TAG_COUNT];
WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE];
WsPrimitiveRole ws_primitive_roles[WS_ROLE_BUCKETS];
WsUiPrepassItem ws_ui_prepass[WS_UI_PREPASS_MAX];
WsUiPrepassNode ws_ui_prepass_nodes[WS_UI_PREPASS_NODE_MAX];
uint32_t ws_ui_prepass_count, ws_ui_prepass_node_count;
uint16_t ws_ui_prepass_rank;
int ws_auto_ui_dense, ws_shared_ui_copy;
uint32_t ws_background_tag_frame, ws_last_tag_stamp;
int ws_background_tags_used;
uint32_t s_bg_phase_frame;
int s_bg_phase_over;
/* Synthetic GTE/GP0 draws update these signals, and native-wide target
* selection reads them even though no guest frame has elapsed. */
uint32_t ws_gte_frame, ws_gte_count, ws_gte_prev_verts;
uint32_t ws_last_gte_stamp, ws_last_world3d_stamp, ws_sust_world3d_stamp;
uint32_t ws_ovh_frame, ws_ovh_count, ws_ovh_prev;
uint32_t ws_last_ovh_stamp, ws_sust_ovh_stamp;
WsSceneLatch ws_scene_latch;
WsSceneHold s_ws_scene_hold;
uint32_t s_ws_fmv_frame_cache;
int s_ws_fmv_cached;
} GpuPassPresentationState;
static GpuPassPresentationState s_pass_presentation;
/* One transfer list keeps save and restore symmetric. The prepass arrays are
* read only through their counts, so copy just their populated prefixes.
* Counters/rings remain cumulative; configuration and callbacks are unchanged. */
static void gpu_pass_provenance_transfer(int restore) {
#define PASS_ARRAY(name) do { \
if (restore) memcpy(name, s_pass_presentation.name, sizeof(name)); \
else memcpy(s_pass_presentation.name, name, sizeof(name)); \
} while (0)
#define PASS_VALUE(name) do { \
if (restore) name = s_pass_presentation.name; \
else s_pass_presentation.name = name; \
} while (0)
PASS_ARRAY(ws_tags);
PASS_ARRAY(ws_hud_anchor_tags);
PASS_ARRAY(ws_background_tags);
PASS_ARRAY(ws_reveal_clear_tags);
PASS_ARRAY(ws_screen_mask_tags);
PASS_ARRAY(ws_radial_screen_mask_tags);
PASS_ARRAY(ws_repeat_rect_tags);
PASS_ARRAY(ws_primitive_roles);
PASS_VALUE(ws_ui_prepass_count);
PASS_VALUE(ws_ui_prepass_node_count);
PASS_VALUE(ws_ui_prepass_rank);
PASS_VALUE(ws_auto_ui_dense);
PASS_VALUE(ws_shared_ui_copy);
PASS_VALUE(ws_background_tag_frame);
PASS_VALUE(ws_background_tags_used);
PASS_VALUE(ws_last_tag_stamp);
PASS_VALUE(s_bg_phase_frame);
PASS_VALUE(s_bg_phase_over);
if (restore) {
memcpy(ws_ui_prepass, s_pass_presentation.ws_ui_prepass,
ws_ui_prepass_count * sizeof(ws_ui_prepass[0]));
memcpy(ws_ui_prepass_nodes, s_pass_presentation.ws_ui_prepass_nodes,
ws_ui_prepass_node_count * sizeof(ws_ui_prepass_nodes[0]));
} else {
memcpy(s_pass_presentation.ws_ui_prepass, ws_ui_prepass,
ws_ui_prepass_count * sizeof(ws_ui_prepass[0]));
memcpy(s_pass_presentation.ws_ui_prepass_nodes, ws_ui_prepass_nodes,
ws_ui_prepass_node_count * sizeof(ws_ui_prepass_nodes[0]));
}
#undef PASS_ARRAY
#undef PASS_VALUE
}
static void gpu_pass_scene_transfer(int restore) {
#define PASS_VALUE(name) do { \
if (restore) name = s_pass_presentation.name; \
else s_pass_presentation.name = name; \
} while (0)
PASS_VALUE(ws_gte_frame);
PASS_VALUE(ws_gte_count);
PASS_VALUE(ws_gte_prev_verts);
PASS_VALUE(ws_last_gte_stamp);
PASS_VALUE(ws_last_world3d_stamp);
PASS_VALUE(ws_sust_world3d_stamp);
PASS_VALUE(ws_ovh_frame);
PASS_VALUE(ws_ovh_count);
PASS_VALUE(ws_ovh_prev);
PASS_VALUE(ws_last_ovh_stamp);
PASS_VALUE(ws_sust_ovh_stamp);
PASS_VALUE(ws_scene_latch);
PASS_VALUE(s_ws_scene_hold);
PASS_VALUE(s_ws_fmv_frame_cache);
PASS_VALUE(s_ws_fmv_cached);
#undef PASS_VALUE
}
static uint8_t s_pass_regs[512];
static uint32_t s_pass_regs_len;
static uint32_t s_pass_poll_count;
@@ -7487,6 +7587,8 @@ int gpu_pass_checkpoint_save(void) {
s_pass_doff_max = g_doff_max_this;
s_pass_doff_cnt = g_doff_cnt_this;
s_pass_split_this = split_trace_this;
gpu_pass_provenance_transfer(0);
gpu_pass_scene_transfer(0);
return 1;
}
@@ -7500,12 +7602,17 @@ void gpu_pass_checkpoint_restore(void) {
g_doff_max_this = s_pass_doff_max;
g_doff_cnt_this = s_pass_doff_cnt;
split_trace_this = s_pass_split_this;
gpu_pass_provenance_transfer(1);
gpu_pass_scene_transfer(1);
gr_set_texture_window(texture_window_value);
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
gr_set_draw_offset(draw_offset_x, draw_offset_y);
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
ws_nw_sync_target();
/* Target sync may classify the scene before render_pass.c restores RAM.
* Do not retain history learned from the synthetic draw or that query. */
gpu_pass_scene_transfer(1);
s_pass_regs_len = 0;
}
@@ -0,0 +1,243 @@
/* Exercise the real GPU checkpoint and GP0 provenance consumers without a GL
* context. RAM rollback deliberately follows GPU restore, as render_pass.c
* does: a later canonical DMA must find the proof for its restored words. */
#define GPU_EXEC_REAL_UI_GROUP
#define main gpu_packet_fixture_main
#include "test_gpu_textured_dot_nw_shift_exec.c"
#undef main
static const uint32_t command_addr = 0x10004u;
static const uint32_t canonical[] = {
0x28020304u, 0, 320, 240u << 16, (240u << 16) | 320u
};
static void packet_to_ram(const uint32_t *words, uint32_t count) {
memcpy(&test_ram[command_addr / 4u], words, count * sizeof(words[0]));
}
static void packet_to_gp0(const uint32_t *words, uint32_t count) {
gp0_cmd_source_addr = command_addr;
gp0_words_needed = (int)count;
memcpy(gp0_cmd_buf, words, count * sizeof(words[0]));
}
static void start_scene(void) {
reset_gpu_state_for_test();
configure_native_wide_16_9();
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_radial_mask_clear(ws_radial_screen_mask_tags);
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
memset(ws_primitive_roles, 0, sizeof(ws_primitive_roles));
ws_ui_prepass_count = ws_ui_prepass_node_count = 0;
ws_ui_prepass_rank = 0xFFFFu;
ws_reset_scene_history();
ws_nw_phase_backdrop = 0;
s_bg_phase_frame = UINT32_MAX;
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
}
static void test_retagged_canonical_packet(void) {
start_scene();
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, -1);
gpu_ws_tag_world_primitive(command_addr - 4u, 1);
gpu_ws_tag_hud_primitive(command_addr - 4u, -1);
const int32_t offset = ws_nw_offset();
assert(offset > 0);
GpuWsTagStats before, after;
gpu_ws_get_tag_stats(NULL, &before);
/* Repeated in-between draws reuse the same packet address and guest frame. */
for (uint32_t pass = 1; pass <= 3; ++pass) {
uint32_t synthetic[5];
memcpy(synthetic, canonical, sizeof(synthetic));
synthetic[1] += pass;
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 5);
packet_to_gp0(synthetic, 5);
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
gpu_ws_tag_world_primitive(command_addr - 4u, 0);
gpu_ws_tag_hud_primitive(command_addr - 4u, 1);
int32_t delta;
assert(psx_ws_prim_in_backdrop() == 1);
assert(ws_nw_explicit_hud_delta(&delta) && delta == offset);
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(psx_ws_prim_in_backdrop() == 1);
assert(ws_nw_explicit_hud_delta(&delta) && delta == -offset);
assert(ws_tagged_world_primitive());
assert(ws_tagged_hud_edge() == -1);
assert(gpu_ws_background_requires_full_composite());
packet_to_gp0(synthetic, 5);
assert(!ws_nw_explicit_background());
assert(!ws_nw_explicit_hud_delta(&delta));
packet_to_gp0(canonical, 5);
}
gpu_ws_get_tag_stats(NULL, &after);
assert(after.tag_calls == before.tag_calls + 3);
assert(after.hit >= before.hit + 6); /* Diagnostic activity is not rolled back. */
}
static void test_pass_only_tags_and_phase(void) {
start_scene();
ws_nw_phase_backdrop = 1;
ws_bg_phase_note(0x28u);
assert(psx_ws_prim_in_backdrop() == 1);
assert(!gpu_ws_background_requires_full_composite());
assert(gpu_pass_checkpoint_save());
gpu_ws_tag_background_prim(command_addr - 4u);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
gpu_ws_tag_world_primitive(command_addr - 4u, 1);
ws_bg_phase_note(0x30u);
assert(gpu_ws_background_requires_full_composite());
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(!ws_nw_explicit_background());
assert(!ws_nw_explicit_hud_delta(NULL));
assert(!gpu_ws_background_stretch_active());
assert(!gpu_ws_background_requires_full_composite());
assert(!ws_tagged_world_primitive());
assert(psx_ws_prim_in_backdrop() == 1); /* Original early-backdrop phase. */
}
static void test_hud_dot_draw(void) {
start_scene();
set_dot_packet(command_addr, 20, 20);
gp0_words_needed = 3;
uint32_t dot[3];
memcpy(dot, gp0_cmd_buf, sizeof(dot));
gpu_ws_tag_hud_prim(command_addr - 4u, -1);
assert(gpu_pass_checkpoint_save());
set_dot_packet(command_addr, 80, 20);
gpu_ws_tag_hud_prim(command_addr - 4u, 1);
exec_dot_and_expect(80 + ws_nw_offset(), 20);
gpu_pass_checkpoint_restore();
packet_to_ram(dot, 3);
packet_to_gp0(dot, 3);
exec_dot_and_expect(20 - ws_nw_offset(), 20);
}
static void test_screen_mask_packet(void) {
start_scene();
gpu_ws_tag_screen_mask_quad(command_addr - 4u);
gpu_ws_tag_radial_screen_mask_quad(command_addr - 4u, 2.f);
assert(gpu_pass_checkpoint_save());
uint32_t synthetic[5];
memcpy(synthetic, canonical, sizeof(synthetic));
synthetic[1] = 16;
packet_to_ram(synthetic, 5);
gpu_ws_tag_screen_mask_quad(command_addr - 4u);
gpu_ws_tag_radial_screen_mask_quad(command_addr - 4u, 3.f);
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
packet_to_gp0(canonical, 5);
assert(ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
command_addr, gp0_cmd_buf, 5, (uint32_t)s_frame_count, NULL));
int32_t x[4] = {0, 320, 0, 320}, y[4] = {0, 0, 240, 240};
assert(ws_nw_radial_mask_transform(x, y));
assert(x[0] == -160 && x[1] == 480); /* Canonical scale 2, not pass scale 3. */
}
static void test_repeat_rect_packet(void) {
start_scene();
const uint32_t rect[] = {0x64808080u, 0, 0, (64u << 16) | 32u};
uint32_t synthetic[4];
memcpy(synthetic, rect, sizeof(synthetic));
synthetic[1] = 8;
packet_to_ram(rect, 4);
packet_to_gp0(rect, 4);
gpu_ws_tag_black_reveal_rect(command_addr - 4u);
gpu_ws_tag_repeat_rect(command_addr - 4u, 128);
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 4);
gpu_ws_tag_black_reveal_rect(command_addr - 4u);
gpu_ws_tag_repeat_rect(command_addr - 4u, 64);
gpu_pass_checkpoint_restore();
packet_to_ram(rect, 4);
packet_to_gp0(rect, 4);
assert(ws_hud_anchor_lookup(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE,
command_addr, gp0_cmd_buf, 4, (uint32_t)s_frame_count, NULL));
assert(ws_repeat_rect_tag_lookup(ws_repeat_rect_tags, command_addr,
gp0_cmd_buf, 4, (uint32_t)s_frame_count) == 128);
}
static void test_scene_evidence(void) {
start_scene();
ws_full_2d = 0;
gpu_ws_set_gte_game_mode(1);
assert(gpu_ws_present_native_43());
assert(gpu_pass_checkpoint_save());
psx_ws_note_gte_project(3);
assert(!gpu_ws_present_native_43());
gpu_pass_checkpoint_restore();
packet_to_ram(canonical, 5);
assert(gpu_ws_present_native_43());
gpu_ws_set_gte_game_mode(0);
}
static void test_sprite_anchor(void) {
start_scene();
const uint32_t anchor_addr = 0x20000u;
ws_anchor_addr = anchor_addr;
CPUState cpu = {0};
cpu.read_word = psx_read_word;
cpu.gpr[4] = command_addr - 4u;
test_ram[anchor_addr / 4u] = 40;
psx_ws_sprite_tag(&cpu);
assert(gpu_pass_checkpoint_save());
test_ram[anchor_addr / 4u] = 80;
psx_ws_sprite_tag(&cpu);
gpu_pass_checkpoint_restore();
test_ram[anchor_addr / 4u] = 40;
int32_t anchor;
assert(ws_tagged_anchor(&anchor) && anchor == 40);
}
static void test_auto_ui_prepass(void) {
start_scene();
gpu_ws_set_auto_ui_squash(1);
const uint32_t hud[] = {
0x28802040u, pack_vertex(20, 20), pack_vertex(40, 20),
pack_vertex(20, 32), pack_vertex(40, 32)
};
uint32_t synthetic[5];
memcpy(synthetic, hud, sizeof(synthetic));
synthetic[1] = pack_vertex(24, 20);
synthetic[3] = pack_vertex(24, 32);
test_ram[(command_addr - 4u) / 4u] = (5u << 24) | 0xFFFFFFu;
packet_to_ram(hud, 5);
gpu_ws_prepass_linked_list(command_addr - 4u);
packet_to_gp0(hud, 5);
int32_t anchor;
assert(ws_auto_ui_anchor(&anchor));
assert(gpu_pass_checkpoint_save());
packet_to_ram(synthetic, 5);
gpu_ws_prepass_linked_list(command_addr - 4u);
gpu_pass_checkpoint_restore();
packet_to_ram(hud, 5);
gpu_ws_validate_linked_list_node(command_addr - 4u, 5);
packet_to_gp0(hud, 5);
assert(ws_auto_ui_anchor(&anchor));
packet_to_gp0(synthetic, 5);
assert(!ws_auto_ui_anchor(&anchor));
gpu_ws_set_auto_ui_squash(0);
}
int main(void) {
test_retagged_canonical_packet();
test_pass_only_tags_and_phase();
test_hud_dot_draw();
test_screen_mask_packet();
test_repeat_rect_packet();
test_sprite_anchor();
test_auto_ui_prepass();
test_scene_evidence();
puts("gpu_render_pass_provenance: PASS");
return 0;
}