mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge feat/crashbash-fw-fixes: GTE lm/sf depth-cue/lighting fix + dispatch shell-window + VBlank pacing
GTE honors per-instruction lm (bit 10) and sf (bit 19) in the depth-cue and lighting family (INTPL/DPCS/DPCT/DPCL/NCDS/NCDT/CDP/...) with the faithful hardware formula and always-on GTE observability rings (gte_ring_dump, gte_intpl_dump, gte_frame_stats, gte_latch_dump). Fixes Crash Bash menu-character strobe/flicker and in-game flattening (signed INTPL vertex tweens were clamped to zero by the hardcoded lm=1). Also: dispatch no longer shadows runtime-relocated game code in the BIOS shell window (native-ok byte compare routes matching text to the game path); stale GPUSTAT-poll VBlank over-delivery fix + present pacing tooling. Cross-title validation (GTE_LM_FIX.md): Crash Bash (fix target), Ape Escape, Tomba 2, Mega Man X6 - all rebuilt branch-flavor (regen included) and verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# GTE `lm`/`sf` fix in the depth-cue & lighting family
|
||||
|
||||
**Branch:** `feat/crashbash-fw-fixes` — commit `c4474b9`
|
||||
**Fixes:** Crash Bash (SCUS-94570) menu-character 30 Hz strobe + in-game
|
||||
character flattening. Root-caused and verified 2026-07-09.
|
||||
|
||||
## What was wrong
|
||||
|
||||
The runtime GTE hardcoded `lm = 1` (clamp IR results to `[0, 0x7FFF]`) on
|
||||
every IR write-back in the shared lighting/depth-cue helpers
|
||||
(`light_transform`, `light_color`, `color_output`, `depth_cue_from_ir`).
|
||||
Real hardware takes `lm` from **bit 10 of the instruction word** — `lm = 0`
|
||||
clamps to `[-0x8000, 0x7FFF]` and preserves negative results.
|
||||
|
||||
`depth_cue_from_ir` also ignored `sf` (bit 19) and skipped the hardware's
|
||||
intermediate step clamp.
|
||||
|
||||
Affected ops: `INTPL`, `DPCS`, `DPCT`, `DPCL`, `NCDS`, `NCDT`, `NCS`, `NCT`,
|
||||
`NCCS`, `NCCT`, `CDP`, `CC`. (`RTPS`/`RTPT`/`MVMVA`/`OP`/`SQR`/`GPF`/`GPL`
|
||||
already honored the instruction word and are untouched.)
|
||||
|
||||
## Why it broke Crash Bash
|
||||
|
||||
Crash Bash tweens its 10 fps keyframe vertex animation to the 30 fps render
|
||||
rate through **INTPL with `lm = 0`** — an off-label, signed-vector use of the
|
||||
color-interpolation op:
|
||||
|
||||
- pose-A vertex → IR1–3
|
||||
- pose-B vertex → far-color regs (FC) via `ctc2`
|
||||
- blend factor → IR0
|
||||
- `INTPL` (`0x0980011`) → lerped vertex read back from IR1–3 (`mfc2`)
|
||||
|
||||
With `lm` forced on, every **negative component** of a tweened vertex clamped
|
||||
to 0, collapsing the character mesh onto its anchor point on tween frames.
|
||||
Keyframe frames (no INTPL) stayed healthy, so with alternating draw buffers
|
||||
exactly one display buffer ever held a healthy pose → the character strobed
|
||||
at 30 Hz on menus and rendered flattened/half-visible in-game. Beetle and
|
||||
real hardware honor per-instruction `lm`, so they never showed it.
|
||||
|
||||
## What changed
|
||||
|
||||
- `light_transform` / `light_color` / `color_output`: `lm` read from the
|
||||
instruction word and passed to every `saturate_ir` write-back.
|
||||
- `depth_cue_from_ir`: rewritten to the faithful hardware formula —
|
||||
`base = IR << 12`; `step = lim±0x8000(((FC << 12) − base) >> sf·12)`
|
||||
(intermediate clamp with `lm` forced OFF, per hardware); `MAC = (base +
|
||||
IR0·step) >> sf·12`; final `IR = lim(MAC, lm)` with `lm` from the
|
||||
instruction. MAC overflow flags checked.
|
||||
- Always-on GTE observability rings (build-dbg debug-server commands):
|
||||
`gte_ring_dump` (per-RTPS/RTPT inputs+outputs), `gte_intpl_dump`
|
||||
(per-INTPL inputs+outputs, frame filter + offset paging),
|
||||
`gte_frame_stats` (per-frame `nproj`/`nsat`/`nflat`/`nintpl`/
|
||||
`nintpl_tiny`), `gte_latch_dump` (retained saturated projections).
|
||||
|
||||
## Regression risk
|
||||
|
||||
Low, and skewed toward correctness:
|
||||
|
||||
- Standard libgte **lighting** calls encode `lm = 1`, so games using the
|
||||
ops as designed see identical clamping to before.
|
||||
- The two real behavior changes are (a) `lm = 0` callers now keep negative
|
||||
results — the bug being fixed — and (b) the depth-cue step is now clamped
|
||||
to ±0x8000 before the IR0 multiply, which only matters when
|
||||
`|FC − IR| > 0x7FFF` (far color very distant from the lit color). Both
|
||||
changes move us **toward** hardware/Beetle behavior, so the only regression
|
||||
class is a game that accidentally depended on our old inaccuracy.
|
||||
- `sf` handling: all common libgte encodings of these ops use `sf = 1`,
|
||||
which reproduces the old shift exactly.
|
||||
|
||||
## Cross-game validation — REQUIRED, all titles, before merge
|
||||
|
||||
This branch does not merge to master until every supported title has been
|
||||
validated against this change. No subset, no sampling.
|
||||
|
||||
**Validated:**
|
||||
|
||||
**Crash Bash (SCUS-94570)** — the fix target. Menu strobe gone;
|
||||
`nintpl_tiny` 335–352/473 → 0–3; character present in both VRAM
|
||||
double-buffers and in all frames of a jittered screenshot burst;
|
||||
menus/animation healthy.
|
||||
|
||||
**Pending (every one required):**
|
||||
|
||||
**Mega Man X6**, **Tomba 2**, **Ape Escape**. Ape Escape is the highest-risk
|
||||
of the three — a heavy GTE user and the most likely to exercise the
|
||||
depth-cue step clamp with distant far colors; Tomba 2 and MMX6 exercise the
|
||||
standard `lm = 1` lighting path that must be bit-identical to before.
|
||||
|
||||
**Per-title procedure (same for all):**
|
||||
|
||||
1. Boot to gameplay through the title's existing verify flow on this branch.
|
||||
2. Capture `gte_frame_stats` over a representative scene and diff
|
||||
`nsat`/`nflat` against a pre-fix baseline built from master + this
|
||||
branch's parent commit — any shift is a stop-and-investigate, not a note.
|
||||
3. Eyeball lit and fogged/depth-cued scenes side-by-side with the Beetle
|
||||
oracle; the oracle is ground truth, not the pre-fix recomp.
|
||||
@@ -1453,9 +1453,23 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " * it to the stale shell image. A genuine pre-start shell address\n";
|
||||
out += " * (game not started) still falls through to normalize(). */\n";
|
||||
out += " extern int fntrace_is_game_started(void);\n";
|
||||
out += " extern int dirty_ram_text_native_ok(uint32_t phys);\n";
|
||||
out += " int game_shell_overlap = fntrace_is_game_started() &&\n";
|
||||
out += " game_phys >= 0x00030000u && game_phys <= 0x0005AFFFu;\n";
|
||||
out += " if (!found && (game_shell_overlap ||\n";
|
||||
out += " /* Shell-window addresses the loaded game EXE image covers with\n";
|
||||
out += " * matching bytes are GAME code, not the BIOS shell \xE2\x80\x94 route them to\n";
|
||||
out += " * the game path even when the game-start latch (a dispatch to\n";
|
||||
out += " * entry_pc) never tripped. Fixes titles whose static text fills\n";
|
||||
out += " * 0x30000-0x5AFFF: Crash Bash reaches its entry 0x2E7B0 via compiled\n";
|
||||
out += " * internal flow, so `started` never latched and normalize() shadowed\n";
|
||||
out += " * real text at 0x3358C to dead shell ROM (unknown-dispatch abort).\n";
|
||||
out += " * The native-ok compare is ground truth: pre-game the RAM holds shell\n";
|
||||
out += " * bytes (differ from the game image -> not routed here -> shell\n";
|
||||
out += " * dispatches); post-load it holds the game EXE (matches -> game path). */\n";
|
||||
out += " int game_text_in_shell_window =\n";
|
||||
out += " game_phys >= 0x00030000u && game_phys <= 0x0005AFFFu &&\n";
|
||||
out += " psx_game_address_in_text(addr) && dirty_ram_text_native_ok(game_phys);\n";
|
||||
out += " if (!found && (game_shell_overlap || game_text_in_shell_window ||\n";
|
||||
out += " (psx_game_address_in_text(addr) && dirty_ram_is_dirty(game_phys)))) {\n";
|
||||
out += " found = dirty_ram_dispatch(cpu, addr, stop_addr);\n";
|
||||
out += " }\n";
|
||||
|
||||
@@ -4678,6 +4678,30 @@ static void handle_irq_state(int id, const char *json)
|
||||
dma_get_dpcr(), dma_get_dicr());
|
||||
}
|
||||
|
||||
/* vblank_rate: report the ONE cycle-paced VBlank authority's raise/deliver
|
||||
* counts, the (normally-off) GPUSTAT-poll fallback raise count, and the
|
||||
* per-frame GP0(E5) draw-offset-Y range/count. Used to confirm the guest is
|
||||
* receiving exactly 60 VBlanks/s (not the ~96/s the stale poll fallback caused)
|
||||
* and to probe double-buffer draw-offset alternation. */
|
||||
static void handle_vblank_rate(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
extern uint64_t g_vblank_raise_count, g_vblank_deliver_count;
|
||||
extern uint64_t g_pollhack_vblank_count;
|
||||
extern int32_t g_doff_min_last, g_doff_max_last;
|
||||
extern uint32_t g_doff_cnt_last;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"cycle_paced_raise\":%llu,"
|
||||
"\"delivered\":%llu,"
|
||||
"\"pollhack_raise\":%llu,"
|
||||
"\"doff_min\":%d,\"doff_max\":%d,\"doff_cnt\":%u}",
|
||||
id,
|
||||
(unsigned long long)g_vblank_raise_count,
|
||||
(unsigned long long)g_vblank_deliver_count,
|
||||
(unsigned long long)g_pollhack_vblank_count,
|
||||
g_doff_min_last, g_doff_max_last, g_doff_cnt_last);
|
||||
}
|
||||
|
||||
static void handle_timers_state(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -5404,6 +5428,94 @@ static void handle_gte_state(int id, const char *json)
|
||||
send_fmt("%s", buf);
|
||||
}
|
||||
|
||||
/* Dump recent GTE RTPS/RTPT projections (inputs + outputs) from the always-on
|
||||
* GTE ring. {"cmd":"gte_ring_dump","count":N,"newest":1,"frame":F} — frame
|
||||
* optional (omit or -1 for all). Used to find flattened/degenerate character
|
||||
* projections and split game-code input bugs from GTE-math bugs. */
|
||||
static void handle_gte_ring_dump(int id, const char *json)
|
||||
{
|
||||
extern unsigned long long gte_rtp_ring_total(void);
|
||||
extern int gte_rtp_ring_dump_json(char *out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter);
|
||||
int count = json_get_int(json, "count", 64);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 512) count = 512;
|
||||
int newest = json_get_int(json, "newest", 1) != 0;
|
||||
long frame = (long)json_get_int(json, "frame", -1);
|
||||
|
||||
size_t BUF_SZ = 256u + (size_t)count * 720u;
|
||||
char *entries = (char *)malloc(BUF_SZ);
|
||||
char *reply = (char *)malloc(BUF_SZ + 256u);
|
||||
if (!entries || !reply) { free(entries); free(reply); send_err(id, "oom"); return; }
|
||||
int n = gte_rtp_ring_dump_json(entries, (int)BUF_SZ, count, newest, frame);
|
||||
snprintf(reply, BUF_SZ + 256u,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"emitted\":%d,\"entries\":[%s]}",
|
||||
id, gte_rtp_ring_total(), n, entries);
|
||||
debug_server_send_line(reply);
|
||||
free(entries); free(reply);
|
||||
}
|
||||
|
||||
/* INTPL (vertex-lerp) ring: inputs (ir0 blend, in=IR1-3 pose A, fc=pose B)
|
||||
* and outputs (mac / out=IR1-3 / flag) per op. offset pages through the
|
||||
* matching entries after the frame filter, so a whole frame is reachable. */
|
||||
static void handle_gte_intpl_dump(int id, const char *json)
|
||||
{
|
||||
extern unsigned long long gte_intpl_ring_total(void);
|
||||
extern int gte_intpl_ring_dump_json(char *out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter,
|
||||
int offset);
|
||||
int count = json_get_int(json, "count", 64);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 512) count = 512;
|
||||
int newest = json_get_int(json, "newest", 1) != 0;
|
||||
long frame = (long)json_get_int(json, "frame", -1);
|
||||
int offset = json_get_int(json, "offset", 0);
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
size_t BUF_SZ = 256u + (size_t)count * 420u;
|
||||
char *entries = (char *)malloc(BUF_SZ);
|
||||
char *reply = (char *)malloc(BUF_SZ + 256u);
|
||||
if (!entries || !reply) { free(entries); free(reply); send_err(id, "oom"); return; }
|
||||
int n = gte_intpl_ring_dump_json(entries, (int)BUF_SZ, count, newest, frame, offset);
|
||||
snprintf(reply, BUF_SZ + 256u,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"emitted\":%d,\"offset\":%d,\"entries\":[%s]}",
|
||||
id, gte_intpl_ring_total(), n, offset, entries);
|
||||
debug_server_send_line(reply);
|
||||
free(entries); free(reply);
|
||||
}
|
||||
|
||||
/* Per-frame GTE projection stats (nproj / nsat / nflat) over recent frames —
|
||||
* shows the alternating flat/normal render pattern. */
|
||||
static void handle_gte_frame_stats(int id, const char *json)
|
||||
{
|
||||
extern int gte_fstat_dump_json(char *out, int outsz, int max_frames);
|
||||
int n = json_get_int(json, "frames", 120);
|
||||
if (n < 1) n = 1; if (n > 512) n = 512;
|
||||
size_t BUF = 256u + (size_t)n * 96u;
|
||||
char *body = (char *)malloc(BUF), *reply = (char *)malloc(BUF + 128u);
|
||||
if (!body || !reply) { free(body); free(reply); send_err(id, "oom"); return; }
|
||||
int emitted = gte_fstat_dump_json(body, (int)BUF, n);
|
||||
snprintf(reply, BUF + 128u, "{\"id\":%d,\"ok\":true,\"emitted\":%d,\"frames\":[%s]}",
|
||||
id, emitted, body);
|
||||
debug_server_send_line(reply); free(body); free(reply);
|
||||
}
|
||||
|
||||
/* Latched degenerate (saturated-output) GTE projections with full inputs. */
|
||||
static void handle_gte_latch_dump(int id, const char *json)
|
||||
{
|
||||
extern unsigned long long gte_latch_total(void);
|
||||
extern int gte_latch_dump_json(char *out, int outsz, int max_count);
|
||||
int n = json_get_int(json, "count", 64);
|
||||
if (n < 1) n = 1; if (n > 256) n = 256;
|
||||
size_t BUF = 256u + (size_t)n * 720u;
|
||||
char *body = (char *)malloc(BUF), *reply = (char *)malloc(BUF + 128u);
|
||||
if (!body || !reply) { free(body); free(reply); send_err(id, "oom"); return; }
|
||||
int emitted = gte_latch_dump_json(body, (int)BUF, n);
|
||||
snprintf(reply, BUF + 128u, "{\"id\":%d,\"ok\":true,\"latch_total\":%llu,\"emitted\":%d,\"entries\":[%s]}",
|
||||
id, gte_latch_total(), emitted, body);
|
||||
debug_server_send_line(reply); free(body); free(reply);
|
||||
}
|
||||
|
||||
static void handle_sio_state(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -7506,6 +7618,45 @@ static void handle_screenshot_file(int id, const char *json)
|
||||
id, path, w, h);
|
||||
}
|
||||
|
||||
/* dump_buffer: dump a raw 512x240 VRAM region starting at display Y = `y` to a
|
||||
* PNG, regardless of what the game currently displays. Used to inspect BOTH
|
||||
* double-buffer halves (y=0 and y=256) coherently in one call to see whether a
|
||||
* strobing character is present in one buffer only. */
|
||||
static void handle_dump_buffer(int id, const char *json)
|
||||
{
|
||||
extern void gl_renderer_sync_cpu(void);
|
||||
gl_renderer_sync_cpu();
|
||||
extern void vk_renderer_sync_cpu(void);
|
||||
vk_renderer_sync_cpu();
|
||||
|
||||
int y0 = json_get_int(json, "y", 0);
|
||||
char path[512];
|
||||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||||
strncpy(path, "psx_buffer.png", sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
|
||||
GpuDisplayInfo di;
|
||||
di.display_x = 0; di.display_y = (uint32_t)y0; di.depth24 = 0;
|
||||
di.disabled = 0; di.width = 512; di.height = 240;
|
||||
|
||||
uint32_t w = 512, h = 240;
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { send_err(id, "cannot open file"); return; }
|
||||
uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * 3);
|
||||
if (!rgb) { fclose(f); send_err(id, "alloc failed"); return; }
|
||||
for (uint32_t y = 0; y < h; y++)
|
||||
for (uint32_t x = 0; x < w; x++) {
|
||||
uint8_t r, g, b;
|
||||
gpu_display_pixel_rgb(&di, x, y, &r, &g, &b);
|
||||
uint8_t *p = rgb + ((size_t)y * w + x) * 3;
|
||||
p[0] = r; p[1] = g; p[2] = b;
|
||||
}
|
||||
int ok = png_write_rgb(f, rgb, w, h);
|
||||
free(rgb); fclose(f);
|
||||
if (!ok) { send_err(id, "png encode failed"); return; }
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"y\":%d}", id, path, y0);
|
||||
}
|
||||
|
||||
/* wide_shot: capture the NATIVE-WIDE present surface (post-compositor, what the
|
||||
* window actually shows in 16:9) to a PNG — unlike screenshot_file, which dumps
|
||||
* the canonical 320 VRAM region (pre-compositor). Pulls the exact buffer the
|
||||
@@ -11564,6 +11715,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "gl_fbo_peek", handle_gl_fbo_peek },
|
||||
{ "gl_vram_diff", handle_gl_vram_diff },
|
||||
{ "irq_state", handle_irq_state },
|
||||
{ "vblank_rate", handle_vblank_rate },
|
||||
{ "cycles_to_next_event", handle_cycles_to_next_event },
|
||||
{ "timers_state", handle_timers_state },
|
||||
{ "cdrom_state", handle_cdrom_state },
|
||||
@@ -11718,6 +11870,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "display_ring_get", handle_display_ring_get },
|
||||
{ "display_ring_aux", handle_display_ring_aux },
|
||||
{ "display_ring_stats", handle_display_ring_stats },
|
||||
{ "dump_buffer", handle_dump_buffer },
|
||||
{ "wide_full", handle_wide_full },
|
||||
{ "wide_shot", handle_wide_shot },
|
||||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||||
@@ -11728,6 +11881,10 @@ static const CmdEntry s_commands[] = {
|
||||
{ "capture_quads", handle_capture_quads },
|
||||
{ "get_quads", handle_get_quads },
|
||||
{ "gte_state", handle_gte_state },
|
||||
{ "gte_ring_dump", handle_gte_ring_dump },
|
||||
{ "gte_intpl_dump", handle_gte_intpl_dump },
|
||||
{ "gte_frame_stats", handle_gte_frame_stats },
|
||||
{ "gte_latch_dump", handle_gte_latch_dump },
|
||||
{ "quit", handle_quit },
|
||||
{ "dispatch_stats", handle_dispatch_stats },
|
||||
{ "dispatch_check", handle_dispatch_check },
|
||||
|
||||
@@ -2152,6 +2152,18 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
if (_gc) return 1;
|
||||
}
|
||||
clean_game_text_miss = psx_game_address_in_text(addr) ? 1 : 0;
|
||||
} else if (psx_game_address_in_text(addr)) {
|
||||
/* RAM at a game-text address diverged from the static EXE image
|
||||
* (runtime-relocated / overlaid / self-modified code the compiled
|
||||
* static function no longer reflects). The live RAM is the truth:
|
||||
* fall through to INTERPRET it here rather than bail (line below) to
|
||||
* the shell shadow (normalize() -> shell ROM). Crash Bash relocates a
|
||||
* code page onto 0x30000 (inside the BIOS shell window); without this
|
||||
* its 0x30FF4 call diverged (native_ok=0) yet was not dirty, so the
|
||||
* interpreter bailed and normalize() shadowed it to dead shell ROM ->
|
||||
* unknown-dispatch abort. Marking it a clean game-text miss lets the
|
||||
* dirty interpreter execute the real RAM bytes. */
|
||||
clean_game_text_miss = 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
+16
-1
@@ -104,7 +104,22 @@ void fntrace_record(CPUState* cpu, uint32_t target) {
|
||||
}
|
||||
|
||||
if (!s_game_started && s_game_entry_phys != 0) {
|
||||
if ((target & 0x1FFFFFFFu) == s_game_entry_phys) {
|
||||
/* Latch on the entry_pc dispatch (the common case) OR the first dispatch
|
||||
* to any game-text address whose RAM already holds the loaded game EXE
|
||||
* image (native-ok). Some titles reach their PS-EXE entry via compiled
|
||||
* internal flow rather than a dispatcher round-trip, so the entry_pc
|
||||
* dispatch never arrives and `started` would never latch — leaving the
|
||||
* shell-window shadow (normalize() maps RAM 0x30000-0x5AFFF -> shell ROM)
|
||||
* active over REAL game text. Crash Bash: entry 0x2E7B0 is reached
|
||||
* internally, so its first shell-window call (0x30FF4 / 0x3358C) got
|
||||
* shadowed to dead shell ROM -> unknown-dispatch abort. The native-ok
|
||||
* test is safe before the game loads: that RAM still holds shell bytes
|
||||
* that differ from the game image, so it cannot fire prematurely. */
|
||||
extern int psx_game_address_in_text(uint32_t addr);
|
||||
extern int dirty_ram_text_native_ok(uint32_t phys);
|
||||
uint32_t _tphys = target & 0x1FFFFFFFu;
|
||||
if (_tphys == s_game_entry_phys ||
|
||||
(psx_game_address_in_text(target) && dirty_ram_text_native_ok(_tphys))) {
|
||||
s_game_started = 1;
|
||||
/* Establish the clean compiled-image baseline now: the boot EXE is fully
|
||||
* loaded into the game-text region (== compiled image) and no gameplay
|
||||
|
||||
+43
-5
@@ -1296,6 +1296,15 @@ static uint32_t draw_area_right, draw_area_bottom;
|
||||
|
||||
/* Draw offset (set by GP0(E5h)) */
|
||||
static int32_t draw_offset_x, draw_offset_y;
|
||||
/* Instrumentation: per-vblank range/count of GP0(E5) draw-offset-Y sets. If a
|
||||
* single frame sets offsets in BOTH the top (y<128) and bottom (y>=128) buffer
|
||||
* bands, the game is drawing different parts of the scene into different display
|
||||
* buffers in the same frame (candidate root of a character-in-one-buffer
|
||||
* strobe). Snapshotted each vblank into the *_last copies, exposed via debug. */
|
||||
int32_t g_doff_min_this = 0x7fffffff, g_doff_max_this = -0x7fffffff;
|
||||
uint32_t g_doff_cnt_this = 0;
|
||||
int32_t g_doff_min_last = 0, g_doff_max_last = 0;
|
||||
uint32_t g_doff_cnt_last = 0;
|
||||
|
||||
/* Texture window raw value (set by GP0(E2h), readback via GP1(10h)) */
|
||||
static uint32_t texture_window_value;
|
||||
@@ -1418,6 +1427,7 @@ static int sq_cap_armed;
|
||||
* flips every ~33,868 GPU clocks (one NTSC frame). */
|
||||
#define GPUSTAT_POLL_VBLANK_THRESHOLD 1000
|
||||
static uint32_t gpustat_poll_count;
|
||||
uint64_t g_pollhack_vblank_count = 0; /* instrumentation: poll-fallback VBlank IRQ count */
|
||||
|
||||
/* ---- Initialization ---- */
|
||||
|
||||
@@ -1499,11 +1509,29 @@ uint32_t gpu_read_gpustat(void) {
|
||||
gpustat_poll_count++;
|
||||
if (gpustat_poll_count >= GPUSTAT_POLL_VBLANK_THRESHOLD) {
|
||||
gpustat_poll_count = 0;
|
||||
lcf ^= 1;
|
||||
psx_irq_raise(0, 0); /* IRQ_VBLANK (GPUSTAT-poll fallback path) */
|
||||
/* DEQUEUE: VBlank fired via the GPUSTAT-poll fallback path (distinct
|
||||
* from the cycle-paced VBlank in psx_check_interrupts). */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, 0xFFFFFFFFu);
|
||||
/* The recompiled BIOS + game carry per-block interrupt checks
|
||||
* (psx_check_interrupts_at) and per-block cycle charging, so tight
|
||||
* LCF/VSync poll loops DO advance guest cycles and let the ONE
|
||||
* cycle-paced VBlank authority (interrupts.c) fire on schedule. The old
|
||||
* "spin forever" premise that justified raising VBlank+LCF from a raw
|
||||
* GPUSTAT read-count is stale — and it injected ~38 fake VBlanks/s
|
||||
* (measured on Crash Bash), delivering ~96/s to the game instead of 60,
|
||||
* over-advancing the game's VSync frame counter and jittering animation
|
||||
* (character strobe). Firing is now OFF by default; PSX_POLLHACK_VBLANK=1
|
||||
* restores the legacy behavior for A/B. */
|
||||
static int poll_fire = -1;
|
||||
if (poll_fire < 0) {
|
||||
const char* e = getenv("PSX_POLLHACK_VBLANK");
|
||||
poll_fire = (e && e[0] == '1') ? 1 : 0;
|
||||
}
|
||||
if (poll_fire) {
|
||||
g_pollhack_vblank_count++; /* count only when actually firing */
|
||||
lcf ^= 1;
|
||||
psx_irq_raise(0, 0); /* IRQ_VBLANK (GPUSTAT-poll fallback path) */
|
||||
/* DEQUEUE: VBlank fired via the GPUSTAT-poll fallback path (distinct
|
||||
* from the cycle-paced VBlank in psx_check_interrupts). */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, 0xFFFFFFFFu);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t stat = 0;
|
||||
@@ -1613,6 +1641,13 @@ static uint16_t rgb888_to_rgb555(uint32_t color24) {
|
||||
|
||||
void gpu_vblank_tick(void) {
|
||||
lcf ^= 1;
|
||||
/* snapshot per-frame draw-offset-Y range for the strobe instrumentation */
|
||||
if (g_doff_cnt_this) {
|
||||
g_doff_min_last = g_doff_min_this;
|
||||
g_doff_max_last = g_doff_max_this;
|
||||
g_doff_cnt_last = g_doff_cnt_this;
|
||||
}
|
||||
g_doff_min_this = 0x7fffffff; g_doff_max_this = -0x7fffffff; g_doff_cnt_this = 0;
|
||||
gpustat_poll_count = 0;
|
||||
psx_irq_raise(0, 0); /* IRQ_VBLANK (gpu_vblank_tick) */
|
||||
if (vblank_callback) vblank_callback();
|
||||
@@ -2428,6 +2463,9 @@ static void gp0_exec_draw_offset(void) {
|
||||
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
|
||||
draw_offset_x = sign_extend(param & 0x7FFu, 11);
|
||||
draw_offset_y = sign_extend((param >> 11) & 0x7FFu, 11);
|
||||
if (draw_offset_y < g_doff_min_this) g_doff_min_this = draw_offset_y;
|
||||
if (draw_offset_y > g_doff_max_this) g_doff_max_this = draw_offset_y;
|
||||
g_doff_cnt_this++;
|
||||
/* Native-wide mirrors framebuffer draws into a separate wide surface (canonical
|
||||
* VRAM stays faithful); the renderer needs the live draw offset to translate
|
||||
* into surface-local coords, so keep gr_set_draw_offset current. */
|
||||
|
||||
+381
-62
@@ -2,6 +2,8 @@
|
||||
#include "cpu_state.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace PSXRecomp {
|
||||
namespace GTE {
|
||||
@@ -62,8 +64,18 @@ static int32_t gte_divide(uint16_t H, uint16_t SZ3, uint32_t& FLAG) {
|
||||
return (int32_t)result;
|
||||
}
|
||||
|
||||
// lm (bit 10) selects the lower clamp bound of every IR write in the
|
||||
// lighting/depth-cue ops: 0 → -0x8000, 1 → 0. sf (bit 19) selects the
|
||||
// accumulator shift. Hardware takes BOTH from the instruction word; these
|
||||
// helpers used to hardcode lm=1, which zeroed every negative component —
|
||||
// Crash Bash lerps signed VERTICES through INTPL (lm=0), so its characters
|
||||
// collapsed onto their anchor point on every tween frame (menu strobe bug).
|
||||
static inline bool gte_instr_lm(uint32_t instr) { return (instr & (1u << 10)) != 0; }
|
||||
static inline int gte_instr_sf(uint32_t instr) { return (instr & (1u << 19)) ? 12 : 0; }
|
||||
|
||||
// Transform a vertex by Light matrix → IR1/IR2/IR3 (step 1 of lighting)
|
||||
static void light_transform(GTEState* gte, int16_t* V) {
|
||||
static void light_transform(GTEState* gte, int16_t* V, uint32_t instr) {
|
||||
const bool lm = gte_instr_lm(instr);
|
||||
int64_t mac1 = ((int64_t)gte->L[0][0] * V[0] +
|
||||
(int64_t)gte->L[0][1] * V[1] +
|
||||
(int64_t)gte->L[0][2] * V[2]) >> 12;
|
||||
@@ -76,13 +88,14 @@ static void light_transform(GTEState* gte, int16_t* V) {
|
||||
gte->MAC1 = static_cast<int32_t>(mac1);
|
||||
gte->MAC2 = static_cast<int32_t>(mac2);
|
||||
gte->MAC3 = static_cast<int32_t>(mac3);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
|
||||
}
|
||||
|
||||
// Apply light color matrix + background color to IR → IR (step 2 of lighting)
|
||||
static void light_color(GTEState* gte) {
|
||||
static void light_color(GTEState* gte, uint32_t instr) {
|
||||
const bool lm = gte_instr_lm(instr);
|
||||
int64_t mac1 = ((int64_t)gte->BK[0] << 12) +
|
||||
(int64_t)gte->LC[0][0] * gte->IR1 +
|
||||
(int64_t)gte->LC[0][1] * gte->IR2 +
|
||||
@@ -98,13 +111,14 @@ static void light_color(GTEState* gte) {
|
||||
gte->MAC1 = static_cast<int32_t>(mac1 >> 12);
|
||||
gte->MAC2 = static_cast<int32_t>(mac2 >> 12);
|
||||
gte->MAC3 = static_cast<int32_t>(mac3 >> 12);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
|
||||
}
|
||||
|
||||
// Multiply IR by RGBC color and push to RGB FIFO (step 3 of color output)
|
||||
static void color_output(GTEState* gte) {
|
||||
static void color_output(GTEState* gte, uint32_t instr) {
|
||||
const bool lm = gte_instr_lm(instr);
|
||||
uint8_t r0 = (gte->RGBC >> 0) & 0xFF;
|
||||
uint8_t g0 = (gte->RGBC >> 8) & 0xFF;
|
||||
uint8_t b0 = (gte->RGBC >> 16) & 0xFF;
|
||||
@@ -114,32 +128,45 @@ static void color_output(GTEState* gte) {
|
||||
gte->MAC1 = static_cast<int32_t>(mac1 >> 12);
|
||||
gte->MAC2 = static_cast<int32_t>(mac2 >> 12);
|
||||
gte->MAC3 = static_cast<int32_t>(mac3 >> 12);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
gte->saturate_color(gte->MAC2 >> 4, 1),
|
||||
gte->saturate_color(gte->MAC3 >> 4, 2));
|
||||
}
|
||||
|
||||
// Depth cue from current IR values using IR0 interpolation toward far color
|
||||
// MAC = FC * 0x1000 - (BK + LC*IR) * 0x1000 ; then IR = (BK+LC*IR) + IR0 * MAC / 0x1000
|
||||
static void depth_cue_from_ir(GTEState* gte) {
|
||||
// Interpolate: result = IR + IR0 * (FC - IR) / 0x1000
|
||||
int64_t mac1 = ((int64_t)gte->FC[0] << 12) - ((int64_t)gte->IR1 << 12);
|
||||
int64_t mac2 = ((int64_t)gte->FC[1] << 12) - ((int64_t)gte->IR2 << 12);
|
||||
int64_t mac3 = ((int64_t)gte->FC[2] << 12) - ((int64_t)gte->IR3 << 12);
|
||||
// Scale by IR0 and add back
|
||||
mac1 = (int64_t)gte->IR1 + ((gte->IR0 * (mac1 >> 12)) >> 12);
|
||||
mac2 = (int64_t)gte->IR2 + ((gte->IR0 * (mac2 >> 12)) >> 12);
|
||||
mac3 = (int64_t)gte->IR3 + ((gte->IR0 * (mac3 >> 12)) >> 12);
|
||||
// Depth cue / interpolate current IR toward the far color using IR0
|
||||
// (common tail of DPCS/DPCT/DPCL/INTPL/NCDS/NCDT/CDP). Hardware:
|
||||
// base = IR << 12
|
||||
// step = lim(((FC << 12) - base) >> (sf*12)) ; ±0x8000 clamp, lm FORCED off
|
||||
// MAC = (base + IR0 * step) >> (sf*12)
|
||||
// IR = lim(MAC, lm) ; lm from the instruction
|
||||
static void depth_cue_from_ir(GTEState* gte, uint32_t instr) {
|
||||
const int shift = gte_instr_sf(instr);
|
||||
const bool lm = gte_instr_lm(instr);
|
||||
int64_t base1 = (int64_t)gte->IR1 << 12;
|
||||
int64_t base2 = (int64_t)gte->IR2 << 12;
|
||||
int64_t base3 = (int64_t)gte->IR3 << 12;
|
||||
int16_t step1 = gte->saturate_ir(
|
||||
(int32_t)(( ((int64_t)gte->FC[0] << 12) - base1) >> shift), 1, false);
|
||||
int16_t step2 = gte->saturate_ir(
|
||||
(int32_t)(( ((int64_t)gte->FC[1] << 12) - base2) >> shift), 2, false);
|
||||
int16_t step3 = gte->saturate_ir(
|
||||
(int32_t)(( ((int64_t)gte->FC[2] << 12) - base3) >> shift), 3, false);
|
||||
int64_t mac1 = (base1 + (int64_t)gte->IR0 * step1) >> shift;
|
||||
int64_t mac2 = (base2 + (int64_t)gte->IR0 * step2) >> shift;
|
||||
int64_t mac3 = (base3 + (int64_t)gte->IR0 * step3) >> shift;
|
||||
gte->check_mac_overflow(mac1, 1);
|
||||
gte->check_mac_overflow(mac2, 2);
|
||||
gte->check_mac_overflow(mac3, 3);
|
||||
gte->MAC1 = static_cast<int32_t>(mac1);
|
||||
gte->MAC2 = static_cast<int32_t>(mac2);
|
||||
gte->MAC3 = static_cast<int32_t>(mac3);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -248,6 +275,286 @@ extern "C" int gte_dome_probe_dump(uint32_t* funcs, uint32_t* counts, int32_t* m
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GTE projection ring (ALWAYS-ON) — records each RTPS/RTPT's inputs (vertex,
|
||||
// rotation matrix, translation, projection config) and outputs (screen XY / Z /
|
||||
// FLAG). A post-hoc probe queries this to find flattened/degenerate character
|
||||
// projections and decide whether the fault is in the INPUTS supplied by the
|
||||
// recompiled game code (bad matrix/vertex) or the GTE math itself.
|
||||
// ---------------------------------------------------------------------------
|
||||
// NOTE: this block is already inside namespace PSXRecomp::GTE (opened line 8),
|
||||
// so types are referred to unqualified.
|
||||
extern "C" { extern uint64_t s_frame_count; }
|
||||
struct GteRtpRec {
|
||||
uint32_t seq, frame, caller_ra, cmd;
|
||||
int16_t V0[3], V1[3], V2[3];
|
||||
int16_t RT[9];
|
||||
int32_t TR[3];
|
||||
uint16_t H; int32_t OFX, OFY;
|
||||
int32_t SXY0, SXY1, SXY2;
|
||||
uint16_t SZ1, SZ2, SZ3;
|
||||
uint32_t FLAG;
|
||||
};
|
||||
#define GTE_RTP_RING_CAP (1u << 18) /* 256K raw entries (~a few seconds) */
|
||||
static GteRtpRec* s_gte_rtp_ring = nullptr;
|
||||
static uint64_t s_gte_rtp_seq = 0;
|
||||
|
||||
/* Per-frame aggregate stats (retained ring over recent distinct frames). Lets a
|
||||
* probe see the alternating flat/normal render pattern without shipping 84k
|
||||
* projections/sec over JSON. nsat = projections whose newest output vertex
|
||||
* saturated off-screen; nflat = RTP whose last-3 output verts are collinear
|
||||
* (area 0) = a flattened triangle. */
|
||||
#define GTE_FSTAT_CAP 512
|
||||
struct GteFStat { uint32_t frame, nproj, nsat, nflat, nintpl, nintpl_tiny; };
|
||||
static GteFStat s_gte_fstat[GTE_FSTAT_CAP];
|
||||
static uint32_t s_gte_fstat_head = 0; /* index of most-recent frame slot */
|
||||
static int s_gte_fstat_valid = 0;
|
||||
|
||||
/* Degenerate-latch: full records of flat/saturated projections, retained (small
|
||||
* ring) so intermittent flat frames are caught regardless of raw-ring roll. */
|
||||
#define GTE_LATCH_CAP (1u << 13) /* 8192 */
|
||||
static GteRtpRec* s_gte_latch = nullptr;
|
||||
static uint64_t s_gte_latch_seq = 0;
|
||||
|
||||
static inline int gte_sxx(int32_t p){ int v=p&0xFFFF; return v>=0x8000? v-0x10000:v; }
|
||||
static inline int gte_syy(int32_t p){ int v=(p>>16)&0xFFFF; return v>=0x8000? v-0x10000:v; }
|
||||
|
||||
static void gte_rtp_record(const GTEState* g, uint32_t cmd) {
|
||||
if (!s_gte_rtp_ring) {
|
||||
s_gte_rtp_ring = (GteRtpRec*)calloc(GTE_RTP_RING_CAP, sizeof(GteRtpRec));
|
||||
if (!s_gte_rtp_ring) return;
|
||||
}
|
||||
GteRtpRec* e = &s_gte_rtp_ring[s_gte_rtp_seq % GTE_RTP_RING_CAP];
|
||||
e->seq = (uint32_t)s_gte_rtp_seq;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->caller_ra = s_gte_caller_ra;
|
||||
e->cmd = cmd;
|
||||
for (int i = 0; i < 3; i++) { e->V0[i]=g->V0[i]; e->V1[i]=g->V1[i]; e->V2[i]=g->V2[i]; }
|
||||
for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) e->RT[r*3+c]=g->RT[r][c];
|
||||
for (int i = 0; i < 3; i++) e->TR[i]=g->TR[i];
|
||||
e->H=g->H; e->OFX=g->OFX; e->OFY=g->OFY;
|
||||
e->SXY0=g->SXY[0]; e->SXY1=g->SXY[1]; e->SXY2=g->SXY[2];
|
||||
e->SZ1=g->SZ[1]; e->SZ2=g->SZ[2]; e->SZ3=g->SZ[3];
|
||||
e->FLAG=g->FLAG;
|
||||
s_gte_rtp_seq++;
|
||||
|
||||
/* classify */
|
||||
int x0=gte_sxx(e->SXY0),y0=gte_syy(e->SXY0);
|
||||
int x1=gte_sxx(e->SXY1),y1=gte_syy(e->SXY1);
|
||||
int x2=gte_sxx(e->SXY2),y2=gte_syy(e->SXY2);
|
||||
int sat = (x2<=-1023||x2>=1023||y2<=-1023||y2>=1023) ? 1 : 0;
|
||||
long area2 = (long)(x1-x0)*(y2-y0) - (long)(x2-x0)*(y1-y0);
|
||||
int flat = (area2==0) ? 1 : 0; /* three output verts exactly collinear */
|
||||
|
||||
/* per-frame stats */
|
||||
uint32_t f=(uint32_t)s_frame_count;
|
||||
if (!s_gte_fstat_valid || s_gte_fstat[s_gte_fstat_head].frame != f) {
|
||||
s_gte_fstat_head = s_gte_fstat_valid ? (s_gte_fstat_head+1)%GTE_FSTAT_CAP : 0;
|
||||
s_gte_fstat_valid = 1;
|
||||
s_gte_fstat[s_gte_fstat_head].frame=f;
|
||||
s_gte_fstat[s_gte_fstat_head].nproj=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nsat=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nflat=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nintpl=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nintpl_tiny=0;
|
||||
}
|
||||
s_gte_fstat[s_gte_fstat_head].nproj++;
|
||||
s_gte_fstat[s_gte_fstat_head].nsat += sat;
|
||||
s_gte_fstat[s_gte_fstat_head].nflat += flat;
|
||||
|
||||
/* latch degenerate (saturated) projections — the "flat/absent" character */
|
||||
if (sat) {
|
||||
if (!s_gte_latch) {
|
||||
s_gte_latch = (GteRtpRec*)calloc(GTE_LATCH_CAP, sizeof(GteRtpRec));
|
||||
if (!s_gte_latch) return;
|
||||
}
|
||||
s_gte_latch[s_gte_latch_seq % GTE_LATCH_CAP] = *e;
|
||||
s_gte_latch_seq++;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint64_t gte_rtp_ring_total(void) { return s_gte_rtp_seq; }
|
||||
extern "C" uint64_t gte_latch_total(void) { return s_gte_latch_seq; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GTE INTPL ring (ALWAYS-ON) — records each INTPL (0x11) vertex-lerp: inputs
|
||||
// (IR0 blend, IR1-3 pose-A vertex, FC pose-B vertex) snapshotted BEFORE the op
|
||||
// and outputs (MAC1-3 / IR1-3 / FLAG) after. Crash Bash tweens character
|
||||
// vertex animation through INTPL; this ring decides whether collapsed tween
|
||||
// output comes from already-tiny inputs (upstream CPU/game-state divergence)
|
||||
// or from the INTPL math itself.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct GteIntplRec {
|
||||
uint32_t seq, frame, caller_ra;
|
||||
int16_t ir0, in1, in2, in3;
|
||||
int32_t fc[3];
|
||||
int32_t mac[3];
|
||||
int16_t out1, out2, out3;
|
||||
uint32_t flag;
|
||||
};
|
||||
#define GTE_INTPL_RING_CAP (1u << 17) /* 128K entries (~4s at menu rates) */
|
||||
static GteIntplRec* s_gte_intpl_ring = nullptr;
|
||||
static uint64_t s_gte_intpl_seq = 0;
|
||||
|
||||
static void gte_intpl_record(const GTEState* g,
|
||||
const int16_t pre_ir[4], const int32_t pre_fc[3]) {
|
||||
if (!s_gte_intpl_ring) {
|
||||
s_gte_intpl_ring = (GteIntplRec*)calloc(GTE_INTPL_RING_CAP, sizeof(GteIntplRec));
|
||||
if (!s_gte_intpl_ring) return;
|
||||
}
|
||||
GteIntplRec* e = &s_gte_intpl_ring[s_gte_intpl_seq % GTE_INTPL_RING_CAP];
|
||||
e->seq = (uint32_t)s_gte_intpl_seq;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->caller_ra = s_gte_caller_ra;
|
||||
e->ir0 = pre_ir[0]; e->in1 = pre_ir[1]; e->in2 = pre_ir[2]; e->in3 = pre_ir[3];
|
||||
for (int i = 0; i < 3; i++) e->fc[i] = pre_fc[i];
|
||||
e->mac[0]=g->MAC1; e->mac[1]=g->MAC2; e->mac[2]=g->MAC3;
|
||||
e->out1=g->IR1; e->out2=g->IR2; e->out3=g->IR3;
|
||||
e->flag=g->FLAG;
|
||||
s_gte_intpl_seq++;
|
||||
|
||||
/* per-frame stats (shares the fstat ring; nintpl_tiny = all outputs small
|
||||
* while at least one input was not — the collapse signature) */
|
||||
uint32_t f=(uint32_t)s_frame_count;
|
||||
if (!s_gte_fstat_valid || s_gte_fstat[s_gte_fstat_head].frame != f) {
|
||||
s_gte_fstat_head = s_gte_fstat_valid ? (s_gte_fstat_head+1)%GTE_FSTAT_CAP : 0;
|
||||
s_gte_fstat_valid = 1;
|
||||
s_gte_fstat[s_gte_fstat_head].frame=f;
|
||||
s_gte_fstat[s_gte_fstat_head].nproj=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nsat=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nflat=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nintpl=0;
|
||||
s_gte_fstat[s_gte_fstat_head].nintpl_tiny=0;
|
||||
}
|
||||
s_gte_fstat[s_gte_fstat_head].nintpl++;
|
||||
{
|
||||
int out_tiny = (e->out1>-48 && e->out1<48 && e->out2>-48 && e->out2<48
|
||||
&& e->out3>-48 && e->out3<48);
|
||||
int in_big = (e->in1<=-48 || e->in1>=48 || e->in2<=-48 || e->in2>=48
|
||||
|| e->in3<=-48 || e->in3>=48
|
||||
|| e->fc[0]<=-48 || e->fc[0]>=48
|
||||
|| e->fc[1]<=-48 || e->fc[1]>=48
|
||||
|| e->fc[2]<=-48 || e->fc[2]>=48);
|
||||
if (out_tiny && in_big) s_gte_fstat[s_gte_fstat_head].nintpl_tiny++;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint64_t gte_intpl_ring_total(void) { return s_gte_intpl_seq; }
|
||||
|
||||
/* Dump INTPL ring entries as JSON array body. offset skips the first N
|
||||
* MATCHING entries (after frame filtering), so a probe can page through a
|
||||
* whole frame regardless of where it sits in the ring. */
|
||||
extern "C" int gte_intpl_ring_dump_json(char* out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter,
|
||||
int offset) {
|
||||
if (!s_gte_intpl_ring || outsz < 64) { if (out && outsz) out[0]=0; return 0; }
|
||||
uint64_t total = s_gte_intpl_seq;
|
||||
if (total == 0) { out[0]=0; return 0; }
|
||||
uint64_t oldest = total > GTE_INTPL_RING_CAP ? total - GTE_INTPL_RING_CAP : 0;
|
||||
int pos = 0, emitted = 0, matched = 0;
|
||||
for (uint64_t k = 0; k < (total - oldest) && emitted < max_count; k++) {
|
||||
uint64_t seq = newest_first ? (total - 1 - k) : (oldest + k);
|
||||
if (seq < oldest || seq >= total) break;
|
||||
const GteIntplRec* e = &s_gte_intpl_ring[seq % GTE_INTPL_RING_CAP];
|
||||
if (frame_filter >= 0 && e->frame != (uint32_t)frame_filter) continue;
|
||||
if (matched++ < offset) continue;
|
||||
if (pos > outsz - 400) break;
|
||||
pos += snprintf(out+pos, outsz-pos,
|
||||
"%s{\"seq\":%u,\"frame\":%u,\"ra\":\"0x%08X\","
|
||||
"\"ir0\":%d,\"in\":[%d,%d,%d],\"fc\":[%d,%d,%d],"
|
||||
"\"mac\":[%d,%d,%d],\"out\":[%d,%d,%d],\"flag\":\"0x%08X\"}",
|
||||
emitted?",":"", e->seq, e->frame, e->caller_ra,
|
||||
e->ir0, e->in1, e->in2, e->in3, e->fc[0], e->fc[1], e->fc[2],
|
||||
e->mac[0], e->mac[1], e->mac[2], e->out1, e->out2, e->out3,
|
||||
e->flag);
|
||||
emitted++;
|
||||
}
|
||||
out[pos] = 0;
|
||||
return emitted;
|
||||
}
|
||||
|
||||
/* Emit per-frame stats (newest first) as JSON array body. */
|
||||
extern "C" int gte_fstat_dump_json(char* out, int outsz, int max_frames) {
|
||||
if (!s_gte_fstat_valid) { if(out&&outsz) out[0]=0; return 0; }
|
||||
int pos=0, emitted=0;
|
||||
for (int k=0; k<GTE_FSTAT_CAP && emitted<max_frames; k++) {
|
||||
int idx=(int)((s_gte_fstat_head + GTE_FSTAT_CAP - k)%GTE_FSTAT_CAP);
|
||||
GteFStat* s=&s_gte_fstat[idx];
|
||||
if (s->nproj==0 && !(k==0)) continue;
|
||||
if (pos>outsz-96) break;
|
||||
pos+=snprintf(out+pos,outsz-pos,
|
||||
"%s{\"frame\":%u,\"nproj\":%u,\"nsat\":%u,\"nflat\":%u,"
|
||||
"\"nintpl\":%u,\"nintpl_tiny\":%u}",
|
||||
emitted?",":"", s->frame,s->nproj,s->nsat,s->nflat,
|
||||
s->nintpl,s->nintpl_tiny);
|
||||
emitted++;
|
||||
}
|
||||
out[pos]=0; return emitted;
|
||||
}
|
||||
|
||||
/* Dump latched degenerate projections (newest first). */
|
||||
extern "C" int gte_latch_dump_json(char* out, int outsz, int max_count) {
|
||||
if (!s_gte_latch || s_gte_latch_seq==0){ if(out&&outsz) out[0]=0; return 0; }
|
||||
uint64_t total=s_gte_latch_seq;
|
||||
uint64_t oldest = total>GTE_LATCH_CAP ? total-GTE_LATCH_CAP : 0;
|
||||
int pos=0, emitted=0;
|
||||
for (uint64_t k=0; k<(total-oldest) && emitted<max_count; k++) {
|
||||
uint64_t seq=total-1-k;
|
||||
if (seq<oldest) break;
|
||||
const GteRtpRec* e=&s_gte_latch[seq % GTE_LATCH_CAP];
|
||||
if (pos>outsz-700) break;
|
||||
pos+=snprintf(out+pos,outsz-pos,
|
||||
"%s{\"frame\":%u,\"ra\":\"0x%08X\",\"cmd\":\"0x%08X\","
|
||||
"\"RT\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],\"TR\":[%d,%d,%d],\"H\":%u,"
|
||||
"\"V0\":[%d,%d,%d],\"V1\":[%d,%d,%d],\"V2\":[%d,%d,%d],"
|
||||
"\"S0\":[%d,%d],\"S1\":[%d,%d],\"S2\":[%d,%d],\"SZ\":[%u,%u,%u],\"FLAG\":\"0x%08X\"}",
|
||||
emitted?",":"", e->frame, e->caller_ra, e->cmd,
|
||||
e->RT[0],e->RT[1],e->RT[2],e->RT[3],e->RT[4],e->RT[5],e->RT[6],e->RT[7],e->RT[8],
|
||||
e->TR[0],e->TR[1],e->TR[2],(unsigned)e->H,
|
||||
e->V0[0],e->V0[1],e->V0[2], e->V1[0],e->V1[1],e->V1[2], e->V2[0],e->V2[1],e->V2[2],
|
||||
gte_sxx(e->SXY0),gte_syy(e->SXY0),gte_sxx(e->SXY1),gte_syy(e->SXY1),
|
||||
gte_sxx(e->SXY2),gte_syy(e->SXY2),(unsigned)e->SZ1,(unsigned)e->SZ2,(unsigned)e->SZ3,e->FLAG);
|
||||
emitted++;
|
||||
}
|
||||
out[pos]=0; return emitted;
|
||||
}
|
||||
|
||||
/* Format up to max_count recent entries as a JSON array body (no outer braces).
|
||||
* If frame_filter >= 0, only entries with that frame are emitted. Returns count. */
|
||||
extern "C" int gte_rtp_ring_dump_json(char* out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter) {
|
||||
if (!s_gte_rtp_ring || outsz < 64) { if (out && outsz) out[0]=0; return 0; }
|
||||
uint64_t total = s_gte_rtp_seq;
|
||||
if (total == 0) { out[0]=0; return 0; }
|
||||
uint64_t oldest = total > GTE_RTP_RING_CAP ? total - GTE_RTP_RING_CAP : 0;
|
||||
int pos = 0, emitted = 0;
|
||||
for (uint64_t k = 0; k < (total - oldest) && emitted < max_count; k++) {
|
||||
uint64_t seq = newest_first ? (total - 1 - k) : (oldest + k);
|
||||
if (seq < oldest || seq >= total) break;
|
||||
const GteRtpRec* e = &s_gte_rtp_ring[seq % GTE_RTP_RING_CAP];
|
||||
if (frame_filter >= 0 && e->frame != (uint32_t)frame_filter) continue;
|
||||
auto sxx = [](int32_t p){ int v=p&0xFFFF; return v>=0x8000? v-0x10000:v; };
|
||||
auto syy = [](int32_t p){ int v=(p>>16)&0xFFFF; return v>=0x8000? v-0x10000:v; };
|
||||
if (pos > outsz - 700) break;
|
||||
pos += snprintf(out+pos, outsz-pos,
|
||||
"%s{\"seq\":%u,\"frame\":%u,\"ra\":\"0x%08X\",\"cmd\":\"0x%08X\","
|
||||
"\"V0\":[%d,%d,%d],\"V1\":[%d,%d,%d],\"V2\":[%d,%d,%d],"
|
||||
"\"RT\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],\"TR\":[%d,%d,%d],"
|
||||
"\"H\":%u,\"OFX\":%d,\"OFY\":%d,"
|
||||
"\"S0\":[%d,%d],\"S1\":[%d,%d],\"S2\":[%d,%d],"
|
||||
"\"SZ\":[%u,%u,%u],\"FLAG\":\"0x%08X\"}",
|
||||
emitted?",":"", e->seq, e->frame, e->caller_ra, e->cmd,
|
||||
e->V0[0],e->V0[1],e->V0[2], e->V1[0],e->V1[1],e->V1[2], e->V2[0],e->V2[1],e->V2[2],
|
||||
e->RT[0],e->RT[1],e->RT[2],e->RT[3],e->RT[4],e->RT[5],e->RT[6],e->RT[7],e->RT[8],
|
||||
e->TR[0],e->TR[1],e->TR[2], (unsigned)e->H, e->OFX, e->OFY,
|
||||
sxx(e->SXY0),syy(e->SXY0), sxx(e->SXY1),syy(e->SXY1), sxx(e->SXY2),syy(e->SXY2),
|
||||
(unsigned)e->SZ1,(unsigned)e->SZ2,(unsigned)e->SZ3, e->FLAG);
|
||||
emitted++;
|
||||
}
|
||||
out[pos] = 0;
|
||||
return emitted;
|
||||
}
|
||||
|
||||
extern "C" void gte_set_display_aspect(int num, int den) {
|
||||
if (num <= 0 || den <= 0) { s_ws_xnum = s_ws_xden = 1; return; }
|
||||
// squash = (4/3) / (num/den) = (4*den) / (3*num); identity for 4:3.
|
||||
@@ -399,15 +706,15 @@ void gte_avsz4(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// NCCS (0x1B) — Normal Color Color Single
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_nccs_internal(GTEState* gte, int16_t* V) {
|
||||
light_transform(gte, V);
|
||||
light_color(gte);
|
||||
color_output(gte);
|
||||
void gte_nccs_internal(GTEState* gte, int16_t* V, uint32_t instr) {
|
||||
light_transform(gte, V, instr);
|
||||
light_color(gte, instr);
|
||||
color_output(gte, instr);
|
||||
}
|
||||
|
||||
void gte_nccs(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_nccs_internal(gte, gte->V0);
|
||||
gte_nccs_internal(gte, gte->V0, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -416,25 +723,25 @@ void gte_nccs(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_ncct(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_nccs_internal(gte, gte->V0);
|
||||
gte_nccs_internal(gte, gte->V1);
|
||||
gte_nccs_internal(gte, gte->V2);
|
||||
gte_nccs_internal(gte, gte->V0, instr);
|
||||
gte_nccs_internal(gte, gte->V1, instr);
|
||||
gte_nccs_internal(gte, gte->V2, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NCDS (0x13) — Normal Color Depth Cue Single
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_ncds_internal(GTEState* gte, int16_t* V) {
|
||||
light_transform(gte, V);
|
||||
light_color(gte);
|
||||
depth_cue_from_ir(gte);
|
||||
color_output(gte);
|
||||
void gte_ncds_internal(GTEState* gte, int16_t* V, uint32_t instr) {
|
||||
light_transform(gte, V, instr);
|
||||
light_color(gte, instr);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
color_output(gte, instr);
|
||||
}
|
||||
|
||||
void gte_ncds(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_ncds_internal(gte, gte->V0);
|
||||
gte_ncds_internal(gte, gte->V0, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -443,18 +750,18 @@ void gte_ncds(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_ncdt(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_ncds_internal(gte, gte->V0);
|
||||
gte_ncds_internal(gte, gte->V1);
|
||||
gte_ncds_internal(gte, gte->V2);
|
||||
gte_ncds_internal(gte, gte->V0, instr);
|
||||
gte_ncds_internal(gte, gte->V1, instr);
|
||||
gte_ncds_internal(gte, gte->V2, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NCS (0x1E) — Normal Color Single (no vertex color multiply)
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_ncs_internal(GTEState* gte, int16_t* V) {
|
||||
light_transform(gte, V);
|
||||
light_color(gte);
|
||||
void gte_ncs_internal(GTEState* gte, int16_t* V, uint32_t instr) {
|
||||
light_transform(gte, V, instr);
|
||||
light_color(gte, instr);
|
||||
// Output directly (no RGBC multiply)
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
@@ -464,7 +771,7 @@ void gte_ncs_internal(GTEState* gte, int16_t* V) {
|
||||
|
||||
void gte_ncs(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_ncs_internal(gte, gte->V0);
|
||||
gte_ncs_internal(gte, gte->V0, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -473,9 +780,9 @@ void gte_ncs(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_nct(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
gte_ncs_internal(gte, gte->V0);
|
||||
gte_ncs_internal(gte, gte->V1);
|
||||
gte_ncs_internal(gte, gte->V2);
|
||||
gte_ncs_internal(gte, gte->V0, instr);
|
||||
gte_ncs_internal(gte, gte->V1, instr);
|
||||
gte_ncs_internal(gte, gte->V2, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -492,7 +799,7 @@ void gte_dpcs(GTEState* gte, uint32_t instr) {
|
||||
gte->IR2 = gte->saturate_ir(g << 4, 2, false);
|
||||
gte->IR3 = gte->saturate_ir(b << 4, 3, false);
|
||||
// Interpolate toward far color using IR0
|
||||
depth_cue_from_ir(gte);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
// Output
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
@@ -513,7 +820,7 @@ void gte_dpct(GTEState* gte, uint32_t instr) {
|
||||
gte->IR1 = gte->saturate_ir(r << 4, 1, false);
|
||||
gte->IR2 = gte->saturate_ir(g << 4, 2, false);
|
||||
gte->IR3 = gte->saturate_ir(b << 4, 3, false);
|
||||
depth_cue_from_ir(gte);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
gte->saturate_color(gte->MAC2 >> 4, 1),
|
||||
@@ -534,11 +841,11 @@ void gte_dpcl(GTEState* gte, uint32_t instr) {
|
||||
gte->MAC1 = (r * gte->IR1) >> 8;
|
||||
gte->MAC2 = (g * gte->IR2) >> 8;
|
||||
gte->MAC3 = (b * gte->IR3) >> 8;
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true);
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true);
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true);
|
||||
gte->IR1 = gte->saturate_ir(gte->MAC1, 1, gte_instr_lm(instr));
|
||||
gte->IR2 = gte->saturate_ir(gte->MAC2, 2, gte_instr_lm(instr));
|
||||
gte->IR3 = gte->saturate_ir(gte->MAC3, 3, gte_instr_lm(instr));
|
||||
// Depth cue toward far color
|
||||
depth_cue_from_ir(gte);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
gte->saturate_color(gte->MAC2 >> 4, 1),
|
||||
@@ -551,7 +858,7 @@ void gte_dpcl(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_intpl(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
depth_cue_from_ir(gte);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
gte->push_rgb(
|
||||
gte->saturate_color(gte->MAC1 >> 4, 0),
|
||||
gte->saturate_color(gte->MAC2 >> 4, 1),
|
||||
@@ -565,9 +872,9 @@ void gte_intpl(GTEState* gte, uint32_t instr) {
|
||||
void gte_cdp(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
// IR1/IR2/IR3 already set (from previous NCS or similar)
|
||||
light_color(gte);
|
||||
depth_cue_from_ir(gte);
|
||||
color_output(gte);
|
||||
light_color(gte, instr);
|
||||
depth_cue_from_ir(gte, instr);
|
||||
color_output(gte, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -576,8 +883,8 @@ void gte_cdp(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_cc(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
light_color(gte);
|
||||
color_output(gte);
|
||||
light_color(gte, instr);
|
||||
color_output(gte, instr);
|
||||
gte->set_error_flag();
|
||||
}
|
||||
|
||||
@@ -971,6 +1278,15 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
|
||||
* (no-op unless the game opts in — early-out on the config flag). */
|
||||
if (func == 0x01 || func == 0x30)
|
||||
psx_ws_note_gte_project(func == 0x30 ? 3 : 1);
|
||||
/* INTPL ring: snapshot inputs before the op mutates IR (outputs recorded
|
||||
* after the switch). */
|
||||
int16_t intpl_pre_ir[4] = {0,0,0,0};
|
||||
int32_t intpl_pre_fc[3] = {0,0,0};
|
||||
if (func == 0x11) {
|
||||
intpl_pre_ir[0]=gte.IR0; intpl_pre_ir[1]=gte.IR1;
|
||||
intpl_pre_ir[2]=gte.IR2; intpl_pre_ir[3]=gte.IR3;
|
||||
intpl_pre_fc[0]=gte.FC[0]; intpl_pre_fc[1]=gte.FC[1]; intpl_pre_fc[2]=gte.FC[2];
|
||||
}
|
||||
switch (func) {
|
||||
case 0x01: gte_rtps(>e, cmd); break;
|
||||
case 0x06: gte_nclip(>e, cmd); break;
|
||||
@@ -999,6 +1315,9 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (func == 0x01 || func == 0x30) gte_rtp_record(>e, cmd);
|
||||
if (func == 0x11) gte_intpl_record(>e, intpl_pre_ir, intpl_pre_fc);
|
||||
|
||||
for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i);
|
||||
for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i);
|
||||
|
||||
|
||||
+77
-2
@@ -1594,8 +1594,19 @@ static void sample_headless_pad_into_sio(int override) {
|
||||
* audio sample generation (735 samples/vblank * 60 = 44100/sec) matched
|
||||
* to the SDL audio device drain rate, eliminating queue overflow drops
|
||||
* and underruns. Uncapped, the host runs the simulation at whatever
|
||||
* speed it can — typically several × realtime — and audio glitches. */
|
||||
* speed it can — typically several × realtime — and audio glitches.
|
||||
*
|
||||
* The wall-clock pacer target; nudged to the host display refresh at
|
||||
* window-creation time (sync-to-host-refresh) so the pacer and SDL PRESENTVSYNC
|
||||
* do not fight — a fixed 59.94 pacer against a 60.00 Hz panel makes rendered
|
||||
* frames land on an uneven vblank count (a 2/3/1 beat) that reads as
|
||||
* moving-object judder/flicker. See g_frame_period_ms. */
|
||||
static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94;
|
||||
/* Live pacer period (ms). Defaults to the PSX rate; set to the host refresh
|
||||
* period when the panel is within ~2% of 60 Hz so 30fps content pads evenly to
|
||||
* two host refreshes. Left at the PSX rate on non-~60Hz panels to avoid running
|
||||
* the sim at the wrong speed. */
|
||||
static double g_frame_period_ms = PSX_FRAME_PERIOD_MS;
|
||||
|
||||
/* ── Host-stack-usage profile (RECURSION_BUG.md §17) ──────────────────────────
|
||||
* The decisive instrument for the long-run freeze. The guest call graph mirrors
|
||||
@@ -1893,7 +1904,7 @@ static void sdl_vblank_present(void) {
|
||||
* hard freeze). */
|
||||
{
|
||||
static FramePacer pacer = { 0 };
|
||||
frame_pacer_wait(&pacer, PSX_FRAME_PERIOD_MS);
|
||||
frame_pacer_wait(&pacer, g_frame_period_ms);
|
||||
}
|
||||
latency_ring_mark(LAT_PACED);
|
||||
|
||||
@@ -2066,6 +2077,47 @@ static void sdl_vblank_present(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Frame blending (CRT-persistence masker for 30fps double-buffered
|
||||
* content). Some games (e.g. Crash Bash menus/characters) leave a
|
||||
* dynamic object in only one of the two display buffers per 30fps cycle,
|
||||
* so it strobes on/off as the display alternates buffers — the static
|
||||
* background, present in both, stays put. Real CRTs hid this via phosphor
|
||||
* persistence; accurate emulators either time it away (cycle accuracy) or
|
||||
* offer a "blend frames" option that averages the last two presented
|
||||
* frames. This is the latter: presentation only, game logic untouched.
|
||||
* Guarded to the plain software present path (no GL/VK/wide/hires/FMV) so
|
||||
* the buffer is a simple present_w*h ARGB grid. PSX_FRAME_BLEND=0 off. */
|
||||
{
|
||||
static int blend_cfg = -1; /* -1 unresolved, 0 off, 1 on */
|
||||
if (blend_cfg < 0) {
|
||||
const char* e = getenv("PSX_FRAME_BLEND");
|
||||
blend_cfg = (e && e[0] == '1') ? 1 : 0; /* default off (masker only) */
|
||||
}
|
||||
const bool simple_sw = !g_gl_active && !g_vk_active && !wide_present &&
|
||||
!di.depth24 && active_scale == 1;
|
||||
if (blend_cfg && simple_sw) {
|
||||
static uint32_t prev_buf[640 * 512];
|
||||
static uint32_t prev_px = 0;
|
||||
const uint32_t npx = present_w * h;
|
||||
if (npx <= (uint32_t)(640 * 512)) {
|
||||
if (prev_px == npx) {
|
||||
for (uint32_t i = 0; i < npx; i++) {
|
||||
const uint32_t a = sdl_pixel_buf[i];
|
||||
const uint32_t b = prev_buf[i];
|
||||
prev_buf[i] = a; /* store this frame (pre-blend) */
|
||||
sdl_pixel_buf[i] =
|
||||
0xFF000000u |
|
||||
(((a & 0xFEFEFEu) >> 1) + ((b & 0xFEFEFEu) >> 1));
|
||||
}
|
||||
} else {
|
||||
/* Resolution changed / first frame: seed, no blend. */
|
||||
for (uint32_t i = 0; i < npx; i++) prev_buf[i] = sdl_pixel_buf[i];
|
||||
prev_px = npx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Update only the active display rectangle. The backing texture is sized
|
||||
@@ -3061,6 +3113,29 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Sync-to-host-refresh: with SDL PRESENTVSYNC on, a fixed 59.94 Hz wall-clock
|
||||
* pacer fights a 60.00 Hz panel — rendered frames slip onto an uneven vblank
|
||||
* count (2/3/1 beat) that reads as moving-object judder. If the panel is
|
||||
* within ~2% of 60 Hz, nudge the pacer to the exact panel period so the pacer
|
||||
* and vsync agree and 30fps content pads to a steady 2 refreshes each.
|
||||
* Non-~60Hz panels keep the PSX rate (vsync then governs; wrong-speed sim is
|
||||
* worse than a benign slow beat). */
|
||||
{
|
||||
SDL_DisplayMode dm;
|
||||
int disp_idx = SDL_GetWindowDisplayIndex(sdl_window);
|
||||
if (disp_idx >= 0 && SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) {
|
||||
double host_hz = (double)dm.refresh_rate;
|
||||
if (host_hz >= 58.8 && host_hz <= 61.2) {
|
||||
g_frame_period_ms = 1000.0 / host_hz;
|
||||
std::printf("psxrecomp: sync-to-host-refresh: pacing to %d Hz panel "
|
||||
"(%.4f ms/frame)\n", dm.refresh_rate, g_frame_period_ms);
|
||||
} else {
|
||||
std::printf("psxrecomp: host panel %d Hz not ~60 Hz; keeping PSX "
|
||||
"59.94 Hz pacing\n", dm.refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* OpenGL backend: create the GL context now. On failure, relabel the
|
||||
* facade back to software (rasterization already runs through software in
|
||||
* this phase) and fall through to the SDL_Renderer present path below. */
|
||||
|
||||
Reference in New Issue
Block a user