mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge remote-tracking branch 'origin/master' into feat/uncharged-call-counted-cycles
# Conflicts: # docs/TCP_COMMANDS.md
This commit is contained in:
@@ -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 `<path>/g<frame>_<index>_a<phase q16>.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":"<name>"}` 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":"<name>"}` 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` | ✓ | | |
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<double>
|
||||
* 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<double>(v.as_integer()) : v.as_floating();
|
||||
}
|
||||
|
||||
static uint32_t parse_hex(const std::string& s, const std::string& field) {
|
||||
try {
|
||||
return static_cast<uint32_t>(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<double>(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<bool>(video, "pgxp_color_correction");
|
||||
if (video.contains("pgxp_depth_threshold"))
|
||||
rt.video_pgxp_depth_threshold = toml::find<double>(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<std::string>(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<bool>(video, "scanlines");
|
||||
}
|
||||
if (video.contains("scanline_strength")) {
|
||||
double s = toml::find<double>(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<bool>(v, "scanlines"); s.has_scanlines = true;
|
||||
});
|
||||
if (v.contains("scanline_strength")) try_get([&]{
|
||||
double d = toml::find<double>(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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
+14
-1
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
+60
-12
@@ -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++;
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
+11
-4
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+35
-41
@@ -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. */
|
||||
|
||||
@@ -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];
|
||||
|
||||
+35
-9
@@ -36,6 +36,7 @@
|
||||
#include "pgxp_hooks.h"
|
||||
#include "cpu_state.h"
|
||||
#include "psx_memory.h"
|
||||
#include "mod_memory.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+109
-4
@@ -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; }
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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+page;++i){
|
||||
if(ref[i]==got[i])continue;
|
||||
if(n++<8){
|
||||
const int pg=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<batches[0],label);
|
||||
printf("tier batches filter=%d mask_set=%d mask_check=%d: LLE %d HLE %d\n",filter,mset,mcheck,batches[0],batches[1]);
|
||||
}
|
||||
free(ref);free(got);
|
||||
verify("tier batching leaves CPU VRAM coherent");
|
||||
}
|
||||
static void verify_stereo_transactions(void) {
|
||||
uint16_t before[16*16], after[16*16];
|
||||
const int32_t views[2][3]={{-24,0,0},{24,0,0}};
|
||||
@@ -495,6 +594,7 @@ int main(int argc,char **argv){
|
||||
check(glb_vram_read(482,252)==0x001f,"palette update visible without replacing indices");
|
||||
verify("retained indices with animated guest CLUT");
|
||||
verify_bank_batching();
|
||||
verify_tier_batching();
|
||||
verify_stereo_transactions();
|
||||
verify_oversize_wide_geometry(scale);
|
||||
verify_camera_plane_clip(scale);
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
#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;}
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
#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"
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
int g_psx_vram_dirty_tracking=0;
|
||||
|
||||
@@ -429,6 +429,34 @@ int main(void) {
|
||||
PGXP_SRC_DATAFLOW);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
|
||||
/* --- the clamp keeps the native position but the validated depth --- */
|
||||
{
|
||||
int32_t x, y; uint16_t z = 0;
|
||||
pgxp_set_tolerance(0.25f);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_NATIVE);
|
||||
CHECK(x == (160 << 16) && y == (80 << 16) && z == SZ3);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
/* a truncation reject (CPU-modified integer) carries no depth */
|
||||
z = 1;
|
||||
CHECK(lookup(ADDR_A, PACKED, 161, 80, &x, &y, &z) == PGXP_SRC_NATIVE);
|
||||
CHECK(z == 0);
|
||||
}
|
||||
|
||||
/* --- trusted-mod guest regions are tracked like RAM --- */
|
||||
{
|
||||
static const uint32_t APERTURE = 0x809D0004u; /* GPU DMA aperture */
|
||||
static const uint32_t MODMEM = 0x9F000040u; /* Expansion 1 memory */
|
||||
int32_t x, y; uint16_t z;
|
||||
produce_at(APERTURE);
|
||||
CHECK(lookup(APERTURE & 0x00FFFFFFu, PACKED, 160, 80, &x, &y, &z) ==
|
||||
PGXP_SRC_DATAFLOW); /* 24-bit DMA tag form */
|
||||
CHECK(x == X16 && y == Y16 && z == SZ3);
|
||||
CHECK(lookup(0x801D0004u, PACKED, 160, 80, nullptr, nullptr, nullptr) !=
|
||||
PGXP_SRC_DATAFLOW); /* never aliases its RAM mirror */
|
||||
produce_at(MODMEM);
|
||||
CHECK(lookup(MODMEM, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
}
|
||||
|
||||
/* --- fallback tier: no address -> position cache, never a depth --- */
|
||||
g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16;
|
||||
{
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user