Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
152 lines
11 KiB
Markdown
152 lines
11 KiB
Markdown
# A7 Fix D round 2 — REAL cause found (object sun+ambient + torch REACH), CHECKPOINT
|
||
|
||
**Date:** 2026-06-19 **Branch:** `claude/thirsty-goldberg-51bb9b` (NOT merged — held at the visual gate)
|
||
**Predecessor:** `docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md`
|
||
**Status:** checkpointed at user request after pinning the root cause. D-1..D-4 are committed +
|
||
correct but **did NOT fix the visible symptom** — they were the wrong subsystem.
|
||
|
||
---
|
||
|
||
## ✅ RESOLVED 2026-06-19 (second session) — the "torch REACH" theory was WRONG; real cause = retail does NOT torch-light OUTDOOR objects at all
|
||
|
||
**The open question is settled, and it overturns this checkpoint's own hypothesis. The fix is NOT
|
||
"shorten torch reach" — it is "outdoor objects receive NO torches."**
|
||
|
||
**Empirical (acdream side, headless dat dump `HoltburgTorchFalloffProbeTests`):** the Holtburg
|
||
neighbourhood has **27 static lights, raw dat Falloff ∈ {3,5,6}** — the dominant orange entrance
|
||
torch (setup `0x020005D8`, colour `(1,0.588,0.314)`) is **Falloff 6** (17 of 27). acdream reads
|
||
this **faithfully** — `LightInfoLoader` just copies `info.Falloff`, no stray ×1.5. There is **NO
|
||
Falloff-4 torch anywhere in Holtburg**, so the predecessor's "retail orange = falloff 4" could not
|
||
be a *different* falloff-4 torch. Both clients read the same dat float → acdream's reach is NOT
|
||
inflated. So "acdream 6 vs retail 4" was a red herring.
|
||
|
||
**Decomp (retail side, read verbatim + corroborated by an independent adversarial workflow
|
||
`wf_07289ba4`):** retail's per-object torch binder `minimize_object_lighting` (0x0054d480) is
|
||
**gated** in `RenderDeviceD3D::DrawMeshInternal` (0x0059f398) by `if (Render::useSunlight == 0)`.
|
||
The OUTDOOR landscape stage runs `Render::useSunlightSet(1)` (`PView::DrawCells` 0x005a485a, right
|
||
before `LScape::draw`), so when the building EXTERIOR shell is drawn
|
||
(`LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell 0x0059f140 → DrawBuilding 0x0059f2a0 →
|
||
CPhysicsPart::Draw → DrawMeshInternal`), torches are **SKIPPED** — the only active light is the
|
||
**sun** (`useSunlightSet(1)` enables `add_active_light(0xffffffff, 0)` = sun + ambient only). The
|
||
static vertex bake (`SetStaticLightingVertexColors` 0x0059cfe0) is **EnvCell-only** (sole caller
|
||
`DrawEnvCell` 0x0059f1f6). **So retail lights outdoor objects with SUN + ambient ONLY — never the
|
||
wall torches.** This exactly explains the checkpoint's own isolation result ("object point lights
|
||
OFF → building matches retail"): retail's outdoor facade gets ZERO torch energy. (Confirming the
|
||
non-bug nature of reach: retail's free-object *hardware* path `config_hardware_light` 0x0059ad30
|
||
uses `Range = falloff × rangeAdjust(1.5)` = LONGER than acdream's ×1.3, with `Diffuse = color×100`
|
||
and att `1/d` — that would blow the facade WHITE if enabled, which is further proof retail never
|
||
enables it outdoors.)
|
||
|
||
**The three retail lighting regimes (now all mapped):**
|
||
1. **EnvCell walls** → static bake (`calc_point_light`, range `falloff×1.3`, wrap, capped), no sun.
|
||
→ acdream mode 1 (EnvCell). ✓ already correct.
|
||
2. **Indoor objects** (`useSunlight==0`) → torches (hardware, no sun). → acdream mode 0 **indoor**.
|
||
3. **Outdoor objects** (`useSunlight==1`) → sun + ambient, **NO torches**. → acdream mode 0 **outdoor**.
|
||
acdream's mode-0 path applied sun **AND** torches to ALL objects — wrong for both 2 and 3.
|
||
|
||
**THE FIX (shipped this session):** in `WbDrawDispatcher.ComputeEntityLightSet`, gate per-object
|
||
torch selection on the object being INDOOR (`ParentCellId` is an EnvCell, `(id&0xFFFF)>=0x0100`)
|
||
via the pure predicate `IndoorObjectReceivesTorches`. Outdoor objects (building shells — ParentCellId
|
||
null; outdoor scenery; outdoor creatures) keep the all-(-1) light set ⇒ sun + ambient only = retail.
|
||
The indoor "no sun" half is already handled by the global sun-kill when the player is inside a cell
|
||
(`UpdateSunFromSky`, intensity 0). Divergence-register row **AP-43** added (documents the residual:
|
||
acdream keys sun/torch on the object's own cell + a per-frame player-inside sun-kill, vs retail's
|
||
per-draw-STAGE `useSunlight` — only matters for through-doorway look-ins). Tests:
|
||
`WbDrawDispatcherTorchGateTests` (7✓), `HoltburgTorchFalloffProbeTests` (dat dump). Build green;
|
||
App 280✓/1skip, Core 1486✓/2skip. **Awaiting the user visual side-by-side at Holtburg before merge.**
|
||
|
||
**DO-NOT-RETRY (this session's corrections to the checkpoint below):** do NOT shorten torch reach /
|
||
change `Falloff×1.3` — acdream reads the dat faithfully and the bake reach is correct for EnvCells.
|
||
The building is an OUTDOOR object; retail gives it no torches. The original checkpoint's "tighten reach
|
||
to ~5m, keep torches ON" plan (below) is SUPERSEDED — keeping torches ON for the outdoor shell at any
|
||
reach is the bug.
|
||
|
||
---
|
||
|
||
## TL;DR — what the visible bug actually is (and is NOT)
|
||
|
||
The user's symptom (Holtburg meeting-hall facade too bright/warm/washed-out + character backs
|
||
lit) is **NOT** the EnvCell bake, the per-channel clamp, the half-Lambert wrap, or the SSBO leak.
|
||
Those are the D-1..D-4 path. **The visible surfaces are mode-0 OBJECTS**, and the cause is:
|
||
|
||
1. **Building facade over-bright** = the **torch REACH is too long** (acdream ~7.8 m vs retail
|
||
~5.2 m), so each entrance torch floods the whole small facade instead of a tight pool.
|
||
**CONFIRMED by isolation**: gating object (mode-0) point lights OFF made the building match
|
||
retail ("looks much better", user 2026-06-19).
|
||
2. **Character backs / slight object over-bright** = the **sun + ambient on objects** (mode 0
|
||
runs both). Ambient is NOT the culprit (it MATCHES retail exactly — see values). The residual
|
||
is small for the character (it ~matches retail), so the dominant visible bug is #1 (torches).
|
||
|
||
## Render-path facts (source-verified, workflow `wf_c4ad8cf8`)
|
||
|
||
- **Building EXTERIOR** = a flat-mesh `WorldEntity` with `IsBuildingShell=true`, `ParentCellId=null`,
|
||
built from `BuildingInfo.ModelId` (`LandblockLoader.cs:79-90`), drawn by **WbDrawDispatcher**
|
||
which hard-sets `uLightingMode=0` (`WbDrawDispatcher.cs:898`). It is **NOT an EnvCell** — so
|
||
**D-4 (EnvCell walls get no sun) never touched it**.
|
||
- **Characters/creatures/players** = ordinary `WorldEntity` dynamics, also drawn by
|
||
WbDrawDispatcher at `uLightingMode=0` (plain Lambert + sun). The mode plumbing is CORRECT
|
||
(mode-0 plain Lambert already zeroes a torch behind a back-face — that part of D-3 works).
|
||
- **EnvCellRenderer** (`uLightingMode=1`, no-sun, wrap) only ever draws **interior** cell shells
|
||
from the dat EnvCell list — never `info.Buildings`, never characters.
|
||
- Render loop: in-world frames go through `RetailPViewRenderer.DrawInside`; the bare
|
||
`WbDrawDispatcher.Draw` (GameWindow.cs:8230) is the no-viewer-cell fallback. Both share the
|
||
ONE `_meshShader` (mesh_modern) program (GameWindow.cs:1845-1857), so `uLightingMode` is one
|
||
shared uniform; each renderer re-sets it before its draws.
|
||
|
||
## Ground truth (live cdb retail + acdream probe, SAME-INSTANT)
|
||
|
||
- **Ambient MATCHES exactly**: acdream `(0.447,0.447,0.495)` == retail `(0.4465,0.4465,0.4951)`.
|
||
→ same sky keyframe → **same time of day; NO time desync** (the earlier "retail 0.3 / acdream
|
||
purple" was sequential-capture drift + acdream's un-synced spawn frame; ignore it).
|
||
- **retail sun** (`world_lights.sunlight` @ 0x008672a0+0x18) = `(0.573, ~0, 0.445)`, magnitude
|
||
**0.725**, colour `(0.98,0.84,0.59)` warm. acdream `sun=1` (active, derived from the same sky
|
||
state via Fix C `|sunVec|=DirBright`). Sun is NOT zero — retail DOES sun-light objects.
|
||
- **retail torches** (golden, a7-fixd-golden2): static, `intensity=100`, `falloff 3/4/5`, warm
|
||
`(1,0.588,0.314)` orange + `(0.98,0.843,0.612)` cream. `calc_point_light` makes a BRIGHT TIGHT
|
||
pool (saturates to full warm to ~4 m, gone by ~5.2 m). Faithful in acdream (LightBake.cs).
|
||
- **acdream torches** ([light-detail]): `range=7.8` (Falloff 6×1.3) and `range=6.5` (Falloff 5).
|
||
acdream `Range = info.Falloff * 1.3f` (`LightInfoLoader.cs:90`) — the 1.3 is correct, NO stray 1.5.
|
||
|
||
## The OPEN question to resolve FIRST on resume (don't guess)
|
||
|
||
acdream's orange torch reads **Falloff 6** (range 7.8); retail's orange torch was captured at
|
||
**Falloff 4** (range 5.2). `6 = 4 × 1.5` (smells like rangeAdjust) BUT they **might be different
|
||
torches** (38 static torches, several orange). **Resolve by comparing the SAME torch's Falloff in
|
||
acdream vs retail, matched by world position** (one focused capture): break/dump acdream's torch
|
||
Falloff for a specific Holtburg torch and the retail `world_lights.static_lights[i].info.falloff`
|
||
for the same one. Then:
|
||
- If acdream reads a **too-large Falloff** for the same torch → fix the dat read / conversion
|
||
(the DatReaderWriter `LightInfo.Falloff` path) so acdream's reach == retail's.
|
||
- If the Falloff matches and reach is genuinely ~7.8 → the building-shell-as-one-object spill is
|
||
the issue; tighten how building shells receive torches (the per-vertex range gate already
|
||
localises, so this is unlikely — favour the Falloff hypothesis).
|
||
|
||
## Proposed fix (after the falloff is confirmed)
|
||
|
||
Tighten acdream's torch reach to match retail (≈5 m), keep torches ON. Building facade then shows
|
||
a tight warm pool by each flame + dark stone elsewhere (retail-faithful). Files: `LightInfoLoader.cs`
|
||
(the Falloff→Range conversion), possibly the DatReaderWriter light read. Add a divergence-register
|
||
row if any conversion deviates. Re-verify visually (the diagnostic that confirmed the cause:
|
||
object point lights OFF == retail-match).
|
||
|
||
## State of the committed work (KEEP — all correct, just off-target for the visible bug)
|
||
|
||
| Commit | What | Verdict |
|
||
|---|---|---|
|
||
| `180b4af` | D-1 clamp point sum on its own | faithful; keep |
|
||
| `39c70f0` | D-2 prep — LightBake conformance test | keep |
|
||
| `cf62793` | D-1 shader clamp | keep |
|
||
| `c62da82` | D-2 EnvCell shell binds own light set (real leak fix) | keep |
|
||
| `b57a53e`/`156dc45` | register AP-35/AP-16 corrections | keep |
|
||
| `0980bea` | D-3 objects plain-Lambert / D-4 EnvCell no-sun | keep; correct but doesn't touch the building (it's an object) |
|
||
|
||
`tools/cdb/a7-fixd-*.cdb` capture scripts are committed. **Diagnostic shader hack reverted**
|
||
(working tree clean). Branch NOT merged — finish the torch-reach fix, visual-verify, then merge.
|
||
|
||
## DO-NOT-RETRY (cost a lot this session)
|
||
|
||
- Don't re-tune the EnvCell bake / per-channel clamp / wrap / SSBO binding for the building — the
|
||
building is a mode-0 OBJECT, none of that path lights it.
|
||
- Don't chase a time-of-day / ambient desync — ambient + time MATCH retail exactly (0.446).
|
||
- Don't "remove the sun" globally — retail DOES sun-light objects (sun 0.725).
|
||
- The visible building bug is the **torch REACH** (confirmed by isolation); start there.
|