acdream/docs/research/2026-06-20-indoor-lighting-regime-handoff.md
Erik f7f3e0887b docs(lighting): indoor lighting regime handoff — file #142 (windowed-interior regime) + #143 (portal dynamic light)
Clean handoff for the next M1.5 "indoor world feels right" session, picking up
the two indoor-lighting gaps the user spotted at the #140 visual gate.

#142 (PRIMARY): windowed-building interiors + look-ins read "like outdoors".
Root cause grounded: retail's lighting regime is per-DRAW-STAGE (PView::DrawCells
draws ALL EnvCells in the useSunlightSet(0) interior stage — torch-lit, no sun,
regardless of SeenOutside), while acdream's is a per-FRAME global keyed on the
player's cell (playerInsideCell). So acdream's windowed interiors (SeenOutside)
+ look-ins stay in the outdoor regime. This is the AP-43 residual surfaced.
Fix direction: make sun+ambient per-draw like AP-43's torches (design fork laid
out for a brainstorm). Resolves AP-43.

#143 (SECONDARY): portal swirl casts no light. acdream registers only static
Setup.Lights; the portal is a retail DYNAMIC light (add_dynamic_light ->
minimize_envcell_lighting). Fix: register a dynamic LightSource for portals.

Handoff doc carries the verified retail decomp (useSunlightSet/PView::DrawCells
stages), current acdream line refs, the three gaps, the fix fork, validation
plan, and DO-NOT-RETRY. Neither issue is a regression from #140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:17:59 +02:00

188 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Indoor lighting regime — HANDOFF (#142 windowed-interior regime, #143 portal dynamic light)
**Date:** 2026-06-20 **Base:** `main` @ `31d7ffd` (A7 #140 + all D.5 work; pushed to both remotes)
**Milestone:** M1.5 "Indoor world feels right" **Start with: #142 (issue #1).**
**Predecessor:** `docs/research/2026-06-19-lighting-a7-fixD-round2-torch-reach-CHECKPOINT.md`
(RESOLVED banner — the #140 outdoor fix). Companion: `claude-memory/reference_retail_ambient_values.md`.
## Where we are
`#140` (outdoor building over-bright near torches) is **SHIPPED + user-confirmed + merged + pushed.**
Real cause: retail lights outdoor objects with SUN + ambient only, never torches (the `useSunlight`
gate); fix = gate per-object torch selection on the object being indoor (`IndoorObjectReceivesTorches`,
`WbDrawDispatcher.cs`). Register row **AP-43**.
At the #140 visual gate the user spotted two INDOOR-lighting gaps (the opposite problem — interiors
too DARK / "like outdoors"). Both are this handoff. **Neither is a regression from #140** — that fix
only *subtracts* torch light from *outdoor* objects.
## The unifying insight (read this first)
acdream's lighting **REGIME** (sun on/off + which ambient) is a **per-FRAME global** keyed on whether
the PLAYER is in a sealed cell. Retail's is **per-DRAW-STAGE**: the outdoor stage runs with the sun
on, the interior-cell stage runs with the sun off + torches on. `#140` fixed the **torch** half of
this mismatch *per-object* (AP-43). **#142 is the SUN + AMBIENT half — i.e. the AP-43 residual, now
surfaced as a visible bug.** Finishing #142 lets us delete/narrow AP-43.
---
# #142 (issue #1) — windowed-building interiors read "like outdoors" [PRIMARY]
### Symptom (user, 2026-06-19, at the #140 gate)
> "Agent of Arcanum house — in retail it is much brighter indoors; when looking into the house it is
> lit, same light when you walk in. In acdream it is NOT lit — looking in and when inside it feels the
> same like it is outdoors."
The **meeting hall** (a more sealed interior) looked OK — the user only flagged its portal (#143),
not its walls. That contrast is the key clue (see "the three gaps").
### Retail mechanism (VERIFIED — read verbatim this session)
`PView::DrawCells` (0x005a4840) draws a frame in two ordered stages:
1. **Outside stage:** `useSunlightSet(1)` (0x005a485a) → `LScape::draw` → outdoor terrain/buildings/
objects, **sun on, torches skipped** (the #140 mechanism).
2. **Interior stage:** `useSunlightSet(0)` (0x005a49f3) → `restore_all_lighting` → loop over **every**
EnvCell in `cell_draw_list``DrawEnvCell` (0x0059f170): walls baked
(`SetStaticLightingVertexColors` 0x0059cfe0), objects torch-lit (`minimize_object_lighting`
0x0054d480, enabled because `useSunlight==0` per `DrawMeshInternal` 0x0059f398), **NO sun.**
3. `useSunlightSet(1)` (0x005a4b5d) restores outdoor mode at the very end.
`useSunlightSet(arg)` (0x0054d450): sets `useSunlight=arg`; `arg==1` enables the SUN as the active
hardware light, `arg==0` enables none (sun off).
**KEY FACT:** `cell_draw_list` holds ALL visible EnvCells — windowed (`SeenOutside`) **and** sealed.
Retail draws every interior in the `useSunlight==0` stage. The regime is **per-stage, never per-
building / per-SeenOutside.** So retail torch-lights *every* building interior, including windowed
ones and look-ins viewed from outside.
### acdream current state (per-FRAME global) — current line refs (@31d7ffd)
- `GameWindow.cs:8061` `playerSeenOutside = playerRoot?.SeenOutside ?? true` — the PLAYER cell's flag.
- `GameWindow.cs:8107` `playerInsideCell = playerRoot is not null && !playerSeenOutside`.
- `GameWindow.cs:8122` `UpdateSunFromSky(kf, playerInsideCell)` → (`:10786`) sets the **global** sun +
ambient: inside → sun `Intensity=0` + flat `(0.2,0.2,0.2)` ambient; outside → keyframe sun + outdoor
ambient.
- That ambient is uploaded ONCE per frame to the SceneLighting UBO (`CurrentAmbient.AmbientColor`,
`:8171`) and read by BOTH mode-0 (objects) and mode-1 (EnvCell shells) in `mesh_modern.vert`.
- **Torches are ALREADY per-cell** (AP-43: `IndoorObjectReceivesTorches` `WbDrawDispatcher.cs:2076`,
used at `:2057`; plus `EnvCellRenderer` `SelectForObject`) — independent of `playerInsideCell`. So
the torch half is fine; **only the SUN + AMBIENT are still per-frame-global.**
### The three gaps (all one root: per-frame-global vs per-stage)
1. **Player OUTSIDE, looking INTO any building (look-in):** `playerSeenOutside=true` → outdoor regime
→ the look-in interior gets sun + outdoor ambient. Retail draws look-in cells in the `useSunlight=0`
stage (torch-lit). → "when looking in, not lit."
2. **Player INSIDE a WINDOWED building** (`SeenOutside=true` cells, e.g. Agent of Arcanum):
`playerInsideCell=false` → outdoor regime → interior gets sun + outdoor ambient. Retail:
`useSunlight=0`, torch-lit. → "when inside, feels like outdoors."
3. **Player INSIDE a SEALED building / dungeon** (`SeenOutside=false`): `playerInsideCell=true`
indoor regime → MATCHES retail. ✓ (the meeting hall + dungeons — why they looked right.)
### Cheap validation FIRST (before any code)
- **Confirm the windowed-vs-sealed split is the discriminator.** Verify the Agent of Arcanum is a
WINDOWED building (its EnvCells' `SeenOutside=true`) and the meeting hall is sealed. Dat flag:
`EnvCellFlags.SeenOutside` (hydrated to `ObjCell.SeenOutside`; see `EnvCell.cs` / `PhysicsDataCache.cs`).
We did NOT pin the Agent of Arcanum's landblock this session — either have the user point at it in
game (`[B.4b] pick` line names clicked objects), or extend `HoltburgTorchFalloffProbeTests` to dump
`SeenOutside` per EnvCell across the Holtburg landblocks and find the windowed buildings.
- **`ACDREAM_PROBE_LIGHT=1`** ([light] line logs `insideCell` / ambient / sun) while standing inside
the Agent of Arcanum vs the meeting hall — confirms each gets the regime predicted above.
### Fix direction (BRAINSTORM this — it is a design fork, not a mechanical port)
Make the SUN + AMBIENT **per-draw-context**, mirroring AP-43's per-object torch decision. The renderer
is batched bindless-MDI, so a per-stage global won't work across mixed batches — per-object is the
natural fit (exact same reasoning that put AP-43 per-object; see the #140 explanation). An object/cell
is "indoor" iff its `ParentCellId` is an EnvCell (reuse `IndoorObjectReceivesTorches`). Then:
- **Indoor draws** (mode-1 EnvCell shells; mode-0 objects with EnvCell `ParentCellId`): SKIP the sun +
use the **indoor** ambient (flat `(0.2,0.2,0.2)` / retail indoor). (mode-1 already skips the sun;
it just needs the indoor ambient. mode-0 indoor objects currently ADD the sun — gate it off.)
- **Outdoor draws:** sun + outdoor ambient (as today).
Open design questions for the brainstorm:
- The shader needs BOTH ambients (indoor + outdoor) + a per-instance "indoor" selector. Options:
(a) add an `indoorAmbient` to the SceneLighting UBO + a per-instance indoor bit (a tiny SSBO like
the light-set, or pack into an existing per-instance field); (b) add a third `uLightingMode` (e.g.
`2 = indoor object`: no sun, indoor ambient, torches); (c) compute both and select.
- `UpdateSunFromSky` must stop branching on `playerInsideCell` and instead provide BOTH regimes every
frame (outdoor sun + outdoor ambient AND the indoor flat ambient), so the shader picks per object.
- **Verify retail's indoor ambient** (the `restore_all_lighting` path + the per-EnvCell ambient): is it
the flat `(0.2,0.2,0.2)` we use, or the cell's own authored ambient? Cross-check before locking it.
**This work RESOLVES the AP-43 residual** (regime becomes per-draw → no doorway/look-in mismatch).
Update/delete AP-43 in the same commit.
### Files
- `GameWindow.cs`: `:8061`/`:8107` (`playerInsideCell`), `:8122` + `:10786` `UpdateSunFromSky` (the
regime source), `:8171` (ambient → UBO).
- `src/AcDream.App/Rendering/Shaders/mesh_modern.vert`: `accumulateLights` (sun loop under
`if (uLightingMode==0)` ~`:193`; ambient `uCellAmbient.xyz` ~`:188`). The sun gate + ambient
selection live here.
- `WbDrawDispatcher.cs`: `IndoorObjectReceivesTorches` (`:2076`) — the indoor predicate to reuse;
`ComputeEntityLightSet` (`:2057`).
- `EnvCellRenderer.cs`: mode-1 draws (`uLightingMode=1`) — need the indoor ambient.
- `LightManager` / the SceneLighting UBO layout (`GlobalLightPacker` is the binding-4 helper) — where a
second ambient + the indoor selector would go.
---
# #143 (issue #2) — portal swirl doesn't light the room [SECONDARY]
### Symptom
Inside the meeting hall, retail's portal swirl visibly tints/lights the room; acdream's portal lights
nothing.
### Retail mechanism
The portal swirl is a **DYNAMIC** light. `add_dynamic_light` (0x0054d420) → `insert_light`
(0x0054d1b0) → `world_lights.dynamic_lights`. `minimize_envcell_lighting` (0x0054c170) enables the
cell's DYNAMIC subset (class 2) as hardware lights → tints the EnvCell walls; `minimize_object_lighting`
(0x0054d480) enables dynamics for objects in the cell too. **Captured params** (predecessor cdb,
`tools/cdb/a7-fixd-*.cdb`): the Holtburg portal dynamic light = `intensity=100, falloff=6,
color=(0.784, 0, 0.784)` (magenta/purple).
### acdream gap
acdream registers ONLY static `Setup.Lights` (`GameWindow.cs` ~`:6404` `RegisterOwnedLight`). It
registers **no dynamic lights** — the portal entity casts no light. (`GpuWorldState.cs:101` even
mentions "unregistering dynamic lights" but none are ever registered.)
### Fix approach
Register a dynamic `LightSource` for portal-swirl entities at their world position with the retail
params (or read the portal model's own dat `Setup.Lights` if it carries one — check the portal GfxObj/
Setup first). It then flows through the existing point-light path (`LightManager.PointSnapshot`
`SelectForObject` → shader), lighting nearby EnvCell walls + indoor objects. It is a POINT light, lives
INSIDE a cell → it must light via the indoor path (the EnvCell bake `SelectForObject` already picks any
registered point light near a cell, so registering it may "just work" once it has a `LightSource`).
Find where portal swirls spawn in acdream (the particle/portal emitter spawn path) and attach the light
there; unregister on despawn (`UnregisterByOwner`). Keep it OUT of the AP-43 outdoor-object gate (it's
indoor). Decomp anchors: `add_dynamic_light` 0x0054d420, `minimize_envcell_lighting` 0x0054c170,
`insert_light` 0x0054d1b0.
---
## Decomp anchors (quick reference)
`useSunlightSet` 0x0054d450 · `useSunlight` gate `DrawMeshInternal` 0x0059f398 · `PView::DrawCells`
0x005a4840 (`useSunlightSet(1)` 0x005a485a / `useSunlightSet(0)` 0x005a49f3 / `useSunlightSet(1)`
0x005a4b5d) · `DrawEnvCell` 0x0059f170 · `SetStaticLightingVertexColors` 0x0059cfe0 · `calc_point_light`
0x0059c8b0 (range = falloff × `static_light_factor` 1.3 @ 0x00820e24) · `minimize_object_lighting`
0x0054d480 · `minimize_envcell_lighting` 0x0054c170 · `add_dynamic_light` 0x0054d420 · `insert_light`
0x0054d1b0 · `config_hardware_light` 0x0059ad30 (`rangeAdjust` 1.5 @ 0x00820cc4 — the dynamic/object
hardware path).
## DO-NOT-RETRY / gotchas
- The OUTDOOR torch gate (#140 / AP-43) is correct + user-confirmed — don't touch it.
- Don't shorten `Falloff × 1.3` — acdream reads the dat falloffs faithfully (the reach is correct).
- The regime is a per-FRAME global; the fix is to make sun+ambient **per-DRAW** (per-object/cell),
mirroring AP-43's torch decision — **NOT** to split into separate render passes (fights the batched
MDI; the per-object route is why AP-43 exists).
- Line numbers above are @`31d7ffd` and WILL drift — re-grep `playerInsideCell` / `UpdateSunFromSky` /
`IndoorObjectReceivesTorches` before editing.
## Verification (the acceptance gate)
Visual side-by-side vs retail at the **Agent of Arcanum** (looking IN from outside + walking IN) and
the **meeting-hall portal**. Expected after #142: interiors are torch-lit/warm both looking-in and
inside; windowed buildings no longer "feel like outdoors." After #143: the portal swirl tints the room.
## Pointers
- Register: **AP-43** (`docs/architecture/retail-divergence-register.md`) — the residual this work
resolves.
- `claude-memory/reference_retail_ambient_values.md` — cdb values incl. the portal dynamic-light
capture + the indoor/outdoor ambient numbers.
- `claude-memory/project_render_pipeline_digest.md` — per-cell light + look-in (#124) + flap context.
- #140 CHECKPOINT (above) — the full outdoor-torch story + the verified `useSunlight` decomp.