fix(render): A7 Fix D D-2 — EnvCell shell binds its own per-cell light set (#140)
The cell shell read whatever light set (SSBO 4/5) WbDrawDispatcher last left bound, lighting walls with a leaked set. EnvCellRenderer now uploads its own binding=4 global lights (frame PointSnapshot via GlobalLightPacker) + a binding=5 per-instance set, computed per cell by LightManager.SelectForObject over the cell's world bounds (mirrors _cellIdToSlot + WbDrawDispatcher.ComputeEntityLightSet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cf62793304
commit
c62da825fe
2 changed files with 106 additions and 0 deletions
|
|
@ -7775,6 +7775,7 @@ public sealed class GameWindow : IDisposable
|
|||
// SceneLighting UBO built below (binding=1) — terrain/sky read those.
|
||||
Lighting.BuildPointLightSnapshot(camPos);
|
||||
_wbDrawDispatcher?.SetSceneLights(Lighting.PointSnapshot);
|
||||
_envCellRenderer?.SetPointSnapshot(Lighting.PointSnapshot); // A7 Fix D (D-2)
|
||||
|
||||
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
||||
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,17 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
private uint _clipSlotBuffer;
|
||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained GL state,
|
||||
// like uViewProjection) instead of reading the SSBO 4/5 WbDrawDispatcher last
|
||||
// left bound. binding=4 = global point-light snapshot (same data/indices as the
|
||||
// dispatcher, via GlobalLightPacker); binding=5 = 8 int indices per instance.
|
||||
private uint _globalLightsSsbo; // binding=4
|
||||
private float[] _globalLightData = new float[AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * 16];
|
||||
private uint _instLightSetSsbo; // binding=5
|
||||
private int[] _lightSetData = new int[1024 * AcDream.Core.Lighting.LightManager.MaxLightsPerObject];
|
||||
private System.Collections.Generic.IReadOnlyList<AcDream.Core.Lighting.LightSource>? _pointSnapshot;
|
||||
private readonly System.Collections.Generic.Dictionary<uint, int[]> _cellLightSetCache = new();
|
||||
|
||||
// Phase U.3: SHARED per-cell clip-region SSBO (binding=2) handed in via
|
||||
// SetClipRegionSsbo (the GameWindow-level ClipFrame buffer). When 0, we bind
|
||||
// our own one-slot no-clip fallback so the shader never reads an unbound SSBO.
|
||||
|
|
@ -231,6 +242,18 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
||||
|
||||
// A7 Fix D (D-2): binding=4 global lights + binding=5 per-instance light set.
|
||||
_gl.GenBuffers(1, out _globalLightsSsbo);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _globalLightsSsbo);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_globalLightData.Length * sizeof(float)), null, GLEnum.DynamicDraw);
|
||||
|
||||
_gl.GenBuffers(1, out _instLightSetSsbo);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _instLightSetSsbo);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * AcDream.Core.Lighting.LightManager.MaxLightsPerObject * sizeof(int)),
|
||||
null, GLEnum.DynamicDraw);
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, 0);
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
||||
}
|
||||
|
|
@ -262,6 +285,17 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
||||
=> _cellIdToSlot = cellIdToSlot;
|
||||
|
||||
/// <summary>
|
||||
/// A7 Fix D (D-2): hand the renderer this frame's point-light snapshot
|
||||
/// (LightManager.PointSnapshot). Call once per frame BEFORE Render, alongside
|
||||
/// the WbDrawDispatcher snapshot wire-in. Indices in the per-cell light sets
|
||||
/// reference this snapshot, which is also uploaded to binding=4 here, so the
|
||||
/// pass is self-contained. Null/empty -> shells receive no point lights.
|
||||
/// </summary>
|
||||
public void SetPointSnapshot(
|
||||
System.Collections.Generic.IReadOnlyList<AcDream.Core.Lighting.LightSource>? snapshot)
|
||||
=> _pointSnapshot = snapshot;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetEnvCellGeomId
|
||||
// Verbatim copy of WB EnvCellRenderManager.cs:94-103.
|
||||
|
|
@ -997,6 +1031,35 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetCellLightSet (A7 Fix D D-2 helper)
|
||||
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
||||
// WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A7 Fix D (D-2): the up-to-8 point lights reaching a cell, by the cell's world
|
||||
// bounding sphere (camera-independent, like WbDrawDispatcher.ComputeEntityLightSet).
|
||||
// Cached per frame; unused slots are -1 (shader adds no point light there).
|
||||
private int[] GetCellLightSet(uint cellId)
|
||||
{
|
||||
if (_cellLightSetCache.TryGetValue(cellId, out var cached)) return cached;
|
||||
|
||||
var set = new int[AcDream.Core.Lighting.LightManager.MaxLightsPerObject];
|
||||
System.Array.Fill(set, -1);
|
||||
|
||||
var snap = _pointSnapshot;
|
||||
if (snap is { Count: > 0 } &&
|
||||
_landblocks.TryGetValue(cellId & 0xFFFF0000u, out var lb) &&
|
||||
lb.EnvCellBounds.TryGetValue(cellId, out var b))
|
||||
{
|
||||
Vector3 center = (b.Min + b.Max) * 0.5f;
|
||||
float radius = (b.Max - b.Min).Length() * 0.5f;
|
||||
AcDream.Core.Lighting.LightManager.SelectForObject(snap, center, radius, set);
|
||||
}
|
||||
_cellLightSetCache[cellId] = set;
|
||||
return set;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RenderModernMDIInternal
|
||||
// Extracted from WB BaseObjectRenderManager.cs:709-848 (single-slot variant).
|
||||
|
|
@ -1016,6 +1079,15 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
int passIdx = (int)renderPass;
|
||||
if (passIdx < 0 || passIdx > 2) return;
|
||||
|
||||
// A7 Fix D (D-2): per-frame per-cell light-set cache (built lazily in
|
||||
// GetCellLightSet below). Clear once here so each cell gets a fresh lookup
|
||||
// using this frame's _pointSnapshot. Called for EVERY pass (opaque AND
|
||||
// transparent); the cache entries are stable within a frame since PointSnapshot
|
||||
// doesn't change between Render calls, so clearing once (at the opaque pass)
|
||||
// and leaving stale entries for the transparent pass would also be correct, but
|
||||
// clearing both is safe and matches WbDrawDispatcher's per-call ComputeEntityLightSet.
|
||||
_cellLightSetCache.Clear();
|
||||
|
||||
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
|
||||
// Without the global VAO nothing can draw, and returning AFTER the pass state
|
||||
// was established leaked it (same early-out shape as the totalDraws==0 leak —
|
||||
|
|
@ -1213,6 +1285,35 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
|
||||
}
|
||||
|
||||
// A7 Fix D (D-2): per-instance 8-int light set, parallel to the transforms,
|
||||
// keyed on the cell each shell instance belongs to (mirrors _clipSlotData).
|
||||
int lightStride = AcDream.Core.Lighting.LightManager.MaxLightsPerObject;
|
||||
if (_lightSetData.Length < uniqueInstanceCount * lightStride)
|
||||
_lightSetData = new int[System.Math.Max(_lightSetData.Length * 2, uniqueInstanceCount * lightStride)];
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
{
|
||||
int[] cellSet = GetCellLightSet(allInstances[i].CellId);
|
||||
System.Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
|
||||
}
|
||||
|
||||
// A7 Fix D (D-2): upload binding=4 (global lights) + binding=5 (per-instance set).
|
||||
int lightCount = AcDream.Core.Lighting.GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
int glUploadCount = lightCount > 0 ? lightCount : 1;
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _globalLightsSsbo);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(glUploadCount * AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * sizeof(float)),
|
||||
null, GLEnum.DynamicDraw);
|
||||
fixed (float* gp = _globalLightData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(glUploadCount * AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * sizeof(float)), gp);
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _instLightSetSsbo);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(uniqueInstanceCount * lightStride * sizeof(int)), null, GLEnum.DynamicDraw);
|
||||
fixed (int* lp = _lightSetData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * lightStride * sizeof(int)), lp);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||
// (globalVao validated at the top of the method — a return here would leak the
|
||||
// pass state established above.)
|
||||
|
|
@ -1228,6 +1329,8 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 4, _globalLightsSsbo); // A7 Fix D (D-2)
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 5, _instLightSetSsbo); // A7 Fix D (D-2)
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
||||
|
|
@ -1443,5 +1546,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
if (_modernBatchBuffer != 0) { _gl.DeleteBuffer(_modernBatchBuffer); _modernBatchBuffer = 0; }
|
||||
if (_clipSlotBuffer != 0) { _gl.DeleteBuffer(_clipSlotBuffer); _clipSlotBuffer = 0; } // Phase U.3
|
||||
if (_fallbackClipRegionSsbo != 0) { _gl.DeleteBuffer(_fallbackClipRegionSsbo); _fallbackClipRegionSsbo = 0; } // Phase U.3
|
||||
if (_globalLightsSsbo != 0) { _gl.DeleteBuffer(_globalLightsSsbo); _globalLightsSsbo = 0; } // A7 Fix D (D-2)
|
||||
if (_instLightSetSsbo != 0) { _gl.DeleteBuffer(_instLightSetSsbo); _instLightSetSsbo = 0; } // A7 Fix D (D-2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue