diff --git a/docs/dev/V0.4.0-PLAN.md b/docs/dev/V0.4.0-PLAN.md index 391f67ec..79f3731d 100644 --- a/docs/dev/V0.4.0-PLAN.md +++ b/docs/dev/V0.4.0-PLAN.md @@ -30,7 +30,7 @@ Sequencing follows M-139's R → Q → F order, split into four phases below. | D252 | Rainbow particles still intermittent on both platforms after the vertex-buffer fix | Second cause unrooted; prime suspect `import_texture_rgba16` fire-tile width/height from tile line size (32×7 vs 16×14). README known issue. | | D245 | Water seam between two patterns, moves with view (REOPENED) | M-142 probe: tc-shift k flips during ordinary rotation; opt-in `GE_D245_FIXEDSHIFT` candidate build-verified, needs A/B vs N64 reference before making it default. | | D303 | Wrong extra long muzzle flash (M16 + others) | **Investigated — pipeline + data ruled out.** Flash path (`gunUpdateAndFire` → `rwmtx[1]/[2]`, `dorottex` DLPRIMARY arms) is byte-identical game code; M16's `MuzzleFlashExtension`=6.0 is the legitimate max stat; Gm16Z flash-node data byte-faithful through d43. Headless runs can't fire weapons (guards don't shoot an idle player), so a TEMP `GE_D303=1` probe in `gfx_pc.cpp` logs every very-tall-thin emitted triangle with full RDP state — **blocked on a live firing capture** (playtest brief item 17). | -| D306/D308 | Z-fighting: Dam intro/truck wheels + general/broader instances | Two reports, likely one family; investigate together. | +| D306/D308 | Z-fighting: Dam intro/truck wheels + general/broader instances | **Investigated — real fighting confirmed, port-vs-faithful open.** Depth precision is equivalent to N64 (24-bit both sides; GE's RSP writes full 16.16 post-transform coords, not s16), so level-authored coincident geometry is the prime suspect and likely fights on N64 too. TEMP `GE_ZF=1` flicker probe in `gfx_pc.cpp` (recurrence-filtered; `GE_ZFBOX` region filter) shows sustained oscillation (counts ~1000) in the Dam-intro truck region. Closing it as faithful needs an N64 reference of the same spot; a PC-only mitigation (float depth) would diverge from N64 and needs a rule-2-style decision. | | D307 | In-level security camera props face backwards on some levels (Bunker) | **Investigated 2026-09-21 — no port-side bug found.** Full orientation path traced (`setupCctv` → per-frame `RotY(unkC8)×unk84`); every input verified N64-faithful against the ROM (PcctvZ switch data, boundpads incl. the legitimate load-time room-scale ×1.8542, record config words); all math helpers are byte-identical `src/game` code. Remaining suspect if it genuinely differs from N64: fast3d handling of the non-orthonormal lens matrix. **Blocked on user repro detail** (which camera/level + screenshot + ideally N64 compare). `GE_D307=1` diagnostic kept in-tree. | | D236 | Surface 1 billboard trees render as a solid wall of texture | **FIXED (M-198, pass 26, `425237f9`):** root cause was a texture-BINDING bug — `TEXTURETYPE_DETAIL` cards drawn from the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; other levels provably untouched. Awaiting user play-test sign-off; probes stripped (`0503d335`). | | D235 | Wrong textures after death/restart (level reload), multiple levels | State-leak on re-entry: texture pool / CI-cache not fully reset. Needs fresh-boot-vs-reload confirmation + surfaces + screenshot, then instrument `texInitPool`/CI-cache clear. | @@ -135,7 +135,11 @@ verification (headless where possible, human eyeball/ear as merge gate): 6. **D303 + D306/D308 z-fighting family** — small visual parity items; batch per commit cadence. (~~D307 camera flip~~ investigated 2026-09-21: no port-side bug found, all inputs verified N64-faithful — blocked on user - repro detail; see Bucket A row.) + repro detail; see Bucket A row.) D303 + D306/D308 both investigated + 2026-07-19: no port-side bug found in either (pipeline/data/precision all + verified N64-faithful); capture probes `GE_D303` and `GE_ZF` are in-tree — + both now blocked on user-side captures (firing repro / N64 reference). + Nothing further to do agent-side until those land. 7. ~~**D236 tree wall**~~ — **done (M-198, pass 26):** `TEXTURETYPE_DETAIL` tile-binding fix in fast3d (`Video.DetailBaseTile`); live-verified, probes stripped. Only the user play-test sign-off remains. @@ -314,6 +318,19 @@ Order by user-visible value: Bunker1/Dam/Depot show only static scene geometry, so the artifact needs a live firing repro. Next: user fires the M16 under `GE_D303=1` and shares the log lines; the spurious quad's RDP state will identify its source. +- **D306/D308 (z-fighting family) investigated — real fighting confirmed, cause + framed.** Verified GE's RSP writes post-transform x/y/z/w as full 32-bit 16.16 + (not s16 8.8), so the port's float pipeline + `GL_DEPTH24_STENCIL8` matches N64 + depth precision — no port-side precision deficit exists. Built a TEMP + `GE_ZF=1` flicker probe in `gfx_pc.cpp` (96×54 screen-cell grid, per-cell depth + history, requires non-monotonic a-b-a oscillation within GE_ZFTOL recurring ≥5× + at the same cell; `GE_ZFBOX` region filter) — earlier center-pair and simple + flicker variants saturated on tessellation noise. Dam (`-level_33`) unattended: + sustained hits in the lower-left truck region with per-cell oscillation counts to + ~970 → D306's "transparent areas" are real depth-test alternation. Prime suspect + is level-authored coincident geometry (fights on N64 too); confirming port-only + needs an N64 reference of the same spot, and a mitigation (e.g. float depth) + would diverge from N64 — parked pending that. - **Plan/doc refresh:** D75 (M-197) and D236 (M-198) marked fixed throughout (Bucket A rows, Phase 1 items 6/7/11, playtest brief); findings index regenerated; README Known Issues to match. diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 1f10e4f2..f2945143 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -183,9 +183,9 @@ D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the D303,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — investigated: pipeline + weapon stats + model conversion ruled out; GE_D303 capture probe in-tree (port/fast3d/gfx_pc.cpp), blocked on a live firing repro." D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…" D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) — -D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated." +D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).",OPEN — confirmed real depth-fighting by the GE_ZF probe (sustained oscillation in the truck region); port-vs-faithful blocked on an N64 reference. D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.",OPEN (investigated 2026-09-21) — -D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated." +D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — investigated: PC depth precision == N64 (24-bit both sides; RSP writes full 16.16), so level-authored coincident geometry is the prime suspect; GE_ZF flicker probe in-tree." D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log). D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2: D311,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 09e0dd3f..d398562b 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -591,9 +591,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D303 | **Some muzzle flashes (M16 named specifically, "and some others") draw wrong — the flash renders very long, extending straight up from the gun (user QA on the v0.3.0 pre-release build, 2026-09-18).** — full `## D303` entry at file tail | OPEN — investigated: pipeline + weapon stats + model conversion ruled out; GE_D303 capture probe in-tree (`port/fast3d/gfx_pc.cpp`), blocked on a live firing repro. | | D304 | **Mouse sensitivity menu overhaul (user request, 2026-09-18): aim speed/turn speed sliders weren't "1:1", had too little range, and (follow-up) risked decoupling into a bad state; F10 menu also had a click-jump bug.** — full `## D304` entry at file tail | FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single `Input.MouseSensitivity` menu row + fixed `overlayUpdateScroll()`'s unconditional bottom-anchor, the actual cause of the reported "menu jumps to the setting below on click"). Build-verified (Windows) both passes; headless overlay smoke test clean after each. **Live F10 playtest done (2026-09-18): user confirmed the collapsed single-sensitivity menu works, the click-jump is gone, and the D251 bottom-row duplication no longer occurs.** | | D305 | **Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, `Game.AllUnlocked` previously enabled on the session/save) — same exact FAULT ADDR 0x62 / `sndHandleEvent` signature as D285, reproduced AFTER D285's own lock fix was already live-verified crash-free on real hardware.** — full `## D305` entry at file tail | FIXED (defensive guard) — `sndHandleEvent`'s preempt-scan (`src/snd.c`) dereferenced its list-walk pointer before ever checking it for NULL (a do-while always runs its body once); D285's `osSetIntMask` lock only prevents another thread from emptying the list mid-scan, not a scan that starts with an already-empty list, so the crash could still happen even with that fix in place. Added the missing `iterState != NULL` guard before the loop, matching D255's established skip-instead-of-dereference precedent. Root desync mechanism (why pool-exhaustion accounting and the list can disagree) still not identified; `Game.AllUnlocked` correlation not proven, only reported. **Live-verified on the Deck (2026-09-18):** user ran two test sessions on Cavern with the updated build, plus toggled `Game.AllUnlocked` off then back on and relaunched — no crash in any of them. | -| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. | +| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | OPEN — confirmed real depth-fighting by the GE_ZF probe (sustained oscillation in the truck region); port-vs-faithful blocked on an N64 reference. | | D307 | **In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18) — NOT the player's view camera.** — full `## D307` entry at file tail | OPEN (investigated 2026-09-21) — no port-side bug found: orientation path traced, every input verified N64-faithful vs ROM (`PcctvZ` switch data, room-scaled boundpads, config words), all math byte-identical `src/game`. Blocked on user repro detail; `GE_D307=1` diagnostic in-tree. | -| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | OPEN — reported/confirmed by the user, not investigated. Explicitly not for v0.3.0; D306 may be one instance of this. | +| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | OPEN — investigated: PC depth precision == N64 (24-bit both sides; RSP writes full 16.16), so level-authored coincident geometry is the prime suspect; GE_ZF flicker probe in-tree. | | D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | **FIXED via D310 — user-playtest-confirmed 2026-09-18** (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. | | D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | **FIXED — user-playtest-confirmed 2026-09-18** (v2: global-list-only sizing via `d310ItemSize`; guards resume, Trevelyan pathing intact, Caverns→intro clean; probe log verified both list classes). PD precedent: PRINT removed, slot 0xAD reassigned. | | D311 | **Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop — D156 guard hole: poisoned `framea` (`model->animframe1`) passes the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).** — full `## D311` entry at file tail | GUARDED (`#ifdef PORT`, D156-guard extension in the same file) — freeze class neutralized; upstream speed/frame poison source OPEN (level reload clears displaced model state). No AI involvement — distinct from D309/D310. | @@ -12228,7 +12228,9 @@ do { **Reported only, not investigated, explicitly not for v0.3.0.** User playtest: the truck's wheels on Dam (seen during the level intro) render oddly, showing what look like transparent areas where they shouldn't be — user's own suspicion is z-fighting. No code read done. May be an instance of the broader z-fighting issue logged separately as D308, or a distinct texture/alpha bug specific to this prop — not distinguished yet. -**Status:** OPEN — logged, no investigation started. Deferred past v0.3.0 by user instruction. +**Investigation (2026-07-19, with D308):** see §D308's investigation block — the two were worked together. The `GE_ZF` flicker probe confirms **real, persistent depth-fighting in the Dam intro's lower-left screen region** (the truck area) — cells there accrue oscillation counts to ~1000 over a 40 s unattended run, i.e. near-equal depths genuinely alternating frame-to-frame, not tessellation noise. The "transparent-looking areas" are consistent with the wheel geometry losing the depth test against coincident surfaces in alternating frames. + +**Status:** OPEN — confirmed as real z-fighting by probe; port-vs-faithful question open (see D308). ## D307 — In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18). @@ -12248,7 +12250,15 @@ do { **Reported/confirmed by the user, not investigated, explicitly not for v0.3.0.** User: "Confirmed occasional z-fighting in some levels due to the level geometry and current state of code" — framed as a general, known-class issue (overlapping/coincident geometry at the current renderer's precision) rather than a single specific bug site. D306 (Dam truck wheels) may be one visible instance of this broader class, not confirmed as the same mechanism. -**Status:** OPEN — logged, no investigation started. Deferred past v0.3.0 by user instruction. +**Investigation (2026-07-19, with D306):** + +- **Depth pipeline precision compared against N64 — equivalent.** GE's RSP (`rsp/graphics/gmain.s` vertex transform) writes post-transform x/y/z/w back to the vertex buffer as **full 32-bit 16.16 fixed-point** (`sdv` stores at +0/+8/+18), NOT s16 8.8 — so the RDP receives full-precision NDC on N64. The port's `gfx_sp_vertex` keeps float (≥ as precise). Both sides write into a **24-bit integer depth buffer** (port: `GL_DEPTH24_STENCIL8`; N64 RDP: 24-bit z-image) with the same near/far from the game's projection matrix. Net: the PC has *no less* depth precision than the N64 — coincident-geometry fighting that exists in the level data fights on both platforms. +- **Probe.** TEMP env-gated `GE_ZF=1` flicker detector in `port/fast3d/gfx_pc.cpp`: a 96×54 screen-cell grid remembers recent per-cell 24-bit depth values; a hit requires non-monotonic oscillation (a-b-a within `GE_ZFTOL`, default 2 units) that **recurs ≥5 times at the same cell** — filters out tessellation gradients and one-off camera-motion coincidences. Optional region filter `GE_ZFBOX="x0 y0 x1 y1"`. +- **Result (Dam, `-level_33`, 40 s unattended, intro plays automatically):** sustained hits in the lower-left screen region (ndc ≈ x∈[-0.1,+0.1], y≈-0.57 — the truck) with per-cell oscillation counts reaching ~970: genuine near-equal depths alternating across hundreds of frames. +- **Open question — port bug or faithful?** Given equivalent precision on both sides, the most likely explanation is **level-authored coincident geometry** (the user's own framing: "due to the level geometry") that fights on N64 too. Proving/disproving requires an N64 reference of the same spot (footage or side-by-side), which is not available in this environment. Secondary suspects if it turns out PC-only: exact `glDepthFunc`/z-range edge cases, or MSAA depth behavior — none evidenced so far. +- **Fix posture if confirmed PC-only:** no game-code lever exists (depth values are the game's); a port-side mitigation would be a precision change (e.g. 32-bit float depth) which would *reduce* fighting but diverge from N64 behavior — needs a rule-2-style decision, not a silent fix. + +**Status:** OPEN — real z-fighting confirmed by probe; port-vs-faithful blocked on an N64 reference of the same spot. ## D309 — Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` (user report + live GDB diagnosis, 2026-09-18). diff --git a/docs/porting-notes.md b/docs/porting-notes.md index f0a79c2d..7c29398f 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1201,6 +1201,12 @@ produce the same visual symptom but need opposite fixes. **Update 2 (same investigation, immediately after): freezing a *value* and freezing a *pointer target* are different operations — treating them the same crashed twice.** Extending the same freeze technique to `render_pos` (a `Model*` field, unlike `field_488.pos` a plain value) by skipping the call that reassigns it (`subcalcmatrices`) seemed equivalent to M-167's "skip the write" but wasn't: `render_pos` is a pointer into a per-tick dynamically-reallocated arena, and skipping its reassignment only works if the pointer it's left holding is still valid — which depends on model lifecycle, not just on this tick's logic. Two fixes in a row tried to detect "is it safe to skip" by inspecting either the pointer's *value* (a sentinel check, `!= NULL && != -1`) or a *cached identity* (comparing the pointer against a remembered "already validated" pointer) — both crashed, because **the allocator reuses model-pointer addresses across genuinely different logical model instances**: a freshly-`CREATE`d model can land at the exact address a previously-removed model occupied, so both the sentinel check and the identity check wrongly conclude "this is already valid" for an instance whose `render_pos` is still an uninitialized tombstone. This is the same "don't guess validity from a value that can coincidentally look right" trap as the paragraph above, encountered a second time from a different angle (a *pointer's* target identity, not a *field's* content). **The robust fix:** never skip the call that establishes the pointer's validity — let it always run — and apply the freeze as a *contents* copy afterward, on top of a buffer that's now unconditionally guaranteed valid. If you must "hold something steady" and that something is reached through a pointer that some other code reassigns, freeze the *pointee's bytes*, not the *act of assignment* — the assignment is usually also the thing keeping the pointer sane. +## D12. Depth precision: the port matches N64 exactly — z-fighting seen on PC is probably faithful (D306/D308) + +When a user reports z-fighting, do NOT assume the software renderer has less depth precision than the RDP. Verified for GE: the RSP's vertex transform (`rsp/graphics/gmain.s`) writes post-transform x/y/z/w back to the vertex buffer as **full 32-bit 16.16 fixed-point** (`sdv` stores), not s16 8.8 — the RDP interpolates from full-precision NDC and quantizes once, into its 24-bit z-image. The port keeps floats through `gfx_sp_vertex` and writes a `GL_DEPTH24_STENCIL8` buffer: same or better precision at every stage, same near/far. So coincident geometry authored into the level fights on both platforms; a PC-only explanation needs positive evidence (an N64 reference of the same spot), not just the existence of the symptom. A mitigation like switching to 32-bit float depth would *reduce* fighting but diverge from N64 behavior — that's a rule-2-style decision, not a silent fix. + +**Probe technique that worked (vs. three variants that didn't):** detecting z-fighting from emitted triangles requires filtering out (a) tessellation gradients — adjacent tris of one surface have near-equal depths by construction — and (b) camera-motion coincidences — as the camera moves, unrelated world points pass through the same screen cell and their depths happen to sit close. What survives both: a coarse screen-cell grid keeping a short per-cell depth history, flagging only **non-monotonic** (a-b-a) oscillation within a small tolerance that **recurs many times at the same cell**. Monotonic drift = gradient; one-off close pairs = motion artifact; sustained recurrence = a real fight. `GE_ZF` in `port/fast3d/gfx_pc.cpp` implements this (`GE_ZFTOL` tolerance, `GE_ZFBOX` region filter). + ## E. Process / method notes - Investigation loop is: reproduce → env-gated capped probe → root-cause diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 5d88a6dd..bf0cbeeb 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1571,6 +1571,84 @@ static void d75d_note_vtx(uint32_t f, uint32_t count, uint32_t dest) { if (dest + count > d75d_batchhi) d75d_batchhi = dest + count; if (d75d_seqlen < 4096) d75d_seq[d75d_seqlen++] = 'V'; } +/* TEMP D306/D308: env-gated z-fighting FLICKER detector (docs/dev/findings.md + * §D306/§D308). A coarse grid of screen cells remembers the depth values seen + * recently at each cell; when a new value arrives that is close to a + * recently-seen-but-different one (within GE_ZFTOL, default 2 units of 24-bit + * quantized NDC z), that is the screen-space signature of genuine z-fighting + * (the winner alternates between two near-equal depths as the camera moves). + * Tessellation noise (adjacent tris of one surface) does not alternate. Gated + * on GE_ZF; optional NDC box filter GE_ZFBOX="x0 y0 x1 y1"; caps output per + * run. Remove once D306/D308 are resolved. */ +static int zf_on = -1; +static int zf_tol = 2; +static float zfbox[4] = { -1.05f, -1.05f, 1.05f, 1.05f }; +static int zfbox_on = 0; +#define ZF_GRIDX 96 +#define ZF_GRIDY 54 +#define ZF_HIST 6 +struct ZfCell { uint32_t hist[ZF_HIST]; uint8_t n; uint16_t osc; }; +static ZfCell zf_grid[ZF_GRIDX * ZF_GRIDY]; +static uint32_t zf_hits = 0; +#define ZF_MIN_OSC 5 // a fight must recur this many times at the same cell +static void zf_note_emit(uint32_t f, struct LoadedVertex* const* v_arr) { + if (zf_on < 0) { + const char* e = getenv("GE_ZF"); + zf_on = (e && e[0]) ? 1 : 0; + const char* t = getenv("GE_ZFTOL"); + if (t) zf_tol = atoi(t); + const char* b = getenv("GE_ZFBOX"); + if (b && sscanf(b, "%f %f %f %f", &zfbox[0], &zfbox[1], &zfbox[2], &zfbox[3]) == 4) + zfbox_on = 1; + } + if (!zf_on || zf_hits >= 600) return; + float cx=0.f, cy=0.f, czw=0.f, wsum=0.f; int ok=0; + for (int i = 0; i < 3; i++) { + float w = v_arr[i]->w; + if (w <= 0.f) continue; + cx += (v_arr[i]->x / w) * w; cy += (v_arr[i]->y / w) * w; czw += (v_arr[i]->z / w) * w; wsum += w; + ok = 1; + } + if (!ok || wsum <= 0.f) return; + cx /= wsum; cy /= wsum; czw /= wsum; + if (cx < -1.0f || cx > 1.0f || cy < -1.0f || cy > 1.0f) return; // on-screen only + if (zfbox_on && (cx < zfbox[0] || cx > zfbox[2] || cy < zfbox[1] || cy > zfbox[3])) return; + uint32_t z24 = (uint32_t)(czw * 255.f); // NDC z in [-1,1] -> 24-bit [0,65535] + int gx = (int)((cx + 1.0f) * 0.5f * ZF_GRIDX); if (gx >= ZF_GRIDX) gx = ZF_GRIDX - 1; + int gy = (int)((cy + 1.0f) * 0.5f * ZF_GRIDY); if (gy >= ZF_GRIDY) gy = ZF_GRIDY - 1; + ZfCell* c = &zf_grid[gy * ZF_GRIDX + gx]; + int dup = 0; + for (uint8_t k = 0; k < c->n; k++) if (c->hist[k] == z24) { dup = 1; break; } + if (!dup) { + // TRUE oscillation: new value close to an OLDER history entry while a + // different value sat between them (a b a), AND the cell's history is + // not a monotonic drift (tessellation gradient of one surface). + int anyup = 0, anydn = 0; + for (uint8_t k = 1; k < c->n; k++) { + if (c->hist[k] > c->hist[k - 1]) anyup = 1; + else if (c->hist[k] < c->hist[k - 1]) anydn = 1; + } + if (!(anyup && anydn)) goto zf_push; // monotonic drift = gradient, not a fight + for (uint8_t k = 0; k + 1 < c->n; k++) { + long dz = (long)z24 - (long)c->hist[k]; + if (dz < 0) dz = -dz; + if (dz > 0 && dz <= (long)zf_tol) { + uint16_t o = ++c->osc; + if (o < ZF_MIN_OSC || (o % 10) != 0 && o != ZF_MIN_OSC) goto zf_push; + zf_hits++; + fprintf(stderr, + "ZF: f=%u z24=%u dprev=%ld osc=%u ndc=(%.3f,%.3f) oml=0x%08x comb=0x%016llx wavg=%.1f\n", + f, z24, dz, (unsigned)o, cx, cy, rdp.other_mode_l, + (unsigned long long)rdp.combine_mode, wsum / 3.0f); + if (zf_hits >= 600) return; + } + } +zf_push: + if (c->n < ZF_HIST) c->hist[c->n++] = z24; + else { memmove(c->hist, c->hist + 1, sizeof(uint32_t) * (ZF_HIST - 1)); c->hist[ZF_HIST - 1] = z24; } + } +} + /* TEMP D303: env-gated detector for the "extra long flash straight up" quad * (M-?? / docs/dev/findings.md §D303). Logs any emitted triangle whose NDC bbox * is tall + thin (aspect > 3, height > 25% of frame) and mostly on-screen — the @@ -2533,6 +2611,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo d75d_note_emit(videoGetFrameCount(), v_arr); // survived all rejection gates -> reaches GL d303_note_emit(videoGetFrameCount(), v_arr); // TEMP D303 + zf_note_emit(videoGetFrameCount(), v_arr); // TEMP D306/D308 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;