Commit graph

4 commits

Author SHA1 Message Date
Erik
d8984e877f fix #176: light pool tracked the camera via flood scoping - collect residents, anchor at player
The seam-floor purple flicker was NOT a draw z-fight. The in-engine
[seam-*] probe (ACDREAM_PROBE_SEAMDRAW - built because RenderDoc cannot
capture this pipeline: it hides GL_ARB_bindless_texture and the
mandatory-modern startup gate throws; AMD GPU rules out Nsight) killed
every double-draw suspect: ONE shell instance per seam cell at the
lifted z, no floor-coincident entity (portal entities sit at z=-12.05),
zero portal depth fans in the sealed Hub. What it caught instead: the
corridor floor's applied light set flipping wholesale with the flood.

Root cause: c500912b scoped BuildPointLightSnapshot by the per-frame
portal flood, on the research doc's gloss of CEnvCell::visible_cell_table
as "the portal-flood visible set". The named decomp refutes the gloss:
add_visible_cell (0x0052de40) DBObj-LOADS absent cells and inserts them;
a cell activation adds itself + its whole dat visible-cell list
(0x0052e228/0x0052e24a); entries leave only via the flush machinery.
It is the RESIDENT-cell registry - gaze can never remove a cell.
add_dynamic_lights (0x0052d410) walks the WHOLE table per frame
(caller 0x00452d30), and insert_light (0x0054d1b0) caps the pool by
distance to Render::player_pos (0x0054d1dd). Retail's pool is a function
of player position only. Ours followed the camera: turning changed the
flood (probe: 8..41 cells across one turn), the six intensity-100
under-room portal purples entered/left the pool, and the wedge blinked.

Fix: BuildPointLightSnapshot(playerWorldPos) collects ALL registered
(=resident) lit lights; over cap keeps dynamics FIRST (retail's separate
7-slot dynamic pool never competes with statics) then nearest-the-player;
the RebuildScopedLights callback is deleted. Live-verified with the probe:
full-circle turn, flood churning 8..41, the floor set held the same 8
identities on every post-spawn frame. The purple wedge SHAPE stays - it
is cdb-proven retail-faithful.

Residual deviation (AP-85 rewritten): single 128 pool vs retail's
7-dynamic/40-static degrade-scaled dual pools - the Hub now shows
7 purples + viewer where retail's cdb showed 4 + viewer + fixture slots;
if the gate reads the wedge as too purple, the A7 dual-pool cap is the
faithful trim.

Pins: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(rewritten to the corrected model),
PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics,
PointSnapshot_OverCap_KeepsNearestThePlayer,
PointSnapshot_ResidentCollection_CellTagDoesNotFilter.
Suites: Core 2599+2skip / App 726+2skip / UI 425 / Net 385.

The [seam-*] probes stay until the visual gate passes, then strip.
Correction banner added to 2026-07-06-a7-per-cell-lighting-pseudocode.md;
outcome banner on the z-fight handoff; ISSUES #176 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:12:31 +02:00
Erik
c500912bf8 feat(lighting): A7 visible-cell light scoping + [indoor-light] probe (NOT the #176/#177 fix)
Port retail's per-frame light collection: the point-light pool is built from ONLY the
currently-visible cells' lights, matching CObjCell::add_*_to_global_lights
(0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a
flat world-space set capped at 128-nearest-camera.

- LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both
  registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics).
- LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff
  CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting
  backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8).
- Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the
  flood resolves prepareCells and before the draws (renderers select from the same
  in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass).
- [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION.
  Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant.

CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe
PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame,
CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool
composition is NOT the cause. #176 real cause = an over-bright purple point light
(intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss
(stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the
camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected;
ISSUES #176/#177 re-diagnosed; render digest banner updated.

Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light
0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md.
Suites green: Core 2595 + 2 skip, App 719 + 2 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 00:35:01 +02:00
Erik
57c2ab735d fix(lighting): #143 — dynamic lights use retail's D3D 1/d attenuation (portal + viewer spread)
The portal swirl's magenta light (and the viewer fill) read as a tight,
concentrated pool vs retail's soft, room-wide tint. Cause: acdream applied its
STATIC dat-bake falloff (1/d^3 distance-cube + range x1.3) to ALL point lights,
including dynamic ones. Retail draws dynamic lights through the D3D hardware
path (config_hardware_light 0x0059ad30): a point light gets Attenuation1=1 =>
att = 1/d (inverse-linear), plain Lambert, range x1.5 (rangeAdjust 0x00820cc4).

Split the two paths by a per-light IsDynamic flag:
- LightSource.IsDynamic; packed into GlobalLight.coneAngleEtc.y (binding=4).
- LightInfoLoader.Load(isDynamic) => range x1.5 + flag (server-object/portal
  lights via the live spawn path); dat-static lights keep x1.3 (default).
- Viewer fill + weenie/portal lights = dynamic; dat torches = static.
- mesh_modern.vert pointContribution: dynamic branch = 1/d att, plain Lambert,
  hard cutoff, no per-light cap (D3D accumulates then saturates via the existing
  min(pointAcc,1)); static branch = the unchanged wrap/norm bake.

This is the portal half of #143 (the magenta light itself now registers + reaches
the walls via the prior weenie-light + landblock-key fix). Refines AP-35: point
lights now split static-bake (1/d^3) vs dynamic-hardware (1/d) by path.

Verified: portal light now range=9 (6x1.5), magenta spreads softly; shader
compiles clean; static torches unchanged (range 5.2/6.5/7.8). User-confirmed the
portal matches retail and the torch-lit interior did not over-brighten.
Core lighting 44/44, App 476 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:14:57 +02:00
Erik
a28a69af71 feat(lighting): Phase G.2 LightSource + LightManager (data + selection)
Retail-faithful 8-light cap selection (r13 §12) — the fixed-function
D3D pipeline's "hardware lights" constraint carried over to modern GL
via UBO-per-draw.

Core layer (AcDream.Core/Lighting):
- LightSource: Kind (Directional/Point/Spot), WorldPosition,
  WorldForward, ColorLinear, Intensity, Range (hard cutoff),
  ConeAngle (spot), OwnerId (entity attachment), IsLit latch.
- CellAmbientState: (AmbientColor, SunColor, SunDirection) sourced from
  R12 sky state for outdoor cells or EnvCell dat for indoor cells.
- LightManager: Register/Unregister/UnregisterByOwner/Clear + Tick
  per frame. Selection matches r13 §12.2 exactly:
  1) Skip unlit + directional.
  2) Compute DistSq for every registered point/spot.
  3) Drop lights outside Range² * 1.1 (10% slack prevents pop).
  4) Sort by DistSq ascending; take up to 7 (slot 0 reserved for Sun).
  5) Slot 0 = Sun (Directional); slots 1..7 = nearest in-range.

Tests (9 new):
- Register/Unregister/Idempotent register.
- Tick picks top 8 by distance when 12 registered.
- Range filter drops far lights (5.0 range, 20m away).
- Range slack includes lights at exactly the boundary.
- Sun reserved at slot 0 across ticks.
- Unlit lights excluded; toggling IsLit brings them back.
- UnregisterByOwner removes all owner's lights.
- DistSq updated each tick for viewer movement.

Build green, 596 tests pass (up from 587).

Next: wire LightManager into the shader UBO pass (G.2 second commit)
and feed Sun from WorldTimeService.CurrentSunDirection per frame.

Ref: r13 §10.2 (D3D attenuation = none inside Range + hard cutoff),
§12 (full port plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:09:51 +02:00