D75 (M-196): refute slot-overwrite hypothesis; narrow logo bug to texture-modulate render path

New env-gated GE_D75D probe (gfx_pc.cpp) captures per-frame V/T token stream
+ per-triangle rejection/emission accounting for the front-end logo window.

Findings (bare front-end boot, logo = frames ~244+, nv=93/nt=1018):
- Slot-overwrite (M-195's leading hypothesis) REFUTED: the DL is streamed
  (V TTT...VTTT interleaved), max triangle vertex index = 15, so no
  cross-batch vertex clobbering can occur.
- Rejection gates are not the cause: of 1018 tris/frame, trivial-reject=0,
  backface-cull ~520 (normal half), ~500 reach GL, all on-screen (NDC in
  [-1,1]^2). Not the invisible flag; vertex colors [0,255]; depth normal.
- PPM analysis: logo window renders 0-33 content px/frame (the 'tiny
  fragment'), vs title quads at 8.36% coverage.
- Narrowed cause: combine mode 0x009ffe4f19ffe4f1 parses to
  rgb a=TEXEL0 c=SHADE (texture-modulate); gfx_generate_cc sets
  used_textures[0]=1, but the material still renders black-on-black. Next
  step: trace where the TEXEL0 sampler is lost between combiner build and
  draw (shader_id0 -> GL program / prg[tm] / texture upload).

Port-layer fast3d only; no rule-2 sign-off needed. Probes env-gated, inert
without GE_D75D (verified: 0 lines in a normal run).
This commit is contained in:
James Dansereau committed 2026-09-21 06:00:28 -04:00
1 parent d4440759cd
commit 327e6fe1a7
3 files changed
+164 -1

No files matched your search

+160
View File
@@ -353,6 +353,9 @@ static const char* acmux_to_string(uint32_t acmux) {
return tbl[acmux];
}
/* forward decl for the env-gated D75D probe (defined further down) */
static bool d75d_env_active(void);
static void gfx_generate_cc(struct ColorCombiner* comb, const ColorCombinerKey& key) {
bool is_2cyc = (key.options & (uint64_t)SHADER_OPT_2CYC) != 0;
@@ -482,6 +485,17 @@ static void gfx_generate_cc(struct ColorCombiner* comb, const ColorCombinerKey&
}
}
}
/* TEMP D75D: one-shot decode of the logo model's combine mode (env-gated via d75d_lo) */
{
static int d75d_cc_once = 0;
if (!d75d_cc_once && d75d_env_active() && key.combine_mode == 0x009ffe4f19ffe4f1ULL) {
d75d_cc_once = 1;
fprintf(stderr, "D75DCC: comb=0x%016llx is_2cyc=%d c0=(rgb a=%u b=%u c=%u d=%u)(al a=%u b=%u c=%u d=%u) utex0=%d\n",
(unsigned long long)key.combine_mode, (int)is_2cyc,
c[0][0][0], c[0][0][1], c[0][0][2], c[0][0][3],
c[0][1][0], c[0][1][1], c[0][1][2], c[0][1][3], (int)used_textures[0]);
}
}
{
uint8_t input_number[16] = { 0 };
int next_input_number = SHADER_INPUT_1;
@@ -1444,6 +1458,138 @@ static inline bool fast3d_ptr_ok(const void *p) {
extern "C" u32 videoGetFrameCount(void); /* port/src/video.c (D75 probe) */
/* D75 DL-sequence dump (M-196, GE_D75D="lo-hi"): the M-195 slot-overwrite
* hypothesis says every G_VTX batch encodes dest_index=0, so a multi-batch
* node's later batches clobber earlier ones and only the last survives. That
* is ONLY harmful if a node's triangles are drawn AFTER a later node's G_VTX
* has overwritten their slots -- i.e. only if the DL is structured batched
* (all vertices for all nodes, then all triangles) rather than streamed
* (each node: its vertices then its triangles). gfx_sp_tri1 copies vertex data
* into buf_vbo eagerly at triangle time, so streaming order makes cross-node
* overwrites harmless exactly like the N64 RDP. This probe records, per frame,
* a compact V/T token stream (V=G_VTX batch, T=triangle) plus max-triangle-
*index vs cumulative-vertices-loaded, to decide which structure GE's front-end
* model DLs actually use. Zero cost unless GE_D75D is set. Remove once D75 is
* root-caused. */
static int d75d_lo = -1, d75d_hi = 0x7FFFFFFF;
static int d75d_init_done = 0;
static uint32_t d75d_frame = 0xFFFFFFFFu;
static char d75d_seq[4096];
static int d75d_seqlen = 0;
static uint32_t d75d_maxtri = 0, d75d_vtxloaded = 0, d75d_batchhi = 0, d75d_nv = 0, d75d_nt = 0;
static uint32_t d75d_rej_triv = 0, d75d_rej_cull = 0, d75d_emitted = 0;
static uint32_t d75d_emit_on = 0, d75d_emit_off = 0; // emitted tris: NDC bbox intersects [-1,1]^2 vs not
static float d75d_ndc_minx=1e9f,d75d_ndc_maxx=-1e9f,d75d_ndc_miny=1e9f,d75d_ndc_maxy=-1e9f;
static uint32_t d75d_emit_invis = 0; // emitted tris with SHADER_OPT_INVISIBLE (G_BL_0+G_BL_CLR_MEM)
static int d75d_cmin=999, d75d_cmax=-1; // min/max over all vertex color channels of emitted tris
static uint32_t d75d_oml = 0xFFFFFFFFu; static int d75d_oml_set = 0;
static float d75d_zmin=1e9f,d75d_zmax=-1e9f; static uint32_t d75d_znan=0; static int d75d_use_tex=-1;
static uint64_t d75d_comb=0; static int d75d_prim[4]={-1,-1,-1,-1}; static int d75d_env[4]={-1,-1,-1,-1};
static int d75d_comb_utex=-1; // comb->used_textures[0] (combiner's own claim)
static void d75d_init(void) {
if (d75d_init_done) return;
d75d_init_done = 1;
const char* e = getenv("GE_D75D");
if (e && *e) {
d75d_lo = atoi(e);
const char* dash = strchr(e, '-');
if (dash) d75d_hi = atoi(dash + 1);
}
}
static bool d75d_env_active(void) { d75d_init(); return d75d_lo >= 0; }
static void d75d_flush(uint32_t f) {
if (f == 0xFFFFFFFFu || d75d_seqlen == 0) return;
fprintf(stderr,
"D75D: f=%u nv=%u nt=%u maxtri=%u vtxloaded=%u batchhi=%u rej_triv=%u rej_cull=%u emitted=%u emit_on=%u emit_off=%u ndc_x=[%.3f,%.3f] ndc_y=[%.3f,%.3f] invis=%u col=[%d,%d] oml=0x%08x omset=%d z=[%.4f,%.4f] znan=%u utex=%d cutex=%d comb=0x%016llx prim=(%d,%d,%d,%d) env=(%d,%d,%d,%d) seq=%.8s%s\n",
f, d75d_nv, d75d_nt, d75d_maxtri, d75d_vtxloaded, d75d_batchhi,
d75d_rej_triv, d75d_rej_cull, d75d_emitted, d75d_emit_on, d75d_emit_off,
d75d_ndc_minx, d75d_ndc_maxx, d75d_ndc_miny, d75d_ndc_maxy,
d75d_emit_invis, d75d_cmin, d75d_cmax, (unsigned)d75d_oml, d75d_oml_set,
d75d_zmin, d75d_zmax, d75d_znan, d75d_use_tex, d75d_comb_utex,
(unsigned long long)d75d_comb, d75d_prim[0],d75d_prim[1],d75d_prim[2],d75d_prim[3],
d75d_env[0],d75d_env[1],d75d_env[2],d75d_env[3],
d75d_seq, (d75d_seqlen >= 4096) ? "...[trunc]" : "");
d75d_seqlen = 0; d75d_maxtri = 0; d75d_vtxloaded = 0; d75d_batchhi = 0; d75d_nv = 0; d75d_nt = 0;
d75d_rej_triv = 0; d75d_rej_cull = 0; d75d_emitted = 0; d75d_emit_on = 0; d75d_emit_off = 0;
d75d_ndc_minx=1e9f;d75d_ndc_maxx=-1e9f;d75d_ndc_miny=1e9f;d75d_ndc_maxy=-1e9f;
d75d_emit_invis = 0; d75d_cmin=999; d75d_cmax=-1; d75d_oml=0xFFFFFFFFu; d75d_oml_set=0;
d75d_zmin=1e9f;d75d_zmax=-1e9f;d75d_znan=0;d75d_use_tex=-1;
d75d_comb=0; for(int i=0;i<4;i++){d75d_prim[i]=-1;d75d_env[i]=-1;} d75d_comb_utex=-1;
}
static void d75d_frame_check(uint32_t f) {
if (f != d75d_frame) { d75d_flush(f); d75d_frame = f; }
}
static void d75d_note_vtx(uint32_t f, uint32_t count, uint32_t dest) {
d75d_init();
if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return;
d75d_frame_check(f);
d75d_nv++;
d75d_vtxloaded += count;
if (dest + count > d75d_batchhi) d75d_batchhi = dest + count;
if (d75d_seqlen < 4096) d75d_seq[d75d_seqlen++] = 'V';
}
static void d75d_note_tri(uint32_t f, uint32_t maxidx) {
d75d_init();
if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return;
d75d_frame_check(f);
d75d_nt++;
if (maxidx > d75d_maxtri) d75d_maxtri = maxidx;
if (d75d_seqlen < 4096) d75d_seq[d75d_seqlen++] = 'T';
}
static void d75d_note_rej(uint32_t f, int which) {
d75d_init();
if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return;
d75d_frame_check(f);
if (which == 0) d75d_rej_triv++; else d75d_rej_cull++;
}
static void d75d_note_emit(uint32_t f, struct LoadedVertex* const* v_arr) {
d75d_init();
if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return;
d75d_frame_check(f);
d75d_emitted++;
// NDC bbox of this triangle (x/w, y/w); count on-screen vs off
float mnx=1e9f,mxx=-1e9f,mny=1e9f,mxy=-1e9f;
for (int i = 0; i < 3; i++) {
float w = v_arr[i]->w;
if (w == 0.f) continue;
float nx = v_arr[i]->x / w, ny = v_arr[i]->y / w;
if (nx<mnx)mnx=nx; if(nx>mxx)mxx=nx; if(ny<mny)mny=ny; if(ny>mxy)mxy=ny;
}
if (mxx < -1e8f) return; // all w==0, degenerate
if (mxx >= -1.05f && mnx <= 1.05f && mxy >= -1.05f && mny <= 1.05f) d75d_emit_on++; else d75d_emit_off++;
if (mnx<d75d_ndc_minx)d75d_ndc_minx=mnx; if(mxx>d75d_ndc_maxx)d75d_ndc_maxx=mxx;
if (mny<d75d_ndc_miny)d75d_ndc_miny=mny; if(mxy>d75d_ndc_maxy)d75d_ndc_maxy=mxy;
// invisible flag + oml + vertex color range
if ((rdp.other_mode_l & (3u << 24)) == ((uint32_t)G_BL_0 << 24) &&
(rdp.other_mode_l & (3u << 20)) == ((uint32_t)G_BL_CLR_MEM << 20)) d75d_emit_invis++;
if (!d75d_oml_set) { d75d_oml = rdp.other_mode_l; d75d_oml_set = 1; }
for (int i = 0; i < 3; i++) {
int cr=v_arr[i]->color.r, cg=v_arr[i]->color.g, cb=v_arr[i]->color.b;
if (cr<d75d_cmin)d75d_cmin=cr; if (cr>d75d_cmax)d75d_cmax=cr;
if (cg<d75d_cmin)d75d_cmin=cg; if (cg>d75d_cmax)d75d_cmax=cg;
if (cb<d75d_cmin)d75d_cmin=cb; if (cb>d75d_cmax)d75d_cmax=cb;
}
}
// called at emit with the texunit-0 bound flag
static void d75d_note_emit_z(uint32_t f, struct LoadedVertex* const* v_arr, int has_tex, int comb_utex) {
d75d_init();
if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return;
d75d_frame_check(f);
for (int i = 0; i < 3; i++) {
float w = v_arr[i]->w;
if (w == 0.f) { d75d_znan++; continue; }
float nz = v_arr[i]->z / w;
if (!std::isfinite(nz)) { d75d_znan++; continue; }
if (nz<d75d_zmin)d75d_zmin=nz; if(nz>d75d_zmax)d75d_zmax=nz;
}
if (d75d_use_tex < 0) d75d_use_tex = has_tex;
if (d75d_comb_utex < 0) d75d_comb_utex = comb_utex;
d75d_comb = rdp.combine_mode;
d75d_prim[0]=rdp.prim_color.r; d75d_prim[1]=rdp.prim_color.g; d75d_prim[2]=rdp.prim_color.b; d75d_prim[3]=rdp.prim_color.a;
d75d_env[0]=rdp.env_color.r; d75d_env[1]=rdp.env_color.g; d75d_env[2]=rdp.env_color.b; d75d_env[3]=rdp.env_color.a;
}
static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* vertices) {
const size_t d75_di0 = dest_index; /* D75 probe: loop below mutates dest_index */
SUPPORT_CHECK(n_vertices <= MAX_VERTICES);
@@ -1780,6 +1926,13 @@ static inline int gfx_lod_tile_offset(const int i) {
}
static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bool is_rect) {
/* D75D: only real model triangles (all indices < MAX_VERTICES); the
* fullscreen-quad helpers use indices MAX_VERTICES+0..3 and would skew maxtri. */
if (vtx1_idx < MAX_VERTICES && vtx2_idx < MAX_VERTICES && vtx3_idx < MAX_VERTICES) {
uint32_t d75d_mx = vtx1_idx > vtx2_idx ? (vtx1_idx > vtx3_idx ? vtx1_idx : vtx3_idx)
: (vtx2_idx > vtx3_idx ? vtx2_idx : vtx3_idx);
d75d_note_tri(videoGetFrameCount(), d75d_mx);
}
struct LoadedVertex* v1 = &rsp.loaded_vertices[vtx1_idx];
struct LoadedVertex* v2 = &rsp.loaded_vertices[vtx2_idx];
struct LoadedVertex* v3 = &rsp.loaded_vertices[vtx3_idx];
@@ -1807,6 +1960,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
bool any_behind_camera = (v1->w < 0) || (v2->w < 0) || (v3->w < 0);
if (!any_behind_camera && (v1->clip_rej & v2->clip_rej & v3->clip_rej)) {
// The whole triangle lies outside the visible area
d75d_note_rej(videoGetFrameCount(), 0);
return;
}
@@ -1994,16 +2148,19 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
switch (rsp.geometry_mode & G_CULL_BOTH) {
case G_CULL_FRONT:
if (cross <= 0) {
d75d_note_rej(videoGetFrameCount(), 1);
return;
}
break;
case G_CULL_BACK:
if (cross >= 0) {
d75d_note_rej(videoGetFrameCount(), 1);
return;
}
break;
case G_CULL_BOTH:
// Why is this even an option?
d75d_note_rej(videoGetFrameCount(), 1);
return;
}
}
@@ -2384,6 +2541,8 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
struct GfxClipParameters clip_parameters = gfx_rapi->get_clip_parameters();
d75d_note_emit(videoGetFrameCount(), v_arr); // survived all rejection gates -> reaches GL
d75d_note_emit_z(videoGetFrameCount(), v_arr, (used_textures[0] || used_textures[1]) ? 1 : 0, comb->used_textures[0] ? 1 : 0);
for (int i = 0; i < 3; i++) {
float z = v_arr[i]->z, w = v_arr[i]->w;
if (clip_parameters.z_is_from_0_to_1) {
@@ -3451,6 +3610,7 @@ static void gfx_run_dl(Gfx* cmd) {
* review alone. Remove once D236 pass 16 concludes. */
g_d236_last_vtx_seg = (uint8_t)(cmd->words.w1 >> 24);
gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1));
d75d_note_vtx(videoGetFrameCount(), C0(0, 16) / sizeof(Vtx), C0(16, 4));
break;
case G_DL: {
if (C0(16, 1) == 0) {