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>
71 lines
3 KiB
C#
71 lines
3 KiB
C#
using System;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.Core.Lighting;
|
||
|
||
/// <summary>
|
||
/// Retail light-source kinds (r13 §12.1).
|
||
/// </summary>
|
||
public enum LightKind
|
||
{
|
||
Directional = 0, // sun, moon — no position, infinite range
|
||
Point = 1, // torch, fireplace, spell aura
|
||
Spot = 2, // cone-shaped (rare in AC, used for a few specific lamps)
|
||
}
|
||
|
||
/// <summary>
|
||
/// Per-frame light record. Used by <see cref="LightManager"/> and fed to
|
||
/// the shader UBO on every draw call.
|
||
///
|
||
/// <para>
|
||
/// Retail semantics (r13 §10.2):
|
||
/// <list type="bullet">
|
||
/// <item><description>
|
||
/// Hard cutoff at <see cref="Range"/> — no smoothstep, no distance
|
||
/// attenuation inside the range. "Crisp bubble of illumination."
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// Max 8 active lights (<see cref="LightManager.MaxActiveLights"/>),
|
||
/// ranked by distance-to-viewer. Slot 0 is reserved for the sun.
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// <see cref="IsLit"/> latches the <c>SetLightHook</c> value so
|
||
/// animations can toggle a light on/off (torch being lit, light
|
||
/// crystal activating).
|
||
/// </description></item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class LightSource
|
||
{
|
||
public LightKind Kind;
|
||
public Vector3 WorldPosition;
|
||
public Vector3 WorldForward; // for Spot/Directional
|
||
public Vector3 ColorLinear = Vector3.One; // R,G,B in [0,1], pre-brightness
|
||
public float Intensity = 1f;
|
||
public float Range = 10f; // metres, hard cutoff
|
||
public float ConeAngle = 0f; // radians, Spot only
|
||
public uint OwnerId; // attached entity id; 0 = world-global
|
||
public uint CellId; // owning cell id (0xLLLLNNNN); 0 = cell-less/global (viewer fill, sun).
|
||
// Retail carries this on the RenderLight (insert_light arg6, +0x6c) for the
|
||
// cross-cell block-offset distance math. #176 correction (2026-07-06): it is
|
||
// NOT a pool filter — retail collects from ALL resident cells
|
||
// (CEnvCell::visible_cell_table = the loaded-cell registry, not the flood);
|
||
// acdream keeps the tag for probes ([indoor-light]/[seam-*]) + future parity.
|
||
public bool IsLit = true; // SetLightHook latch
|
||
public bool IsDynamic; // #143: true = D3D hardware path (1/d att, range×1.5);
|
||
// false = static dat-baked bake (1/d³, range×1.3)
|
||
|
||
// Cached each frame by LightManager.
|
||
public float DistSq;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Per-cell ambient + sun state. For outdoor cells this comes from the
|
||
/// R12 sky state; for indoor cells the EnvCell dat carries a per-cell
|
||
/// ambient override (r13 §3).
|
||
/// </summary>
|
||
public readonly record struct CellAmbientState(
|
||
Vector3 AmbientColor,
|
||
Vector3 SunColor,
|
||
Vector3 SunDirection);
|