docs(A7): Town Network "too dark" root-caused — ambient RULED OUT (live cdb 0.2 white == retail), cause is the 463>128 light cap

Investigation handoff for the deferred A7 per-cell light-scoping fix (#79/#93/#176/#177).
Ambient verified retail-faithful three ways (decomp + dat SeenOutside + live retail
cdb capture: SetWorldAmbientLight level 0x3e4ccccd == 0.2f bit-exact). No fix applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-09 10:18:05 +02:00
parent 5d013dcb10
commit 388f3ed307

View file

@ -0,0 +1,141 @@
# A7 dungeon lighting — Town Network "too dark" ROOT-CAUSED to the light cap (ambient RULED OUT). Pickup handoff.
**Date:** 2026-07-09
**Status:** investigation COMPLETE (report-only). Fix NOT started — deferred to a new
implementation session (this doc + the separate pickup prompt).
**Milestone:** M1.5 — Indoor world feels right. Critical path item **A7 dungeon
lighting (#79/#93/#176/#177)**.
**Read FIRST in the new session:** `claude-memory/project_render_pipeline_digest.md`,
`claude-memory/reference_retail_ambient_values.md`, then the two 2026-07-06 A7 docs
(`docs/research/2026-07-06-176-177-handoff-A7-lighting.md` +
`docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md` — the source-confirmed
retail per-cell light model, **including its two CORRECTION banners**), then this file.
---
## The user's report
> "We are still missing some lighting indoors. It's darker than retail… Not the
> torches, the ambient lighting. I'm checking in the Town Network — we have a
> fountain in the main room but it's much darker and not lit up as in retail."
The user's *observation is correct* (the room IS too dark). The *attribution*
("ambient") is what this investigation tested — and disproved.
## VERDICT: the ambient is retail-faithful. The darkness is the LIGHT CAP.
Confirmed **three independent ways** that acdream's indoor ambient is correct:
1. **Decomp.** `CellManager::ChangePosition` (0x004559B0) sets the world ambient on a
two-way branch on the player cell's `seen_outside` flag: sealed
(`seen_outside==0`) → `SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)` (flat 0.2
white); seen-outside/outdoor → `SetWorldAmbientLight(calc_object_light(),
LScape::ambient_color)` (bright sky ambient). acdream mirrors this exactly in
`GameWindow.UpdateSunFromSky` (flat 0.2 white when `playerInsideCell`).
2. **This cell's dat flag.** The `[light]` probe in the Town Network fountain room:
`insideCell=True ambient=(0.2,0.2,0.2) sun=0 playerCell=0x00070144`. acdream reads
`SeenOutside` faithfully off the player's physics cell (`CellGraph.CurrCell`), so
`insideCell=True` ⇒ the dat flag for 0x00070144 is genuinely "sealed."
3. **Live retail cdb capture (2026-07-09).** Attached cdb to retail acclient.exe
(v11.4186, MATCHES `refs/acclient.pdb`), bp on `SmartBox::SetWorldAmbientLight`
(`?SetWorldAmbientLight@SmartBox@@QAEXMK@Z`; args thiscall: level=float `dwo(@esp+4)`,
color32 `dwo(@esp+8)`). Portalling from outdoors INTO the Town Network:
| phase | levelraw | level | color32 | |
|---|---|---|---|---|
| outdoor | `0x3ee210c9`/`0x3ee2237d` | ≈0.44 | `0xffc864ff` = (200,100,255) purple | sky ambient |
| **Town Network interior** | **`0x3e4ccccd`** | **0.20** | `0xffffffff` white | **matches acdream exactly** |
`0x3e4ccccd` == `0.2f` bit-exact. **Retail runs the Town Network on the same 0.2
flat white ambient acdream does.** Raising the ambient would make acdream *wrong*.
## The REAL cause (the deferred A7 issue, now with a bigger repro)
The `[light]` probe headline: **`registeredLights=463`, cap `MaxGlobalLights=128`,
`activeLights=8`.** The Town Network hub registers **463** fixtures;
`LightManager.BuildPointLightSnapshot(playerWorldPos)` keeps only the **128 nearest the
player** over the WHOLE resident set, evicting ~335/frame. `SelectForObject` (the
faithful per-object ≤8 pick) can only choose from the surviving 128, so the hub's fill
lighting is starved and the room falls back to mostly the (correct) 0.2 ambient →
reads dark. Retail keeps the same 0.2 ambient but scopes its light pool to the
portal-flood **visible cells** (tiny 40+7 cap never bites), so every visible cell keeps
its own fixtures → the room reads bright. This is exactly **#79/#93/#176/#177**, now
reproduced at 463 fixtures (bigger than the Facility Hub's 366 where it was found).
## Current code state (verify — this doc dates fast)
- `src/AcDream.Core/Lighting/LightManager.cs`: `MaxGlobalLights = 128`;
`BuildPointLightSnapshot(Vector3 playerWorldPos)` caps the whole `_all` resident set
at 128 **nearest-player** (camera-invariant — the camera-coupling bug is already
fixed). There is **no** visible-cell scoping right now.
- The visible-cell scoping WAS shipped once (`c500912b`) and **reverted** — the first
attempt scoped by the *camera*-flood and churned the pool as the camera turned (the
#176 seam-floor blink). See the pseudocode doc's CORRECTION #2.
- Renderers already select per-cell (`EnvCellRenderer.cs:~1088`) and per-object
(`WbDrawDispatcher.cs:~2095`) from `LightManager.PointSnapshot`. The only defect is
how `PointSnapshot` is *built*.
## The fix shape (A7.L1 — port the visibility-scoped per-frame collection)
Retail's model (source-confirmed, pseudocode doc §1): each cell owns a light list;
per frame ONLY the visible cells push their lights into a small global player-nearest
pool (40 static + 7 dynamic). The candidate pool is pre-scoped by *visibility*, so the
tiny cap never bites. Port THAT, not a bigger cap:
1. **Build the `[indoor-light]` per-cell composition probe FIRST** (the A7 handoff's
A7.L1 apparatus — prints per-cell active-light SET MEMBERSHIP: cellId → {pos, color,
attenuation, dir, isDynamic}, plus registered/pool/dropped counts). The current
`[light]` probe prints COUNTS, not membership — it can't show which cell lost which
light. Build the membership probe before touching the pool.
2. **Tag each `LightSource` with its owning cell id** (`CellId`, populated at every
registration site from `entity.ParentCellId`; viewer light stays `CellId==0`).
Retail's `add_*_light(info, cellId, frame)` carries exactly this.
3. **Feed the per-frame pool from the currently-visible cells** (the portal-flood set
the renderer already computes), not the whole `_all` set — CORRECTLY this time:
scope by the **player/visible-cell flood**, NOT the camera position (that was the
reverted attempt's bug). The pool is then naturally bounded and the 128 cap stops
biting. Un-skip
`LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`.
4. **(Fix #2, follow-on) static curve for stationary fixtures** — a server-spawned
wall lantern is stationary → static 1/d³ (range×1.3), reserve `isDynamic`
(1/d, range×1.5) for genuinely MOVING lights. This is over-BRIGHTENING, not the
darkness — separate slice.
5. **(Fix #3) hunt the striped floor artifact** with the full pool on (see the
2026-07-06 handoff §3).
## DO-NOT-RETRY / gotchas
- **Do NOT raise the ambient.** 0.2 white is retail-verified three ways (this doc).
- **Do NOT re-scope the pool by CAMERA position** (the reverted `c500912b` bug →
#176 seam blink). Scope by the visible-cell flood / player.
- **Do NOT just raise `MaxGlobalLights`** (128→1024 was tried `4d25e04d`, reverted —
it exposed the through-floor purple wash + stripe artifacts the cap was masking).
- **cdb note for next time:** the `Position→objcell_id` offset used here
(`dwo(dwo(@esp+4))` at `ChangePosition` entry) was WRONG — the cell id printed stuck
at a constant. The `level`/`color32` capture on `SetWorldAmbientLight` was correct.
Find the real `Position` layout (or read the `Render::player_pos` global set at
0x00455ab6) before trusting a cdb-printed cell id.
## Apparatus / evidence produced this session
- Live probe (already in the tree): `ACDREAM_PROBE_LIGHT=1``[light]` counts;
`ACDREAM_PROBE_CELL=1``[cell-transit]`. Fountain-room reading:
`insideCell=True ambient=(0.2,0.2,0.2) registeredLights=463 activeLights=8
playerCell=0x00070144`.
- Retail cdb scripts (throwaway, in the session scratchpad): captured retail interior
ambient = 0.2 white. The `SetWorldAmbientLight` bp recipe above is reusable.
## References
- `docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md` — the retail model
(source-confirmed) + BOTH correction banners. **The spec for the port.**
- `docs/research/2026-07-06-176-177-handoff-A7-lighting.md` — the #176/#177 root cause,
the reverted cap-raise, the tooling built.
- `claude-memory/reference_retail_ambient_values.md` — ambient SSOT (now with the
2026-07-09 live-verified 0.2 interior confirmation).
- Retail anchors: `CellManager::ChangePosition` 0x004559B0;
`SmartBox::SetWorldAmbientLight` 0x004530a0; `CObjCell::add_light` 0x0052b1d0;
`CEnvCell::add_dynamic_lights` 0x0052d410; `Render::insert_light` 0x0054d1b0
(cap 40+7); `Render::minimize_object_lighting` 0x0054d480; `calc_point_light`
0x0059c8b0 (static 1/d³ curve).