diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index abc639f5..68e0be81 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -2213,3 +2213,15 @@ breadcrumb pointer. Make host callback tables thread-owned and bind each DLL on its executing source thread, as the worker already does. Production-loader cross-thread lifetime checks pass O0/O2. Refresh and ordered gates continue; no push. Invalid cache/profile attempts are retained separately in R4 receipts. + +2026-10-21 BRIEF-5 critical-window profile: callback fix retains original-off +identity; quiet 60.000 Hz / zero hitches / 64.0% p99 remains failed. The paired +3000..4200 source profiles have identical native calls, zero misses, mean CPU +53.63% off / 52.05% on. Publication alone adds 2.375% wall samples: RAM streaming +copy, PGXP pages and GPU regions. Remove the GPU upload/checkpoint scratch +copies (2.38 MiB; both are fully populated before use); refuse capture of an +active checkpoint. Narrow PGXP publication pages from 256 to 64 words to avoid +10 KiB copies per isolated store. Partial-transfer/private-checkpoint, provenance, +page-boundary/reuse and O0/O2 upload history tests pass. Correct the shared GPU +fixture TLS declaration found by that check. No general hot-path changes. Continue +ordered production qualification; no push. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 7fe19b79..1c008d0d 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1113,6 +1113,23 @@ if(BUILD_TESTING) endif() add_test(NAME depth24_present_hold_exec_test COMMAND depth24_present_hold_exec_test) + add_executable(gpu_snapshot_scratch_test + tests/test_gpu_snapshot_scratch.c + src/psx_video_timing.c + src/gpu_timeline.c) + target_include_directories(gpu_snapshot_scratch_test PRIVATE include src) + if(MINGW) + target_compile_options(gpu_snapshot_scratch_test PRIVATE -flto) + target_link_options(gpu_snapshot_scratch_test PRIVATE -flto) + endif() + if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(gpu_snapshot_scratch_test PRIVATE + -ffunction-sections -fdata-sections) + target_link_options(gpu_snapshot_scratch_test PRIVATE + $,-Wl$-dead_strip,-Wl$--gc-sections>) + endif() + add_test(NAME gpu_snapshot_scratch_test COMMAND gpu_snapshot_scratch_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/src/gpu.c b/runtime/src/gpu.c index c4533ef2..3a360b1b 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -10221,14 +10221,23 @@ void gpu_execution_snapshot_destroy(GpuExecutionSnapshot *snapshot) { free(snapshot); } int gpu_execution_snapshot_capture(GpuExecutionSnapshot *snapshot) { - if (!snapshot || s_gpu_execution) return 0; + if (!snapshot || s_gpu_execution || s_pass_regs_len) return 0; ColorLut **lut = (ColorLut **)gpu_execution_region(snapshot, &s_screen_lut); if (*lut) color_lut_destroy(*lut); snapshot->ready = 0; snapshot->frame = s_frame_count; snapshot->recent_mdec = mdec_recently_active(WS_FMV_HYSTERESIS); - for (size_t i = 0; i < sizeof s_gpu_execution_regions / sizeof s_gpu_execution_regions[0]; ++i) - memcpy(snapshot->slots[i], s_gpu_execution_regions[i].live, s_gpu_execution_regions[i].bytes); + for (size_t i = 0; i < sizeof s_gpu_execution_regions / sizeof s_gpu_execution_regions[0]; ++i) { + const void *live = s_gpu_execution_regions[i].live; + /* Derived working storage belongs to the consumer: CPU upload commit + * fills every used pixel from VRAM, and checkpoint save fills the + * provenance image before restore can read it. Do not publish either + * multi-megabyte scratch buffer from the source owner. */ + if (live == &vram_write_pixels || live == &s_pass_presentation || live == &s_pass_regs) + continue; + memcpy(snapshot->slots[i], live, s_gpu_execution_regions[i].bytes); + } + *(uint32_t *)gpu_execution_region(snapshot, &s_pass_regs_len) = 0; /* Host allocations are not machine registers. Build a private colour LUT * lazily if a replay requests scanout, and omit the live diagnostic census. * The scene predicate is immutable code and executes against bound state. */ diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 2530e044..9833cf37 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -96,7 +96,8 @@ struct PGXPValue { #define PGXP_REG_LO 33 static PSX_THREAD_LOCAL PGXPValue *s_ram = nullptr; /* lazily allocated: RAM + mod regions */ -enum { PGXP_EXEC_PAGE_WORDS = 256, +/* Fine publication pages avoid copying 10 KiB for one changed shadow word. */ +enum { PGXP_EXEC_PAGE_WORDS = 64, PGXP_EXEC_PAGES = (PGXP_SHADOW_WORDS + PGXP_EXEC_PAGE_WORDS - 1) / PGXP_EXEC_PAGE_WORDS }; /* Tracking starts only when a replay consumer requests a snapshot. Ordinary * execution does not allocate version tables. A replay marks its modified diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 465cde62..7c059a65 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -6,6 +6,7 @@ * Draw calls that tests observe are recorded below. */ uint64_t s_frame_count; +PSX_THREAD_LOCAL int g_psx_render_pass_active; PSX_THREAD_LOCAL uint64_t psx_cycle_count; /* gpu_timeline.c stamps notes with it */ int g_exec_phase; PSX_THREAD_LOCAL uint32_t g_debug_current_func_addr; diff --git a/runtime/tests/test_gpu_snapshot_scratch.c b/runtime/tests/test_gpu_snapshot_scratch.c new file mode 100644 index 00000000..5c4e68c4 --- /dev/null +++ b/runtime/tests/test_gpu_snapshot_scratch.c @@ -0,0 +1,37 @@ +/* Derived upload/checkpoint storage is private scratch, not source input. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include "../src/gpu.c" +#include "gpu_exec_stubs.inc" +int main(void) { + /* Publish half-way through a transfer. Completed upload reconstructs all + * staging pixels, including the prefix received by the source before it + * published. Neither old staging contents nor later source writes leak. */ + gpu_write_gp0(0xA0000000u); gpu_write_gp0(0); gpu_write_gp0(0x00010004u); + gpu_write_gp0(0x22221111u); + memset(vram_write_pixels, 0xCD, sizeof vram_write_pixels); + GpuExecutionSnapshot *image = gpu_execution_snapshot_create(); + assert(image && gpu_execution_snapshot_capture(image)); + vram[0] = 0x5555; + assert(gpu_execution_snapshot_bind(image)); + gpu_write_gp0(0x44443333u); + assert((*gpu_context_gp0_state()) == GP0_IDLE); + const uint16_t expected[] = {0x1111,0x2222,0x3333,0x4444}; + assert(!memcmp(*gpu_context_vram_write_pixels(), expected, sizeof expected)); + /* Checkpoint save initializes the private provenance scratch before use. */ + (*gpu_context_texture_window_value()) = 17; + assert(gpu_pass_checkpoint_save()); + (*gpu_context_texture_window_value()) = 29; + gpu_pass_checkpoint_restore(); + assert((*gpu_context_texture_window_value()) == 17); + gpu_execution_snapshot_unbind(); + assert(vram[0] == 0x5555 && gp0_state == GP0_VRAM_WRITE); + assert(gpu_pass_checkpoint_save()); + assert(!gpu_execution_snapshot_capture(image)); + gpu_pass_checkpoint_restore(); + assert(gpu_execution_snapshot_capture(image)); + gpu_execution_snapshot_destroy(image); + return 0; +} diff --git a/runtime/tests/test_pgxp_execution_snapshot.cpp b/runtime/tests/test_pgxp_execution_snapshot.cpp index 7db1c4ad..53465f1a 100644 --- a/runtime/tests/test_pgxp_execution_snapshot.cpp +++ b/runtime/tests/test_pgxp_execution_snapshot.cpp @@ -8,14 +8,14 @@ extern "C" int gte_geometry_correction_lookup(uint32_t, int32_t*, int32_t*) { re extern "C" int gte_geometry_correction_lookup_probe(uint32_t, int32_t*, int32_t*) { return 0; } extern "C" int psx_mod_gpu_dma_memory_contains(uint32_t, uint32_t) { return 0; } static constexpr uint32_t address = 0x80001000, packed = 0x000C0008; -static void seed(int32_t x) { +static void seed(int32_t x, uint32_t at = address) { pgxp_gte_push_sxy(x, 12 << 16, 300, packed); - pgxp_store_gte_reg(address, 15); + pgxp_store_gte_reg(at, 15); } -static void check(int32_t x) { +static void check(int32_t x, uint32_t at = address) { int32_t actual_x, y; uint16_t z; - CHECK(pgxp_load_precise_word(address, packed, &actual_x, &y, &z)); + CHECK(pgxp_load_precise_word(at, packed, &actual_x, &y, &z)); CHECK(actual_x == x && y == 12 << 16 && z == 300); } int main() { @@ -86,6 +86,23 @@ int main() { pgxp_execution_snapshot_unbind(); }); invalidated.join(); + /* Adjacent words across a publication-page boundary remain independent, + * including worker writes to a page the source has not changed. */ + seed(8 << 16, address + 252); seed(9 << 16, address + 256); + CHECK(pgxp_execution_snapshot_capture(snapshot)); + std::thread boundary([&] { + CHECK(pgxp_execution_snapshot_bind(snapshot)); + check(8 << 16, address + 252); check(9 << 16, address + 256); + seed(10 << 16, address + 252); seed(11 << 16, address + 256); + pgxp_execution_snapshot_unbind(); + }); boundary.join(); + seed(12 << 16, address + 256); + CHECK(pgxp_execution_snapshot_capture(snapshot)); + std::thread recapture([&] { + CHECK(pgxp_execution_snapshot_bind(snapshot)); + check(8 << 16, address + 252); check(12 << 16, address + 256); + pgxp_execution_snapshot_unbind(); + }); recapture.join(); pgxp_execution_snapshot_destroy(other); pgxp_execution_snapshot_destroy(snapshot); return 0;