From 23da815dda66cd03a7b07fd01ada1168b7c0f39e Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:58:01 -0400 Subject: [PATCH] D236 pass 10: refute "nondeterministic instantiation", it's room occlusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the rule-2-clarification pass-10 brief in findings.md, applied the new three-bucket taxonomy to D280's leading theory that the 0x0c184b50 discrete-tree-card render-mode class instantiates inconsistently across runs of the deterministic -level_36 boot (~4 tris vs ~720). Added a small env-gated diagnostic (GE_D236RM, port/fast3d/gfx_pc.cpp gfx_sp_tri1) counting that class's per-frame triangle count, heartbeat- logged every 300 frames even at zero. Three independent -level_36 launches (one with sustained GE_INPUTSCRIPT camera rotation) produced byte-identical per-frame counts, falsifying the "run-to-run dependent" premise outright. Cross-referencing with the existing GE_D104 room-census probe in the same run shows the count drops to zero at the exact frame the player's curRoom transitions from 12 (17-20 rooms drawn, an outdoor vista) to 18 (1-5 rooms, an enclosed indoor space) via the boot's own scripted intro camera — ordinary portal/room occlusion, not a bug. None of buckets (a)/(b)/(c) apply; no src/game or port/ fix made or warranted. The original "wall not discrete sprites" symptom is untouched by this pass — narrowed the real next step to getting a camera into room 12 facing the tree line directly, in docs/dev/findings.md. Added the generalisable "deterministic boot flag != deterministic camera path" quirk to porting-notes.md §E. findings-index.csv regenerated. --- docs/dev/GE-ENV-PROBES.md | 1 + docs/dev/findings-index.csv | 2 +- docs/dev/findings.md | 75 +++++++++++++++++++++++++++++++++++-- docs/porting-notes.md | 16 ++++++++ port/fast3d/gfx_pc.cpp | 24 ++++++++++++ 5 files changed, 114 insertions(+), 4 deletions(-) diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 701cbdc8..6153c373 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -56,6 +56,7 @@ Two classes: | `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) | | `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) | | `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) | +| `GE_D236RM` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, unconditional getenv, no state kept when unset) | M-155/pass-10 diag for D236: per-frame (`num_dls`) triangle count of the `0x0c184b50` render-mode class (`other_mode_l` exact match), heartbeat-logged every 300 frames even at zero. **REFRAMED D280's "run-to-run dependent instantiation" as ordinary room-based occlusion**, not a CPU-side gating bug — see `## D236` pass-10 writeup. | **live** (D236 OPEN, kept for future passes) | ## Dxx diagnostic probes — finding closed unless noted diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 07ef3781..9930fbfc 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -132,7 +132,7 @@ D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cau D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)." D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).… D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.… -D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).",OPEN — ninth pass closed with no fix; see D276/D280 +D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN — tenth pass refuted the ""instantiation is nondeterministic"" theory (ordinary room-based occlusion, not a bug); original wall-vs-sprites symptom still unresolved; see D276/D280/M-155" D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c) D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)… diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 13668fd8..75e0c993 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -539,7 +539,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** See the other D233 row for full detail. | | D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108), mechanism unexplained.** See the other D234 row for detail. | | D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.… | -| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** — full `## D236` entry at file tail | OPEN — ninth pass closed with no fix; see D276/D280 | +| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** — full `## D236` entry at file tail | OPEN — tenth pass refuted the "instantiation is nondeterministic" theory (ordinary room-based occlusion, not a bug); original wall-vs-sprites symptom still unresolved; see D276/D280/M-155 | | D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…** — full `## D237` entry at file tail | OPEN — not designed; port-only | | D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** — full `## D238` entry at file tail | OPEN — not designed; port-only (`port/src/input.c` + `config.c`) | | D239 | **QoL ask (M-106, user QA): click-to-lock vs always-grab parity.** — full `## D239` entry at file tail | CLOSED — executed in the v0.2.0 prep pass (port-only)… | @@ -10660,9 +10660,9 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A ## D236 — Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report). -**Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** Eight passes so far (D265, D266–D270, D271, D272, D273, D274, D275–D276). Ruled out: IA4/IA8 decode (as originally scoped), the blend pipeline, fog combine (as originally scoped), portal-culling over-draw (D271, real bug, fixed, not the cause), converter/UV-conversion bugs (D272, code-read clean), the CI palette bit-layout (D273/D274, tested twice against real content), "mips absent" (D274, confirmed present/enabled), "identical UV window" *for the CI8 classes* (D272). **Current leading picture (D276, M-134): the actual visible weave at confirmed weave pixels is painted by a different, previously-dismissed class — an IA8 blocky-noise texture (`oml=0xc81049d8`/`fmt=3`) sampled with a fixed, byte-identical 2×-wrapped UV window and low fog, reproducing D265's original "same window" observation exactly (D269's CI8-classes framing had reframed away from that lead prematurely).** A third, unattributed CI8 class (`0x0c184b50`) may be the reference's actual discrete-tree layer and shows an erratic z/w suggesting its own occlusion/billboard bug — D236 may split into two root causes. **Ninth pass (D280, M-135): fog math verified N64-faithful (±2 truncation over 2505 verts), the IA8 noise class visually confirmed as the weave's painter (HF 14.8→2.9 when collapsed), `0xc8102078` ruled out (terrain, 1.4M tris), and the depth-race read inverted — `0x0c184b50` tree cards are IN FRONT of the noise quad (z/w smaller=closer) yet still don't visibly win; under deterministic `-level_36` boot they barely instantiate at all (~4 tris vs 720 in a free run). No quick win: remaining work is CPU-side instantiation/room-streaming, not a port fix.** Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. +**Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** Eight passes so far (D265, D266–D270, D271, D272, D273, D274, D275–D276). Ruled out: IA4/IA8 decode (as originally scoped), the blend pipeline, fog combine (as originally scoped), portal-culling over-draw (D271, real bug, fixed, not the cause), converter/UV-conversion bugs (D272, code-read clean), the CI palette bit-layout (D273/D274, tested twice against real content), "mips absent" (D274, confirmed present/enabled), "identical UV window" *for the CI8 classes* (D272). **Current leading picture (D276, M-134): the actual visible weave at confirmed weave pixels is painted by a different, previously-dismissed class — an IA8 blocky-noise texture (`oml=0xc81049d8`/`fmt=3`) sampled with a fixed, byte-identical 2×-wrapped UV window and low fog, reproducing D265's original "same window" observation exactly (D269's CI8-classes framing had reframed away from that lead prematurely).** A third, unattributed CI8 class (`0x0c184b50`) may be the reference's actual discrete-tree layer and shows an erratic z/w suggesting its own occlusion/billboard bug — D236 may split into two root causes. **Ninth pass (D280, M-135): fog math verified N64-faithful (±2 truncation over 2505 verts), the IA8 noise class visually confirmed as the weave's painter (HF 14.8→2.9 when collapsed), `0xc8102078` ruled out (terrain, 1.4M tris), and the depth-race read inverted — `0x0c184b50` tree cards are IN FRONT of the noise quad (z/w smaller=closer) yet still don't visibly win; under deterministic `-level_36` boot they barely instantiate at all (~4 tris vs 720 in a free run). No quick win: remaining work is CPU-side instantiation/room-streaming, not a port fix.** **Tenth pass (M-155): that CPU-side-instantiation framing itself is REFUTED — three independent deterministic re-runs of the same `-level_36` boot (one with sustained camera rotation) proved the class's per-frame count is byte-identical, not run-to-run dependent; the earlier "~4 vs 720" comparison was an outdoor-vs-indoor room mismatch (the boot's scripted intro walks the camera out of the tree-bearing room at a fixed frame), not nondeterminism. No fix; the original wall-vs-discrete-sprites symptom is unresolved and untouched by this pass.** Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. -**Status (from §F table):** OPEN — ninth pass closed with no fix; see D276/D280 +**Status (from §F table):** OPEN — tenth pass refuted the "instantiation is nondeterministic" theory (ordinary room-based occlusion, not a bug); original wall-vs-sprites symptom still unresolved; see D276/D280/M-155 **Next pass framing (post rule-2-clarification):** @@ -10718,6 +10718,75 @@ REPORT: (a) the exact struct/field/counter gating instantiation, file:line, §A1 if this turns out to be another instance of the pattern. ``` +**Pass 10 result (M-155, 2026-09-16): D280's "instantiation is run-to-run +dependent" premise is REFUTED — it's ordinary room-based occlusion, not a +CPU-side gating bug. No fix; this closes the specific mystery the pass-10 +brief posed, D236 itself stays OPEN on the original "wall not discrete +sprites" symptom.** + +Added a small env-gated diagnostic (`GE_D236RM`, `port/fast3d/gfx_pc.cpp` +`gfx_sp_tri1`, see `GE-ENV-PROBES.md`) that counts triangles per frame +(`num_dls`) whose `other_mode_l == 0x0c184b50` (the discrete-tree-card +render-mode signature from D280), heartbeat-logging every 300 frames even +at zero so a class going silent is visible, not just absent. + +Ran the deterministic `-level_36` boot **three times independently** +(two static, one with `GE_INPUTSCRIPT="1:SRIGHT"` sustained rotation) — +**all three produced byte-identical per-frame counts**: 4 tris/frame for +frames 1-536, 2 tris/frame for frames 537-747, then **zero for the rest +of a 55-60s run** (verified to frame 3299, well past D280's tick-1500 +sample point). This alone falsifies "not pinned by the boot flag" as +stated — the boot IS fully deterministic frame-by-frame; D280's +comparison must have been against session state, not process determinism. + +Cross-referencing with the already-in-tree `GE_D104` room-census probe +(`bg.c:654`, unrelated finding, still live) in the same run pinpoints the +mechanism exactly: **the class's count drops to zero at the same tick +(~9601 controller-reads ≈ frame ~740-748) that `curRoom` transitions from +12 to 18, and `roomsDrawn` collapses from 17-20 rooms to 1-5.** Room 12's +neighbour set (17-20 rooms, several with small portal-window bboxes — +consistent with a big outdoor vista) is where the tree-card class draws; +room 18 (near-fullscreen bbox, 1-5 rooms visible — consistent with an +enclosed indoor space) is where it permanently stops, and stays at zero +through the rest of the capture regardless of continuous `SRIGHT` +rotation. This is the deterministic `-level_36` boot's own scripted +intro camera walking from an outdoor establishing shot into the player's +actual indoor start point — the class legitimately has nothing to draw +once indoors, the same way any outdoor-only geometry would correctly stop +rendering behind walls on real N64. D280's "~4 tris on deterministic +boot vs ~720 in a free run" was comparing this boot's post-intro *indoor* +sample against a live session's *outdoor* one — an apples-to-oranges +comparison, not evidence of nondeterministic instantiation. + +**Bucket classification: none of (a)/(b)/(c) — the room-transition +explanation accounts for the entire observed variance without invoking +an ABI misread, a port-layer timing bug, or a genuine N64 divergence.** +No `src/game` or `port/` edit made or warranted by this pass. + +**What this does NOT resolve:** whether, from *inside* room 12 looking +directly at the tree line, the discrete tree cards draw enough +individual, correctly-placed triangles to look like trees rather than +merging into D276's IA8 noise "wall" — this pass never got the camera +into that outdoor room's tree-facing view (the scripted intro's own path +through room 12 stays at low counts, 4→2, never approaching D280's 720 +figure). **Next pass, concrete:** find (or script) a camera path that +stays in room 12 and faces the tree line directly — e.g. `GE_D104` to +locate room 12's screen-space bbox windows facing the trees, then a +`GE_INPUTSCRIPT` sequence that moves (not just rotates) the player +toward one — and re-run `GE_D236RM` there; a healthy count scaling up +toward the ~720 range as the camera closes on the tree line would confirm +the CPU-side instantiation is fine and the remaining D236 mystery is +purely a rendering/blend-order question (D276's territory); a count that +stays anomalously low even facing the trees head-on from inside room 12 +would be the first real evidence of a genuine instantiation bug and +should restart the bucket-(a)/(b)/(c) check from there. + +Probe left in tree (`GE_D236RM`, env-gated, zero cost unset, cataloged in +`GE-ENV-PROBES.md`). Confidence: HIGH on the room-transition mechanism +(three independent deterministic reproductions, direct room-index +correlation); LOW-to-none on the original "wall vs discrete sprites" +symptom, which this pass didn't reach. + ## D237 — QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash… **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash `Game.NoHitFlash`, and other easily-exposed config keys), and organise rows by category (mouse settings together, video together, etc.).** The overlay (`port/src/optionsoverlay.c`) already renders over the registered `config.c` option list; what's missing is wheel-scroll nav, a broader row set, and grouping headers. Cross-ref `OPTIONS-MENU-PLAN.md` §3 (v1 minimum set) + QOL-INVENTORY "PD has, GE lacks". diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 7d4d7577..9d6cbffe 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1129,6 +1129,22 @@ made mutually acceptable (dual validation / one-time migration). fixed-tick mode was assessed not-narrow (redesigns retrace/tick semantics); design is in §F D117 if someone picks it up, after which `framediff.py --exact` becomes usable. +- **A "deterministic boot" flag (`-level_XX`) pins the level and starting + state, but NOT the camera's later path through it — don't assume two + captures from the same boot flag are comparing the same view.** D236 + pass 10 (M-155) found a render-mode-class triangle count that looked + "run-to-run dependent" across separate captures using the same + `-level_36` boot; three independent re-launches (including one with a + full `GE_INPUTSCRIPT` sustained-rotation sweep) actually produced + byte-identical, fully deterministic per-frame counts — the real + variance was the boot's own scripted intro camera walking from an + outdoor room into an indoor one at a fixed frame, and the two captures + being compared had sampled before vs. after that walk (or against an + unrelated live/free-play session entirely). **Before concluding a + count/state is nondeterministic across runs of the same boot flag, + correlate against a room/position probe (`GE_D104`-style) in the SAME + run** to rule out "the camera simply isn't where you assumed it was + yet" before chasing an instantiation or streaming bug. - **Anything blocking on the scheduler thread throttles the whole sim** (D186, cf. D134). The gfx task runs synchronously on `src/sched.c`'s thread, and that same thread delivers VI-retrace events; any `sysSleep` / diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 68dbd920..79a39d4e 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1637,6 +1637,30 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo } } + /* D236 pass 10 (M-155, TEMP): does the 0x0c184b50 render-mode class's + * per-frame triangle count actually vary frame-to-frame WITHIN one + * deterministic -level_36 boot, or is D280's "run-to-run dependent" + * observation only visible ACROSS separate process launches (which + * would point at room-streaming state that isn't reset/seeded the + * same way twice, not at true per-frame nondeterminism)? Log the + * count once per display list (frame) while it's nonzero. Remove + * once D236 pass 10 concludes. */ + if (getenv("GE_D236RM")) { + static uint32_t d236rm_last_dl = 0xFFFFFFFFu; + static uint32_t d236rm_count = 0; + extern uint32_t num_dls; + if (num_dls != d236rm_last_dl) { + if (d236rm_count != 0 || (num_dls % 300) == 0) { + fprintf(stderr, "D236RM dl=%u count=%u\n", d236rm_last_dl, d236rm_count); + } + d236rm_last_dl = num_dls; + d236rm_count = 0; + } + if (rdp.other_mode_l == 0x0c184b50u) { + d236rm_count++; + } + } + if ((rsp.geometry_mode & G_CULL_BOTH) != 0) { float dx1 = v1->x / (v1->w) - v2->x / (v2->w); float dy1 = v1->y / (v1->w) - v2->y / (v2->w);