From 8d3bd5732a7276dcf2a65d7d3b617d1a9b09b21b Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:27:22 -0400 Subject: [PATCH] D320/D288: extend GE_D318T with generic pin detector; refine GE_D288 probe; cache bg.c hot-path getenvs D320 (Phase 1 item 1): d318TimelineTick (GE_D318T=1) now also runs a generic D318-class pin DETECTOR for the sweep's flagged lists (Facility ai_19, Control ai_9, Depot ai_12): any chr in ACT_ATTACK with a live-fire attacktype (no TARGET_AIM_ONLY bit) whose modelGetAnimFrame is byte-stable for 600 ticks logs one 'D320T: PIN?' line with AI-list id/offset/atk/ent/ mot/unk54, repeating every further 600 stable ticks. Detection only -- the recovery watchdog stays hardcoded to ai_22 by design (D320's explicit instruction: generalize only after a second list actually freezes in play). Legit states excluded/self-clearing: monologue pre-aims keep the AIM_ONLY bit; working fire cycles re-init through chrlvTickAttackCommon so their frame does not stay byte-stable for 10 s. Smoke: 90 s Facility run, zero false positives, existing D318T timeline unaffected. D288 (Phase 1 item 3): per M-155's revised next step, the GE_D288 probe now skips any_behind_camera==1 triangles (the x/w divide is meaningless for w<=0 -- that subset was pure false-lead noise) and logs the render-mode word (oml) so a live capture can ID the culprit asset against level tables. Re-ran bare -level_20: 1965 sane hits in the 550-755 window, still zero hits for the target color RGB(83,62,44) -- consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; headless probing is at its limit here, live capture is next. bg.c getenv caching (D302-class sweep follow-up): GE_D104 (per room pass, per frame), GE_D63 primary+secondary render logs (per room, per frame), and the two GE_D154 inner-GDL-walk sites (per bullet hit) now use the cached static pattern. All #ifdef PORT blocks; zero behavior change when unset. --- port/fast3d/gfx_pc.cpp | 42 ++++++++------ port/src/d318watchdog.c | 123 ++++++++++++++++++++++++++++++++++++++++ src/game/bg.c | 20 +++++-- 3 files changed, 164 insertions(+), 21 deletions(-) diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 31938c6a..3f3425d8 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1761,23 +1761,32 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo return; } - /* D288 diag (M-152, frame-gated M-155): the silo intro's - * screen-filling stray triangle (bounded to level_20 frames - * ~562-741) isn't explained by this function's existing D233/D106 - * guards on static review -- log any triangle whose clip-space NDC - * bbox covers an unreasonable chunk of the screen, or whose bbox - * math goes non-finite, together with each vertex's w and - * any_behind_camera, to catch it at draw time. M-155: the first - * ungated run fired 116k times in ~1500-2000 frames -- bbox_frac - * >0.15 alone is a common occurrence for ordinary near-camera - * geometry, not a distinguishing signature. Gated to the reported - * frame window (num_dls, same pattern as GE_D236RM below) and the + /* D288 diag (M-152, frame-gated M-155, any_behind-restricted + * M-155-follow-up): the silo intro's screen-filling stray triangle + * (bounded to level_20 frames ~562-741) isn't explained by this + * function's existing D233/D106 guards on static review -- log any + * triangle whose clip-space NDC bbox covers an unreasonable chunk + * of the screen, together with each vertex's w and the render-mode + * word (oml), to catch it at draw time. M-155: the first ungated + * run fired 116k times in ~1500-2000 frames -- bbox_frac >0.15 + * alone is a common occurrence for ordinary near-camera geometry, + * not a distinguishing signature. Gated to the reported frame + * window (num_dls, same pattern as GE_D236RM below) and the * threshold raised well above the ~0.15 "normal" baseline M-155 - * established, so a hit here is actually rare. Remove once D288 is - * root-caused. */ + * established, so a hit here is actually rare. M-155 follow-up: + * for any_behind_camera==1 triangles the x/w perspective divide is + * meaningless (w near zero or negative -> arbitrarily large + * "bbox_frac" for perfectly ordinary geometry), which is exactly + * why the code above defers those to GL's own clipper -- so the + * probe now skips them entirely; a hit here has comfortably + * positive w's and is a genuine large-NDC-bbox triangle. oml is + * logged so the culprit's material/model can be identified against + * the level's asset tables (the flat RGB(83,62,44) color grep + * found nothing in the .c sources -- binary model blobs). Remove + * once D288 is root-caused. */ static int ge_d288 = -1; if (ge_d288 < 0) ge_d288 = getenv("GE_D288") != NULL; - if (ge_d288) { + if (ge_d288 && !any_behind_camera) { extern uint32_t num_dls; if (num_dls >= 550 && num_dls <= 755) { float minx = std::fmin(std::fmin(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w); @@ -1786,8 +1795,9 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo float maxy = std::fmax(std::fmax(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w); float bbox_frac = (maxx - minx) * (maxy - miny) / 4.0f; // NDC quad is [-1,1]^2 if (!std::isfinite(bbox_frac) || bbox_frac > 0.5f) { - fprintf(stderr, "D288: dl=%u tri w=(%.6f,%.6f,%.6f) bbox_frac=%.3f any_behind=%d color=(%d,%d,%d)\n", - num_dls, v1->w, v2->w, v3->w, bbox_frac, (int)any_behind_camera, + fprintf(stderr, "D288: dl=%u tri w=(%.6f,%.6f,%.6f) bbox_frac=%.3f oml=0x%08x color=(%d,%d,%d)\n", + num_dls, v1->w, v2->w, v3->w, bbox_frac, + (unsigned)rdp.other_mode_l, v1->color.r, v1->color.g, v1->color.b); } } diff --git a/port/src/d318watchdog.c b/port/src/d318watchdog.c index b704163c..e3bcd62a 100644 --- a/port/src/d318watchdog.c +++ b/port/src/d318watchdog.c @@ -93,6 +93,10 @@ /* chrai.c defines these but chrai.h does not declare them. */ extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList); +/* src/bondaicommands.h:467 -- kept local so this port file doesn't pull in + * the AI-command header just for one bit constant. */ +#define D320_TARGET_AIM_ONLY 0x0020 /* "Aim at target instead of firing" */ + /* chraction.c internal (not in chraction.h): the fresh-attack entry point. * Signature from the definition; call form mirrors chrlvTickAttack's own * re-init at chraction.c:7401. */ @@ -434,6 +438,125 @@ void d318TimelineTick(void) (unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed, (unsigned)objectiveregisters1, (unsigned)h, (int)g_ActiveChrsCount); } + + /* D320: generic D318-class pin DETECTION for the sweep's flagged lists + * (Facility ai_19, Control ai_9, Depot ai_12 -- see findings D320). + * Detection only: it logs, it never intervenes; the recovery watchdog + * above stays hardcoded to ai_22 by design until a second list actually + * freezes in play (D320's explicit instruction). + * + * Signature: a chr in ACT_ATTACK whose attacktype is LIVE FIRE (no + * TARGET_AIM_ONLY bit -- it is supposed to be shooting) and whose + * modelGetAnimFrame is byte-stable for 600 consecutive ticks (10 s). A + * guard that should be firing but whose animation never advances is the + * exact D318 frozen shape on any list. The two legitimate long-stable + * states are excluded or self-clearing: monologue pre-aims keep the + * AIM_ONLY bit (excluded), and a working fire cycle re-inits through + * chrlvTickAttackCommon's "endframe <= frame" block, so its frame does + * not stay byte-stable for 10 s. One line at first detection, then one + * per further 600 stable ticks while it persists; a frozen run keeps + * printing, an escaped run stops after the single line (or none). */ + { + typedef struct { s16 chrnum; u32 framebits; s32 stable; s32 nextlog; } + D320Pin; + static D320Pin s_pins[8]; + static int s_npins = 0; + s32 i, j; + + for (i = 0; i < g_ActiveChrsCount; i++) + { + ChrRecord *c = &g_ActiveChrs[i]; + union { f32 f; u32 u; } fr; + D320Pin *p = NULL; + + if (c->actiontype != ACT_ATTACK || c->model == NULL) + { + continue; + } + if ((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY) + { + continue; /* legitimate hold pose (monologue pre-aim) */ + } + + fr.f = modelGetAnimFrame(c->model); + for (j = 0; j < s_npins; j++) + { + if (s_pins[j].chrnum == c->chrnum) + { + p = &s_pins[j]; + break; + } + } + if (!p) + { + if (s_npins < 8) + { + p = &s_pins[s_npins++]; + } + else + { + continue; /* table full: drop the newest, keep tracking */ + } + p->chrnum = c->chrnum; + p->framebits = fr.u; + p->stable = 0; + p->nextlog = 600; + } + + if (p->framebits == fr.u) + { + p->stable++; + } + else + { + p->framebits = fr.u; + p->stable = 0; + p->nextlog = 600; + } + + if (p->stable >= p->nextlog) + { + bool g = FALSE; + s32 aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1; + + osSyncPrintf("D320T: t=%d PIN? c%d aiid=0x%04x%s off=%d atk=0x%x ent=%d " + "mot=%d unk54=%u frame=%.2f stable=%d ticks (D318-class pin candidate)\n", + (int)g_GlobalTimer, (int)c->chrnum, + (unsigned)aid, g ? "G" : "", (int)c->aioffset, + (unsigned)c->act_attack.attacktype, + (int)c->act_attack.entityid, + (s32)c->act_attack.type_of_motion, + (unsigned)c->act_attack.unk54, + (double)fr.f, p->stable); + p->nextlog = p->stable + 600; + } + } + + /* Drop entries for chrs that left the tracked state so a later + * re-entry starts a fresh confirmation window. */ + for (i = 0; i < s_npins; i++) + { + bool alive = FALSE; + + for (j = 0; j < g_ActiveChrsCount; j++) + { + ChrRecord *c = &g_ActiveChrs[j]; + + if (c->chrnum == s_pins[i].chrnum && c->actiontype == ACT_ATTACK + && c->model != NULL + && !((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY)) + { + alive = TRUE; + break; + } + } + if (!alive) + { + s_pins[i] = s_pins[--s_npins]; + i--; + } + } + } } #endif /* PORT */ diff --git a/src/game/bg.c b/src/game/bg.c index bff3d848..3476f08d 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -651,7 +651,9 @@ Gfx *sub_GAME_7F0B3C8C(Gfx *gdl) /* TEMP D104: room render-pass geometry-visibility probe. */ { static s32 d104c = 0; - if (getenv("GE_D104") && (d104c++ % 600) == 0 && d104c < 12000) { + static int d104on = -1; /* D302-class cache: this runs per room pass, per frame */ + if (d104on < 0) d104on = getenv("GE_D104") != NULL; + if (d104on && (d104c++ % 600) == 0 && d104c < 12000) { s32 rr; osSyncPrintf("D104 tick=%d curRoom=%d roomsDrawn=%d budget=%d :", d104c, (s32)g_CurrentPlayer->curRoomIndex, @@ -2952,7 +2954,9 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index) #if defined(PORT) /* TEMP D63: log primary GDL at render time (env GE_D63=1) */ - if (getenv("GE_D63")) { + static int d63on_p = -1; /* D302-class cache: per room, per frame */ + if (d63on_p < 0) d63on_p = getenv("GE_D63") != NULL; + if (d63on_p) { u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info; osSyncPrintf("D63 bgRenderRoomPrimary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", room_index, (void *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info, @@ -2997,7 +3001,9 @@ Gfx *bgRenderRoomSecondary(Gfx *gdl, s32 room_index) #if defined(PORT) /* TEMP D63: log secondary GDL at render time (env GE_D63=1) */ - if (getenv("GE_D63")) { + static int d63on_s = -1; /* D302-class cache: per room, per frame */ + if (d63on_s < 0) d63on_s = getenv("GE_D63") != NULL; + if (d63on_s) { u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info; osSyncPrintf("D63 bgRenderRoomSecondary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", room_index, (void *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info, @@ -3509,7 +3515,9 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV idx[2] = (((u8 *) gdl)[7] / 10) - vtxoff; #endif #ifdef PORT - if (getenv("GE_D154")) + static int d154on = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */ + if (d154on < 0) d154on = getenv("GE_D154") != NULL; + if (d154on) osSyncPrintf("D154 TRI1 w1=%08x idx=%d,%d,%d\n", (unsigned)(u32)gdl->words.w1, (int)idx[0], (int)idx[1], (int)idx[2]); #endif @@ -3694,7 +3702,9 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV } #endif #ifdef PORT - if (getenv("GE_D154")) + static int d154on4 = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */ + if (d154on4 < 0) d154on4 = getenv("GE_D154") != NULL; + if (d154on4) osSyncPrintf("D154 TRI4 s2=%d w0=%08x w1=%08x idx2=%d,%d,%d\n", (int)s2, (unsigned)(u32)gdl->words.w0, (unsigned)(u32)gdl->words.w1, (int)idx2[0], (int)idx2[1], (int)idx2[2]);