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.
|
// SceneLighting UBO built below (binding=1) — terrain/sky read those.
|
||||||
Lighting.BuildPointLightSnapshot(camPos);
|
Lighting.BuildPointLightSnapshot(camPos);
|
||||||
_wbDrawDispatcher?.SetSceneLights(Lighting.PointSnapshot);
|
_wbDrawDispatcher?.SetSceneLights(Lighting.PointSnapshot);
|
||||||
|
_envCellRenderer?.SetPointSnapshot(Lighting.PointSnapshot); // A7 Fix D (D-2)
|
||||||
|
|
||||||
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
||||||
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,17 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
private uint _clipSlotBuffer;
|
private uint _clipSlotBuffer;
|
||||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
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
|
// Phase U.3: SHARED per-cell clip-region SSBO (binding=2) handed in via
|
||||||
// SetClipRegionSsbo (the GameWindow-level ClipFrame buffer). When 0, we bind
|
// 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.
|
// 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,
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||||
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
(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.ShaderStorageBuffer, 0);
|
||||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -262,6 +285,17 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
||||||
=> _cellIdToSlot = 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
|
// GetEnvCellGeomId
|
||||||
// Verbatim copy of WB EnvCellRenderManager.cs:94-103.
|
// 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
|
// RenderModernMDIInternal
|
||||||
// Extracted from WB BaseObjectRenderManager.cs:709-848 (single-slot variant).
|
// Extracted from WB BaseObjectRenderManager.cs:709-848 (single-slot variant).
|
||||||
|
|
@ -1016,6 +1079,15 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
int passIdx = (int)renderPass;
|
int passIdx = (int)renderPass;
|
||||||
if (passIdx < 0 || passIdx > 2) return;
|
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.
|
// §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
|
// 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 —
|
// 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);
|
(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.
|
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||||
// (globalVao validated at the top of the method — a return here would leak the
|
// (globalVao validated at the top of the method — a return here would leak the
|
||||||
// pass state established above.)
|
// 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).
|
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
|
||||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
||||||
BindClipRegionBinding2();
|
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.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||||
|
|
||||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
_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 (_modernBatchBuffer != 0) { _gl.DeleteBuffer(_modernBatchBuffer); _modernBatchBuffer = 0; }
|
||||||
if (_clipSlotBuffer != 0) { _gl.DeleteBuffer(_clipSlotBuffer); _clipSlotBuffer = 0; } // Phase U.3
|
if (_clipSlotBuffer != 0) { _gl.DeleteBuffer(_clipSlotBuffer); _clipSlotBuffer = 0; } // Phase U.3
|
||||||
if (_fallbackClipRegionSsbo != 0) { _gl.DeleteBuffer(_fallbackClipRegionSsbo); _fallbackClipRegionSsbo = 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