Revert "Campaign V slice V4a" - it lost world multisampling

This reverts ceec3bc4. Two independent reasons, either sufficient.

The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.

The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.

This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.

The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.

Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.

Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-27 18:27:52 +02:00
parent ceec3bc440
commit 9aaf97e785
334 changed files with 3841 additions and 3661 deletions

View file

@ -1,4 +1,4 @@
// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
// production cell-rendering pipeline for indoor visibility, replacing the
// broken "cell as WorldEntity with MeshRef(envCellId)" approach that the
// four reverted RR7 variants couldn't fix.
@ -12,7 +12,7 @@
//
// Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base
// class owns the landblock-streaming loop (Update, _pendingGeneration,
// _uploadQueue). acdream's StreamingController already does that work —
// _uploadQueue). acdream's StreamingController already does that work
// running a parallel loop would compete for dat I/O. Instead, streaming builds
// a private EnvCellLandblockBuild and CommitLandblock publishes the completed
// snapshot on the render thread.
@ -29,7 +29,7 @@ using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
internal sealed unsafe class EnvCellRenderer :
public sealed unsafe class EnvCellRenderer :
IDisposable,
IEnvCellLandblockPublisher
{
@ -39,7 +39,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly WbFrustum _frustum;
// Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF).
// WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary<ushort, ObjectLandblock> _landblocks —
// WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary<ushort, ObjectLandblock> _landblocks
// we use uint (full LB id) because acdream uses 32-bit landblock keys throughout.
private readonly ConcurrentDictionary<uint, EnvCellLandblock> _landblocks = new();
@ -64,7 +64,7 @@ internal sealed unsafe class EnvCellRenderer :
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
private bool _initialized;
// List pool — copied from WB ObjectRenderManagerBase.
// List pool copied from WB ObjectRenderManagerBase.
// WB ObjectRenderManagerBase.cs:83-86: protected readonly List<List<InstanceData>> _listPool = new(); protected int _poolIndex = 0;
private readonly List<List<InstanceData>> _listPool = new();
private int _poolIndex = 0;
@ -78,7 +78,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly ThreadLocal<PrepareScratch> _prepareScratch =
new(() => new PrepareScratch(), trackAllValues: true);
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
// Modern-MDI scratch buffers (single slot we re-upload every frame).
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
private uint _mdiCommandBuffer;
@ -95,7 +95,7 @@ internal sealed unsafe class EnvCellRenderer :
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
private uint _clipSlotBuffer;
private int _clipSlotCapacity;
private uint[] _clipSlotData = Array.Empty<uint>();
@ -156,17 +156,17 @@ internal sealed unsafe class EnvCellRenderer :
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
// Vulkan global texture descriptor array (binding=9,
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
// sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
// sharing WbDrawDispatcher's EnvCellRenderer never had a TextureCache
// dependency and nothing requires index agreement between renderers (each
// rebinds its own buffer to binding=9 immediately before its own draw
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
// appears (rare — see FlushAndBindTextureTable).
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
// appears (rare see FlushAndBindTextureTable).
private readonly GlBindlessHandleTable _textureTable = new();
private uint _textureTableSsbo;
private int _textureTableSsboCapacityBytes;
// Reusable scratch arrays — avoid per-frame allocation.
// Reusable scratch arrays avoid per-frame allocation.
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
@ -192,7 +192,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = new();
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
// Static render-state tracking matches WB BaseObjectRenderManager.cs:24-28.
// Shared across all manager instances on the same GL context.
private static uint _currentVao;
private static CullMode? _currentCullMode;
@ -204,8 +204,8 @@ internal sealed unsafe class EnvCellRenderer :
// inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell
// filter, the trim window, mesh render-data availability (the snapshot bakes
// per-cell transparency from TryGetRenderData), or the view-projection.
// NeedsPrepare existed since A8 but was never read — this wires it. The VP
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
// NeedsPrepare existed since A8 but was never read this wires it. The VP
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
// R-A2 note) while any real camera motion crosses it in the same frame.
private Matrix4x4 _preparedViewProjection;
private Vector3 _preparedCameraPosition;
@ -223,14 +223,14 @@ internal sealed unsafe class EnvCellRenderer :
public bool IsDisposed { get; private set; }
public LastFrameStats Stats => _lastFrameStats;
internal struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
private LastFrameStats _lastFrameStats;
/// <summary>
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
/// A divergence between expected and actual values would indicate a pool-
/// management regression — exactly the bug class the 2026-05-28 audit caught.
/// management regression exactly the bug class the 2026-05-28 audit caught.
/// </summary>
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
{
@ -338,20 +338,20 @@ internal sealed unsafe class EnvCellRenderer :
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
// Phase U.4: per-frame cellIdCellClip-slot map for the cell shells. When
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
// gated to that cell's portal-clip region. When null (U.3 path), every
// instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0
// (no-clip) — but the caller's Render filter already restricts the draw to the
// (no-clip) but the caller's Render filter already restricts the draw to the
// map's keys, so that fallback should not fire in practice.
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
/// <summary>
/// Phase U.4: install the per-frame cellId→slot map used to gate cell shells
/// Phase U.4: install the per-frame cellIdslot map used to gate cell shells
/// to their portal-clip regions. Call once per frame BEFORE
/// <see cref="Render(WbRenderPass, HashSet{uint}?)"/>. Pass null to revert to
/// the U.3 no-clip behavior (every shell instance → slot 0).
/// the U.3 no-clip behavior (every shell instance slot 0).
/// </summary>
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
=> _cellIdToSlot = cellIdToSlot;
@ -386,7 +386,7 @@ internal sealed unsafe class EnvCellRenderer :
surfaces);
// ---------------------------------------------------------------------------
// CommitLandblock — render-thread transaction boundary
// CommitLandblock render-thread transaction boundary
// ---------------------------------------------------------------------------
/// <summary>
@ -586,7 +586,7 @@ internal sealed unsafe class EnvCellRenderer :
int? renderRadius = null)
{
// Phase U.4 fix: stash the view-projection so Render() can upload it itself.
// Stashed even when the gate below skips the rebuild — Render must always
// Stashed even when the gate below skips the rebuild Render must always
// project with the CURRENT frame's matrix (the U.4 stale-matrix root cause).
_lastViewProjection = viewProjection;
@ -614,7 +614,7 @@ internal sealed unsafe class EnvCellRenderer :
return;
}
// Prepare gate: every snapshot input unchanged → keep the active snapshot.
// Prepare gate: every snapshot input unchanged keep the active snapshot.
// (Same-thread discipline makes the version sample exact: publish, release
// tickets, and this method all run on the render thread.)
if (_hasPreparedSnapshot
@ -633,7 +633,7 @@ internal sealed unsafe class EnvCellRenderer :
lock (_renderLock) { _poolIndex = 0; }
// WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant
// because we don't need camera-LB tracking for the snapshot — just frustum tests.
// because we don't need camera-LB tracking for the snapshot just frustum tests.
// WB EnvCellRenderManager.cs:262:
// Filter loaded landblocks by GpuReady + Instances non-empty.
@ -674,7 +674,7 @@ internal sealed unsafe class EnvCellRenderer :
PrepareScratch scratch = _prepareScratch.Value!;
// WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
// WB EnvCellRenderManager.cs:279-295: fast path LB fully inside.
if (testResult == FrustumTestResult.Inside)
{
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
@ -692,7 +692,7 @@ internal sealed unsafe class EnvCellRenderer :
return;
}
// WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
// WB EnvCellRenderManager.cs:298-324: slow path per-cell frustum test.
HashSet<uint> visibleCells = scratch.VisibleCells;
visibleCells.Clear();
foreach (var kvp in lb.EnvCellBounds)
@ -804,13 +804,13 @@ internal sealed unsafe class EnvCellRenderer :
/// <summary>
/// Pure half of the prepare gate's camera test (regression-tested without a
/// GL context, same pattern as <see cref="CreateCommittedSnapshot"/>).
/// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
/// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
/// jitter but dirties on any real movement (a slow walk moves 20+ mm/frame).
/// Position must not be tested through the matrix — the view-projection's
/// Position must not be tested through the matrix the view-projection's
/// translation row scales with world coordinates (~5e4 in AC), where a
/// relative tolerance would mask sub-meter motion. Rows 1–3 of
/// view × projection are position-independent (rotation × projection), so a
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
/// relative tolerance would mask sub-meter motion. Rows 13 of
/// view × projection are position-independent (rotation × projection), so a
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
/// projection (FOV/aspect/near/far) change.
/// </summary>
internal static bool CameraApproximatelyEqual(
@ -926,7 +926,7 @@ internal sealed unsafe class EnvCellRenderer :
// Verbatim port of WB EnvCellRenderManager.cs:395-511.
// Deviations from WB (all documented):
// - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5).
// - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state.
// - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) no editor state.
// - Replace RenderModernMDI(base) with private RenderModernMDIInternal.
// - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader
// class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing
@ -947,7 +947,7 @@ internal sealed unsafe class EnvCellRenderer :
/// filter (the drawable visible cells from the PView traversal; each cell's
/// shell instances are clip-gated to its CellClip slot by the caller's
/// binding=3 map). NOTE: this is NOT the old two-pipe RenderInsideOut approach
/// — that flat camera-inside-building stencil pass was deleted in Phase U.1.
/// that flat camera-inside-building stencil pass was deleted in Phase U.1.
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
/// </summary>
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
@ -979,7 +979,7 @@ internal sealed unsafe class EnvCellRenderer :
// WB EnvCellRenderManager.cs:403-404:
_shader.Use();
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
// EnvCellRenderManager.cs:405 restore the pool cursor to the
// high-water mark Prepare's merge phase reached, so any
// GetPooledList calls below return lists past the snapshot's
// owned region. Original code used `snapshot.BatchedByCell.Count`
@ -999,7 +999,7 @@ internal sealed unsafe class EnvCellRenderer :
// RenderInsideOutAcdream stencil pipeline) change the actual GL
// state without updating these caches. The cache then lies, and
// the per-batch SetCullMode in RenderModernMDIInternal skips its
// glCullFace call — leaving stale cull state from the prior
// glCullFace call leaving stale cull state from the prior
// consumer. For a cottage with mixed CullMode batches, half the
// walls end up culled and the user sees "missing walls".
//
@ -1012,15 +1012,15 @@ internal sealed unsafe class EnvCellRenderer :
_shader.SetInt("uRenderPass", (int)renderPass);
_shader.SetInt("uFilterByCell", 0);
_shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) throwaway diagnostic.
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
// moving"): upload uViewProjection HERE rather than inheriting it from
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
// this the opaque shells used the PREVIOUS frame's matrix — a stale
// gl_Position against this frame's clip planes → pose-dependent clipping,
// this the opaque shells used the PREVIOUS frame's matrix a stale
// gl_Position against this frame's clip planes pose-dependent clipping,
// worst while moving. Same self-contained-GL-state precedent as the
// 2026-05-28 cull-state cache fix above.
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
@ -1059,7 +1059,7 @@ internal sealed unsafe class EnvCellRenderer :
else if (filter is null)
{
RebuildUnfilteredGroups(snapshot);
// WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
// WB EnvCellRenderManager.cs:418-429: optimized path global groups.
foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds)
{
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
@ -1144,7 +1144,7 @@ internal sealed unsafe class EnvCellRenderer :
renderPass);
}
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights DROPPED (no editor state).
// WB EnvCellRenderManager.cs:506-509: cleanup.
_shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
@ -1170,12 +1170,12 @@ internal sealed unsafe class EnvCellRenderer :
? dc.renderData.Batches[0].IndexCount / 3
: 0) * dc.count;
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) THROWAWAY.
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
// present in the prepared snapshot, and its geometry/flags. Answers why the
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
// prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
// (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
// interior walls/ceiling don't appear: NOSNAP / gfx=0 no shell geometry
// prepared for the cell; idx>0 + zh>0 prepared but missing bindless texture
// (invisible); idx>0 + zh=0 + tr=0 opaque geometry drawn (fault is depth/
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
@ -1217,7 +1217,7 @@ internal sealed unsafe class EnvCellRenderer :
/// <summary>
/// True if the cell's prepared snapshot has any transparent render batch.
/// The pview shell pass uses this to skip the (heavy per-frame) transparent
/// <see cref="Render"/> call for opaque-only cells — most cell geometry is
/// <see cref="Render"/> call for opaque-only cells most cell geometry is
/// opaque walls/floors/ceilings, so this removes the bulk of the per-cell
/// transparent draws. Read-only; mirrors the [shell] probe's batch scan.
/// </summary>
@ -1227,7 +1227,7 @@ internal sealed unsafe class EnvCellRenderer :
// ---------------------------------------------------------------------------
// 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.
// 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
@ -1248,21 +1248,21 @@ internal sealed unsafe class EnvCellRenderer :
var snap = _pointSnapshot;
// Landblocks are keyed by the streaming landblock id 0xXXYYFFFF
// (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock
// (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 so the landblock
// key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key
// (0xXXYY0000) NEVER matched a registered landblock, so this lookup always
// missed: SelectForObject never ran and every EnvCell wall received ZERO
// point lights (the entire "indoor torches/lanterns don't light the room"
// bug — confirmed by the [cell-light] probe: inBounds=False for every cell).
// bug confirmed by the [cell-light] probe: inBounds=False for every cell).
if (snap is { Count: > 0 } &&
_landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, 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;
// #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL
// #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) ALL
// dynamic lights on every cell (stable), not the per-object sphere-overlap cull that
// let the portal set flip as the flood shifted → floor-lighting flap.
// let the portal set flip as the flood shifted floor-lighting flap.
AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set);
}
cached.FrameGeneration = _lightFrameGeneration;
@ -1414,9 +1414,9 @@ internal sealed unsafe class EnvCellRenderer :
int passIdx = (int)renderPass;
if (passIdx < 0 || passIdx > 2) return;
// §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
// 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
// see the comment on the state-establish block below).
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
if (globalVao == 0) return;
@ -1468,14 +1468,14 @@ internal sealed unsafe class EnvCellRenderer :
// transparent). Restored to opaque defaults at the end of the draw loop so a
// Transparent pass can't leak into later draws.
//
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
// interior) set Blend-on/DepthMask-off and then returned at the count check
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
// whole screen dropped to the fog-tinted clear color — onset-locked to the
// whole screen dropped to the fog-tinted clear color onset-locked to the
// building-flood merge (the first frame a flooded building shell draws), holding
// until camera rotation dropped the cell from the flood. From here down every
// path reaches the end-of-pass restore.
@ -1689,14 +1689,14 @@ internal sealed unsafe class EnvCellRenderer :
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
// the map (shouldn't happen — the Render filter is the map's keys) and the
// the map (shouldn't happen the Render filter is the map's keys) and the
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
// instance to slot 0 (no-clip) retail draws cell shells WHOLE.
if (_cellIdToSlot is null
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
{
@ -1728,7 +1728,7 @@ internal sealed unsafe class EnvCellRenderer :
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
// sets read through the just-cleared cache against THIS frame's
// _pointSnapshot — the exact data the SSBO upload below carries.
// _pointSnapshot the exact data the SSBO upload below carries.
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
@ -1766,7 +1766,7 @@ internal sealed unsafe class EnvCellRenderer :
PersistActiveDynamicBufferCapacities();
// 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.)
if (_currentVao != globalVao)
{
@ -1865,16 +1865,16 @@ internal sealed unsafe class EnvCellRenderer :
}
// ---------------------------------------------------------------------------
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) throwaway apparatus.
// The in-engine replacement for the RenderDoc pixel-history the pipeline
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
// startup gate throws). Per opaque pass: for each target cell — flood
// can't have (RenderDoc hides GL_ARB_bindless_texture our mandatory-modern
// startup gate throws). Per opaque pass: for each target cell flood
// membership, every shell instance (count + translation, F3 z shows the
// +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
// +0.02 lift; n2 for one (cell,gfx) = the runtime double-draw), and the
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
// intensity; raw indices shuffle when the pool rebuilds). Plus the
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
// ~12). Change-deduped block with a 2 s heartbeat: a purple identity
// flipping with flood membership = the snapshot-scope mechanism; two
// coincident instances = the z-fight. See RenderingDiagnostics.
// ---------------------------------------------------------------------------
@ -1995,8 +1995,8 @@ internal sealed unsafe class EnvCellRenderer :
/// Uploads <see cref="_textureTable"/>'s handles to <see cref="_textureTableSsbo"/>
/// when a new one was registered since the last flush, then (re)binds it at
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
/// A genuinely new handle is rare new dat surfaces/atlases, not every
/// frame so this is not part of the ring-buffered per-frame SSBO set;
/// see GlBindlessHandleTable's doc comment.
/// </summary>
private void FlushAndBindTextureTable()
@ -2068,7 +2068,7 @@ internal sealed unsafe class EnvCellRenderer :
GLEnum.DynamicDraw,
"allocating EnvCell fallback clip SSBO");
allocated = true;
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
// One CellClip slot, all zeros: count 0 shader passes every plane.
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
zero.Clear();
fixed (byte* p = zero)
@ -2103,12 +2103,12 @@ internal sealed unsafe class EnvCellRenderer :
private List<InstanceData> GetPooledList()
{
// Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse
// Mirrors WB ObjectRenderManagerBase.cs:1221-1233 the reuse
// branch MUST clear the list before returning. PrepareRenderBatches'
// merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`,
// which assumes the list is empty. Without the clear, lists grow
// unbounded across frames and each frame's draw includes all prior
// frames' stale data. Original port omitted the Clear() call — root
// frames' stale data. Original port omitted the Clear() call root
// cause of post-Wave-5 visual chaos (FIX 2026-05-28). See
// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
lock (_listPool)