diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 1cbc4402..3cdc9f90 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -37,6 +37,8 @@ D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-lef D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —" D229,OPEN,"Water on IsWater levels renders green and pulses blue<->green on an interval (M-101, user playtest, Frigate -level_26; Surface 2 also an IsWater level).","OPEN — pre-existing (A/B proven), mechanism partly identified, not root-caused." D230,OPEN,In-level music quality:,OPEN — not investigated. +D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —" +D234,OPEN,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",OPEN — not root-caused. D228,OPEN,AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"OPEN — newly split out, not investigated." D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —" D203,OPEN,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",OPEN — negative idle repros only; not yet reproduced with input. @@ -114,8 +116,6 @@ D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).","FIX PROPOSED, NOT APPLIED (front.c is owned by another agent this session)." 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). -D233,OPEN,All levels (intermittent):,OPEN — not investigated; needs a GE_PCDUMP capture at a failing doorway -D234,OPEN,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",OPEN — not investigated 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 — not investigated D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 1e0f9cdf..411e7b1d 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -410,6 +410,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D227 | **Sky renders as two independently-moving halves — reads as "two sky processes" (M-93, user QA on PR #43's debug build, `-level_22` Statue and Cradle).** Each half of the screen shows a different sky pattern, and the two halves scroll/move at different rates relative to each other. Surfaced *while* verifying D176(a)'s perspective-correct-interpolation fix — D176(a)'s fix measurably improved the scroll-speed symptom on the visible pattern but did not touch this; this is a distinct defect, not a regression from that PR (unclear yet whether it pre-dates PR #43 or was merely easier to notice once the scroll speed calmed down enough to see the seam). User flagged one confound worth ruling out first: might be a widescreen/aspect-ratio artifact of rendering at modern resolutions rather than a logic bug — original N64 output is 4:3/{something narrower}, and the sky-quad tiling math (`skyPortRenderPoly`, `sub_GAME_7F097388` upstream) may assume the original screen split. | **FIXED (M-100b, PR #43) — root cause was a unit error, and it also fixed the "sky scrolls too fast" complaint (M-96) and the over-tiling.** `unk20`/`unk24` are already in the RDP's native S10.5 1/32-texel units; M-82 read them as texel counts and baked `tc = (S - fold) * 32`, making every sky texcoord 32x too large and overflowing the `s16` `tc`. Human play-test confirmed by the user on the previously-failing levels. Full write-up: §H "D227 — M-100b". Historical investigation trail below. **OPEN — visual evidence captured + a concrete mechanistic hypothesis found (M-94), not yet fixed.** Headless `-level_22` capture confirms a real, visible crease/discontinuity in the cloud texture — not a rendering artifact of viewing it live, and present in a single static frame (rules out the "just camera pan" explanation for at least this instance). Screenshot: `docs/img/bugs/d227-sky-seam-statue.png`. **Leading hypothesis:** `skyPortRenderPoly` (D176(a)'s fix) computes `wScale` per call — i.e. per individual triangle in the sky fan (`skyRenderTri`/`skyRenderFull` issue up to 5 separate `skyPortRenderPoly` calls per frame from a 5-vertex fan, `sky.c:900-920`/`1372-1393`) — from that triangle's own 3 vertices' `unk0c`. The position math is exact for any `wScale` (verified algebraically: `clip.x = (ndcX·w/wScale)·wScale = ndcX·w` regardless of `wScale`), so this is not a logic bug in the fix itself, but the `s16`-quantized `ob[]` storage means each triangle's shared-edge vertices get **independently re-quantized at a different granularity** depending on that triangle's own `wScale` — a vertex sitting on the seam between two triangles can round to a measurably different `ob[]` value in each triangle's independent call, producing a small position/continuity crack exactly at triangle boundaries. This would read as "two adjacent regions with different patterns" without requiring two draw calls or two cloud layers — one `skyPortRenderPoly` call producing a slightly different quantization per neighboring primitive is sufficient. **Not yet confirmed** — did not isolate camera motion from sky-scroll motion cleanly enough to independently verify the "each half moves at a different rate" part of the user's report, only the static crease. **M-95: FIXED (the static-crease part) + headless-verified.** Implemented the fix direction above: `skyPortBeginFan()`/`skyPortEndFan()` (new, `#ifdef PORT`, `src/game/sky.c`) compute one shared `wScale` across every vertex in the whole fan (`sp274[]`/`sp94[]`, up to 5 verts), called once right after each fan's vertex array is populated and before its 1-3 `skyRenderTri`/`skyRenderFull` calls; `skyPortRenderPoly` now uses that shared value when set, falling back to its old per-call-local computation otherwise (defensive — no other caller exists today, checked: `grep` finds `skyRenderTri`/`skyRenderFull` used nowhere outside this file). **Verified:** same-frame before/after screenshots at `-level_22` frame 345 — `docs/img/bugs/d227-sky-seam-statue.png` (before, visible kink ~x=1000/1280) vs `docs/img/bugs/d227-sky-seam-statue-fixed.png` (after, same frame, continuous streaks, kink gone). Full 21-level `verify.sh sweep`: 21/21 PASS both before and after this change (no regressions). `bunker1`'s `worst_cell` framediff number is unaffected by this change — reproduced identically on the pre-fix build, it's the already-known stale-golden-baseline issue (see M-84/M-83 notes; owed a re-baseline, unrelated to sky.c). **Cradle checked too:** the "black rectangle" artifact visible there is confirmed pre-existing (identical on a pre-fix build capture) — that's the separate, already-documented FOV-coupled black-sky-past-frustum-edge symptom (D218/D222 family), not this defect; didn't chase it here. **Not fully closed:** did not re-verify the "moves at a different rate" half of the original report post-fix (the M-94 addendum's static-camera diff measurement wasn't repeated on the fix); the aspect-ratio-artifact alternate theory was never separately confirmed or ruled out, though the fix visibly closing the crease at a fixed resolution is itself decent evidence the crease specifically was a code-side seam, not a display-scaling artifact. Human eyeball still owed before merge, same as every other rendering fix landed without display access this session. **M-96: human eyeball done — seam/crack STILL PRESENT on Statue with the M-95 fix in.** The static-crease theory (verified by the frame-345 screenshot diff) is therefore not the whole story, or not the dominant contributor to what's actually visible in live play; the never-repeated "moves at a different rate" temporal theory from the original report is the next thing to check — a single-frame diff can't see it. **D227 NOT resolved; PR #43 stays in draft.** **Also M-96: user reports the N64 sky should be *almost static* and this build's scroll is *extremely fast* — stronger than M-93's "still reads too fast," bears on D176(a)'s fix magnitude itself, not just this seam; possibly the same wrong-magnitude tc-delta drives both symptoms, not yet investigated.** Full write-up: §H "D227 — M-96". **M-98: seam residual FIXED (same bug class as M-95, one field over) + scroll-rate quantitatively measured.** Re-derivation confirmed the tick-rate side is clean: `skyTick()` (unmodified N64 logic) adds `g_ClockTimer` (verified via `frametiming.c` to run ~1/tick under normal load, D193/D204's territory) to `g_SkyCloudOffset` once per logic tick — not a per-render-frame or double-counted path, ruling out a global-timing explanation for "extremely fast" (that would show up as everything sped up, not just the sky). The seam, though: M-95 shared `wScale` across a whole sky fan but **left `skyPortRenderPoly`'s S/T fold (`foldS`/`foldT`) computed per-call, from only that triangle's own 2-4 verts** — the exact same class of bug M-95 fixed for position, just one field over, and never touched. Two triangles in one fan can independently floor a shared vertex's S/T to a different multiple of 64 texels; nominally invisible (GL_REPEAT period 64) but each triangle bakes `(S - fold) * 32` into its own `s16` Vtx buffer, and two folds 64 texels apart produce tc values 2048 apart — large enough to crack at the shared edge. Fix: extended `skyPortBeginFan()`/`skyPortEndFan()` to also compute one shared fold across the whole fan (`s_skyFanFoldS/T`), threaded into `skyPortRenderPoly` the same way as `wScale`. Verified: `-level_22` frame 345 (same frame M-95's before/after used) now shows continuous streaks with no visible crease at the ~x=1000/1280 location, vs. the still-visible crease in `docs/img/bugs/d227-sky-seam-statue.png`; `verify.sh bunker1` PASS (worst_cell unchanged, the known stale-golden-baseline number, not a regression). **Scroll-rate data point (the M-96/M-97-requested measurement):** cross-correlating consecutive-frame captures (`GE_PCDUMP=345-355:1`) at a static camera gives a consistent **~7 px/tick horizontal shift at 640px capture width** — a full 640px screen-width sweep roughly every 90 ticks (~1.5s at 60Hz), which is hard quantitative confirmation of "extremely fast," not just a subjective read. **Root cause of that magnitude is NOT yet found** — `g_SkyCloudOffset`'s accumulation (1 texel/tick) and the vertex S/T geometry it feeds (`sub_GAME_7F097388`) are unmodified N64 logic, so if the rate is genuinely wrong relative to N64, the bug must be in how many on-screen pixels one texel-tick maps to (camera-to-sky-plane geometry, FOV, or a scale baked into the D176(a) Path B substitution) rather than in the accumulator itself — not investigated this pass. Human eyeball still owed on both the seam-close and the (unfixed) scroll-speed complaint. **M-99: human playtest — seam STILL PRESENT after M-98, AND confirmed view-angle-dependent (moves as the mouse/view moves); also reported still bad on Cradle and present on Surface.** Ruled out the cross-quad water/cloud scale-mismatch theory on the two `IsWater==0` levels tested (Statue, Cradle) — untested on any `IsWater==1` level. Headless camera-pan repro via `GE_INPUTSCRIPT` confirmed NOT to work (doesn't reach real mouselook) — this needs either a human playtest or a new debug hook to chase further. Full write-up + ranked candidate list for next session: §H "D227 — M-99". **M-100: ROOT CAUSE FOUND AND PROVEN — `s16` texture-coordinate overflow.** A user-supplied emulator-vs-ours screenshot pair on Surface (same framing) showed the real symptom is a *radial starburst*, not a crack — which redirected the investigation off the quantization-mismatch track M-94→M-98 had been on. New `GE_D227V` per-vertex probe on the Cradle frame-350 camera: the sky is one quad spanning **29,830 texels in S and 29,929 in T**, but `Vtx.tc` is S10.5 in an `s16` = **±1024 texels**, so **7 of its 8 texture coordinates overflow and wrap to arbitrary values**; the fold cannot help against a span 29x the representable range. The quad is split `gSP2Triangles(0,1,3, 0,3,2)` on the 0–3 diagonal and vertices 0 and 3 are exactly the two that keep one coordinate in range — hence "two halves with different patterns, split by a line", the original D227 report verbatim. All four w positive, so no projective pole. Apparent view-dependence needs no view-dependent mechanism: S/T depend on view, so the wrap point moves. **Confirmed by diagnostic build:** baking tc at 1-texel instead of 1/32-texel granularity (32x range, deliberately wrong scale) makes the starburst and the split *completely disappear* (`docs/img/bugs/d227-cradle-tcscale-diagnostic.png`) — no second defect hiding behind it. **D227 == D176(a) "Defect 2" == M-98's "Cradle tower starburst"; M-98 misfiled that capture as unrelated, which is what made D227 look headlessly-unreproducible — it reproduces statically.** M-95/M-98 stay (per-fan consistency is what makes subdivision seamless). **Fix not implemented:** subdivide until span < ~960 texels/primitive; subdivision math and cut placement are worked out (S is linear in w, so cut uniformly in w — uniform-in-screen is badly wrong), but the vertex budget is an open decision (unchecked 50 KB/frame pool; pure tessellation wants ~40% of it). See §H "D227 — M-100". **M-100b: FIXED — and the real cause is one step further back: a UNIT ERROR.** M-82 ("Defect 1") read `unk20`/`unk24` as texel counts and baked `tc` as `(S - fold) * 32` to convert to S10.5. They are not texels: `skyRender` sets them as `worldpos * 0.1f` and the N64 path hands them to the RDP coefficient block unscaled (`skyRenderTri`: `S * unk34 * sp368`), so they are **already in the RDP's native S10.5 1/32-texel units**. That stray `* 32` made every sky texcoord 32x too large and caused **three symptoms tracked as separate bugs**: (1) D227 (spans of ~30,000 *texels* vs an `s16` holding ±1024 → overflow → starburst); (2) over-tiling (~466 repeats of the 64-texel cloud per quad instead of ~15); and (3) **M-96's "the N64 sky is nearly static, ours races"**, measured at ~7 px/tick in M-98 — `g_SkyCloudOffset` advances 1 unit/tick, which is 1/32 texel, not 1. **The scroll complaint and D227 were the same bug**, which is why M-98's tick-path re-derivation kept coming back clean: the accumulator was never wrong, its unit was. **Fix:** `tc = (S - fold)` 1:1, fold rebased to a whole tile period (2048 units); the span then fits an `s16` unaided — no tessellation, no tile shift, which is also why the original N64 code never needed either. **Verified:** Cradle frame 350 (`docs/img/bugs/d227-cradle-fixed-m100.png`) soft cloud bands at correct scale, no starburst/split/moiré; `GE_D227V` reports `kS=kT=0` and max tc 31,513 of 32,767; Statue frame 345 clean; `verify.sh bunker1` PASS. M-95/M-98 kept (per-fan consistency); the tile-shift machinery kept as a graceful-degradation safety valve (only ~4% headroom left on the observed span). **Human play-test still owed** — both prior "fixes" passed static checks and failed live. See §H "D227 — M-100b". | | D229 | **Water on `IsWater` levels renders green and pulses blue<->green on an interval (M-101, user playtest, Frigate `-level_26`; Surface 2 also an IsWater level).** Reported alongside green weapon textures on Frigate/Surface 2 — **that half was NOT a bug**: the branch under test was 2 commits behind main and those 2 commits were exactly the D217 fixes (#47 texpool alignment, #49 CI cache palette-content key); after merging main the user confirms the gun is good. | **OPEN — pre-existing (A/B proven), mechanism partly identified, not root-caused.** Built plain `main` with none of the D227 work: `-level_26` frame 340 shows the **same green** (only the tiling density differs), so D229 is independent of D227. The quad is `sky.c`'s `IsWater` block1 (Frigate confirmed `IsWater=1` via `GE_D176` — which also settles M-99's ranked item 1: that path is textured and does go through `skyPortRenderPoly`). Draw state from `sub_GAME_7F09343C` (`unk_092E50.c:278`): tile 0 and tile 1 both RGBA/16b at **the same TMEM 0**, `masks=maskt=5`, tile 1 offset by `gDPSetTileSize(1, uls=90, ult=150, lrs=0, lrt=0)`, 2-cycle `LERP(TEXEL1, TEXEL0, PRIM_LOD_FRAC)` with cycle 2 `= COMBINED * SHADE` and the LOD fraction driven by **`sinf()`** — that sine IS the reported pulsing, so **one texunit resolves blue and the other green** and the sine fades between them. **Ruled out: the shade path** — `skyChooseWaterVtxColour` with measured `env RGB=16,48,96` / `WaterRGB=255,255,150` spans dark blue to pale yellow and can never be green; measured pixels ~`(5,70..175,20..37)` (R and B crushed) put the green on the texel side. **Not established:** why one texunit decodes green when both tiles share TMEM and format. One unproven oddity: `uls/lrs` are `uint16_t` and `width = ((lrs-uls)>>2)+1` gives tile 1 `-22` -> **65514**, though the matching negative `tex_width2` makes the sub-tile-window branch skip and fall through to the correct mask-based 32-texel wrap, so it may be harmless. **Next:** widen the `GE_D172` multitex probe (it filters on `tile1.tmem != tile0.tmem`, equal here, so it never fires) to dump both texunits' decoded state; capture a *sequence* across the sine period, since a single frame is useless (frame 340 reads green on all builds); and check `skywaterimages[2]`'s real format vs the RGBA/16b `sub_GAME_7F09343C` re-declares. **D227 is deliberately decoupled:** M-100b's safety valve takes `allowShift` and the water quad passes FALSE, leaving that path byte-identical to pre-D227 — shifting both tiles was observed live to flatten the cross-fade into constant green. Full write-up: §H "D229 — M-101". | | D230 | **In-level music quality: a bass instrument sounds corrupted on PC, correct on N64 reference (M-105, user reports; repro refined M-106).** Filed to give the post-D77 residual its own label — D77 (in-level music *absence*) is RESOLVED (PR #42, missing `return` in `sub_GAME_7F0C0BF0` zeroed the seqp volume on the only in-level trigger path), so "music doesn't play" claims in tracked docs are stale; what actually remains is this quality gap. **M-106 repro, deterministic (not intermittent):** Control's elevator track and its regular in-level track both carry the bad instrument throughout; Cavern's elevator track has it too; Runway's track is otherwise fine but the same instrument enters ~5–7s after the track starts and sounds bad from then on. User confirmed against original N64 reference audio that a bass instrument belongs there on Runway — this is **not** a wrong-instrument/wrong-bank selection (rules out the D77 trigger-path family and the seqp-volume fix's neighborhood); the instrument choice is correct, only its *sound* is wrong on PC. Determinism (same track, same spot, every time) points at a static decode/synthesis bug for this instrument's wavetable/ADPCM data — most likely low-frequency-specific (pitch-shift/downsample artifacting, a wrap/loop-point error, or a clipping/gain issue that only bites bass-range samples) — rather than a live mixing/voice-stealing issue (D147/D152/D202 family), which would be intermittent. The mixer layer and the trigger layer are both proven working by every track that sounds right, so the suspect neighborhood is the seq/instrument-bank decode path (`csplayer.c`/`synthesizer.c`, bank loading), not `port/` audio plumbing. | **OPEN — not investigated.** Repro: Control (elevator + regular track, throughout), Cavern (elevator, throughout), Runway (~5–7s in). Next: isolate the instrument/patch id these three tracks share and diff its wavetable/ADPCM decode against a known-good (non-bass) instrument on the same tracks. | +| 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 (terminals, furniture) in that space still draw (M-106, user QA report; screenshots `docs/img/bugs/20260912-cullingbug-bugged.png` vs `-notbugged.png`).** Root-caused M-107 (session-016TaKy6, port/fast3d only): **fast3d's CPU-side "trivial reject" AND-test in `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp:1455-1460`) has a latent w-sign bug independent of D222/D106.** The per-vertex outcode bits computed in `gfx_sp_vertex` (`x<-w`/`x>w`/`y<-w`/`y>w`/`z>w`, `gfx_pc.cpp:1372-1386`) are only valid half-space tests when `w>0`; for a vertex that has crossed behind the camera plane (`w<0` — common when the camera is close to large room-structure polygons, e.g. walls/ceiling trusses near a doorway) the comparison sense is flipped, so its outcode can come out wrong. If that spurious bit happens to match the other two (genuinely correct) vertices' bits, `v1->clip_rej & v2->clip_rej & v3->clip_rej` fires and the whole triangle is dropped before it ever reaches OpenGL — even though GL's own homogeneous clipper (which *does* handle `w<0` correctly) would have rendered the visible part of it. Confirmed against the two screenshots: same room/camera/ammo-HUD, one frame is missing the room's ceiling trusses/walls (replaced by flat sky) while the console/terminal props in the same room still draw fully — exactly the "big background polys vanish, small prop models don't" split you'd expect, since large near-camera polygons are far more likely to have a vertex cross behind the eye than small self-contained prop meshes. The backface-cull code 6 lines below (`gfx_pc.cpp:1462-1495`) already special-cases this exact `w<0` hazard for its own cross-product sign (`(v1->w<0)^(v2->w<0)^(v3->w<0)`) — the trivial-reject block above it never got the same treatment. **Independent of D222** (D222 is a game-logic frustum-cull bug in `src/game/fr.c`/`bondview2.c` that only manifests at non-default `Video.FovScale`; this is a GPU-pipeline clip-rejection bug in `port/fast3d/` that fires at any FovScale, including the default 100, whenever the camera gets close to a large polygon — matching the report's "all levels, near doors" framing with no FovScale mentioned). Also independent of D106 (D106 fixed the *game-side* portal-visibility bbox computation for a different z==0 edge case in `bg.c`; this is the *renderer-side* per-triangle clip decision, a different stage of the pipeline entirely) — same neighbourhood/symptom vocabulary ("things vanish near a portal-adjacent boundary"), different bug. | **FIXED (port/fast3d/gfx_pc.cpp, `gfx_sp_tri1`) — high confidence on mechanism, UNVERIFIED visually.** Fix: skip the trivial-reject AND-test entirely whenever any of the triangle's 3 vertices has `w<0`, deferring to GL's clipper instead (mirrors the cull code's existing w-sign handling one block below). Change only ever *adds* triangles that were previously dropped — cannot make anything newly invisible, so it's a safe, narrowly-targeted fix with no game-logic touch (fast3d is PC-only code, no N64 build parity to preserve, no `#ifdef PORT` needed). **Build verified green** (`./build-pc.sh` equivalent — MSYS2 mingw64 cmake+ninja, full `ge007.x86_64.exe` link succeeds, only one pre-existing unrelated `-Wformat` warning in the same file). **Could not visually verify** — this worktree has no ROM in `./data/` and no interactive display, so `GE_PCDUMP`/`framediff` against a live Dam/doorway scene was not possible here. **A human must run:** build normally, then `-level_02` (Frigate, matches the screenshot's ship-corridor geometry) or any doorway-heavy interior with `GE_PCDUMP=` before/after this commit, walk up close to a room boundary/wall, and confirm the flicker is gone. | +| D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** Investigated M-107 (session-016TaKy6, port/fast3d scope): **NOT root-caused — no fast3d-side mechanism found that plausibly explains "only the first of N repeated instances renders."** Ruled out: fast3d has no matrix/model instance cache that could explain a first-instance-only bug (the D217-family "keyed by address, not content" cache-bug class doesn't recur here — the *only* content-addressed cache in fast3d is the texture cache, already excluded from this investigation per the M-106 triage note); the modelview matrix stack (`gfx_pc.cpp:105`, depth 11, push properly gated `< 11` at `gfx_pc.cpp:1220`) is correctly bounded and would affect every level's nested-DL geometry, not just Dam's towers specifically; the D233 w<0 clip-rejection bug (above) doesn't fit either — towers are viewed from a distance along the dam, not close enough for a camera-behind-vertex condition, and D233's mechanism is per-triangle/per-frame flicker, not a stable "always the same instances missing" pattern. **Leading hypothesis, NOT verified:** GE's road/structure-instancing for repeated architecture typically goes through the room-graph's bytecode "vis program" (`VISOP_*` opcodes interpreted in `bg.c` ~L4450-4620, e.g. `VISOP_PUSH_IF_ROOM_IN_RANGE`, `VISOP_ADD_VISIBLE_ROOM`) or through per-instance room/portal definitions in the level's compiled BG data — both are level *data*, not code reachable via static analysis without the ROM (this worktree has none extracted). Could equally be a per-instance object/prop transform or cull-volume bug in `src/game/propobj.c`/`chrprop.c` (out of this task's `port/fast3d/` scope, and out of the `src/game/*` model/anim exclusion in the brief regardless). **No fix attempted — do not have enough evidence to touch code.** | **OPEN — not root-caused.** Next step needs either (a) a `GE_PCDUMP`/BG-data dump on `-level_10` (Dam) with the ROM present, comparing tower 1 vs tower 2+'s room/portal/object records, or (b) a targeted `port/src/preprocess/` or level-data probe distinguishing "renders once, never again" (matrix/cache bug) from "never queued for rendering" (visibility-program/data bug) for towers 2-N. Likely a separate bug from D233/D222, not the same root cause — see D233's entry for why. | | D228 | **AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix — a separate failure mode from the green wrong-palette defect (M-97, user playtest of PR #47, `-level_34` FACILITY).** Split out of D217 defect ①, exactly as the M-90 write-up anticipated ("do not close D217 defect ① on this merge — reopen/retitle as the AK47 + NPC-world-model white-texture residual"). **Playtest result that motivates the split:** with PR #47's `texInitPool` 8-byte arena alignment in, the *green* symptom is GONE — PP7/PPK grip and hammer render correct, Bond's watch face correct, no green cast on hands or the 3rd-person world model. But **NPC-held AK47s still render with white patches**, i.e. tiles sampling a missing/undecoded texture rather than a wrong-but-present palette. White ≠ wrong colour: a wrong palette binds a real TLUT with wrong contents (D217's mechanism, now fixed), whereas white is the signature of a texture that never got decoded/uploaded at all, or a tile descriptor pointing at an empty/cleared texpool slot. The M-87 matrix described these as distinct from the start ("AK47 viewmodel has white (missing-texture) patches on the magazine + barrel" vs. the green/blue colour corruption) — this playtest confirms they have distinct root causes, since fixing one left the other untouched. | **OPEN — newly split out, not investigated.** Next: `-level_34` with `GE_D217TEX=1`/`GE_TEXDUMP` while an AK47-carrying guard is on screen; check whether the AK47's affected tiles ever reach `import_texture` at all (a decode that never ran → white) versus reaching it with empty/zeroed source bytes (a decode that ran on a cleared arena slot). The texpool two-ended allocator (`image.c` `texLoad`, `leftpos`/`rightpos`) and the per-level weapon-model texture load path are the obvious suspects — an AK47 is not a level-start weapon on Facility, so its textures load later than the PP7's and may be hitting an exhausted or differently-based pool. Compare against a level where the AK47 IS a starting/early weapon. D217 · D161 lineage. | | D204 | **PC audio ran ~2 % below real time permanently: GE's AI feedback loop (`src/audi.c:531`) has a ~3 ms setpoint that PC scheduling jitter clears, so the SDL queue starves and the device pads playback with silence (FIXED, measured).** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd retrace to the audio client) and asks for `g_MinFrameSize`=720 samples per block = 21600/s against a 22050 Hz device — structurally 2 % short, relying on 784-sample top-up blocks that `audi.c` only requests once the reported AI length falls under ~69 frames (3 ms). Fine on N64 (double-buffered AI, exact VI interrupt); on PC the queue empties before the loop reacts, and the padded silence is unrecoverable time. **Fix (F5, port-only):** `audioGetAiLengthBytes()` subtracts a 1024-frame (~46 ms) cushion before reporting, so the loop tops up while slack remains; `audi.c`'s control law untouched (rule #2 clean). Measured A/B in one binary via `GE_D204_OLD`: **rt 0.980 → 1.000, q min 0 → 368, drop 0.** Also: F1 correct AI_LEN_REG single-buffer semantics (robustness, measures as a no-op), F3 `Audio.QueueLimit` 8192→2880 + non-silent drops (NB: an existing `ge007.ini` pins it), F4 oversize invariant guard, and `GE_D204=1` cheap audio-health monitor (`rt`/`q`/`drop`/`max`, safe to leave on for a full playtest). **Falsified in-session, do not re-open:** the u32 wrap at `audi.c:531` is real (high-water 2064 frames > the 789 threshold) but harmless — `frameSamples` is `s16` (`audi.c:145`), so it lands negative and audi.c's own lower clamp catches it; max block stays 3136 B vs the 3156 B allocation, no heap overrun. **Corrects two M-63 claims:** `soundIndex=109` is guards' return fire (count was double-logged; the "32.6 ms cadence" is just the 2880-byte block quantum every `dumppos` is rounded to) — closed negative; and M-63's "13 % of real time" was `GE_MIXERTRACE`'s own 25 MB unbuffered log starving the audio thread (same repro without it: 58.0 s / 60 s). Does NOT explain D202. | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed | | D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** Idle repro attempts on WSL Ubuntu with the actual `dist/goldeneye-pc-port-0.1.0-linux-x86_64.tar.gz` are **negative**: 120 s idle → frame 3300+ clean exit (the intro auto-advances, so "sitting as Bond" is covered), and a ~4-min stick-forward-only run → frame 6900+ also survived. So the trigger needs real gameplay input (fire/action) or is Deck-environment-specific. **Leading suspect: the D191 family** — both known v0.1.0 field crashes are SIGSEGVs in `modelGetNodeRwData` (`model.c:478`) over a truncated/NULL `root->Parent` node link, and both were triggered by *gameplay state* (first guard kill on Bunker ii; post-cutscene on Statue), never by idling; Facility opens into the Ourumov + soldier-squad scripted beat (D193a instance), which fits the trigger profile. Alternatives: Deck GL driver path (RADV/zink vs llvmpipe) or 40/50/60 Hz timing (D193 family). **Next:** (1) WSL run under gdb with a walk+fire `GE_INPUTSCRIPT` — generate the script into a file first (earlier attempts were blocked by a `bash -lc` quoting quirk that emptied the loop var, so no fire buttons ever went out); on SIGSEGV compare the fault PC against `modelGetNodeRwData`; (2) if not reproducible on WSL, get `ge007.crash.log` from the Deck (the Linux build writes it — proven by a stale one in the smoke tree) + ask what Bond was doing at crash time; optionally a gdb run from the SteamOS terminal. | OPEN — negative idle repros only; not yet reproduced with input. | diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 5c34209c..105a7a9d 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -592,6 +592,24 @@ through a converter or a runtime bswap fixup reads scrambled. - Portal near-plane: z==0 clip points project to ±1e20; x86-64 float garbage can come back `min>max` / non-finite and slip past a degenerate-box check that clamps to full-screen on N64 (D106). +- fast3d CPU-side "trivial reject" (`v1->clip_rej & v2->clip_rej & + v3->clip_rej` in `gfx_sp_tri1`) trusts per-vertex outcodes (`x<-w`, + `x>w`, etc.) that are only valid half-space tests when `w>0`. A vertex + behind the camera (`w<0`) flips the comparison sense, so its outcode can + be wrong; if that spurious bit happens to match the other two (correct) + vertices', the AND-reduction drops a triangle GL's own clipper would + have rendered correctly. Bites large near-camera polygons (room + walls/ceilings close to a doorway) far more than small prop models — + reads as "background geometry flickers away, props keep drawing" (D233). + The backface-cull code right below already special-cases this same + w-sign hazard for its cross-product sign; the trivial-reject block + didn't. Fix pattern: skip the AND-test (don't trivially reject) whenever + any vertex has `w<0` — safe, since it only ever adds triangles, never + drops more. General lesson: any CPU-side "skip this triangle" heuristic + in fast3d needs the same w<0-behind-camera exemption the existing + backface culling already has, or it'll silently eat near-camera geometry + exactly where a port is most likely to get scrutinized (doorways, + tight corridors). - **D74 wrap-block is DEAD CODE** (`gfx_pc.cpp:1546/1551`): guard is `cms & G_TX_WRAP` but `G_TX_WRAP == 0`, so always false (line 1887 does it right with `cms == G_TX_WRAP`). And if it did run it indexes diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 6bd11b7c..0c08b44c 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1453,7 +1453,26 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo struct LoadedVertex* v_arr[3] = { v1, v2, v3 }; if ((rsp.extra_geometry_mode & G_NO_CLIPPING_EXT) == 0) { - if (v1->clip_rej & v2->clip_rej & v3->clip_rej) { + /* D233: the outcode bits set in gfx_sp_vertex (x<-w / x>w / y<-w / + * y>w / z>w) are only valid half-space tests when w>0. A vertex + * that has crossed behind the camera plane (w<0) flips the sense of + * those comparisons, so its clip_rej bits can come out wrong. If + * that spurious bit happens to match the other two (genuinely + * correct) vertices' bits, the AND-reduction below fires and drops + * the whole triangle before it ever reaches the GPU -- even though + * OpenGL's own homogeneous clipper (which handles w<0 correctly) + * would have rendered the visible portion of it. This hits large + * polygons near the camera (room walls/ceilings near a doorway, + * where the camera is close enough for a vertex to sit behind it) + * far more than small prop models, matching D233's "room geometry + * intermittently vanishes near doors, furniture/terminals still + * draw" report. Fix: never trust the trivial-reject AND test when + * any vertex has a negative w -- defer to GL's clipper instead, + * same as the backface-cull code just below already does for the + * same w-sign hazard. Worst case for the (rare) mixed-sign case is + * a few extra triangles reaching the GPU; never fewer. */ + 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 return; }