diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index fa91b69e..a2cf15bb 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -65,7 +65,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `render_pass_dump` | ✓ | | `path`, `count` | Write the images (the game's own frame, then each pass in phase order) of the next `count` frames that get passes as `/g__a.png` | | `render_pass_refuse` | ✓ | | `on` | Make the OpenGL backend decline render passes (`status` 2, BACKEND), as a renderer mode without them would; tests a plugin's fallback. `PSX_RENDER_PASS_REFUSE=1` does the same from start | | `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate. Also `texture_correction`. Its `pgxp` object adds the engine's per-lookup counters, the `position_fallback` / `preserve_projection` / `culling` switches, `ppp_produced` and `ppp_window_fallback` (exact projections outside the agreement window, shadowed with the IR path instead), geometry-corrected triangles split into `tri_precise` / `tri_mixed` / `tri_native` (the mixed share is the mesh-cracking exposure), `rect_bypass` / `rect_partial` (fully / partly precise textured quads that matched the 2D rectangle shortcut; only the former leave it) and `nclip_precise` / `nclip_disagree` / `nclip_corrected` (NCLIPs with an exact determinant, whose exact sign differs from the integer one, and that precise culling corrected); `ENHANCEMENTS.md` G1.11/G1.12 | -| `pgxp` | ✓ | | `geometry`, `texture`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling` (all optional) | Live-tune PGXP for same-scene A/B: arm or disarm geometry / texture correction and set the engine knobs, without a restart. `culling` changes guest NCLIP results, so a toggle mid-run makes the run diverge from one without it. Replies with the resulting state (`enabled`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling`, `suppress`, `active`). `ENHANCEMENTS.md` G1.10-G1.12 | +| `pgxp` | ✓ | | `geometry`, `texture`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling` (all optional) | Live-tune PGXP for same-scene A/B: arm or disarm geometry / texture correction and set the engine knobs, without a restart. `culling` changes guest NCLIP results, so a toggle mid-run makes the run diverge from one without it. Replies with the resulting state (`enabled`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling`, `suppress`, `active`). `ENHANCEMENTS.md` G1.10-G1.12 Also reports the renderer side: `depth_buffer`, cumulative `depth_tris`, `depth_clears`, `seam_tris`, and `depth_clear_reasons` (config, area, fill, threshold, present) | | `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | | `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | | `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | @@ -106,6 +106,8 @@ Columns: **N** = native, **D** = DuckStation oracle. | `mmio_clear` | ✓ | | — | Reset the MMIO write ring | | `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | | `gcp_ring` | ✓ | | `frame_lo`, `frame_hi`, then `path` or `key` + `top` | Always-on guest-cycle profile ring (512K samples, one every `period` = 4096 guest cycles by default, `PSX_GCP_PERIOD`; about 63 guest seconds). Each sample: cycle, frame, `weight` (sample periods crossed, so idle-skipped waits stay credited to the waiting code), backend `phase`, exception flag, last block leader (`block`, debug-tools builds), last function entered (`fn`), guest `$ra`, native shard in progress, static-dispatch stamp. No args: ring stats. `path`: write the window as CSV. `key` = `fn`/`block`/`ra`/`native`/`disp`: top `top` keys by guest-cycle share in the window | +| `input_route_start` | ✓ | | optional `at_frame` | Start the route queued with `input_route_append` (`buttons`, `frames` per step, counted per pad poll). With `at_frame` the route waits for that vblank count, so it replays on the same guest frames in every run (frame-exact A/B with input) | +| `batch_sites` | ✓ | | — | Always-on OpenGL batch-flush attribution: cumulative flushes of non-empty batches by caller source line in `gpu_gl_renderer.c`, as `tex` and `flat` arrays of `[line, count]`. Diff two reads to attribute a window (every flush call site is covered by the `flush_*_batch()` macros) | | `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) | | `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint | | `pc_break_list` | | ✓² | — | List active execute breakpoints | @@ -484,9 +486,9 @@ between Play and TCP availability. ## Complete command index (generated) -**366 commands registered** — 353 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**367 commands registered** — 354 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -79 of 366 have prose above; **287 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. +82 of 367 have prose above; **285 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. @@ -498,6 +500,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `audio_wav` | ✓ | ✓ | | | `autocompile_run` | ✓ | | | | `autocompile_status` | ✓ | | | +| `batch_sites` | ✓ | | ✓ | | `bios_info` | ✓ | | ✓ | | `bioscall_dump` | ✓ | | | | `c0_history` | ✓ | | | @@ -648,9 +651,9 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `host_profile` | ✓ | | | | `idle_skip` | ✓ | | | | `imask_trace` | ✓ | | | -| `input_route_append` | ✓ | | | +| `input_route_append` | ✓ | | ✓ | | `input_route_clear` | ✓ | | | -| `input_route_start` | ✓ | | | +| `input_route_start` | ✓ | | ✓ | | `input_route_status` | ✓ | | | | `input_route_stop` | ✓ | | | | `insn_freeze` | ✓ | | | diff --git a/overlay_toolchain/include/pgxp.h b/overlay_toolchain/include/pgxp.h index 755b9eb4..4fbecd2f 100644 --- a/overlay_toolchain/include/pgxp.h +++ b/overlay_toolchain/include/pgxp.h @@ -224,6 +224,7 @@ typedef struct PGXPStats { uint64_t value_mismatch; /* shadow present but wrong word (stale) */ uint64_t trunc_reject; /* integer part disagreed with native parse */ uint64_t tolerance_reject; + uint64_t depth_only; /* tolerance-clamped to native, depth kept */ uint64_t w_valid; /* lookups that also carried a usable depth */ uint64_t produced; /* RTPS/RTPT projections pushed into shadows */ uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */ @@ -353,6 +354,7 @@ typedef struct PGXPTriRecord { uint32_t word[3]; /* the packet words */ int32_t x16[3]; /* position handed to the rasterizer (16.16, */ int32_t y16[3]; /* draw offset and widescreen already applied) */ + uint16_t z[3]; /* validated GTE depth handed on (0 = none) */ } PGXPTriRecord; void pgxp_note_triangle_detail(const PGXPTriRecord *rec); /* Returns the total number of triangles ever recorded. */ diff --git a/overlay_toolchain/include/spu.h b/overlay_toolchain/include/spu.h index 31bf7b96..1964fcac 100644 --- a/overlay_toolchain/include/spu.h +++ b/overlay_toolchain/include/spu.h @@ -8,7 +8,19 @@ extern "C" { #endif void spu_init(void); -void spu_render(int16_t* out_stereo, int frames); +/* Guest-clocked SPU: render every whole sample owed up to the current guest + * cycle (768 cycles per 44.1 kHz sample) into the output queue. Every SPU + * register/DMA/CD-input access does this first; the host pump calls it, then + * drains the queue. */ +void spu_catch_up(void); +uint32_t spu_output_available(void); +uint32_t spu_output_pop(int16_t* out_stereo, uint32_t max_frames); +void spu_output_drop(uint32_t frames); /* oldest first */ +void spu_output_flush(void); +uint64_t spu_output_overflow_drops(void); +/* Host gate: while it returns nonzero (rollback resimulation, FMV-skip mute) + * rendered samples advance SPU state but are not queued. */ +void spu_set_output_gate(int (*suppressed)(void)); typedef struct SpuDebugInfo { uint32_t ctrl; @@ -170,6 +182,7 @@ const void* spu_shadow_tap_buffer(void); int spu_shadow_tap_count(void); uint32_t spu_snapshot_bytes(void); +int spu_snapshot_len_ok(uint32_t len); void spu_snapshot_write(uint8_t *p); int spu_snapshot_read(const uint8_t *p, uint32_t len); uint8_t *spu_get_ram_ptr(void); diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index e14b3f6f..85fcf467 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -279,6 +279,13 @@ const char* pad_mode_to_string(int mode) { } // Parse a hex string ("0x...") to uint32_t. Throws on malformed input. +/* A TOML number key: `0` and `0.0` both mean zero. toml::find + * rejects an integer literal and failed the whole load on `key = 0`. */ +static double toml_number(const toml::value& table, const char* key) { + const toml::value& v = toml::find(table, key); + return v.is_integer() ? static_cast(v.as_integer()) : v.as_floating(); +} + static uint32_t parse_hex(const std::string& s, const std::string& field) { try { return static_cast(std::stoul(s, nullptr, 16)); @@ -803,7 +810,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& } if (video.contains("pgxp_tolerance")) { rt.video_pgxp_tolerance = - toml::find(video, "pgxp_tolerance"); + toml_number(video, "pgxp_tolerance"); } if (video.contains("pgxp_position_fallback")) { rt.video_pgxp_position_fallback = @@ -833,7 +840,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (video.contains("pgxp_color_correction")) rt.video_pgxp_color_correction = toml::find(video, "pgxp_color_correction"); if (video.contains("pgxp_depth_threshold")) - rt.video_pgxp_depth_threshold = toml::find(video, "pgxp_depth_threshold"); + rt.video_pgxp_depth_threshold = toml_number(video, "pgxp_depth_threshold"); if (video.contains("pgxp_seam")) { const auto m = toml::find(video, "pgxp_seam"); if (m == "off") rt.video_pgxp_seam = 0; @@ -858,7 +865,7 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.video_scanlines = toml::find(video, "scanlines"); } if (video.contains("scanline_strength")) { - double s = toml::find(video, "scanline_strength"); + double s = toml_number(video, "scanline_strength"); if (s < 0.0) s = 0.0; if (s > 1.0) s = 1.0; rt.video_scanline_strength = s; @@ -2884,7 +2891,7 @@ UserSettings load_user_settings(const fs::path& path) { s.scanlines = toml::find(v, "scanlines"); s.has_scanlines = true; }); if (v.contains("scanline_strength")) try_get([&]{ - double d = toml::find(v, "scanline_strength"); + double d = toml_number(v, "scanline_strength"); if (d < 0.0) d = 0.0; if (d > 1.0) d = 1.0; s.scanline_strength = d; s.has_scanline_strength = true; diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index 24899566..eeedc5a4 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -458,6 +458,17 @@ static void test_pgxp_title_keys() { "PGXP renderer keys are honoured"); fs::remove(p); + /* Integer literals for number keys (Crash sets the threshold to 0). */ + p = write_game_toml("psxrecomp_pgxp_render_int.toml", + "[video]\n" + "pgxp_depth_threshold = 0\n" + "pgxp_tolerance = 1\n" + "scanline_strength = 1\n"); + gc = PSXRecompV4::load_game_config(p); + check(gc.runtime.video_pgxp_depth_threshold == 0.0 && gc.runtime.video_pgxp_tolerance == 1.0, + "integer literals load for number keys"); + fs::remove(p); + p = write_game_toml("psxrecomp_pgxp_render_bad.toml", "[video]\n" "pgxp_seam = \"huge\"\n"); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index e6ae0f5f..ab2abb6b 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1547,6 +1547,11 @@ if(BUILD_TESTING) add_test(NAME spu_pitch_zero_test COMMAND spu_pitch_zero_test) + # Guest-clocked SPU: accesses advance it to the current cycle (beads-eio.3.321). + add_executable(spu_guest_clock_test tests/test_spu_guest_clock.c) + target_include_directories(spu_guest_clock_test PRIVATE include) + add_test(NAME spu_guest_clock_test COMMAND spu_guest_clock_test) + add_executable(spu_gaussian_test tests/test_spu_gaussian.c) target_include_directories(spu_gaussian_test PRIVATE include) add_test(NAME spu_gaussian_test COMMAND spu_gaussian_test) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 9686a04d..0b2eabbe 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -303,6 +303,9 @@ int gl_renderer_get_pgxp_color_perspective(void); void gl_renderer_set_pgxp_seam(int mode); int gl_renderer_get_pgxp_seam(void); void gl_renderer_set_pgxp_depth_threshold(float sz); +/* Cumulative depth clears by cause: config, drawing-area change, fill, + * average-SZ jump-back threshold, displayed frame. */ +void gl_renderer_pgxp_depth_clear_reasons(uint64_t out[5]); void gl_renderer_pgxp_render_stats(uint64_t *depth_tris, uint64_t *depth_clears, uint64_t *seam_tris); @@ -540,6 +543,10 @@ void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3], /* Cumulative textured-batch diagnostics: total, then flushes caused by * isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */ void gl_renderer_batch_diag(uint64_t out[9]); +/* Cumulative flushes of non-empty batches by caller source line in + * gpu_gl_renderer.c (kind 0 textured, 1 flat/gouraud). Returns entries + * written; diff two reads to attribute a window. */ +int gl_renderer_batch_sites(int kind, int *lines, uint64_t *counts, int cap); /* Texture-window batching ([video] texture_window_batching; default off). * On: textured prims with different GP0(E2h) texture windows share a batch diff --git a/runtime/include/mod_memory.h b/runtime/include/mod_memory.h index bab68684..3c8e6307 100644 --- a/runtime/include/mod_memory.h +++ b/runtime/include/mod_memory.h @@ -18,6 +18,11 @@ extern "C" { #define PSX_MOD_GPU_DMA_APERTURE_SIZE (8u * 1024u * 1024u) #define PSX_MOD_GPU_DMA_GUEST_BASE 0x80800000u +/* Trusted-mod guest memory in Expansion 1 (psx_mod_memory_alloc; KSEG0 + * 0x9F000000). Open bus until the first allocation. */ +#define PSX_MOD_MEMORY_BASE 0x1F000000u +#define PSX_MOD_MEMORY_SIZE (1u * 1024u * 1024u) + static inline int psx_mod_gpu_dma_aperture_offset_for( uint32_t address, uint32_t width, uint32_t used, uint32_t *offset) { /* CPU addresses retain all physical bits after KSEG normalization. The diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index 1c3025f7..f063f31e 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -228,6 +228,7 @@ typedef struct PGXPStats { uint64_t value_mismatch; /* shadow present but wrong word (stale) */ uint64_t trunc_reject; /* integer part disagreed with native parse */ uint64_t tolerance_reject; + uint64_t depth_only; /* tolerance-clamped to native, depth kept */ uint64_t w_valid; /* lookups that also carried a usable depth */ uint64_t produced; /* RTPS/RTPT projections pushed into shadows */ uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */ @@ -357,6 +358,7 @@ typedef struct PGXPTriRecord { uint32_t word[3]; /* the packet words */ int32_t x16[3]; /* position handed to the rasterizer (16.16, */ int32_t y16[3]; /* draw offset and widescreen already applied) */ + uint16_t z[3]; /* validated GTE depth handed on (0 = none) */ } PGXPTriRecord; void pgxp_note_triangle_detail(const PGXPTriRecord *rec); /* Returns the total number of triangles ever recorded. */ diff --git a/runtime/include/spu.h b/runtime/include/spu.h index 31bf7b96..1964fcac 100644 --- a/runtime/include/spu.h +++ b/runtime/include/spu.h @@ -8,7 +8,19 @@ extern "C" { #endif void spu_init(void); -void spu_render(int16_t* out_stereo, int frames); +/* Guest-clocked SPU: render every whole sample owed up to the current guest + * cycle (768 cycles per 44.1 kHz sample) into the output queue. Every SPU + * register/DMA/CD-input access does this first; the host pump calls it, then + * drains the queue. */ +void spu_catch_up(void); +uint32_t spu_output_available(void); +uint32_t spu_output_pop(int16_t* out_stereo, uint32_t max_frames); +void spu_output_drop(uint32_t frames); /* oldest first */ +void spu_output_flush(void); +uint64_t spu_output_overflow_drops(void); +/* Host gate: while it returns nonzero (rollback resimulation, FMV-skip mute) + * rendered samples advance SPU state but are not queued. */ +void spu_set_output_gate(int (*suppressed)(void)); typedef struct SpuDebugInfo { uint32_t ctrl; @@ -170,6 +182,7 @@ const void* spu_shadow_tap_buffer(void); int spu_shadow_tap_count(void); uint32_t spu_snapshot_bytes(void); +int spu_snapshot_len_ok(uint32_t len); void spu_snapshot_write(uint8_t *p); int spu_snapshot_read(const uint8_t *p, uint32_t len); uint8_t *spu_get_ram_ptr(void); diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index f76a1fc6..aa30edcf 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -375,6 +375,9 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/present_thread.c ${PSXRECOMP_ROOT}/runtime/src/frame_gen.c ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c + ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_lle.c + ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_hle.c + ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_select.c ${PSXRECOMP_ROOT}/runtime/src/gpu_hd_textures.cpp ${PSXRECOMP_ROOT}/runtime/src/hd_texture_pack.cpp ${PSXRECOMP_ROOT}/runtime/src/duckstation_texture_pack.cpp @@ -601,6 +604,18 @@ else() endif() endif() +# OpenGL textured-batch grouping tier (runtime/src/gl_batch_policy.h; +# recomp-template HLE.md). LLE, the reference, draws every semi-transparent +# primitive alone. HLE draws painter-ordered runs of opaque primitives and +# blend modes 0/1/3 as one dual-source draw. Build-time only: use separate +# build directories. Both tiers always compile. +set(PSX_GL_BATCH_IMPL "LLE" CACHE STRING "OpenGL textured batching: LLE or HLE") +set_property(CACHE PSX_GL_BATCH_IMPL PROPERTY STRINGS LLE HLE) +if(NOT PSX_GL_BATCH_IMPL MATCHES "^(LLE|HLE)$") + message(FATAL_ERROR "PSX_GL_BATCH_IMPL must be LLE or HLE") +endif() +message(STATUS "psxrecomp: GL textured batching = ${PSX_GL_BATCH_IMPL}") + # Local rewind: full rbengine when netplay is on; otherwise compile snap_ring.c # only (no recomp-net / sched / hash_confirm). Never both — duplicate symbols. # Defaults ON because local rewind is useful for every PSX title. If a title @@ -2082,6 +2097,9 @@ function(psxrecomp_add_runtime_target target) if(PSX_SHELLWIN_INTERP) target_compile_definitions(${target} PRIVATE PSX_SHELLWIN_INTERP_DEFAULT=1) endif() + if(PSX_GL_BATCH_IMPL STREQUAL "HLE") + target_compile_definitions(${target} PRIVATE PSX_GL_BATCH_HLE=1) + endif() # Developer-channel mod features do not ship. A contributor reaches them by # cloning the repo and building locally; a release build must not carry diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 4727cecc..33a850a1 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -73,6 +73,7 @@ extern int gpu_snapshot_read(const uint8_t* p, uint32_t len); extern uint32_t spu_snapshot_bytes(void); extern void spu_snapshot_write(uint8_t* p); extern int spu_snapshot_read(const uint8_t* p, uint32_t len); +extern int spu_snapshot_len_ok(uint32_t len); extern uint8_t* spu_get_ram_ptr(void); extern uint32_t spu_get_ram_bytes(void); extern uint32_t cdrom_snapshot_bytes(void); @@ -779,7 +780,7 @@ static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) { case BS_SEC_CLOCK: return len == 8u; case BS_SEC_GPU: return len == gpu_snapshot_bytes(); case BS_SEC_VRAM: return len == VRAM_SIZE; - case BS_SEC_SPU: return len == spu_snapshot_bytes(); + case BS_SEC_SPU: return spu_snapshot_len_ok(len); case BS_SEC_SPURAM: return len == spu_get_ram_bytes(); case BS_SEC_CDROM: return len == cdrom_snapshot_bytes(); case BS_SEC_DMA: return len == dma_snapshot_bytes(); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index bef01cca..391248c4 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -322,6 +322,9 @@ static uint32_t s_input_route_count = 0; static uint32_t s_input_route_index = 0; static uint32_t s_input_route_remaining = 0; static int s_input_route_active = 0; +/* input_route_start at_frame: the route waits for this vblank count, so a + * route replays on the same guest frames in every run (0 = start now). */ +static uint64_t s_input_route_start_frame = 0; /* ---- Frontend turbo override ---- */ static volatile int s_turbo_enabled = 0; @@ -5374,7 +5377,7 @@ static void handle_geom_correction(int id, const char *json) "\"culling\":%d," "\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu," "\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu," - "\"tolerance_reject\":%llu,\"w_valid\":%llu," + "\"tolerance_reject\":%llu,\"depth_only\":%llu,\"w_valid\":%llu," "\"produced\":%llu,\"swc2_stores\":%llu,\"ppp_produced\":%llu," "\"ppp_window_fallback\":%llu," "\"tri_precise\":%llu,\"tri_mixed\":%llu,\"tri_native\":%llu," @@ -5402,6 +5405,7 @@ static void handle_geom_correction(int id, const char *json) (unsigned long long)ps.value_mismatch, (unsigned long long)ps.trunc_reject, (unsigned long long)ps.tolerance_reject, + (unsigned long long)ps.depth_only, (unsigned long long)ps.w_valid, (unsigned long long)ps.produced, (unsigned long long)ps.swc2_stores, @@ -5560,8 +5564,9 @@ static void handle_pgxp_miss_ring(int id, const char *json) * with any packet vertex inside [x0,x1]x[y0,y1] (packet integers). Returns * the newest N matches (default 4096) oldest first. Per triangle: packet * address, opcode, and per vertex the packet word, its integers, the 16.16 - * position handed to the rasterizer and its source (0 native, 1 dataflow, - * 2 fallback). Join vertices by word across triangles to find cracks. */ + * position handed to the rasterizer, its source (0 native, 1 fallback, + * 2 dataflow) and the validated GTE depth handed on (0 none). Join vertices + * by word across triangles to find cracks. */ static void handle_pgxp_tri_ring(int id, const char *json) { const PGXPTriRecord *ring; uint32_t cap; @@ -5606,8 +5611,8 @@ static void handle_pgxp_tri_ring(int id, const char *json) r->op, r->pass); for (int i = 0; i < 3; i++) p += (size_t)snprintf(buf + p, sz - p, - "%s[%u,\"0x%08X\",%d,%d,%d,%d,%u]", i ? "," : "", r->vidx[i], r->word[i], - r->raw_x[i], r->raw_y[i], r->x16[i], r->y16[i], r->src[i]); + "%s[%u,\"0x%08X\",%d,%d,%d,%d,%u,%u]", i ? "," : "", r->vidx[i], r->word[i], + r->raw_x[i], r->raw_y[i], r->x16[i], r->y16[i], r->src[i], r->z[i]); p += (size_t)snprintf(buf + p, sz - p, "]}"); } snprintf(buf + p, sz - p, "]}"); @@ -5711,14 +5716,28 @@ static void handle_pgxp(int id, const char *json) if (*p == '-' || (*p >= '0' && *p <= '9') || *p == '.') pgxp_set_tolerance((float)strtod(p, NULL)); } + /* Renderer side (G1.14): depth-buffer state and cumulative counts of + * depth-tested triangles, depth clears and seam-expanded triangles. */ + uint64_t depth_tris = 0, depth_clears = 0, seam_tris = 0; + gl_renderer_pgxp_render_stats(&depth_tris, &depth_clears, &seam_tris); + uint64_t dr[5]; + gl_renderer_pgxp_depth_clear_reasons(dr); send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d," "\"tolerance\":%.3f,\"position_fallback\":%d," "\"preserve_projection\":%d,\"culling\":%d," - "\"suppress\":%u,\"active\":%d}", + "\"suppress\":%u,\"active\":%d,\"depth_buffer\":%d," + "\"depth_tris\":%llu,\"depth_clears\":%llu,\"seam_tris\":%llu," + "\"depth_clear_reasons\":{\"config\":%llu,\"area\":%llu,\"fill\":%llu," + "\"threshold\":%llu,\"present\":%llu}}", id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(), pgxp_position_fallback(), pgxp_preserve_projection(), pgxp_culling(), - (unsigned)pgxp_test_suppress_depth(), pgxp_test_active()); + (unsigned)pgxp_test_suppress_depth(), pgxp_test_active(), + gl_renderer_get_pgxp_depth(), (unsigned long long)depth_tris, + (unsigned long long)depth_clears, (unsigned long long)seam_tris, + (unsigned long long)dr[0], (unsigned long long)dr[1], + (unsigned long long)dr[2], (unsigned long long)dr[3], + (unsigned long long)dr[4]); } static void handle_gpu_state(int id, const char *json) @@ -7196,7 +7215,8 @@ static void handle_audio_stats(int id, const char *json) "\"out\":{\"active\":%d,\"mode\":\"%s\",\"host_rate\":%d," "\"fill_ms\":%.1f,\"target_ms\":%.1f,\"underruns\":%llu," "\"overflow_drops\":%llu," - "\"correction\":%.5f}}", + "\"correction\":%.5f}," + "\"spu_out\":{\"queued\":%u,\"drops\":%llu}}", id, (unsigned long long)st.tap_frames[0], (unsigned long long)st.tap_nonzero[0], @@ -7221,7 +7241,9 @@ static void handle_audio_stats(int id, const char *json) fill_ms, target_ms, (unsigned long long)out_underruns, (unsigned long long)overflow_drops, - correction); + correction, + spu_output_available(), + (unsigned long long)spu_output_overflow_drops()); } static void handle_audio_wav(int id, const char *json) @@ -8267,7 +8289,6 @@ static void handle_input_route_append(int id, const char *json) static void handle_input_route_start(int id, const char *json) { - (void)json; if (s_input_route_count == 0) { send_err(id, "input route is empty"); return; } @@ -8277,10 +8298,13 @@ static void handle_input_route_start(int id, const char *json) s_pad_type_override = -1; s_input_route_index = 0; s_input_route_remaining = s_input_route[0].frames; + const int at = json_get_int(json, "at_frame", 0); + s_input_route_start_frame = at > 0 && (uint64_t)at > s_frame_count ? (uint64_t)at : 0u; s_input_route_active = 1; send_fmt("{\"id\":%d,\"ok\":true,\"steps\":%u,\"start_frame\":%llu}\n", id, (unsigned)s_input_route_count, - (unsigned long long)s_frame_count); + (unsigned long long)(s_input_route_start_frame ? s_input_route_start_frame + : s_frame_count)); } static void handle_input_route_stop(int id, const char *json) @@ -8655,6 +8679,28 @@ static void handle_frame_perf(int id, const char *json) n43[13], n43[15]); } +/* batch_sites: cumulative non-empty batch flushes by caller line in + * gpu_gl_renderer.c, "tex" and "flat" as [line,count] pairs. Diff two reads + * to attribute a window (always on; no arming). */ +static void handle_batch_sites(int id, const char *json) +{ + (void)json; + char buf[8192]; + int len = snprintf(buf, sizeof buf, "{\"id\":%d,\"ok\":true", id); + for (int kind = 0; kind < 2; kind++) { + int lines[128]; uint64_t counts[128]; + int n = gl_renderer_batch_sites(kind, lines, counts, 128); + len += snprintf(buf + len, sizeof buf - (size_t)len, ",\"%s\":[", + kind ? "flat" : "tex"); + for (int i = 0; i < n && len < (int)sizeof buf - 64; i++) + len += snprintf(buf + len, sizeof buf - (size_t)len, "%s[%d,%llu]", + i ? "," : "", lines[i], (unsigned long long)counts[i]); + len += snprintf(buf + len, sizeof buf - (size_t)len, "]"); + } + snprintf(buf + len, sizeof buf - (size_t)len, "}"); + send_fmt("%s", buf); +} + /* gl_ws_ablate mode=<0..3>: native-wide mirror ablation for perf attribution. * 0 normal | 1 skip mirror passes (incl. wide_clear) | 2 mirror state churn, no * draws | 3 mirror draws stay on the hr FBO (no per-pass FBO rebind; corrupts @@ -15804,6 +15850,7 @@ static const CmdEntry s_commands[] = { { "gpu_timeline", handle_gpu_timeline }, { "nclip_stats", handle_nclip_stats }, { "frame_perf", handle_frame_perf }, + { "batch_sites", handle_batch_sites }, { "post_aa", handle_post_aa }, { "fmv_chroma", handle_fmv_chroma }, { "texture_lod", handle_texture_lod }, @@ -16962,7 +17009,8 @@ int debug_server_is_connected(void) int debug_server_get_input_override(void) { - if (s_input_route_active && s_input_route_index < s_input_route_count) { + if (s_input_route_active && s_input_route_index < s_input_route_count && + s_frame_count >= s_input_route_start_frame) { int current = (int)s_input_route[s_input_route_index].buttons; if (s_input_route_remaining > 0 && --s_input_route_remaining == 0) { s_input_route_index++; diff --git a/runtime/src/gl_batch_policy.h b/runtime/src/gl_batch_policy.h new file mode 100644 index 00000000..19682f1f --- /dev/null +++ b/runtime/src/gl_batch_policy.h @@ -0,0 +1,36 @@ +#ifndef GL_BATCH_POLICY_H +#define GL_BATCH_POLICY_H + +/* Textured-batch grouping for gpu_gl_renderer.c, a build-time tier + * (runtime.cmake PSX_GL_BATCH_IMPL = LLE | HLE; recomp-template HLE.md). + * + * Contract, both tiers: classify one textured primitive from its blend mode + * (semi: -1 opaque, 0..3 the PS1 modes), the destination mask-check state + * (GP0(E6h) bit 1) and whether an opted-in immutable texture bank already + * permits dual-source batching. Returns its batch key and whether it must be + * drawn alone. Keys: -1 opaque passes; 0..3 the two-pass STP split with that + * blend; 4 the single-pass dual-source draw, which blends modes 0/1/3 per + * fragment and gives opaque texels (STP=0 or an opaque primitive) destination + * factor zero. The renderer still splits batches on every other key (mask-set, + * filter, texture window, backdrop gate, depth mode, texture source) and + * flushes before any primitive samples VRAM a queued draw has written. + * Either tier draws in submission (painter) order and produces the LLE pixels. + * + * LLE: the reference. Every semi-transparent primitive is drawn alone. + * HLE: opaque primitives and modes 0/1/3 share one painter-ordered + * dual-source draw while mask checking is off. GL blends the primitives + * of one draw in order, so the result equals drawing them one by one. + * Mode 2 (subtractive) and mask-checked draws keep the LLE behaviour. */ +typedef struct { + int key; + int isolate; +} GlBatchClass; + +GlBatchClass gl_batch_policy_lle(int semi, int mask_check, int bank_batch); +GlBatchClass gl_batch_policy_hle(int semi, int mask_check, int bank_batch); + +/* The tier this build selected. */ +GlBatchClass gl_batch_classify(int semi, int mask_check, int bank_batch); +const char *gl_batch_policy_name(void); + +#endif diff --git a/runtime/src/gl_batch_policy_hle.c b/runtime/src/gl_batch_policy_hle.c new file mode 100644 index 00000000..d4c53768 --- /dev/null +++ b/runtime/src/gl_batch_policy_hle.c @@ -0,0 +1,20 @@ +/* Textured-batch grouping, HLE tier (gl_batch_policy.h). + * + * The dual-source draw (key 4) composites each fragment from its own + * primitive's blend mode: opaque texels get destination factor zero, modes + * 0/1/3 their PS1 factors. GL applies blending for the primitives of one draw + * in submission order, so one draw of a painter-ordered run equals drawing + * its primitives one at a time. That holds while no primitive in the run reads + * what an earlier one wrote: the renderer flushes before a primitive samples + * a texture page or CLUT the queue has drawn to (flush_pack_if_sampling), and + * mask checking (whose stencil an opaque batch fixes up after its colour) + * keeps the LLE classification here. */ +#include "gl_batch_policy.h" + +GlBatchClass gl_batch_policy_hle(int semi, int mask_check, int bank_batch) { + if (!mask_check && semi != 2) { + GlBatchClass c = {4, 0}; + return c; + } + return gl_batch_policy_lle(semi, mask_check, bank_batch); +} diff --git a/runtime/src/gl_batch_policy_lle.c b/runtime/src/gl_batch_policy_lle.c new file mode 100644 index 00000000..98a835ad --- /dev/null +++ b/runtime/src/gl_batch_policy_lle.c @@ -0,0 +1,24 @@ +/* Textured-batch grouping, LLE tier (gl_batch_policy.h). */ +#include "gl_batch_policy.h" + +GlBatchClass gl_batch_policy_lle(int semi, int mask_check, int bank_batch) { + GlBatchClass c; + /* Dual-source (4) carries semi modes 0/1/3 with mask checking off; opaque + * keeps its own key. Mixing opaque+semi under one key needs every queued + * primitive to composite in order, which this tier does not rely on. */ + if (semi < 0) + c.key = -1; + else if (!mask_check && semi != 2) + c.key = 4; + else + c.key = semi; + /* STP draw-ORDER correctness. The conservative two-pass path draws pass + * 1 = every prim's STP=0 texels then pass 2 = every prim's STP=1 texels, + * so a behind prim's semi texels would overwrite a front prim's opaque + * texels (Tomba AP-block / CTR intro flaps). Isolate EVERY + * semi-transparent textured prim: drain the open batch, draw this prim + * alone, let opaque prims keep batching. Cost is one draw per semi prim. + * An opted-in immutable bank may batch the single-pass dual-source cases. */ + c.isolate = semi >= 0 && !bank_batch; + return c; +} diff --git a/runtime/src/gl_batch_policy_select.c b/runtime/src/gl_batch_policy_select.c new file mode 100644 index 00000000..d007275e --- /dev/null +++ b/runtime/src/gl_batch_policy_select.c @@ -0,0 +1,16 @@ +/* Binds the textured-batch tier this build selected (gl_batch_policy.h). + * runtime.cmake defines PSX_GL_BATCH_HLE from PSX_GL_BATCH_IMPL; both tiers + * are always compiled so the LLE floor keeps building. */ +#include "gl_batch_policy.h" + +#if PSX_GL_BATCH_HLE +GlBatchClass gl_batch_classify(int semi, int mask_check, int bank_batch) { + return gl_batch_policy_hle(semi, mask_check, bank_batch); +} +const char *gl_batch_policy_name(void) { return "HLE"; } +#else +GlBatchClass gl_batch_classify(int semi, int mask_check, int bank_batch) { + return gl_batch_policy_lle(semi, mask_check, bank_batch); +} +const char *gl_batch_policy_name(void) { return "LLE"; } +#endif diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 115cc5e6..12c20343 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -4646,8 +4646,9 @@ static void prepare_precise_triangle(int i0, int i1, int i2, any_precise = 1; n_precise++; } - /* PGXP depth (G1.14): only a dataflow shadow carries a validated SZ. */ - vz[i] = src == PGXP_SRC_DATAFLOW ? sz : 0; + /* PGXP depth (G1.14): only a value-validated dataflow shadow carries + * SZ, also when the seam clamp kept the native position. */ + vz[i] = sz; /* Native-wide edge recovery is not PGXP: it does not count in n_precise. */ if (native_wide_projection_x(addr, word, raw_x, raw_y, &px)) any_precise = 1; @@ -4660,6 +4661,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2, rec.word[i] = word; rec.x16[i] = fx[i]; rec.y16[i] = fy[i]; + rec.z[i] = vz[i]; } if (geometry) { pgxp_note_triangle(n_precise); /* G1.1 crack exposure: mixed share */ @@ -4668,12 +4670,17 @@ static void prepare_precise_triangle(int i0, int i1, int i2, rec.op = (uint8_t)(gp0_cmd_buf[0] >> 24); pgxp_note_triangle_detail(&rec); } - if (!any_precise) { + const int depth = want_depth && vz[0] && vz[1] && vz[2]; + if (!any_precise && !depth) { gr_set_precise_triangle(0, 0,0, 0,0, 0,0); return; } + /* A triangle with validated depth on every vertex is proven projected + * geometry even when the seam clamp left all three at their integers: + * fx/fy then are exactly those integers (plus offsets), so the position + * is unchanged and only the depth test is added. */ gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]); - if (want_depth && vz[0] && vz[1] && vz[2]) + if (depth) gr_set_depth_triangle(1, (float)vz[0], (float)vz[1], (float)vz[2]); } diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 640aab06..d05df396 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -80,6 +80,7 @@ #include "gpu_gl_renderer.h" #include "gpu_hd_textures.h" #include "mod_texture_banks.h" +#include "gl_batch_policy.h" #include "frame_interpolation.h" #include "render_pass_plan.h" #include "render_pass.h" @@ -293,8 +294,13 @@ static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits s * draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide * margins go stale + hr gets garbage — perf probe only). */ static int s_ws_ablate = 0; -static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */ -static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ +static void flush_tex_batch_impl(void); /* textured-prim batch — defined below, flushed from coherency points */ +static void flush_flat_batch_impl(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ +/* Every flush call site is attributed (gl_renderer_batch_sites): a flush that + * draws a non-empty batch counts against its caller's source line. */ +static void batch_site_note(int kind, int line); +#define flush_tex_batch() (batch_site_note(0, __LINE__), flush_tex_batch_impl()) +#define flush_flat_batch() (batch_site_note(1, __LINE__), flush_flat_batch_impl()) static PFN_glGenRenderbuffers p_glGenRenderbuffers; static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers; static PFN_glBindRenderbuffer p_glBindRenderbuffer; @@ -777,6 +783,12 @@ static float s_pgxp_depth_threshold = 4096.0f; /* SZ units, as DuckStation * static int s_depth_need_clear = 1, s_depth_used = 0; static float s_depth_last_avg = 0.0f; static uint64_t s_depth_clears = 0, s_depth_tris = 0, s_seam_tris = 0; +/* Why each depth clear ran (pgxp TCP depth_clear_reasons): the request + * pending when it ran. */ +enum { DCLR_CONFIG, DCLR_AREA, DCLR_FILL, DCLR_THRESHOLD, DCLR_PRESENT, DCLR_N }; +static int s_depth_need_reason = DCLR_CONFIG; +static uint64_t s_depth_clear_reason[DCLR_N]; +#define DEPTH_REQUEST_CLEAR(r) (s_depth_need_clear = 1, s_depth_need_reason = (r)) /* TEX program uniforms. */ static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; @@ -3074,7 +3086,6 @@ static void ensure_cpu(void) { static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ -static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */ static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { s_scene_prims++; int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; @@ -3225,7 +3236,7 @@ int gl_renderer_pgxp_render_wanted(void) { void gl_renderer_set_pgxp_depth(int on) { GL_RT_SYNC("set_pgxp_depth"); s_pgxp_depth = on ? 1 : 0; - s_depth_need_clear = 1; + DEPTH_REQUEST_CLEAR(DCLR_CONFIG); s_pz_valid = 0; pgxp_render_wanted_update(); } @@ -3244,6 +3255,10 @@ void gl_renderer_set_pgxp_seam(int mode) { int gl_renderer_get_pgxp_seam(void) { return s_pgxp_seam; } void gl_renderer_set_pgxp_depth_threshold(float sz) { GL_RT_SYNC("set_pgxp_depth_threshold"); s_pgxp_depth_threshold = sz; } +void gl_renderer_pgxp_depth_clear_reasons(uint64_t out[5]) { + GL_RT_SYNC("pgxp_depth_clear_reasons"); + for (int i = 0; i < DCLR_N; i++) out[i] = s_depth_clear_reason[i]; +} void gl_renderer_pgxp_render_stats(uint64_t *depth_tris, uint64_t *depth_clears, uint64_t *seam_tris) { GL_RT_SYNC("pgxp_render_stats"); @@ -3874,7 +3889,7 @@ static void hiw_flush_tail(void) { s_hq_vn = 0; } -static void flush_tex_batch(void) { +static void flush_tex_batch_impl(void) { if (s_tb_n == 0) return; wide_stencil_ready(); /* before any of this batch's GL state */ int nverts = s_tb_n, semi = s_tb_semi, dmode = s_tb_depth; @@ -3987,7 +4002,29 @@ static void flat_batch_draw_hr_lines(int nverts, int nl) { if (nverts > pos) glDrawArrays(GL_TRIANGLES, pos, nverts - pos); } -static void flush_flat_batch(void) { +#define BATCH_SITE_CAP 128 +typedef struct { int line; uint64_t count; } BatchSite; +static BatchSite s_batch_sites[2][BATCH_SITE_CAP]; +static void batch_site_note(int kind, int line) { + if (kind ? s_fb_n == 0 : s_tb_n == 0) return; + BatchSite *t = s_batch_sites[kind]; + for (unsigned i = (unsigned)line % BATCH_SITE_CAP, n = 0; n < BATCH_SITE_CAP; + i = (i + 1) % BATCH_SITE_CAP, n++) { + if (t[i].line == line) { t[i].count++; return; } + if (!t[i].line) { t[i].line = line; t[i].count = 1; return; } + } +} +int gl_renderer_batch_sites(int kind, int *lines, uint64_t *counts, int cap) { + GL_RT_SYNC("batch_sites"); + int n = 0; + for (int i = 0; i < BATCH_SITE_CAP && n < cap; i++) { + const BatchSite *b = &s_batch_sites[kind ? 1 : 0][i]; + if (b->line) { lines[n] = b->line; counts[n] = b->count; n++; } + } + return n; +} + +static void flush_flat_batch_impl(void) { if (s_fb_n == 0) return; wide_stencil_ready(); /* before any of this batch's GL state */ if (s_fb_mode != GL_TRIANGLES && !hiw_on()) @@ -4149,6 +4186,7 @@ static void depth_clear_now(void) { s_fb_n = 6; flush_flat_batch(); s_depth_clears++; + s_depth_clear_reason[s_depth_need_reason]++; { FILE *f = pgxp_tri_log(); if (f) fprintf(f, "C\n"); } } /* Before a depth-tested triangle: clear when the drawing area changed since @@ -4159,7 +4197,7 @@ static void depth_before_tri(void) { const float avg = (s_pz[0] + s_pz[1] + s_pz[2]) * (1.0f / 3.0f); if (s_depth_used && s_pgxp_depth_threshold > 0.0f && avg - s_depth_last_avg >= s_pgxp_depth_threshold) - s_depth_need_clear = 1; + DEPTH_REQUEST_CLEAR(DCLR_THRESHOLD); s_depth_last_avg = avg; if (s_depth_need_clear) { s_depth_need_clear = 0; @@ -4555,34 +4593,13 @@ static void gpu_textured_triangle(const int *xs, const int *ys, flush_flat_batch(); /* painter order: flat GEO before textured */ int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ - /* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes - * 0/1/3 when mask-check is off — never coalesce opaque into that key. - * Mixing opaque+semi under u_semimode==4 made additive particle/glow - * batches (CTR Naughty Dog intro binary funnel) paint over later - * opaque crate flaps whenever submission order and STP bits disagreed - * with the dual-source path's assumptions. */ - int batch_semi; - if (semi < 0) - batch_semi = -1; - else if (!s_mask_check && semi != 2) - batch_semi = 4; - else - batch_semi = semi; - /* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass - * path draws pass 1 = every prim's STP=0 texels then pass 2 = every - * prim's STP=1 texels — a behind prim's semi texels then overwrite a - * front prim's opaque texels (Tomba AP-block / CTR intro flaps). The - * dual-source single-pass path avoids that WITHIN one prim, but - * batching many overlapping semi quads (digit particles + glow) still - * mis-orders against neighbouring opaque geometry. Isolate EVERY - * semi-transparent textured prim: drain the open batch, draw this - * prim alone (composited fully before the next), let opaque prims - * keep batching. Cost is one draw per semi prim. A separately opted-in - * immutable bank may batch the single-pass dual-source cases: it - * cannot alias a render target, keeps painter order, and still splits - * on opaque transitions, bank/state changes, masking or subtraction. */ - int isolate = semi >= 0 && - !mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi); + /* Batch key and isolation come from the build's tier + * (gl_batch_policy.h): LLE draws every semi prim alone; HLE keeps + * painter-ordered opaque + modes 0/1/3 runs in one dual-source draw. */ + const GlBatchClass batch_class = gl_batch_classify(semi, s_mask_check, + mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi)); + const int batch_semi = batch_class.key; + const int isolate = batch_class.isolate; const int tdmode = pgxp_tri_depth_mode(semi); if (tdmode) depth_before_tri(); int reason = -1; @@ -4981,7 +4998,7 @@ static void glb_set_depth_triangle(int enabled, float z0, float z1, float z2) { } static void glb_set_draw_area(int x1,int y1,int x2,int y2) { if (s_depth_used && (x1 != s_area_x1 || y1 != s_area_y1 || x2 != s_area_x2 || y2 != s_area_y2)) - s_depth_need_clear = 1; /* PGXP depth: a new drawing area starts clean */ + DEPTH_REQUEST_CLEAR(DCLR_AREA); /* PGXP depth: a new drawing area starts clean */ flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } @@ -5089,7 +5106,7 @@ static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui } static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ if (pass_refuse_write("fill", x, y, w, h)) return; - if (s_depth_used) s_depth_need_clear = 1; /* PGXP depth: the image under it is gone */ + if (s_depth_used) DEPTH_REQUEST_CLEAR(DCLR_FILL); /* PGXP depth: the image under it is gone */ if (cpu_raster_required()) { int faithful = native_draw_begin(); sw_fill_rect(x,y,w,h,c); @@ -5936,6 +5953,7 @@ static int init_gpu_raster(void) { "mask-bit stencil, texture window, GPU copy/upload)\n", s_out_scale, s_hiw ? " windowed" : ""); } + fprintf(stdout, "psxrecomp: GL textured batching = %s\n", gl_batch_policy_name()); return 1; } @@ -7061,7 +7079,7 @@ static void gl_perf_present_enter(void) { s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; /* PGXP depth (G1.14): every displayed frame starts with a clear depth * buffer, whatever the game does with its drawing areas. */ - if (s_depth_used) s_depth_need_clear = 1; + if (s_depth_used) DEPTH_REQUEST_CLEAR(DCLR_PRESENT); /* Replay: taken on the emulation thread when the present was recorded. */ { extern void psx_ws_dbg_gate_frame_snapshot(void); if (!rth_replaying()) psx_ws_dbg_gate_frame_snapshot(); } if (!s_pf_on || rthf_owns_timer()) return; @@ -12312,6 +12330,9 @@ static int fg_generate(double t, int swap) { const int real_dneed = s_depth_need_clear, real_dused = s_depth_used; const float real_davg = s_depth_last_avg; const uint64_t real_dtris = s_depth_tris, real_dclears = s_depth_clears; + uint64_t real_dreason[DCLR_N]; + memcpy(real_dreason, s_depth_clear_reason, sizeof real_dreason); + const int real_dneed_reason = s_depth_need_reason; s_pz_valid = 0; s_depth_need_clear = 1; DirtyRect pack = s_pack_dirty, sten = s_stencil_stale, cpu = s_cpu_dirty; @@ -12418,6 +12439,8 @@ static int fg_generate(double t, int swap) { s_pc_valid = real_pc; s_pq_valid = real_pq; s_pz_valid = real_pz; s_depth_need_clear = real_dneed; s_depth_used = real_dused; s_depth_last_avg = real_davg; s_depth_tris = real_dtris; s_depth_clears = real_dclears; + memcpy(s_depth_clear_reason, real_dreason, sizeof real_dreason); + s_depth_need_reason = real_dneed_reason; s_pack_dirty = pack; s_stencil_stale = sten; s_cpu_dirty = cpu; s_stencil_valid = sten_valid; s_gpu_dirty = gpu_dirty; memcpy(s_present_dirty, pres_dirty, sizeof pres_dirty); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c7990bd6..e74a632f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -3989,7 +3989,6 @@ static void sdl_audio_pump(bool discard_output = false) { * (queue full / !drc / no device) while the other kept advancing. */ const uint32_t bytes_per_frame = sizeof(int16_t) * 2u; const bool legacy = audio_legacy_mode(); - const int netplay = psx_netplay_active(); static int had_audio = 0; uint32_t queued = 0; /* RENDER event b: bytes (legacy) / fill ms (bridge) */ int host_queue_ok = 0; @@ -4035,20 +4034,15 @@ static void sdl_audio_pump(bool discard_output = false) { host_queue_ok = 1; } - /* Faithful sample budget: the SPU is clocked by the GUEST, not by host - * presents. 33.8688 MHz / 44100 Hz = exactly 768 guest cycles per output - * frame, so production tracks guest time precisely — including the real - * NTSC 59.94 Hz vblank — instead of assuming 60.00 Hz per present, which - * built in a systematic -0.1% production deficit (measured: 43950/s - * produced vs 44100/s consumed = recurring ring underruns no +/-0.5% - * DRC trim could absorb during jitter spikes). */ - extern uint64_t psx_cycle_count; - static uint64_t last_cycles = 0; - static uint64_t cycle_carry = 0; - const uint64_t now_cycles = psx_cycle_count; + /* The SPU is clocked by the GUEST (spu.c spu_catch_up: 768 cycles per + * 44.1 kHz sample), and every SPU access already advanced it to the + * instruction that made it. The pump only brings it to "now" and drains + * the samples it produced; nothing here changes SPU state, so host + * backpressure, a missing device or a dropped burst cannot fork peers. */ + spu_catch_up(); if (g_audio_cycle_resync) { - last_cycles = now_cycles; - cycle_carry = 0; + /* Savestate/rewind load: queued output belongs to the old timeline. */ + spu_output_flush(); g_audio_cycle_resync = 0; if (legacy && sdl_audio_device) psx_sdl_audio_clear(sdl_audio_device); @@ -4056,37 +4050,24 @@ static void sdl_audio_pump(bool discard_output = false) { g_audio_unmute_resync = 1; /* skip mute-drain underrun reports */ return; } - if (last_cycles == 0) last_cycles = now_cycles; - uint64_t delta = (now_cycles - last_cycles) + cycle_carry; - last_cycles = now_cycles; - int frames = (int)(delta / 768u); - cycle_carry = delta % 768u; + int frames = (int)spu_output_available(); if (frames <= 0) return; - if (frames > 2048 && !netplay) { - /* Offline: a burst beyond one buffer (e.g. right after an unmute or a - * long stall) renders one full buffer and DROPs the remainder of the - * debt — mute-model freeze rather than time-compressing a backlog. - * Netplay never drops: both peers must consume the same guest debt. */ - frames = 2048; - cycle_carry = 0; + const int buf_frames = (int)(sizeof(sdl_audio_buf) / (2 * sizeof(int16_t))); + if (frames > buf_frames) { + /* A burst beyond one buffer (after an unmute or a long stall) hands the + * newest buffer to the host and drops the older output, never the + * SPU time it represents. */ + if (discard_output) g_turbo_audio_sink_frames += (uint64_t)(frames - buf_frames); + spu_output_drop((uint32_t)(frames - buf_frames)); + frames = buf_frames; } audio_trace_event(AUDIO_EV_RENDER, (uint32_t)frames, queued); - /* Catch up in ≤2048-frame chunks (sdl_audio_buf capacity). Only the last - * chunk may be handed to the host queue — earlier chunks advance state - * only (avoids dumping seconds of catch-up into the device ring). */ - int remaining = frames; - int host_frames = 0; - while (remaining > 0) { - const int chunk = remaining > 2048 ? 2048 : remaining; - spu_render(sdl_audio_buf, chunk); - remaining -= chunk; - host_frames = chunk; - if (discard_output) { - g_turbo_audio_sink_frames += (uint64_t)chunk; - audio_trace_event(AUDIO_EV_SINK_DROP, (uint32_t)chunk, 0); - } + const int host_frames = (int)spu_output_pop(sdl_audio_buf, (uint32_t)frames); + if (discard_output) { + g_turbo_audio_sink_frames += (uint64_t)host_frames; + audio_trace_event(AUDIO_EV_SINK_DROP, (uint32_t)host_frames, 0); } if (discard_output) return; @@ -4569,6 +4550,14 @@ static void runtime_perf_diag_tick() { enum AudioGate { AUDIO_GATE_NORMAL = 0, AUDIO_GATE_MUTED = 1, AUDIO_GATE_SINK = 2 }; static AudioGate s_audio_gate = AUDIO_GATE_NORMAL; +/* spu.c output gate: samples rendered during rollback resimulation (already + * heard once) and during the FMV-skip mute advance the SPU but are not + * queued for the host. */ +static int spu_output_suppressed(void) { + return psx_netplay_is_resimulating() || psx_selfcheck_resim_active() || + s_audio_gate == AUDIO_GATE_MUTED; +} + /* Invoked from the guest-derived VBlank edge (interrupts.c). */ static void sdl_audio_pump_midframe(void) { /* Device optional: SPU still advances from guest cycles (netplay-safe). */ @@ -4612,7 +4601,10 @@ static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) { const float g0 = (1.0f - (float)sdl_audio_fadein_left / (float)sdl_audio_fade_samples) * vol; sdl_audio_fadein_left = 0; - spu_render(sdl_audio_buf, tail); + /* Fade out what the SPU has already produced; rendering ahead of + * the guest clock would advance SPU state out of guest time. */ + spu_catch_up(); + tail = (int)spu_output_pop(sdl_audio_buf, (uint32_t)tail); sdl_audio_gain_ramp(sdl_audio_buf, tail, g0, 0.0f); audio_trace_event(AUDIO_EV_MUTE, (uint32_t)tail, 0); if (sdl_audio_device && audio_legacy_mode()) { @@ -18768,6 +18760,7 @@ session_reboot: if (g_headless) { audio_trace_init(); psx_set_midframe_audio_pump(sdl_audio_pump_midframe); + spu_set_output_gate(spu_output_suppressed); sdl_audio_pump_midframe(); } #endif @@ -18904,6 +18897,7 @@ session_reboot: * depend on a successful host open (Win↔Linux aux/spu fork). Routed * through the gated wrapper so turbo mute/sink still apply. */ psx_set_midframe_audio_pump(sdl_audio_pump_midframe); + spu_set_output_gate(spu_output_suppressed); /* Establish the cycle-zero audio epoch before the first device deadline. * Otherwise the first D-1 quiet-prefix service becomes the epoch and shifts * every nominal 768-cycle sample boundary. */ diff --git a/runtime/src/memory.c b/runtime/src/memory.c index bbb82abf..a0b63a73 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -39,8 +39,8 @@ #define RAM_SIZE PSX_MAIN_RAM_BACKING_BYTES #define SCRATCHPAD_SIZE 1024 #define BIOS_ROM_SIZE (512 * 1024) -#define MOD_MEMORY_BASE 0x1F000000u -#define MOD_MEMORY_SIZE (1u * 1024u * 1024u) +#define MOD_MEMORY_BASE PSX_MOD_MEMORY_BASE +#define MOD_MEMORY_SIZE PSX_MOD_MEMORY_SIZE static uint8_t ram[RAM_SIZE]; static uint8_t scratchpad[SCRATCHPAD_SIZE]; diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index ab650c6e..b37936fd 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -36,6 +36,7 @@ #include "pgxp_hooks.h" #include "cpu_state.h" #include "psx_memory.h" +#include "mod_memory.h" #include #include @@ -75,13 +76,21 @@ struct PGXPValue { }; /* Shadow covers the host RAM backing so the opt-in 8 MB map tracks its high - * banks; retail sessions only ever touch the low 2 MiB of it. */ -#define PGXP_RAM_WORDS (PSX_MAIN_RAM_BACKING_BYTES >> 2) + * banks; retail sessions only ever touch the low 2 MiB of it. The same array + * continues with the trusted-mod guest regions: the GPU DMA aperture (an + * enhancement's enlarged primitive arena, e.g. Crash widescreen) and the + * Expansion 1 mod memory. Packets built there are tracked exactly like + * packets in RAM; without this every vertex an enlarged arena carried drew + * native and had no depth. */ +#define PGXP_RAM_WORDS (PSX_MAIN_RAM_BACKING_BYTES >> 2) +#define PGXP_APERTURE_WORDS (PSX_MOD_GPU_DMA_APERTURE_SIZE >> 2) +#define PGXP_MODMEM_WORDS (PSX_MOD_MEMORY_SIZE >> 2) +#define PGXP_SHADOW_WORDS (PGXP_RAM_WORDS + PGXP_APERTURE_WORDS + PGXP_MODMEM_WORDS) #define PGXP_SCRATCH_WORDS (0x400u >> 2) /* 1 KB scratchpad */ #define PGXP_REG_HI 32 #define PGXP_REG_LO 33 -static PGXPValue *s_ram = nullptr; /* lazily allocated, 72 MiB VA */ +static PGXPValue *s_ram = nullptr; /* lazily allocated: RAM + mod regions */ static PGXPValue s_scratch[PGXP_SCRATCH_WORDS]; static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */ static PGXPValue s_gte[32]; /* GTE data registers */ @@ -137,7 +146,7 @@ extern "C" void pgxp_invalidate_all(void) { if (++s_gen == 0) { ck_wrapped(); /* generation wrapped: physically clear so stale slots can't revive */ - if (s_ram) std::memset(s_ram, 0, PGXP_RAM_WORDS * sizeof(PGXPValue)); + if (s_ram) std::memset(s_ram, 0, PGXP_SHADOW_WORDS * sizeof(PGXPValue)); std::memset(s_scratch, 0, sizeof(s_scratch)); std::memset(s_gpr, 0, sizeof(s_gpr)); std::memset(s_gte, 0, sizeof(s_gte)); @@ -155,7 +164,7 @@ extern "C" void pgxp_set_enabled(int enabled) { s_store_ring = (PGXPStoreRecord *)std::calloc(PGXP_STORE_RING_CAP, sizeof(PGXPStoreRecord)); if (enabled && !s_ram) { - s_ram = (PGXPValue *)std::calloc(PGXP_RAM_WORDS, sizeof(PGXPValue)); + s_ram = (PGXPValue *)std::calloc(PGXP_SHADOW_WORDS, sizeof(PGXPValue)); if (s_ram) resized = 1; else @@ -315,11 +324,19 @@ extern "C" uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap) /* Address mapping + validation */ /* ------------------------------------------------------------------------- */ -/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). */ +/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). + * Unallocated mod-region addresses map too: nothing stores there, and every + * consumer validates the shadow against the actual guest word. */ static inline PGXPValue *pgxp_ptr(uint32_t addr) { uint32_t m = addr & 0x1FFFFFFFu; if (m < PSX_MAIN_RAM_WINDOW_BYTES) /* RAM + its mirrors (live map) */ return s_ram ? &s_ram[psx_ram_canonical_offset(m) >> 2] : nullptr; + if (m - PSX_MOD_GPU_DMA_APERTURE_BASE < PSX_MOD_GPU_DMA_APERTURE_SIZE) + return s_ram ? &s_ram[PGXP_RAM_WORDS + + ((m - PSX_MOD_GPU_DMA_APERTURE_BASE) >> 2)] : nullptr; + if (m - PSX_MOD_MEMORY_BASE < PSX_MOD_MEMORY_SIZE) + return s_ram ? &s_ram[PGXP_RAM_WORDS + PGXP_APERTURE_WORDS + + ((m - PSX_MOD_MEMORY_BASE) >> 2)] : nullptr; if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */ return &s_scratch[(m & 0x3FCu) >> 2]; return nullptr; @@ -356,7 +373,7 @@ static int s_ck_deferred = 0; static inline size_t ck_index(const PGXPValue *pv) { if (pv >= s_scratch && pv < s_scratch + PGXP_SCRATCH_WORDS) - return (size_t)PGXP_RAM_WORDS + (size_t)(pv - s_scratch); + return (size_t)PGXP_SHADOW_WORDS + (size_t)(pv - s_scratch); return (size_t)(pv - s_ram); } @@ -401,7 +418,7 @@ extern "C" void pgxp_checkpoint_begin(void) { if (s_ck_depth++ != 0) return; /* the outermost pass journals */ if (!s_ck_bits) { s_ck_bits = (uint8_t *)std::calloc( - ((size_t)PGXP_RAM_WORDS + PGXP_SCRATCH_WORDS + 7u) / 8u, 1); + ((size_t)PGXP_SHADOW_WORDS + PGXP_SCRATCH_WORDS + 7u) / 8u, 1); if (!s_ck_bits) s_ck_lossy = 1; } s_ck_ram = s_ram; @@ -1665,17 +1682,26 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word, if (why == 1) { if (count) s_stats.trunc_reject++; have = 0; + pz = 0; } else if (why == 2) { + /* The seam clamp (G1.10) keeps the native position, but the + * shadow still matched this exact word and its integers: its + * GTE depth describes this vertex. Depth testing needs only + * that, not the sub-pixel position. */ if (count) s_stats.tolerance_reject++; + if (have != PGXP_SRC_DATAFLOW) pz = 0; have = 0; } + } else { + pz = 0; } if (!have) { if (count) s_stats.native++; + if (count && pz != 0) s_stats.depth_only++; *x16 = int_x << 16; *y16 = int_y << 16; - *sz = 0; + *sz = pz; return PGXP_SRC_NATIVE; } diff --git a/runtime/src/spu.c b/runtime/src/spu.c index 0e3c0340..5239bad0 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -52,6 +52,18 @@ static uint32_t endx_latch; static uint32_t kon_latch; static uint32_t koff_latch; +/* Guest-clocked SPU state (see spu_catch_up). */ +#define SPU_CYCLES_PER_SAMPLE 768u +#define SPU_OUT_FRAMES 16384u +static int16_t s_out[SPU_OUT_FRAMES * 2u]; +static uint32_t s_out_head, s_out_count; +static uint64_t s_out_drops; +static uint64_t s_clock; /* guest cycle the SPU state is current to */ +static uint32_t s_clock_carry; /* snapshot-restored phase, applied lazily */ +static int s_clock_valid; +static int s_catching_up; +static int (*s_output_suppressed)(void); + /* ---- SPU IRQ (I_STAT bit 9) ------------------------------------------- */ /* Register 0x1F801DA4 holds the IRQ address in 8-byte units (value << 3 = * byte address). While SPUCNT bit 6 is set, ANY SPU RAM access touching @@ -419,6 +431,7 @@ void spu_cd_audio_reset(void) { } void spu_cd_audio_push(const int16_t* stereo, int frames) { + spu_catch_up(); /* samples owed so far consume the CD data queued before this push */ if (!stereo || frames <= 0) return; uint32_t in_frames = (uint32_t)frames; @@ -981,9 +994,12 @@ void spu_init(void) { s_shadow_tap_on = 0; s_shadow_tap_frame = 0; spu_shadow_reset(); + s_clock_valid = 0; + s_clock_carry = 0; + spu_output_flush(); } -void spu_render(int16_t* out_stereo, int frames) { +static void spu_render_block(int16_t* out_stereo, int frames) { if (!out_stereo || frames <= 0) return; uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)]; @@ -1308,10 +1324,81 @@ void spu_render(int16_t* out_stereo, int frames) { /* T1 tap: the SPU's final output block as handed to the host layer * (post-shadow, pre host fade/mute). Placed here so every spu_render - * caller — the vblank pump and the turbo fade tail — is covered. */ + * block spu_catch_up renders is covered. */ audio_trace_pcm(AUDIO_TAP_SPU_OUT, out_stereo, frames); } +/* ---- Guest-clocked SPU (beads-eio.3.321) ------------------------------ + * The SPU runs on the guest clock: 768 CPU cycles per 44.1 kHz sample. + * spu_catch_up() renders every whole sample owed up to the current guest + * cycle before any guest-visible SPU access (register read/write, DMA, CD + * input push) and before a snapshot, so what the guest reads (ENVX, ENDX, + * capture position, IRQ flag) reflects every sample produced by now. A + * KEYON's envelope therefore starts moving at the next sample, not at the + * next host audio pump. Rendered output queues in s_out until the host + * pump drains it; host-side drops never change SPU state. */ + +void spu_set_output_gate(int (*suppressed)(void)) { s_output_suppressed = suppressed; } + +static void spu_out_push(const int16_t *src, uint32_t frames) { + for (uint32_t i = 0; i < frames; i++) { + if (s_out_count == SPU_OUT_FRAMES) { /* host fell behind: drop oldest */ + s_out_head = (s_out_head + 1u) % SPU_OUT_FRAMES; + s_out_count--; + s_out_drops++; + } + const uint32_t at = (s_out_head + s_out_count) % SPU_OUT_FRAMES; + s_out[at * 2u] = src[i * 2u]; + s_out[at * 2u + 1u] = src[i * 2u + 1u]; + s_out_count++; + } +} + +void spu_catch_up(void) { + const uint64_t now = psx_get_cycle_count(); + if (!s_clock_valid) { + s_clock = now >= s_clock_carry ? now - s_clock_carry : now; + s_clock_carry = 0; + s_clock_valid = 1; + } + if (now < s_clock) { s_clock = now; return; } /* guest clock rewound */ + uint64_t frames = (now - s_clock) / SPU_CYCLES_PER_SAMPLE; + if (!frames || s_catching_up) return; + s_catching_up = 1; + s_clock += frames * SPU_CYCLES_PER_SAMPLE; + const int keep = !(s_output_suppressed && s_output_suppressed()); + static int16_t block[2048 * 2]; + while (frames) { + const int n = frames > 2048u ? 2048 : (int)frames; + spu_render_block(block, n); + if (keep) spu_out_push(block, (uint32_t)n); + frames -= (uint64_t)n; + } + s_catching_up = 0; +} + +uint32_t spu_output_available(void) { return s_out_count; } + +uint32_t spu_output_pop(int16_t *out_stereo, uint32_t max_frames) { + uint32_t n = s_out_count < max_frames ? s_out_count : max_frames; + for (uint32_t i = 0; i < n; i++) { + out_stereo[i * 2u] = s_out[s_out_head * 2u]; + out_stereo[i * 2u + 1u] = s_out[s_out_head * 2u + 1u]; + s_out_head = (s_out_head + 1u) % SPU_OUT_FRAMES; + } + s_out_count -= n; + return n; +} + +void spu_output_drop(uint32_t frames) { + if (frames > s_out_count) frames = s_out_count; + s_out_head = (s_out_head + frames) % SPU_OUT_FRAMES; + s_out_count -= frames; +} + +void spu_output_flush(void) { s_out_head = 0; s_out_count = 0; } +uint64_t spu_output_overflow_drops(void) { return s_out_drops; } + void spu_debug_info(SpuDebugInfo* out) { if (!out) return; memset(out, 0, sizeof(*out)); @@ -1339,6 +1426,7 @@ void spu_debug_info(SpuDebugInfo* out) { } uint32_t spu_read(uint32_t addr) { + spu_catch_up(); if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) { uint32_t idx = reg_index(addr); if (idx < SPU_REG_COUNT) { @@ -1428,6 +1516,7 @@ uint32_t spu_read(uint32_t addr) { } void spu_write(uint32_t addr, uint32_t value) { + spu_catch_up(); if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) { uint32_t idx = reg_index(addr); if (idx < SPU_REG_COUNT) { @@ -1534,6 +1623,7 @@ void spu_write(uint32_t addr, uint32_t value) { } void spu_dma_write(uint32_t word) { + spu_catch_up(); spu_irq_check(transfer_addr, 4u); if (transfer_addr + 3 < SPU_RAM_SIZE) { spu_ram[transfer_addr] = (uint8_t)(word & 0xFF); @@ -1549,6 +1639,7 @@ void spu_dma_write(uint32_t word) { * exact mirror of spu_dma_write. Titles carry state through SPU RAM across * EXE transitions via this path; returning zeros breaks them. */ uint32_t spu_dma_read(void) { + spu_catch_up(); uint32_t word = 0; spu_irq_check(transfer_addr, 4u); if (transfer_addr + 3 < SPU_RAM_SIZE) { @@ -1726,12 +1817,19 @@ static int spu_r_voice(PstR *r, int idx) { #define SPU_SNAPSHOT_TAIL_BYTES \ (20u + 1u + 2u + 4u + 4u + 1u + 4u + 4u + 4u + 4u + 4u + 2u * (2u + 4u)) -uint32_t spu_snapshot_bytes(void) { +/* + 4: guest-clock phase (cycles since the SPU's last whole sample). States + * written before the guest-clocked SPU lack it and still load (phase 0). */ +static uint32_t spu_snapshot_bytes_v1(void) { return (uint32_t)(SPU_REG_COUNT * 2u) + (SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES) + SPU_SNAPSHOT_TAIL_BYTES; } +uint32_t spu_snapshot_bytes(void) { return spu_snapshot_bytes_v1() + 4u; } +int spu_snapshot_len_ok(uint32_t len) { + return len == spu_snapshot_bytes() || len == spu_snapshot_bytes_v1(); +} void spu_snapshot_write(uint8_t *p) { + spu_catch_up(); PstW w; uint32_t n = spu_snapshot_bytes(); pst_w_init(&w, p, n); @@ -1758,11 +1856,12 @@ void spu_snapshot_write(uint8_t *p) { pst_w_i16(&w, sweep_main_env[ch].level); pst_w_u32(&w, sweep_main_env[ch].divider); } + pst_w_u32(&w, (uint32_t)(psx_get_cycle_count() - s_clock)); } int spu_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; - if (len != spu_snapshot_bytes()) return 0; + if (!spu_snapshot_len_ok(len)) return 0; pst_r_init(&r, p, len); for (uint32_t i = 0; i < SPU_REG_COUNT; i++) if (!pst_r_u16(&r, &spu_regs[i])) return 0; @@ -1784,6 +1883,12 @@ int spu_snapshot_read(const uint8_t *p, uint32_t len) { !pst_r_u32(&r, &sweep_main_env[ch].divider)) return 0; } + uint32_t phase = 0; + if (len == spu_snapshot_bytes() && !pst_r_u32(&r, &phase)) return 0; + /* Applied at the next catch-up, once the guest clock is restored too. */ + s_clock_carry = phase % SPU_CYCLES_PER_SAMPLE; + s_clock_valid = 0; + spu_output_flush(); return 1; } uint8_t* spu_get_ram_ptr(void){ return spu_ram; } diff --git a/runtime/tests/test_gl_frame_gen.c b/runtime/tests/test_gl_frame_gen.c index 193f010c..841ace6e 100644 --- a/runtime/tests/test_gl_frame_gen.c +++ b/runtime/tests/test_gl_frame_gen.c @@ -36,6 +36,9 @@ static void fixture_window(int slot); #include "gpu_gl_renderer.c" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" +#include "gl_batch_policy_lle.c" +#include "gl_batch_policy_hle.c" +#include "gl_batch_policy_select.c" /* gpu_render.c (the facade) observes draws for HD texture packs: no pack here. */ int gpu_hd_textures_active(void){return 0;} int gpu_hd_textures_dump_enabled(void){return 0;} diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index e4da089b..b4dcf632 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -2,6 +2,13 @@ #include "gpu_gl_renderer.c" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" +#include "gl_batch_policy_lle.c" +#include "gl_batch_policy_hle.c" +/* Both textured-batch tiers in one binary: the fixture selects per scene. */ +static int test_batch_hle; +GlBatchClass gl_batch_classify(int semi,int mask_check,int bank_batch){ + return test_batch_hle?gl_batch_policy_hle(semi,mask_check,bank_batch):gl_batch_policy_lle(semi,mask_check,bank_batch);} +const char *gl_batch_policy_name(void){return test_batch_hle?"HLE":"LLE";} #include "gpu_timeline.c" uint64_t psx_cycle_count=0; static int test_full_composite; @@ -74,6 +81,98 @@ static void verify_bank_batching(void) { psx_mod_set_texture_bank_batching(0);s_tex_filter=0; glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0); } +/* HLE textured batching must reproduce the LLE pixels. One scene, drawn by + * each tier from the same VRAM: overlapping painter-ordered runs of opaque + * prims and every blend mode, 16-bit and 4-bit CLUT pages, raw and modulated + * texels with STP=0/STP=1/transparent words, every mask-set/mask-check and + * filter combination, and prims that sample a page an earlier queued prim + * wrote. Each pass first resets every VRAM word the scene reads; every + * hr-surface pixel of the scene and the pages is compared. */ +enum { TB_X=96, TB_Y=288, TB_W=192, TB_H=160 }; +static uint16_t tb_page16[128*64], tb_page4[64*64], tb_clut[16]; +static void tier_batch_scene(int filter,int mask_set,int mask_check){ + glb_vram_transfer_in(512,0,128,64,tb_page16); + glb_vram_transfer_in(640,0,64,64,tb_page4); + glb_vram_transfer_in(768,0,16,1,tb_clut); + glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0); + glb_fill_rect(TB_X,TB_Y,TB_W,TB_H,0x2108); + glb_fill_rect(512,64,128,64,0x1084); + /* A mask-set strip so mask-check scenes have protected pixels to keep. */ + glb_set_mask_bits(1,0);glb_draw_flat_rect(TB_X+40,TB_Y+30,90,20,0x3def); + s_tex_filter=filter;glb_set_mask_bits(mask_set,mask_check); + for(int i=0;i<60;++i){ + const int semi=(i%5)-1, x=TB_X+(i*29)%150, y=TB_Y+(i*17)%120; + glb_set_semi_transparency(semi>=0,semi>=0?semi:0); + const int four=(i/6)&1, raw=(i/12)&1; + const uint16_t tp=four?(uint16_t)0x00A:(uint16_t)0x108; + const uint32_t c=raw?0x808080:(i&1?0x60a0c0:0xc08040); + glb_draw_shaded_textured_triangle(x,y,(i*7)&31,(i*5)&31,c, + x+40,y+3,((i*7)&31)+40,(i*5)&31,0x808080,x+5,y+38,(i*7)&31,((i*5)&31)+30,c, + four?768:0,0,tp,raw); + } + /* Feedback: draw into the 16-bit page, then sample what was just drawn. */ + for(int i=0;i<6;++i){ + glb_set_semi_transparency(i&1,i%4); + glb_draw_shaded_textured_triangle(514+i*8,66,0,0,0x808080, + 540+i*8,66,26,0,0x808080,514+i*8,100,0,34,0x808080,0,0,0x108,1); + glb_draw_shaded_textured_triangle(TB_X+10+i*25,TB_Y+120,2+i*8,66,0x808080, + TB_X+40+i*25,TB_Y+122,30+i*8,66,0x808080,TB_X+12+i*25,TB_Y+156,2+i*8,98,0x808080,0,0,0x108,1); + } + flush_flat_batch();flush_tex_batch(); + glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0);s_tex_filter=0; +} +static void hr_read(int x,int y,int w,int h,uint32_t *out){ + flush_flat_batch();flush_tex_batch();hiw_flush_queue(); + const int S=s_hr_scale; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,s_hr_fbo); + glPixelStorei(GL_PACK_ALIGNMENT,4); + glReadPixels(x*S,y*S,w*S,h*S,GL_RGBA,GL_UNSIGNED_BYTE,out); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0); +} +static void verify_tier_batching(void){ + for(int i=0;i<128*64;++i){ + const int x=i&127,y=i>>7; + uint16_t v=(uint16_t)(((x*3)&31)|(((y*5)&31)<<5)|(((x^y)&31)<<10)); + if(((x>>2)^(y>>2))&1)v|=0x8000; /* STP=1 blocks */ + if((x+y)%9==0)v=0; /* transparent */ + tb_page16[i]=v; + if(i<64*64)tb_page4[i]=(uint16_t)(i*0x1357u); + } + for(int i=0;i<16;++i)tb_clut[i]=(uint16_t)(i==0?0:((i*0x0843u)&0x7fff)|(i&4?0x8000:0)); + const int S=s_hr_scale; + const size_t scene=(size_t)TB_W*TB_H*S*S, page=(size_t)128*128*S*S; + uint32_t *ref=malloc((scene+page)*4),*got=malloc((scene+page)*4); + if(!ref||!got){check(0,"tier batching alloc");free(ref);free(got);return;} + for(int filter=0;filter<2;++filter)for(int mset=0;mset<2;++mset)for(int mcheck=0;mcheck<2;++mcheck){ + int batches[2]; + for(test_batch_hle=0;test_batch_hle<2;++test_batch_hle){ + const int before=s_cw_batches; + tier_batch_scene(filter,mset,mcheck); + batches[test_batch_hle]=s_cw_batches-before; + uint32_t *dst=test_batch_hle?got:ref; + hr_read(TB_X,TB_Y,TB_W,TB_H,dst);hr_read(512,0,128,128,dst+scene); + } + test_batch_hle=0; + size_t n=0; + for(size_t i=0;i=scene;const size_t j=pg?i-scene:i;const int w=(pg?128:TB_W)*S; + fprintf(stderr," diff at native (%d,%d) sub (%d,%d): LLE %08x HLE %08x\n", + (pg?512:TB_X)+(int)(j%w)/S,(pg?0:TB_Y)+(int)(j/w)/S,(int)(j%w)%S,(int)(j/w)%S,ref[i],got[i]); + } + } + char label[96]; + snprintf(label,sizeof label,"HLE batching pixel equivalence filter=%d mask_set=%d mask_check=%d",filter,mset,mcheck); + if(n)fprintf(stderr,"%s: %zu hr pixels differ\n",label,n); + check(n==0,label); + snprintf(label,sizeof label,"HLE batch reduction filter=%d mask_set=%d mask_check=%d (%d vs %d)",filter,mset,mcheck,batches[1],batches[0]); + check(mcheck?batches[1]<=batches[0]:batches[1]*2 position cache, never a depth --- */ g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16; { diff --git a/runtime/tests/test_spu_fidelity.c b/runtime/tests/test_spu_fidelity.c index beedfed6..a0d1069e 100644 --- a/runtime/tests/test_spu_fidelity.c +++ b/runtime/tests/test_spu_fidelity.c @@ -73,7 +73,7 @@ static int16_t g_render_buf[4096 * 2]; static void render_n(int frames) { while (frames > 0) { int n = frames > 4096 ? 4096 : frames; - spu_render(g_render_buf, n); + spu_render_block(g_render_buf, n); frames -= n; } } @@ -308,7 +308,7 @@ static void test_reverb_write_gating_and_cd_path(void) { /* Reverb tail: CD input stops; the wet path must keep the output alive * (this is the reverb-on-CD-only / FMV behaviour in one assert). */ memset(g_render_buf, 0, sizeof(g_render_buf)); - spu_render(g_render_buf, 64); + spu_render_block(g_render_buf, 64); CHECK(buf_has_nonzero(g_render_buf, 64 * 2)); } @@ -375,7 +375,7 @@ static void test_noise_generator(void) { setup_voice(0, 0x0600, 0x3FFF, 0x3FFF); wr(R_KON_LO, 0x0001); memset(g_render_buf, 0, sizeof(g_render_buf)); - spu_render(g_render_buf, 512); + spu_render_block(g_render_buf, 512); CHECK(!buf_has_nonzero(g_render_buf, 512 * 2)); /* Noise mode: same setup + NON bit -> non-silent, non-constant output. */ @@ -387,7 +387,7 @@ static void test_noise_generator(void) { wr(R_NON_LO, 0x0001); wr(R_KON_LO, 0x0001); memset(g_render_buf, 0, sizeof(g_render_buf)); - spu_render(g_render_buf, 512); + spu_render_block(g_render_buf, 512); CHECK(buf_has_nonzero(g_render_buf, 512 * 2)); /* Count distinct left-channel values: the LFSR must keep moving. */ @@ -425,7 +425,7 @@ static void test_direct_volume_decode(void) { wr(R_MAINVOLL, 0x3FFF); wr(R_MAINVOLR, 0x3FFF); push_cd_const(1, 0x1000, 0x1000); - spu_render(g_render_buf, 1); + spu_render_block(g_render_buf, 1); CHECK(g_render_buf[0] == 4094); CHECK(g_render_buf[1] == 4094); } @@ -533,7 +533,7 @@ static void test_savestate_roundtrip(void) { /* Timeline A. */ static int16_t out_a[256 * 2], out_b[256 * 2]; - spu_render(out_a, 256); + spu_render_block(out_a, 256); CHECK(buf_has_nonzero(out_a, 256 * 2)); /* noise + reverb tail audible */ /* State moved on... */ @@ -553,7 +553,7 @@ static void test_savestate_roundtrip(void) { CHECK(sweep_voice_env[6][0].divider == sweep_div_0); CHECK(rev_out_l == rev_out_l_0); - spu_render(out_b, 256); + spu_render_block(out_b, 256); CHECK(memcmp(out_a, out_b, sizeof(out_a)) == 0); /* A truncated blob is rejected. */ diff --git a/runtime/tests/test_spu_guest_clock.c b/runtime/tests/test_spu_guest_clock.c new file mode 100644 index 00000000..cf6eb6ff --- /dev/null +++ b/runtime/tests/test_spu_guest_clock.c @@ -0,0 +1,132 @@ +/* + * Guest-clocked SPU (beads-eio.3.321): every SPU access advances the SPU to + * the current guest cycle, so a game that keys a voice on and then polls its + * envelope (ENVX) sees it rise without any host audio pump. Crash Bandicoot's + * AudioUpdate keys a voice off when it reads "key on, envelope 0"; with the + * SPU advanced only at the per-frame pump, its spin sound was cut the frame + * it started. + * + * The test includes spu.c so it can drive the guest clock directly. + * Build/run: ctest -R spu_guest_clock_test + */ + +#include +#include +#include + +#include "../src/spu.c" + +uint64_t s_frame_count; +static uint64_t g_cycle; +uint64_t psx_get_cycle_count(void) { return g_cycle; } +void audio_trace_pcm(int tap, const int16_t *stereo, int frames) { (void)tap; (void)stereo; (void)frames; } +void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) { (void)kind; (void)a; (void)b; } +void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; } +uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t len) { (void)data; (void)len; return crc; } +bool spu_shadow_enabled(void) { return false; } +void spu_shadow_reset(void) {} +void spu_shadow_process(int16_t *canon, int frames) { (void)canon; (void)frames; } + +static int failures; +#define CHECK(condition, message) \ + do { \ + if (!(condition)) { fprintf(stderr, "FAIL: %s\n", message); failures++; } \ + else printf("ok: %s\n", message); \ + } while (0) + +#define VOICE0(off) (0x1F801C00u + (off)) +static int s_suppress; +static int suppressed(void) { return s_suppress; } + +static void stage_voice0(void) { + spu_init(); + g_cycle = 0; + for (uint32_t b = 0; b < 8u; b++) { /* nonzero ADPCM, last block loops */ + uint8_t *blk = spu_ram + 0x1000u + b * 16u; + blk[0] = 0x00; + blk[1] = b == 7u ? 0x03 : 0x00; + memset(blk + 2, 0x77, 14); + } + spu_write(0x1F801DAAu, 0x8000u); /* SPU on */ + spu_write(VOICE0(0x0), 0x3FFFu); /* volume L/R */ + spu_write(VOICE0(0x2), 0x3FFFu); + spu_write(VOICE0(0x4), 0x1000u); /* pitch 1.0 */ + spu_write(VOICE0(0x6), 0x1000u >> 3); /* start address */ + spu_write(VOICE0(0x8), 0x000Fu); /* fastest linear attack, SL max */ + spu_write(VOICE0(0xA), 0x0000u); + spu_write(VOICE0(0xE), 0x1070u >> 3); /* repeat address */ +} + +static void test_envelope_rises_between_accesses(void) { + stage_voice0(); + g_cycle = 768u * 10u + 100u; + spu_write(0x1F801D88u, 0x0001u); /* KEYON voice 0 */ + CHECK(spu_output_available() == 10u, "the write first renders the 10 samples owed"); + CHECK(spu_read(VOICE0(0xC)) == 0u, "ENVX is 0 within the KEYON sample"); + g_cycle += 768u * 3u; + const uint32_t env = spu_read(VOICE0(0xC)); + CHECK(env > 0u, "ENVX rises three samples after KEYON with no host pump"); + CHECK(spu_output_available() == 13u, "the read rendered exactly the 3 owed samples"); + int16_t out[16 * 2]; + CHECK(spu_output_pop(out, 16u) == 13u && spu_output_available() == 0u, + "the host pump drains what the guest clock produced"); +} + +static void test_snapshot_keeps_clock_phase(void) { + stage_voice0(); + g_cycle = 768u * 20u + 400u; + spu_catch_up(); + const uint32_t n = spu_snapshot_bytes(); + uint8_t *snap = (uint8_t *)malloc(n); + spu_snapshot_write(snap); + CHECK(spu_snapshot_len_ok(n) && spu_snapshot_len_ok(n - 4u) && !spu_snapshot_len_ok(n - 1u), + "current and pre-clock snapshot lengths validate, others do not"); + + g_cycle = 768u * 5000u + 400u; /* restored guest clock, same phase */ + CHECK(spu_snapshot_read(snap, n) == 1, "snapshot with clock phase loads"); + spu_catch_up(); + CHECK(spu_output_available() == 0u, "no samples owed right after restore"); + g_cycle += 367u; + spu_catch_up(); + CHECK(spu_output_available() == 0u, "one cycle short of the next sample boundary"); + g_cycle += 1u; + spu_catch_up(); + CHECK(spu_output_available() == 1u, "phase carried: next sample lands on the boundary"); + + g_cycle = 768u * 9000u + 123u; + CHECK(spu_snapshot_read(snap, n - 4u) == 1, "pre-clock snapshot loads (phase 0)"); + spu_catch_up(); + g_cycle += 767u; + spu_catch_up(); + CHECK(spu_output_available() == 0u, "pre-clock snapshot anchors the clock at load"); + free(snap); +} + +static void test_gate_and_rewind(void) { + stage_voice0(); + spu_set_output_gate(suppressed); + g_cycle = 768u * 4u; + spu_write(0x1F801D88u, 0x0001u); + spu_output_flush(); + s_suppress = 1; + g_cycle += 768u * 6u; + const uint32_t env = spu_read(VOICE0(0xC)); + CHECK(env > 0u && spu_output_available() == 0u, + "suppressed output still advances SPU state"); + s_suppress = 0; + spu_set_output_gate(NULL); + const uint64_t clock = s_clock; + g_cycle = 768u * 2u; /* guest clock rewound (state load) */ + spu_catch_up(); + CHECK(s_clock == g_cycle && s_clock < clock && spu_output_available() == 0u, + "a rewound guest clock re-anchors without rendering"); +} + +int main(void) { + test_envelope_rises_between_accesses(); + test_snapshot_keeps_clock_phase(); + test_gate_and_rewind(); + if (failures) { fprintf(stderr, "%d failure(s)\n", failures); return 1; } + puts("spu_guest_clock_test: ok"); + return 0; +}