acdream/docs/research/2026-07-09-a7-townnet-ambient-verdict-lightcap-handoff.md
Erik 388f3ed307 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>
2026-07-09 10:18:05 +02:00

8.4 KiB
Raw Blame History

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).