D195: restore G_TEXTURE_GEN envmap UV generation -- fixes black reflective materials game-wide

GE uses real N64 RSP environment-mapping (G_TEXTURE_GEN) for every
shiny/reflective material: gold/silver weapon skins, chrome trim, and
Control's reflective console glass. An earlier session (D72.1) removed
the PD-inherited envmap UV-generation block wholesale from gfx_sp_vertex
to fix the Rareware logo, reasoning "GE never generates texture
coordinates from vertex normals" -- true for the logo (lit, but doesn't
set G_TEXTURE_GEN) but false in general. Every envmap draw was left
sampling whatever raw tc[] happened to be authored (near 0), landing on
a fixed corner texel of the reflection texture regardless of view angle
-- reading as solid black for a typically dark-edged reflection map.

Restored the envmap UV generation (ported from the reference Perfect
Dark PC port's already-correct implementation), gated specifically on
rsp.geometry_mode & G_TEXTURE_GEN so D72.1's actual fix is untouched
(the logo never sets this bit) while every other envmap draw gets real
reflection coordinates back.

User-verified live: console glass and gold/silver guns are now visibly
reflective instead of flat black. The remaining "not translucent"
complaint on the glass is confirmed NOT a bug -- it was never see-through
on the original N64 either (reflective by design, not transparent).

Full 21-level sweep crash-free post-fix.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
This commit is contained in:
James Dansereau committed 2026-09-12 21:02:53 -04:00
1 parent 1f7f804733
commit a088bb51e7
4 files changed
+70 -15

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ D190,?,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64)
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —
D194,OPEN,Mouse input needs another tuning pass —,"OPEN — root cause and fix direction known, not implemented."
D195,OPEN,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","OPEN — not investigated (M-49), needs a capture"
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —"
D197,OPEN,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).","OPEN — not investigated (M-49), needs a capture"
D198,FIXED,alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c).,FIXED (src/libultra/audio/load.c) —
D199,PARTIAL,Phase-3 software audio mixer:,"LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
21 D191
22 D193
23 D194
24 D195
25 D197
26 D198
27 D199
+1 -1
View File
@@ -394,7 +394,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D191 | **Windows `-O2` release-bundle crash: SIGSEGV in `modelGetNodeRwData` (`src/game/model.c:478`) — the `root->Parent` node-tree walk (M-49, user playtests of the v0.1.0 win64 bundle).** **Two instances, identical fault PC `0x1400801b1`:** (1) fresh save, killed the first guard right after escaping the cell in Bunker ii; (2) Statue (`-level_49`?), right after the Trevelyan meeting cutscene. `ge007.crash.log` (auto-dump; symbols intact in the shipped exe, `addr2line` resolves): PC = `modelGetNodeRwData` at `model.c:478` — the `switch (root->Opcode & 0xff)`, i.e. `root` (a `ModelNode *`) is bad; `addr2line` puts it in the inlined self-recursion at `:532`, so the `while (root->Parent) { root = root->Parent; … }` walk (`model.c:527-540`) handed a garbage node into the recursive call. Instance 1: `Rdx = 0x70267bc000000000` — a `~0x70266xxx` heap address in the **high 32 bits, zero low half** = 32-bit pointer field read as 64-bit (or a byte-swapped `u32` pair). Instance 2: `Rdx = 0`, FAULT ADDR `0x0` — `root` (or `root->Parent`) resolved to NULL. Both: `Rcx=Rdi` = the `~0x7008xxxx`/`~0x7007xxxx` object ptr, `R8 = 0x1d` (29). So the `Parent` chain in some model contains a **truncated or NULL link** — pointer-width / byte-swap family (D3x, D119 `weapons_held[]->chr` type-pun, D122/D126/D132 propDef-stride). NOT weapon-fire-specific — common factor is `modelGetNodeRwData` running on a character/cutscene model whose node tree has a bad `Parent`. Crash logs saved: `scratchpad/crashlogs/`. **Repro aid:** shipped exe has full symbols; `debug-crash.ps1` in the bundle dir runs it under gdb, `GE_D51=1` → `d52rw.log` rwdata-pool trace. The crash.c EBP backtrace breaks immediately (`#01: 0x1d`) — need a real `bt` + `p *root` + `p *Objinst` at the fault. Not yet reproduced under a debugger; no fix. | FIXED + PLAYTEST-VERIFIED (M-79). User replayed Bunker ii (first-guard kill) and Statue (post-Trevelyan cutscene) on the fix build — **both no longer crash** → 21/21 solo levels crash-free (v0.2.0 gate met; Cradle's remaining issue is D193, not a crash). Build clean; bunker1 golden framediff 3/3. **Real gdb backtrace (M-79):** the fault is NOT in the `root->Parent` walk — `root` arrives *already* garbage (`0x70267a9000000000` = a half-word-swapped `u32` `0x70267a90`) from frame #1 `bondviewSelectCuff(model=0x70076808, header=0x700763a0, switchindex=29)`, called from `gunUpdateAndFire(GUNRIGHT)` on the frame the player fires. `bondviewSelectCuff` byte-indexes `header->Switches` (a `ModelNode *` array — **8-byte stride on PC**, widened per D43/D45) with `offset = switchindex << 2` (N64 4-byte stride), so every `base[N]` deref lands mid-slot and yields a misaligned half-pointer that is non-NULL by luck → `modelGetNodeRwData(root=garbage)`. **Exactly the D140 bug class** — D140 fixed the identical pattern in `gunfire.c:1740`; this call site was missed. Fix: PORT-only `offset = switchindex * (s32) sizeof(ModelNode *)`; the `base = (u8*)switches + offset` math and the `#else` N64 line are unchanged. ABI/pointer-width only (D3x class), no game-logic change. The Statue instance (`Rdx=0`, NULL fault addr) is the same site landing on a slot whose bytes read as 0. |
| D193 | **NPC AI locomotion is too slow — characters don't move to their destinations at the original game's rate (M-49, user playtest; clarified after the FULL 21-level playthrough — "the biggest overall flaw").** Not (primarily) an animation-playback bug — the *travel speed* of scripted/AI movement is wrong: guards, named NPCs (Trevelyan, Ourumov) and friendlies (Natalya) all take noticeably longer to run/walk to their goal positions than on N64. General, not chr-specific. **Leading hypothesis — the D117/D134/D155/D156 wall-clock-timing family:** `chr.c` / `chrai.c` integrate movement as `pos += speed * g_GlobalTimerDelta` and step sim loops `while (i < g_ClockTimer)` / `-= g_ClockTimer` (`chr.c:1437,1446,1491-1502,1862,2359,2569`; `chrai.c:76`). On the port `osGetCount()` is free-running wall-clock (D117), `g_GlobalTimerDelta`/`g_ClockTimer` are derived from it, and D155's `FRAMETIMING_PORT_MAX_CATCHUP = 6` clamps `nextFrameTime`. If the effective per-second sum of `g_GlobalTimerDelta` (or the `g_ClockTimer` tick count) runs low — quantization, the catch-up clamp biting during normal play, or a units mismatch (`g_JP_GlobalTimerDelta` vs `g_GlobalTimerDelta`, `chr.c:1435/1437`) — every delta-scaled AI motion integrates slow while **player** movement (input-driven per frame) still feels right, which is exactly the reported split. Related: **D170** (Ourumov/Trevelyan "flee" looks wrong — flagged there as possibly the same locomotion-speed issue rather than an AI divergence). Also possible: `chrGoToBond/Chr(SPEED_RUN/WALK/SPRINT)` speed enum → path-follow step, or pathfinding advancing ≤1 waypoint per frame-quantized tick. **Instances seen (M-49 full playthrough):** (a) Facility — the post-scripted-scene beat with Ourumov + the soldier squad: the soldiers **walk in super slowly and take a long time to path into firing positions** before they start shooting (this is regular squad combat AI, not a named-NPC script — good evidence the slowdown is general delta-scaled `chr` locomotion / path-following, not one ailist); (b) the named-NPC flee behaviour on Silo (Ourumov) and Cradle (Trevelyan) — see D170; (c) **Natalya (the escort NPC) always walks really slow** — on every escort level (Bunker ii, Statue, Control, Archives) she trails far behind, forcing the player to wait. Escort follow-AI is the same `chrGoToChr`/path-follow path as (a). **WORST CASE — Cradle (`-level_51`) is "bugged out completely" due to Trevelyan's behaviour (M-49).** The final level is Trevelyan-fleeing-and-scripted end to end (he runs the antenna cradle, climbs, drops to the cradle floor, the helicopter beat); if his locomotion timing / path-following is off he never hits the trigger points and the level's scripted progression stalls or goes haywire. This makes D193 effectively a **can't-properly-finish-the-game** bug, not just cosmetic sluggishness. **Next:** log `g_GlobalTimerDelta` + `g_ClockTimer` per frame during normal play and check the 1-second sum vs the N64's ~60; time a guard's run across a known distance vs reference footage; then watch Trevelyan's chr state / current AI command on Cradle. | **FIXED + user-playtest-verified (M-81) — root cause is D209**: `act_ubytes.padding[45]`, a raw-byte union alias for `act_gopos.unk59` (the SPEED tier), lands on offset 45 which is `unk59` on N64 but is byte 5 of `waypoints[1]` at 64-bit — always `0x00`. So the locomotion-animation selector `get_sound_at_range()` always received tier 0 and bound `ANIM_DATA_walking`/`walking_unarmed` for **every AI chr in the game**, and since travel is anim-root-motion driven they all moved at walk pace no matter what the ailist commanded. User verified on Cradle: Trevelyan and the guards run. Full elimination trail below — **steady-state timing keystone MEASURED and the "sim sums low" hypothesis is CONTRADICTED (M-80).** Added a `GE_D193=1` per-wall-second probe in `waitForNextFrame()` (`src/game/frametiming.c`, `#ifdef PORT`, env-gated): logs render frames, sim ticks delivered (= sum of the `deltaFrames` fed to `updateFrameCounters` = sum of `g_ClockTimer`), catch-up clamp hits, and max raw pre-clamp delta. Built ntsc-final, ran `-level_09 / _20 / _27 / _34` idle-after-load, ~20 s each. **Every level, rock-steady: render = 30.0 fps, sim = 60.0 ticks/s, clamped = 0, maxraw = 2.** i.e. each rendered frame carries `g_ClockTimer = 2` and the per-second sum is *exactly* the N64's ~60 — the wall-clock timer is NOT running low in steady state, and `FRAMETIMING_PORT_MAX_CATCHUP` never bites in normal play. VSync on/off made no difference (still 30/60). So the B1 premise ("`g_GlobalTimerDelta`/`g_ClockTimer` sum low over a real second") is wrong for the common case; D193 must come from elsewhere. **Residual quantization risk (not yet observed):** `nextFrameTime = (dt_counts + 387937) / 775875` is integer-rounded per frame with no remainder carry, so a scene that renders at a *steady* rate between 30 and 60 that is not an integeLine truncated
| D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** Two parts. **(a) Aim mode (RMB) is near bang-bang, not proportional** (`port/src/input.c:697-707`): any per-poll delta past `AIM_MOVE_THRESH` emits stick `m = clamp(61 + |Δ|·(MouseAimSpeed/100)·AIM_GAIN, 61, 60+AimBand)` = `[61,80]` — with the M-29 defaults (`MouseAimSpeed=16`, `AIM_GAIN=4.0`, `AimBand=20`) `m` is already 61 (fast) at ~1.5 px/poll and saturates at 80 by ~30 px/poll, so the usable proportional band is tiny and sits near the top of GE's aim range → "way too sensitive," turn rate barely tracks how fast you actually move the mouse. Needs a real response curve (low floor, wider proportional band, maybe a gamma), not a threshold+clamp. **(b) Overall sensitivity correlates with movement/frame rate** (user's words: "correlated with movement speed, does not feel good"): `input.c` emits raw stick counts **per controller poll** with no `g_GlobalTimerDelta` / polls-per-frame normalization (hipfire yaw `sx += edx·(MouseTurnSpeed/100)·6.0` at `:709`; aim as above). GE integrates the stick scaled by its sim delta, and the port's poll cadence vs render/sim cadence drifts with scene load — so effective sensitivity shifts when the framerate does (which reads as "when I'm moving"). Fix direction: accumulate mouse delta and convert to stick using a fixed reference dt (or feed the game a dt-consistent value), so a given hand motion = a given view rotation regardless of fps. Lineage: D118a (hipfire pitch digital vs analog), D165 (menu pointer), D166 (hipfire pitch pulses), D180-B3 (`MouseAimSpeed` 25→16). New adjacent QoL ask, root-caused → **D223** (mouse-wheel direction should reverse weapon cycle direction, currently both directions cycle forward). Config knobs exist (`Input.MouseAimSpeed/AimBand/MouseTurnSpeed/HipfirePitchSpeed`) but the *shape* of the mapping is the problem. **M-87 design-goal note (user):** overall mouse feel should move toward **Quake Remaster / the Perfect Dark PC port** — raw, direct, responsive 1:1 mouse-look — while explicitly **not losing GE's original aiming nuance** (the analog-stick aim-mode vs digital-hipfire split this whole D118/D194 lineage exists to preserve). Reframes the fix direction from "just widen the proportional band" to: normalize by a fixed reference dt first (part (b), the frame-rate coupling — this alone should fix most of "feels unresponsive"), then re-derive the aim-mode response curve, and treat raw hipfire mouse-look (PD/Quake-style, bypassing the N64 digital-stick emulation entirely when the player is *not* in aim mode) as a real design option to evaluate against "keep GE's aim-mode feel," not assume the N64 stick-emulation path is mandatory everywhere. **M-87 — found it: GE already has exactly this control scheme, unused by the port.** User asked why vertical mouse feels less responsive than horizontal; investigation found the port forces `controldef = CONTROLLER_CONFIG_HONEY` (1.1), under which hipfire pitch is genuinely digital-only (C-up/C-down) while yaw is analog — an intentional N64-era asymmetry (D166), not a port bug, but avoidable. **`CONTROLLER_CONFIG_SOLITARE` (1.2) / `GOODNIGHT` (1.4)** — user's own recollection, verified in code — redirect forward/back/strafe-L/R onto digital C-buttons/D-pad (`bondview2.c:5195-5253`, `moveData.digitalStep{Forward,Back,Left,Right}`), which frees the **entire primary analog stick** for continuous, unclamped pitch **and** yaw: `moveData.canNaturalPitch`/`canNaturalTurn` (set `!insightaimmode`, i.e. active outside aim mode, not gated behind holding aim) drive `analogPitch/70.0f` / `analogTurn/70.0f` directly (`bondview2.c:5950`/`6008`) — no threshold, no digital step, no `[61,80]`-style clamp band (that clamp is specific to the *aim-mode* zoom-turn path, a separate codepath, `insightaimmode && stick_y>60`). `analogPitch`/`analogTurn` default to the primary stick's raw X/Y for every 1.x scheme (`bondview2.c:4881-4882`) unless a 2.x dual-controller scheme overrides them from a second controller. **This is architecturally identical to a modern FPS mouselook**: continuous analog look on the stick, discrete step movement on buttons — a legitimate, unmodified original GE control scheme, not a port invention. Recommended fix: force the port's `controldef` to SOLITAIRE or GOODNIGHT instead of HONEY, map WASD to the digital step-movement buttons (already close to how keyboard hipfire movement works today) and mouse X/Y directly to the primary stick's continuous X/Y — this should resolve the vertical-vs-horizontal asymmetry completely (no more digital-pulse emulation needed at all, D166 becomes largely moot) and likely fixes much of "feels unresponsive" too, since it's direct continuous stick input rather than any thresholded emulation. The frame-rate-coupling issue (part b) is orthogonal and still needs fixing regardless of scheme. Cross-ref D166, D22Line truncated
| D195 | **Transparency / alpha surfaces broken on Control (M-49, user playtest)** — Natalya's console room, the glass partitions + translucent projected wall displays. Failure mode not yet specified by the reporter (no `GE_PCDUMP` capture taken). Likely a fast3d render-mode / alpha-blend path defect, not a decode one — neighbours D161 (CI8/LUT decode), D172, D176(b) (both blend-adjacent) but those are texture-format bugs, this reads as a blend-state one; check D128 portal adjacency in case the glass geometry is portal-culled wrong instead. Logged in `GRAPHICS-BACKLOG.md`. **Next:** `GE_PCDUMP` capture on Control near the console room, compare RDP render-mode words against N64 `gmain.s` for the glass/display materials. | OPEN — not investigated (M-49), needs a capture |
| D195 | **Transparency / alpha surfaces broken on Control (M-49, user playtest)** — Natalya's console room, the glass partitions + translucent projected wall displays. Originally reported as a blend-state defect. Reinvestigated this session (M-116) after the user reported the glass rendering **solid black** — traced to a real, separate, much bigger bug: `G_TEXTURE_GEN` (RSP environment-mapped texture coordinates, used by shiny/reflective materials game-wide — gold/silver weapon skins, chrome trim, and this glass) was never implemented in fast3d. D72.1 (an earlier session) removed the PD-inherited envmap UV-generation block wholesale to fix the Rareware logo, reasoning "GE never generates texture coordinates from vertex normals" — true for the logo (lit, but does not set `G_TEXTURE_GEN`) but false in general: every envmap draw was left sampling whatever raw `tc[]` happened to be authored (near 0), landing on a fixed corner texel of the reflection map every frame regardless of view angle — for a typically dark-edged reflection texture, a solid near-black surface. | **FIXED (`port/fast3d/gfx_pc.cpp`, `gfx_sp_vertex`) — restored envmap UV generation gated on the real `G_TEXTURE_GEN` bit** (ported from the reference Perfect Dark PC port's already-correct implementation), so D72.1's actual fix is untouched (the logo never sets this bit) while every other envmap draw gets real reflection coordinates. **User-verified live:** console glass and shiny gold/silver guns are now visibly reflective instead of flat black. Confirmed crash-free on a full 21-level sweep. **The remaining "not translucent" complaint is NOT a bug**: user confirmed after seeing the fixed reflective glass that it was never see-through on the original N64 either — it's a reflective (mirror-like) surface by design, not transparent glass. Traced (before the user's confirmation settled it) to the material's own combine formula, `alpha = (TEXEL0/1 LOD-crossfade alpha) * SHADE_alpha + PRIMITIVE_alpha` — an additive constant term that saturates alpha near 1.0 by design, matching "reflective, mostly opaque" intent, not a blend-state defect. **Closed.** Cross-ref: this same envmap fix likely also explains part of D228 (white NPC-weapon patches) if any of those are chrome/shiny weapon variants — worth checking before further D228 investigation. |
| D197 | **Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).** Reads as `G_TX_CLAMP` not honoured (or a non-power-of-two wrap period) on a head/face texture tile — the texture repeats around the mesh instead of clamping at the UV edge. Not yet isolated to a specific character or texture; no `GE_PCDUMP`/`GE_TEXDUMP` capture taken. Likely fast3d texture-tile setup (clamp/wrap mode bits from the tile descriptor not round-tripped) rather than a data bug, given it's cosmetic and geometry-independent. **Next:** `GE_TEXDUMP` the offending head texture's tile descriptor on Silo, diff clamp/mirror bits against `gmain.s`'s expectations for that material. | OPEN — not investigated (M-49), needs a capture |
| D198 | **`alAdpcmPull`/`_decodeChunk` used `K0_TO_PHYS` (an unconditional `& 0x1FFFFFFF` mask) on 64-bit heap pointers (`src/libultra/audio/load.c`).** `K0_TO_PHYS` is the N64 kseg0→physical mask, valid only because N64 pointers fit in 32 bits; on PC's 64-bit heap it truncates any pointer whose value exceeds 512MB, corrupting the `aLoadADPCM`/`aSetLoop`/`aADPCMdec` DRAM addresses at 3 call sites (`load.c:71`, `:468`, `:474`). Every sibling call site in `env.c`/`resample.c`/`reverb.c` already resolves state pointers via `osVirtualToPhysical` (the port shim, `port/src/libultra.c`), which is bit-identical to `K0_TO_PHYS` for real N64 kseg0 addresses but correctly PC-shimmed. **Fix (ABI-layer, `#ifdef`-free — same call resolves correctly on both platforms):** replace the 3 `K0_TO_PHYS(...)` calls with `osVirtualToPhysical(...)`. No behavior change on N64 (identical result for kseg0 pointers); fixes PC. Surfaced while building the Phase-3 software mixer (D199) — the acmd list these opcodes feed now actually executes on PC instead of being ignored. | FIXED (`src/libultra/audio/load.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation |
| D199 | **Phase-3 software audio mixer: macro-swap `aXxx` opcode execution + reverse-engineered `aSetBuffer` persistent-context semantics (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`).** The RSP audio ucode (`aspMain`) that would normally execute GE's acmd list never runs on PC (`port/src/ucode.c`); adapting the Perfect Dark PC port's macro-swap trick (`docs/dev/AUDIO-PLAN.md`), `include/PR/abi.h` now `#include`s `port/include/mixer.h` under `#ifdef PORT`, which redefines every `aXxx` macro to call an `Impl` function in `mixer.c` immediately against a small software "DMEM" scratch buffer, instead of packing RSP command words for later execution. GE uses the classic IDO libaudio ABI (verified against every call site in `src/libultra/audio/*.c` + `src/libultrare/audio/{env,reverb}.c`), not PD's differently-shaped "naudio New" ABI — DSP math (ADPCM decode, linear resample, linear envelope mix) is the same Nintendo/SGI ucode PD's `mixer.c` already ported; only the ABI/addressing layer is GE-specific and new here. Several opcodes (`aADPCMdec`, `aResample`, `aEnvMixer`, `aLoadBuffer`, `aSaveBuffer`) take only a state/DRAM pointer — their DMEM source/dest addresses and byte counts come from the most recent `aSetBuffer` call(s), which there is no RSP disassembly to check against; the persistent-context shape (`sCtx.{in,out,count,dryR,wetL,wetR}`) was reconstructed purely from reading every `aSetBuffer`+opcode call-site pair across `resample.c`/`load.c`/`reverb.c`/`mainbus.c`/`save.c`/`env.c` (see the field comments on `sCtx` in `mixer.c:52-82`). `aSegmentImpl` is a no-op — GE's audio DRAM addresses are already resolved via `osVirtualToPhysical` before reaching the mixer (see D198), so segment/base never factor into DMEM addressing here. **Risk:** the `aSetBuffer` context reconstruction is an inference, not a verified spec — if a call site this session didn't audit sets the context differently, audio for that path will mix from the wrong DMEM offsets. **Verified:** `-level_09` now runs full music + SFX 120s+ crash-free (post-D200 fix); no cross-level or opcode-level audio-correctness verification done yet (no golden-audio comparison tooling exists). | LANDED (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`) — functional on `-level_09`, context-inference risk noted, needs a wider level sweep |
+19
View File
@@ -1125,4 +1125,23 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
geometry (viewport height/width, screen offset, etc.) back to its decomp
origin — a visually surprising but *exactly reproduced* N64 quirk isn't a
port defect, and the fix is to close the finding, not to write code.
- **D195 — a targeted decomp-side fix (D72) blanket-disabled a whole PC-side
feature instead of gating it on the GBI bit that actually distinguishes the
broken case from the working one.** `gfx_sp_vertex()`'s PD-inherited
envmap (`G_TEXTURE_GEN`) UV-generation block was removed wholesale to fix
the Rareware logo (D72.1), on the reasoning "GE never generates texture
coordinates from vertex normals." That's true for the logo specifically
(lit, but doesn't set `G_TEXTURE_GEN`) but false for the engine in
general: GE uses real RSP envmap generation for every shiny/reflective
material (gold/silver weapon skins, chrome trim, reflective glass), all of
which DO set the bit. Deleting the feature instead of gating it on
`rsp.geometry_mode & G_TEXTURE_GEN` (which the reference PD port already
does correctly) silently broke every one of those materials game-wide —
they'd sample a fixed, usually-dark corner texel forever, reading as solid
black. **Lesson:** when a PD-inherited block causes a wrong result for one
specific draw, check what GBI bit *that draw* fails to gate on before
assuming the whole mechanism doesn't apply to GE — a feature real N64
hardware supports and GE's own assets exercise is not "PD-only" just
because the first investigation happened to hit it on a draw that
shouldn't have taken that path. §F **D195**.
§F **D247**.
+49 -13
View File
@@ -1390,19 +1390,55 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti
d->color.g = g > 255 ? 255 : g;
d->color.b = b > 255 ? 255 : b;
/* D72 (docs/internals.md): unlike the PD port, GE never
* generates texture coordinates from vertex normals. This PD-
* inherited block overwrote the authored tc[] UVs on every
* lit+textured surface (lookat_enabled defaults to true here),
* which smeared the Rareware-logo quads into a diagonal gold
* wedge: their corner UVs (0x0010..0x03F0 = full 32x32 coverage)
* were replaced by normal-derived values. GE's ground truth:
* every DL that could env-map sets gsDPSetTextureLUT(G_TT_NONE),
* stage rendering always has a camera lookat yet stage textures
* are surface-fixed on hardware, and the logo vertex batches
* carry authored atlas-slice UVs. So U/V stay as computed from
* tc[] above. (gSPLookAt state is still tracked — it may matter
* for lighting fidelity later.) */
/* D195/D72 correction: GE DOES use RSP-generated (environment-
* mapped) texture coordinates -- shiny gold/silver weapon skins
* and Control's reflective console glass all set G_TEXTURE_GEN,
* the real GBI bit that gates this on real hardware. D72.1
* originally removed this whole PD-inherited block to fix the
* Rareware logo (a lit surface that does NOT set G_TEXTURE_GEN,
* so it was never supposed to hit this path in the first place)
* but over-corrected: it deleted the feature instead of gating
* it on the bit that actually distinguishes the two cases. With
* no envmap UV generation, every G_TEXTURE_GEN draw sampled its
* texture at whatever raw tc[] happened to be authored (usually
* ~0), landing on the same corner texel every frame regardless
* of view angle -- for a typical dark-edged reflection map, a
* solid near-black surface instead of a shiny/reflective one.
* Gating on G_TEXTURE_GEN preserves D72.1's actual fix (the logo
* never sets this bit, so it's unaffected) while restoring the
* envmap effect for the draws that do. */
if (rsp.geometry_mode & G_TEXTURE_GEN) {
float dotx = 0, doty = 0;
if (rsp.lookat_enabled) {
dotx += vcn->x * rsp.current_lookat_coeffs[0][0];
dotx += vcn->y * rsp.current_lookat_coeffs[0][1];
dotx += vcn->z * rsp.current_lookat_coeffs[0][2];
doty += vcn->x * rsp.current_lookat_coeffs[1][0];
doty += vcn->y * rsp.current_lookat_coeffs[1][1];
doty += vcn->z * rsp.current_lookat_coeffs[1][2];
dotx /= 127.0f;
doty /= 127.0f;
} else {
float tvcn[3];
calculate_normal_dir(vcn, tvcn);
dotx = tvcn[0];
doty = tvcn[1];
}
dotx = clampf(dotx, -1.0f, 1.0f);
doty = clampf(doty, -1.0f, 1.0f);
if (rsp.geometry_mode & G_TEXTURE_GEN_LINEAR) {
dotx = acosf(-dotx) / 4.0f;
doty = acosf(-doty) / 4.0f;
} else {
dotx = (dotx + 1.0f) / 4.0f;
doty = (doty + 1.0f) / 4.0f;
}
U = (int32_t)(dotx * rsp.texture_scaling_factor.s);
V = (int32_t)(doty * rsp.texture_scaling_factor.t);
}
} else {
d->color.r = vcn->r;
d->color.g = vcn->g;