fix(gte): honor lm/sf from the instruction in the depth-cue/lighting family; add always-on GTE observability rings

The depth-cue tail (INTPL/DPCS/DPCT/DPCL/NCDS/NCDT/CDP) and the lighting
helpers (light_transform / light_color / color_output) hardcoded lm=1 on
every IR write-back, clamping all negative components to zero. Crash Bash
tweens its vertex animation through INTPL (cmd 0x0980011, lm=0): pose A in
IR1-3, pose B in the far-color registers, blend in IR0. With lm forced on,
every negative vertex component collapsed to 0 on tween frames, squashing
characters onto their anchor point. At 10fps keyframes / 30fps renders that
made the menu character strobe (2 of 3 renders flattened, so exactly one
display buffer ever held a healthy pose) and in-game characters flatten.
Beetle/hardware render the same frames correctly.

depth_cue_from_ir is now the faithful hardware formula: base = IR<<12,
step = lim(((FC<<12) - base) >> sf*12) with the forced-unsigned-off
intermediate clamp, MAC = (base + IR0*step) >> sf*12, final IR clamp with
lm from the instruction word. sf (bit 19) is honored instead of assuming 12.
The lighting helpers take lm from the instruction too (games light with
lm=1, so their behavior is unchanged).

Observability (always-on, build-dbg commands):
- GTE projection ring (gte_ring_dump): every RTPS/RTPT's V/RT/TR/H/OFX/OFY
  inputs and SXY/SZ/FLAG outputs, frame-stamped, with caller ra.
- GTE INTPL ring (gte_intpl_dump): every INTPL's IR0/IR1-3/FC inputs
  (snapshotted pre-op) and MAC/IR/FLAG outputs, with frame filter AND
  offset paging so a whole frame is reachable.
- gte_frame_stats: per-frame nproj/nsat/nflat/nintpl/nintpl_tiny —
  nintpl_tiny (all-tiny output from non-tiny input) is the collapse
  signature and pinned this bug to the INTPL math in one query.
- gte_latch_dump: retained ring of saturated projections.

Verified on Crash Bash SELECT GAME TYPE: nintpl_tiny 335-352/473 -> 0-3,
vertex buffer at 0x80056AC8 no longer alternates healthy/collapsed, and a
20-shot jittered screenshot burst shows the character in every frame
(was bimodal present/absent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-09 23:19:21 -07:00
parent 3d05c9e98b
commit c4474b9340
2 changed files with 473 additions and 62 deletions
+92
View File
@@ -5427,6 +5427,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;
@@ -11496,6 +11584,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 },
+381 -62
View File
@@ -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(&gte, cmd); break;
case 0x06: gte_nclip(&gte, 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(&gte, cmd);
if (func == 0x11) gte_intpl_record(&gte, intpl_pre_ir, intpl_pre_fc);
for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(&gte, i);
for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(&gte, i);