A week on the indoor render (Phase U.4 → U.4c → 2026-05-31) fixed the flap but
produced NO shippable progress: walls/ceiling don't seal, outdoor terrain is
visible from inside (#78), the enclosure reads grey/transparent. Root cause is
ARCHITECTURAL, not a bug.
Evidence this session (direct, via the new [shell] probe + screenshots) RULED OUT
every subsystem except the gating architecture: the interior cell shells render
fine (geometry/texture/opaque/depth all correct, zh=0 tr=0); the visibility
traversal computes correct sets + non-empty portal clips; cull mode is fine; the
camera/eye thread was a detour. The residual is that OUTDOOR geometry is not gated
to portal openings when indoors, and acdream enforces visibility THREE inconsistent
ways (TerrainClipMode / per-cell shell clip / entity ParentCellId filter with an
outdoor-stab bypass) instead of retail's ONE PView gate.
This commit is the reset handoff + documentation, not a code fix:
- docs/research/2026-05-31-render-architecture-reset-handoff.md — canonical: honest
state, evidence ledger (ruled-out / do-not-repeat), the mapped 3-gate patchwork,
the retail PView target (one traversal → one gate for ALL geometry), the reset
mission, and a copy-paste pickup prompt.
- docs/architecture/acdream-architecture.md — new "Render Pipeline" SSOT section
(current divergence + unified-PView target + the one rule: compute visibility
once, enforce it once). (Doc has pre-existing corruption below this section —
flagged for separate cleanup.)
- Apparatus: ACDREAM_PROBE_SHELL → [shell] (EnvCellRenderer per-cell prepared/drawn
geometry + flags) added to RenderingDiagnostics + EnvCellRenderer. Throwaway.
- docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md —
spec for e099b4c (camera collision; now parked as orthogonal to the seam).
Next session: STOP point-fixing; do the architecture reset to a single PView gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1413 lines
65 KiB
C#
1413 lines
65 KiB
C#
// 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.
|
|
//
|
|
// Sources ported byte-for-byte:
|
|
// GetEnvCellGeomId <- WB EnvCellRenderManager.cs:94-103
|
|
// PrepareRenderBatches <- WB EnvCellRenderManager.cs:247-373
|
|
// Render(filter:) <- WB EnvCellRenderManager.cs:395-511
|
|
// RenderModernMDIInternal <- WB BaseObjectRenderManager.cs:709-848 (single-slot variant)
|
|
// PopulatePartGroups <- WB EnvCellRenderManager.cs:572-580
|
|
// AddToGroups / AddToCellGroup <- WB EnvCellRenderManager.cs:375-393
|
|
//
|
|
// 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 —
|
|
// running a parallel loop would compete for dat I/O. Instead, we expose
|
|
// RegisterCell(...) as the seam: callers populate our instance store at
|
|
// the point they already hydrate cells (GameWindow.BuildInteriorEntitiesForStreaming).
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using DatReaderWriter.Enums;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering.Wb;
|
|
|
|
public sealed unsafe class EnvCellRenderer : IDisposable
|
|
{
|
|
private readonly GL _gl;
|
|
private readonly ObjectMeshManager _meshManager;
|
|
private readonly WbFrustum _frustum;
|
|
|
|
// Per-landblock storage. Key = full 32-bit landblock id (e.g. 0xA9B40000).
|
|
// 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();
|
|
|
|
// Active snapshot (atomic swap under _renderLock).
|
|
// WB EnvCellRenderManager.cs:71: private VisibilitySnapshot _activeSnapshot = new();
|
|
private readonly object _renderLock = new();
|
|
private EnvCellVisibilitySnapshot _activeSnapshot = new();
|
|
|
|
// Shader (set by caller via Initialize).
|
|
// Uses acdream's legacy Shader type (not WB's GLSLShader) to match the
|
|
// existing wire-in pattern in GameWindow.cs where _meshShader is loaded
|
|
// for mesh_modern.{vert,frag} and shared across multiple consumers.
|
|
// API mapping: Bind() -> Use(), SetUniform(s, int) -> SetInt(s, int),
|
|
// SetUniform(s, Vector4) -> SetVec4(s, Vector4).
|
|
private AcDream.App.Rendering.Shader? _shader;
|
|
|
|
// Phase U.4 root-cause fix: the view-projection captured in PrepareRenderBatches,
|
|
// re-uploaded by Render() so the cell-shell pass is self-contained and does NOT
|
|
// inherit WbDrawDispatcher's uViewProjection (which the opaque pass would read one
|
|
// frame stale, since it draws BEFORE the dispatcher's upload). Same matrix the
|
|
// portal clip planes are computed with (envCellViewProj).
|
|
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
|
|
private bool _initialized;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
private int _mdiCommandCapacity = 1024;
|
|
private uint _modernInstanceBuffer;
|
|
private int _modernInstanceCapacity = 1024;
|
|
private uint _modernBatchBuffer;
|
|
private int _modernBatchCapacity = 1024;
|
|
// mesh_modern.vert's SSBO InstanceData is only mat4 transform. The CPU
|
|
// InstanceData below also carries CellId/Flags for filtering, so upload a
|
|
// packed transform array instead of the 80-byte CPU struct.
|
|
private Matrix4x4[] _gpuInstanceTransforms = Array.Empty<Matrix4x4>();
|
|
|
|
// 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.
|
|
private uint _clipSlotBuffer;
|
|
private uint[] _clipSlotData = Array.Empty<uint>();
|
|
|
|
// 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.
|
|
private uint _sharedClipRegionSsbo;
|
|
private uint _fallbackClipRegionSsbo;
|
|
|
|
// 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>();
|
|
|
|
// 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;
|
|
|
|
public bool NeedsPrepare { get; private set; } = true;
|
|
public bool IsDisposed { get; private set; }
|
|
|
|
public LastFrameStats Stats => _lastFrameStats;
|
|
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.
|
|
/// </summary>
|
|
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
|
|
{
|
|
int poolTotal;
|
|
lock (_listPool) poolTotal = _listPool.Count;
|
|
int hwm;
|
|
lock (_renderLock) hwm = _activeSnapshot.PostPreparePoolIndex;
|
|
return (poolTotal, hwm);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Phase A8 audit probe (2026-05-28 visual-gate-#1 follow-up).
|
|
/// One-shot per (cellId, gfxObjId) pair: dumps batch counts + CullModes +
|
|
/// transparency flags + bindless-handle-non-zero status, so the operator
|
|
/// can read offline and identify why specific polys (e.g., floors) aren't
|
|
/// rendering. Set <c>ACDREAM_A8_AUDIT=1</c> to enable.
|
|
/// <para>Returns a deduplicated audit-line list per Render snapshot
|
|
/// (one entry per (cellId, gfxObjId) seen in BatchedByCell). The caller
|
|
/// (GameWindow EmitEnvCellProbe) prints these and tracks which pairs
|
|
/// have already been logged.</para>
|
|
/// </summary>
|
|
public IReadOnlyList<string> CollectCellAuditLines(HashSet<(uint cellId, ulong gfxObjId)> alreadyLogged)
|
|
{
|
|
var lines = new List<string>();
|
|
lock (_renderLock)
|
|
{
|
|
var snap = _activeSnapshot;
|
|
foreach (var (cellId, gfxDict) in snap.BatchedByCell)
|
|
{
|
|
foreach (var (gfxObjId, transforms) in gfxDict)
|
|
{
|
|
var key = (cellId, gfxObjId);
|
|
if (alreadyLogged.Contains(key)) continue;
|
|
alreadyLogged.Add(key);
|
|
|
|
var rd = _meshManager.TryGetRenderData(gfxObjId);
|
|
if (rd is null)
|
|
{
|
|
lines.Add($"[a8-audit] cell=0x{cellId:X8} gfx=0x{gfxObjId:X10} instances={transforms.Count} renderData=null");
|
|
continue;
|
|
}
|
|
int totalIdx = 0;
|
|
var cullModes = new HashSet<DatReaderWriter.Enums.CullMode>();
|
|
int translucent = 0;
|
|
int additive = 0;
|
|
int zeroHandle = 0;
|
|
foreach (var b in rd.Batches)
|
|
{
|
|
totalIdx += b.IndexCount;
|
|
cullModes.Add(b.CullMode);
|
|
if (b.IsTransparent) translucent++;
|
|
if (b.IsAdditive) additive++;
|
|
if (b.BindlessTextureHandle == 0) zeroHandle++;
|
|
}
|
|
var cullList = string.Join(",", cullModes);
|
|
lines.Add(
|
|
$"[a8-audit] cell=0x{cellId:X8} gfx=0x{gfxObjId:X10} instances={transforms.Count} " +
|
|
$"isSetup={rd.IsSetup} batches={rd.Batches.Count} totalIdx={totalIdx} " +
|
|
$"cull=[{cullList}] translucent={translucent} additive={additive} zeroHandle={zeroHandle}");
|
|
}
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constructor + Initialize
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public EnvCellRenderer(GL gl, ObjectMeshManager meshManager, WbFrustum frustum)
|
|
{
|
|
_gl = gl;
|
|
_meshManager = meshManager;
|
|
_frustum = frustum;
|
|
}
|
|
|
|
public void Initialize(AcDream.App.Rendering.Shader shader)
|
|
{
|
|
_shader = shader;
|
|
AllocateMdiBuffers();
|
|
_initialized = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AllocateMdiBuffers
|
|
// Mirrors WB BaseObjectRenderManager.cs:62-127 (slot-0 only).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private void AllocateMdiBuffers()
|
|
{
|
|
// MDI command buffer (DrawIndirectBuffer)
|
|
_gl.GenBuffers(1, out _mdiCommandBuffer);
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
|
_gl.BufferData(GLEnum.DrawIndirectBuffer,
|
|
(nuint)(_mdiCommandCapacity * sizeof(DrawElementsIndirectCommand)), null, GLEnum.DynamicDraw);
|
|
|
|
// Per-frame scratch instance SSBO (binding=0)
|
|
_gl.GenBuffers(1, out _modernInstanceBuffer);
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernInstanceCapacity * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
|
|
|
// Per-batch data SSBO (binding=1)
|
|
_gl.GenBuffers(1, out _modernBatchBuffer);
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernBatchCapacity * sizeof(ModernBatchData)), null, GLEnum.DynamicDraw);
|
|
|
|
// Phase U.3: per-instance clip-slot SSBO (binding=3), sized to the
|
|
// instance capacity. Uploaded all-zeros each frame in RenderModernMDIInternal.
|
|
_gl.GenBuffers(1, out _clipSlotBuffer);
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
|
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, 0);
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO
|
|
/// (binding=2) created by <see cref="ClipFrame.UploadShared"/>. The renderer
|
|
/// re-binds it to binding=2 immediately before its MDI. Pass 0 to fall back to
|
|
/// the internal one-slot no-clip region buffer.
|
|
/// </summary>
|
|
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
|
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
|
|
|
// Phase U.4: per-frame cellId→CellClip-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
|
|
// 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
|
|
/// 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).
|
|
/// </summary>
|
|
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
|
=> _cellIdToSlot = cellIdToSlot;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GetEnvCellGeomId
|
|
// Verbatim copy of WB EnvCellRenderManager.cs:94-103.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Returns a deduplicated geometry ID for an EnvCell based on its environment,
|
|
/// cell structure index, and surface IDs. Bit 33 is set to distinguish from
|
|
/// per-cell IDs (which use bit 32).
|
|
/// Source: WB EnvCellRenderManager.cs:94-103 (verbatim).
|
|
/// </summary>
|
|
public static ulong GetEnvCellGeomId(uint environmentId, ushort cellStructure, List<ushort> surfaces)
|
|
{
|
|
// WB EnvCellRenderManager.cs:95-102 verbatim:
|
|
var hash = 17L;
|
|
hash = hash * 31 + (int)environmentId;
|
|
hash = hash * 31 + cellStructure;
|
|
foreach (var surface in surfaces) {
|
|
hash = hash * 31 + surface;
|
|
}
|
|
// Use bit 33 to indicate deduplicated EnvCell geometry (to avoid collision with bit 32 per-cell geometry)
|
|
return (ulong)hash | 0x2_0000_0000UL;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RegisterCell — streaming seam
|
|
// Called by GameWindow.BuildInteriorEntitiesForStreaming at landblock-load
|
|
// time, ONCE per EnvCell that has non-null EnvironmentId.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Registers a single EnvCell and its static objects into the per-landblock
|
|
/// pending list. Call <see cref="FinalizeLandblock"/> after all cells for a
|
|
/// landblock have been registered.
|
|
/// </summary>
|
|
public void RegisterCell(
|
|
uint landblockId,
|
|
uint envCellId,
|
|
DatReaderWriter.DBObjs.EnvCell envCell,
|
|
DatReaderWriter.Types.CellStruct cellStruct,
|
|
Matrix4x4 cellTransform,
|
|
Vector3 cellWorldPosition,
|
|
Quaternion cellRotation,
|
|
IReadOnlyList<(uint StaticObjectId, Vector3 LocalPos, Quaternion Rot, bool IsSetup, Matrix4x4 Transform)> staticObjects)
|
|
{
|
|
// 1. Compute the deduplicated cell-geometry id (matches WB GetEnvCellGeomId).
|
|
var cellGeomId = GetEnvCellGeomId(envCell.EnvironmentId, envCell.CellStructure, envCell.Surfaces);
|
|
|
|
// 2. Trigger mesh prep for the cell geometry on ObjectMeshManager.
|
|
// This populates ObjectMeshManager._renderData[cellGeomId] when complete.
|
|
_ = _meshManager.PrepareEnvCellGeomMeshDataAsync(cellGeomId, envCell.EnvironmentId,
|
|
envCell.CellStructure, envCell.Surfaces);
|
|
|
|
// 3. Compute local bounds from cellStruct vertex array.
|
|
var localBounds = ComputeLocalBoundsFromCellStruct(cellStruct);
|
|
var worldBounds = TransformBoundingBox(localBounds, cellTransform);
|
|
|
|
// 4. Create the per-cell SceneryInstance.
|
|
var cellInstance = new EnvCellSceneryInstance
|
|
{
|
|
ObjectId = cellGeomId,
|
|
InstanceId = envCellId, // uint InstanceId maps to the cell id
|
|
IsBuilding = true,
|
|
IsEntryCell = false, // could be derived from entry-portal walk; default false
|
|
WorldPosition = cellWorldPosition,
|
|
LocalPosition = Vector3.Zero,
|
|
Rotation = cellRotation,
|
|
Scale = Vector3.One,
|
|
Transform = cellTransform,
|
|
LocalBoundingBox = localBounds,
|
|
BoundingBox = worldBounds,
|
|
};
|
|
|
|
var lb = _landblocks.GetOrAdd(landblockId, id => new EnvCellLandblock
|
|
{
|
|
GridX = (int)((id >> 24) & 0xFFu),
|
|
GridY = (int)((id >> 16) & 0xFFu),
|
|
});
|
|
|
|
lock (lb.Lock)
|
|
{
|
|
lb.PendingInstances ??= new List<EnvCellSceneryInstance>(capacity: 32);
|
|
lb.PendingInstances.Add(cellInstance);
|
|
lb.PendingEnvCellBounds ??= new Dictionary<uint, WbBoundingBox>();
|
|
lb.PendingEnvCellBounds[envCellId] = worldBounds;
|
|
|
|
// Add static-object instances inside the cell.
|
|
foreach (var stab in staticObjects)
|
|
{
|
|
// Trigger mesh prep for the stab too (idempotent — ObjectMeshManager dedupes).
|
|
_ = _meshManager.PrepareMeshDataAsync(stab.StaticObjectId, stab.IsSetup);
|
|
|
|
WbBoundingBox stabBoundsLocal;
|
|
var rawBounds = _meshManager.GetBounds(stab.StaticObjectId, stab.IsSetup);
|
|
if (rawBounds.HasValue)
|
|
stabBoundsLocal = new WbBoundingBox(rawBounds.Value.Min, rawBounds.Value.Max);
|
|
else
|
|
stabBoundsLocal = new WbBoundingBox(Vector3.Zero, Vector3.Zero);
|
|
|
|
var stabBoundsWorld = TransformBoundingBox(stabBoundsLocal, stab.Transform);
|
|
|
|
lb.PendingInstances.Add(new EnvCellSceneryInstance
|
|
{
|
|
ObjectId = stab.StaticObjectId,
|
|
InstanceId = 0,
|
|
IsSetup = stab.IsSetup,
|
|
IsBuilding = false,
|
|
Transform = stab.Transform,
|
|
BoundingBox = stabBoundsWorld,
|
|
LocalBoundingBox = stabBoundsLocal,
|
|
Scale = Vector3.One,
|
|
Rotation = stab.Rot,
|
|
// CurrentPreviewCellId = 0; the per-cell CellId is written during PopulatePartGroups
|
|
CurrentPreviewCellId = envCellId, // carry the parent cellId for PartGroups dispatch
|
|
});
|
|
|
|
// Union the cell bounds with the stab bounds.
|
|
var current = lb.PendingEnvCellBounds[envCellId];
|
|
lb.PendingEnvCellBounds[envCellId] = WbBoundingBox.Union(current, stabBoundsWorld);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically promotes the pending instance list to the committed list,
|
|
/// computes total EnvCell bounds, populates PartGroups, and marks the
|
|
/// landblock ready for rendering.
|
|
/// </summary>
|
|
public void FinalizeLandblock(uint landblockId)
|
|
{
|
|
if (!_landblocks.TryGetValue(landblockId, out var lb)) return;
|
|
lock (lb.Lock)
|
|
{
|
|
if (lb.PendingInstances is not null)
|
|
{
|
|
lb.Instances = lb.PendingInstances;
|
|
lb.PendingInstances = null;
|
|
}
|
|
if (lb.PendingEnvCellBounds is not null)
|
|
{
|
|
lb.EnvCellBounds = lb.PendingEnvCellBounds;
|
|
lb.PendingEnvCellBounds = null;
|
|
}
|
|
|
|
// Compute total bounds union across all cells.
|
|
var total = new WbBoundingBox(new Vector3(float.MaxValue), new Vector3(float.MinValue));
|
|
foreach (var b in lb.EnvCellBounds.Values)
|
|
total = WbBoundingBox.Union(total, b);
|
|
lb.TotalEnvCellBounds = total;
|
|
|
|
// Populate PartGroups (mirrors WB EnvCellRenderManager.cs:572-580).
|
|
PopulatePartGroups(lb);
|
|
|
|
lb.InstancesReady = true;
|
|
lb.MeshDataReady = true;
|
|
lb.GpuReady = true;
|
|
}
|
|
NeedsPrepare = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a landblock from the renderer. Future PrepareRenderBatches will exclude it.
|
|
/// </summary>
|
|
public void RemoveLandblock(uint landblockId)
|
|
{
|
|
_landblocks.TryRemove(landblockId, out _);
|
|
NeedsPrepare = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PopulatePartGroups
|
|
// Verbatim port of WB EnvCellRenderManager.cs:572-580.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Rebuilds BuildingPartGroups and StaticPartGroups from the committed Instances list.
|
|
/// Must be called under lb.Lock.
|
|
/// Source: WB EnvCellRenderManager.cs:572-580 (verbatim).
|
|
/// </summary>
|
|
private void PopulatePartGroups(EnvCellLandblock lb)
|
|
{
|
|
// WB EnvCellRenderManager.cs:573-574:
|
|
lb.StaticPartGroups.Clear();
|
|
lb.BuildingPartGroups.Clear();
|
|
|
|
// WB EnvCellRenderManager.cs:575-579:
|
|
foreach (var instance in lb.Instances)
|
|
{
|
|
var targetGroup = instance.IsBuilding ? lb.BuildingPartGroups : lb.StaticPartGroups;
|
|
// WB: var cellId = instance.CurrentPreviewCellId != 0 ? instance.CurrentPreviewCellId : instance.InstanceId.DataId;
|
|
// We store the parent cellId in CurrentPreviewCellId (for stabs) or InstanceId (for cell geometry).
|
|
var cellId = instance.CurrentPreviewCellId != 0 ? instance.CurrentPreviewCellId : instance.InstanceId;
|
|
PopulateRecursive(targetGroup, instance.ObjectId, instance.IsSetup, instance.Transform, cellId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively walks a Setup's parts (or handles a plain GfxObj directly),
|
|
/// adding one <see cref="InstanceData"/> per leaf GfxObj to the target group.
|
|
/// Mirrors WB ObjectRenderManagerBase.PopulateRecursive.
|
|
/// </summary>
|
|
private void PopulateRecursive(
|
|
Dictionary<ulong, List<InstanceData>> group,
|
|
ulong objectId,
|
|
bool isSetup,
|
|
Matrix4x4 transform,
|
|
uint cellId)
|
|
{
|
|
if (isSetup)
|
|
{
|
|
// For a Setup, TryGetRenderData returns a record whose SetupParts lists
|
|
// (partId, partTransform) pairs. We recursively handle each part.
|
|
var rd = _meshManager.TryGetRenderData(objectId);
|
|
if (rd is null || !rd.IsSetup) return;
|
|
|
|
foreach (var (partId, partTransform) in rd.SetupParts)
|
|
{
|
|
// Compose: world = partTransform (relative to setup) * setup world transform.
|
|
//
|
|
// FIX 2026-05-28: detect nested Setups via the high-byte
|
|
// 0x02 convention (Setup IDs start with 0x02; plain GfxObj
|
|
// IDs start with 0x01). Mirrors WB
|
|
// ObjectRenderManagerBase.cs:813. Original port hardcoded
|
|
// isSetup:false which silently dropped any nested Setup —
|
|
// its TryGetRenderData returns IsSetup=true, and Render's
|
|
// `if (!renderData.IsSetup)` guard then skips the draw.
|
|
var combined = partTransform * transform;
|
|
bool partIsSetup = (partId >> 24) == 0x02UL;
|
|
PopulateRecursive(group, partId, partIsSetup, combined, cellId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Plain GfxObj — just add one InstanceData.
|
|
if (!group.TryGetValue(objectId, out var list))
|
|
{
|
|
list = new List<InstanceData>();
|
|
group[objectId] = list;
|
|
}
|
|
list.Add(new InstanceData
|
|
{
|
|
Transform = transform,
|
|
CellId = cellId,
|
|
Flags = 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PrepareRenderBatches
|
|
// Verbatim port of WB EnvCellRenderManager.cs:247-373.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Frustum-culls all registered landblocks and builds a new
|
|
/// <see cref="EnvCellVisibilitySnapshot"/> that the render thread consumes.
|
|
/// Call once per frame, before <see cref="Render"/>.
|
|
/// Source: WB EnvCellRenderManager.cs:247-373 (verbatim).
|
|
/// </summary>
|
|
public void PrepareRenderBatches(
|
|
Matrix4x4 viewProjection,
|
|
Vector3 cameraPosition,
|
|
HashSet<uint>? filter = null,
|
|
int? centerLbX = null,
|
|
int? centerLbY = null,
|
|
int? renderRadius = null)
|
|
{
|
|
// Phase U.4 fix: stash the view-projection so Render() can upload it itself.
|
|
_lastViewProjection = viewProjection;
|
|
|
|
// WB EnvCellRenderManager.cs:249-250:
|
|
if (!_initialized || cameraPosition.Z > 4000) return;
|
|
|
|
if (filter is { Count: 0 })
|
|
{
|
|
lock (_renderLock)
|
|
{
|
|
_poolIndex = 0;
|
|
_activeSnapshot = new EnvCellVisibilitySnapshot();
|
|
_activeSnapshotGlobalGroups = new Dictionary<ulong, List<InstanceData>>();
|
|
_activeSnapshotGlobalGfxObjIds = new List<ulong>();
|
|
NeedsPrepare = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:251-253:
|
|
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.
|
|
|
|
// WB EnvCellRenderManager.cs:262:
|
|
// Filter loaded landblocks by GpuReady + Instances non-empty.
|
|
var landblocks = new List<EnvCellLandblock>();
|
|
foreach (var lb in _landblocks.Values)
|
|
{
|
|
if (centerLbX.HasValue && centerLbY.HasValue && renderRadius.HasValue)
|
|
{
|
|
if (Math.Abs(lb.GridX - centerLbX.Value) > renderRadius.Value ||
|
|
Math.Abs(lb.GridY - centerLbY.Value) > renderRadius.Value)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (lb.GpuReady && lb.Instances.Count > 0)
|
|
landblocks.Add(lb);
|
|
}
|
|
if (landblocks.Count == 0) return;
|
|
|
|
// WB EnvCellRenderManager.cs:265-267:
|
|
// Use ThreadLocal to avoid contention on ConcurrentDictionaries during parallel grouping.
|
|
using var threadLocalBatchedByCell =
|
|
new ThreadLocal<Dictionary<uint, Dictionary<ulong, List<InstanceData>>>>(
|
|
() => new(), trackAllValues: true);
|
|
using var threadLocalGlobalGroups =
|
|
new ThreadLocal<Dictionary<ulong, List<InstanceData>>>(
|
|
() => new(), trackAllValues: true);
|
|
|
|
// WB EnvCellRenderManager.cs:269:
|
|
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
|
|
|
|
// WB EnvCellRenderManager.cs:270-325:
|
|
Parallel.ForEach(landblocks, parallelOptions, lb =>
|
|
{
|
|
lock (lb.Lock)
|
|
{
|
|
var testResult = _frustum.TestBox(lb.TotalEnvCellBounds);
|
|
if (testResult == FrustumTestResult.Outside) return;
|
|
|
|
var lbBatchedByCell = threadLocalBatchedByCell.Value!;
|
|
var lbGlobalGroups = threadLocalGlobalGroups.Value!;
|
|
|
|
// WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
|
|
if (testResult == FrustumTestResult.Inside)
|
|
{
|
|
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
|
|
foreach (var instanceData in instances)
|
|
{
|
|
if (filter != null && !filter.Contains(instanceData.CellId)) continue;
|
|
AddToGroups(lbBatchedByCell, lbGlobalGroups, instanceData.CellId, gfxObjId, instanceData);
|
|
}
|
|
foreach (var (gfxObjId, instances) in lb.StaticPartGroups)
|
|
foreach (var instanceData in instances)
|
|
{
|
|
if (filter != null && !filter.Contains(instanceData.CellId)) continue;
|
|
AddToGroups(lbBatchedByCell, lbGlobalGroups, instanceData.CellId, gfxObjId, instanceData);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
|
|
var visibleCells = new HashSet<uint>();
|
|
foreach (var kvp in lb.EnvCellBounds)
|
|
{
|
|
var cellId = kvp.Key;
|
|
if (filter != null && !filter.Contains(cellId)) continue;
|
|
if (_frustum.Intersects(kvp.Value))
|
|
visibleCells.Add(cellId);
|
|
}
|
|
|
|
if (visibleCells.Count > 0)
|
|
{
|
|
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
|
|
foreach (var instanceData in instances)
|
|
{
|
|
if (visibleCells.Contains(instanceData.CellId))
|
|
AddToGroups(lbBatchedByCell, lbGlobalGroups, instanceData.CellId, gfxObjId, instanceData);
|
|
}
|
|
foreach (var (gfxObjId, instances) in lb.StaticPartGroups)
|
|
foreach (var instanceData in instances)
|
|
{
|
|
if (visibleCells.Contains(instanceData.CellId))
|
|
AddToGroups(lbBatchedByCell, lbGlobalGroups, instanceData.CellId, gfxObjId, instanceData);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// WB EnvCellRenderManager.cs:327-373: merge thread-locals + atomic swap.
|
|
|
|
var newBatchedByCell = new Dictionary<uint, Dictionary<ulong, List<InstanceData>>>();
|
|
var newVisibleGroups = new Dictionary<ulong, List<InstanceData>>();
|
|
var newVisibleGfxObjIds = new List<ulong>();
|
|
|
|
// WB EnvCellRenderManager.cs:333-347: merge per-cell batches.
|
|
foreach (var localBatchedByCell in threadLocalBatchedByCell.Values)
|
|
{
|
|
foreach (var cellKvp in localBatchedByCell)
|
|
{
|
|
if (!newBatchedByCell.TryGetValue(cellKvp.Key, out var gfxDict))
|
|
{
|
|
gfxDict = new Dictionary<ulong, List<InstanceData>>();
|
|
newBatchedByCell[cellKvp.Key] = gfxDict;
|
|
}
|
|
foreach (var gfxKvp in cellKvp.Value)
|
|
{
|
|
if (!gfxDict.TryGetValue(gfxKvp.Key, out var list))
|
|
{
|
|
list = GetPooledList();
|
|
gfxDict[gfxKvp.Key] = list;
|
|
}
|
|
list.AddRange(gfxKvp.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:349-358: merge global groups.
|
|
foreach (var localGlobalGroups in threadLocalGlobalGroups.Values)
|
|
{
|
|
foreach (var kvp in localGlobalGroups)
|
|
{
|
|
if (!newVisibleGroups.TryGetValue(kvp.Key, out var list))
|
|
{
|
|
list = GetPooledList();
|
|
newVisibleGroups[kvp.Key] = list;
|
|
newVisibleGfxObjIds.Add(kvp.Key);
|
|
}
|
|
list.AddRange(kvp.Value);
|
|
}
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:361-372: atomic swap under _renderLock.
|
|
//
|
|
// FIX 2026-05-28 (pool aliasing root cause): capture _poolIndex's
|
|
// high-water mark from the merge phase into the snapshot's
|
|
// PostPreparePoolIndex BEFORE the reset to 0. Render reads it back
|
|
// to set its pool cursor past the snapshot's owned lists. Without
|
|
// this capture, Render's filter-path GetPooledList returns lists
|
|
// the snapshot is still referencing, corrupting per-frame instance
|
|
// data. See docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
|
lock (_renderLock)
|
|
{
|
|
_activeSnapshot = new EnvCellVisibilitySnapshot
|
|
{
|
|
BatchedByCell = newBatchedByCell,
|
|
VisibleLandblocks = landblocks,
|
|
PostPreparePoolIndex = _poolIndex,
|
|
// VisibleGroups / VisibleGfxObjIds are stored as extra fields below.
|
|
};
|
|
// Stash the global groups on the snapshot for use in the unfiltered render path.
|
|
_activeSnapshotGlobalGroups = newVisibleGroups;
|
|
_activeSnapshotGlobalGfxObjIds = newVisibleGfxObjIds;
|
|
|
|
_poolIndex = 0;
|
|
NeedsPrepare = false;
|
|
}
|
|
}
|
|
|
|
// Extra fields to carry global groups out of PrepareRenderBatches into Render().
|
|
// WB stores these in VisibilitySnapshot; we keep them as sibling fields since our
|
|
// EnvCellVisibilitySnapshot only exposes BatchedByCell (per spec Task 4).
|
|
private Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
|
|
private List<ulong> _activeSnapshotGlobalGfxObjIds = new();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AddToGroups (static helper)
|
|
// Verbatim port of WB EnvCellRenderManager.cs:375-393.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private static void AddToGroups(
|
|
Dictionary<uint, Dictionary<ulong, List<InstanceData>>> batchedByCell,
|
|
Dictionary<ulong, List<InstanceData>> globalGroups,
|
|
uint cellId,
|
|
ulong gfxObjId,
|
|
InstanceData data)
|
|
{
|
|
// WB EnvCellRenderManager.cs:377-381: add to global grouping.
|
|
if (!globalGroups.TryGetValue(gfxObjId, out var globalList))
|
|
{
|
|
globalList = new List<InstanceData>();
|
|
globalGroups[gfxObjId] = globalList;
|
|
}
|
|
globalList.Add(data);
|
|
|
|
// WB EnvCellRenderManager.cs:383-392: add to per-cell grouping.
|
|
if (!batchedByCell.TryGetValue(cellId, out var gfxDict))
|
|
{
|
|
gfxDict = new Dictionary<ulong, List<InstanceData>>();
|
|
batchedByCell[cellId] = gfxDict;
|
|
}
|
|
if (!gfxDict.TryGetValue(gfxObjId, out var list))
|
|
{
|
|
list = new List<InstanceData>();
|
|
batchedByCell[cellId][gfxObjId] = list;
|
|
}
|
|
list.Add(data);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Render
|
|
// 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.
|
|
// - 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
|
|
// wire-in pattern in GameWindow.cs where _meshShader is loaded once
|
|
// for mesh_modern.{vert,frag} and shared with WbDrawDispatcher.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public void Render(WbRenderPass renderPass)
|
|
{
|
|
// WB EnvCellRenderManager.cs:396:
|
|
Render(renderPass, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws all visible EnvCells (and their static objects) for the given pass.
|
|
/// When <paramref name="filter"/> is non-null, only cells whose CellId is in
|
|
/// the set are drawn. As of Phase U.4 this is the portal-visibility SHELL
|
|
/// 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.
|
|
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
|
/// </summary>
|
|
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
|
{
|
|
// WB EnvCellRenderManager.cs:400:
|
|
if (!_initialized || _shader is null || _shader.Program == 0) return;
|
|
|
|
lock (_renderLock)
|
|
{
|
|
var snapshot = _activeSnapshot;
|
|
// 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
|
|
// 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`
|
|
// (number of cells, e.g. 18) which has no relation to the pool
|
|
// index and pointed back into snapshot data, corrupting it
|
|
// mid-Render. See docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
|
_poolIndex = snapshot.PostPreparePoolIndex;
|
|
|
|
// FIX 2026-05-28: invalidate static GL-state caches at start of Render.
|
|
// Mirrors WB EnvCellRenderManager.cs:404-410:
|
|
// CurrentVAO = 0; CurrentIBO = 0; CurrentAtlas = 0;
|
|
// CurrentInstanceBuffer = 0; CurrentCullMode = null;
|
|
//
|
|
// These caches let SetCullMode / BindVertexArray skip redundant GL
|
|
// calls when the state is already correct. BUT: between two Render()
|
|
// invocations, OTHER consumers (WbDrawDispatcher, terrain, the
|
|
// 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
|
|
// consumer. For a cottage with mixed CullMode batches, half the
|
|
// walls end up culled and the user sees "missing walls".
|
|
//
|
|
// Forcing the cache to null/0 at entry guarantees each Render call
|
|
// re-establishes the GL state it expects.
|
|
_currentVao = 0;
|
|
_currentCullMode = null;
|
|
|
|
// WB EnvCellRenderManager.cs:406-409: uniform state setup.
|
|
_shader.SetInt("uRenderPass", (int)renderPass);
|
|
_shader.SetInt("uFilterByCell", 0);
|
|
|
|
// 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,
|
|
// worst while moving. Same self-contained-GL-state precedent as the
|
|
// 2026-05-28 cull-state cache fix above.
|
|
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
|
|
|
|
var allInstances = new List<InstanceData>();
|
|
var drawCalls = new List<(ObjectRenderData renderData, int count, int offset)>();
|
|
|
|
if (filter == null)
|
|
{
|
|
// WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
|
|
foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds)
|
|
{
|
|
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
|
|
{
|
|
var renderData = _meshManager.TryGetRenderData(gfxObjId);
|
|
if (renderData != null && !renderData.IsSetup)
|
|
{
|
|
drawCalls.Add((renderData, transforms.Count, allInstances.Count));
|
|
allInstances.AddRange(transforms);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// WB EnvCellRenderManager.cs:431-468: filtered path.
|
|
// Group by gfxObjId within the filtered cells to minimize draw calls.
|
|
var filteredGroups = new Dictionary<ulong, List<InstanceData>>();
|
|
var ownedLists = new HashSet<List<InstanceData>>();
|
|
|
|
foreach (var cellId in filter)
|
|
{
|
|
if (!snapshot.BatchedByCell.TryGetValue(cellId, out var gfxDict)) continue;
|
|
foreach (var (gfxObjId, transforms) in gfxDict)
|
|
{
|
|
if (transforms.Count == 0) continue;
|
|
if (!filteredGroups.TryGetValue(gfxObjId, out var list))
|
|
{
|
|
list = transforms; // Optimization: just use the first list
|
|
filteredGroups[gfxObjId] = list;
|
|
}
|
|
else
|
|
{
|
|
if (list == transforms) continue;
|
|
|
|
// If we don't own this list yet, we must clone it before adding to it
|
|
if (!ownedLists.Contains(list))
|
|
{
|
|
var newList = GetPooledList();
|
|
newList.AddRange(list);
|
|
list = newList;
|
|
filteredGroups[gfxObjId] = list;
|
|
ownedLists.Add(list);
|
|
}
|
|
list.AddRange(transforms);
|
|
}
|
|
}
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:461-468:
|
|
foreach (var (gfxObjId, transforms) in filteredGroups)
|
|
{
|
|
var renderData = _meshManager.TryGetRenderData(gfxObjId);
|
|
if (renderData != null && !renderData.IsSetup)
|
|
{
|
|
drawCalls.Add((renderData, transforms.Count, allInstances.Count));
|
|
allInstances.AddRange(transforms);
|
|
}
|
|
}
|
|
}
|
|
|
|
// WB EnvCellRenderManager.cs:470-483:
|
|
if (allInstances.Count > 0)
|
|
{
|
|
// WB uses: if (_useModernRendering) { RenderModernMDI(...) } else { legacy }
|
|
// We always use modern (Phase N.5 mandatory).
|
|
RenderModernMDIInternal(_shader, drawCalls, allInstances, renderPass);
|
|
}
|
|
|
|
// 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));
|
|
_shader.SetInt("uRenderPass", (int)renderPass);
|
|
_gl.BindVertexArray(0);
|
|
_currentVao = 0;
|
|
|
|
// No cull restore at exit, matching WB's manager pattern: the
|
|
// last SetCullMode call reflects actual GL state, and the next
|
|
// Render call invalidates `_currentCullMode` before issuing its
|
|
// own per-batch state. The Landblock->None override below can
|
|
// intentionally leave cull disabled for the following IndoorPass,
|
|
// preserving the shipped Gate #5 baseline while deeper evidence is
|
|
// gathered.
|
|
|
|
// Update frame stats for probe emission at the call site.
|
|
_lastFrameStats.CellsRendered = filter?.Count ?? snapshot.BatchedByCell.Count;
|
|
_lastFrameStats.TrianglesDrawn = 0;
|
|
foreach (var dc in drawCalls)
|
|
_lastFrameStats.TrianglesDrawn += (dc.renderData.Batches.Count > 0
|
|
? dc.renderData.Batches[0].IndexCount / 3
|
|
: 0) * dc.count;
|
|
|
|
// 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/
|
|
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
|
|
if (renderPass == WbRenderPass.Opaque
|
|
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
|
|
{
|
|
var sb = new System.Text.StringBuilder(256);
|
|
sb.Append("[shell] filter=").Append(filter?.Count ?? -1)
|
|
.Append(" drawCalls=").Append(drawCalls.Count)
|
|
.Append(" inst=").Append(allInstances.Count)
|
|
.Append(" tris=").Append(_lastFrameStats.TrianglesDrawn);
|
|
if (filter != null)
|
|
{
|
|
foreach (var cellId in filter)
|
|
{
|
|
if (!snapshot.BatchedByCell.TryGetValue(cellId, out var gfxDict))
|
|
{
|
|
sb.Append(" [0x").Append(cellId.ToString("X8")).Append(":NOSNAP]");
|
|
continue;
|
|
}
|
|
int gfxN = 0, tf = 0, batch = 0, idx = 0, tr = 0, zh = 0;
|
|
foreach (var (gfxObjId, transforms) in gfxDict)
|
|
{
|
|
gfxN++; tf += transforms.Count;
|
|
var rd = _meshManager.TryGetRenderData(gfxObjId);
|
|
if (rd != null)
|
|
foreach (var b in rd.Batches)
|
|
{ batch++; idx += b.IndexCount; if (b.IsTransparent) tr++; if (b.BindlessTextureHandle == 0) zh++; }
|
|
}
|
|
sb.Append(" [0x").Append(cellId.ToString("X8"))
|
|
.Append(":gfx=").Append(gfxN).Append(" tf=").Append(tf)
|
|
.Append(" batch=").Append(batch).Append(" idx=").Append(idx)
|
|
.Append(" tr=").Append(tr).Append(" zh=").Append(zh).Append(']');
|
|
}
|
|
}
|
|
System.Console.WriteLine(sb.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RenderModernMDIInternal
|
|
// Extracted from WB BaseObjectRenderManager.cs:709-848 (single-slot variant).
|
|
// Groups draw calls by CullMode (+ additive flag), uploads per-frame SSBOs,
|
|
// issues glMultiDrawElementsIndirect.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private void RenderModernMDIInternal(
|
|
AcDream.App.Rendering.Shader shader,
|
|
List<(ObjectRenderData renderData, int count, int offset)> drawCalls,
|
|
List<InstanceData> allInstances,
|
|
WbRenderPass renderPass)
|
|
{
|
|
// WB BaseObjectRenderManager.cs:710-713:
|
|
if (drawCalls.Count == 0 || allInstances.Count == 0) return;
|
|
|
|
int passIdx = (int)renderPass;
|
|
if (passIdx < 0 || passIdx > 2) return;
|
|
|
|
// WB BaseObjectRenderManager.cs:715-716:
|
|
shader.Use();
|
|
shader.SetInt("uFilterByCell", 0);
|
|
|
|
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
|
|
// background, flickering when moving"): establish this pass's BLEND + DepthMask
|
|
// state OURSELVES rather than inheriting it. Render(Opaque) runs right after the
|
|
// terrain draw (which sets neither) and after particles / last frame's transparent
|
|
// pass — so whatever left GL_BLEND enabled made the OPAQUE shells composite their
|
|
// (often sub-1.0 alpha) wall textures against the bluish clear color (terrain is
|
|
// Skip'd indoors), toggling with per-frame ordering → flicker. Mirror the working
|
|
// WbDrawDispatcher passes (Disable(Blend)+DepthMask(true) opaque;
|
|
// Enable(Blend)+DepthMask(false) transparent). Restored to opaque defaults at the
|
|
// end of the draw loop so a Transparent pass can't leak into later draws.
|
|
if (renderPass == WbRenderPass.Transparent)
|
|
{
|
|
_gl.Enable(EnableCap.Blend);
|
|
_gl.DepthMask(false);
|
|
}
|
|
else
|
|
{
|
|
_gl.Disable(EnableCap.Blend);
|
|
_gl.DepthMask(true);
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:718-740: group batches by CullMode + additive flag.
|
|
var batchesByCullMode = new Dictionary<int, List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>>();
|
|
int totalDraws = 0;
|
|
|
|
foreach (var call in drawCalls)
|
|
{
|
|
foreach (var batch in call.renderData.Batches)
|
|
{
|
|
// WB BaseObjectRenderManager.cs:723-731: pass-filter.
|
|
if (renderPass != WbRenderPass.SinglePass)
|
|
{
|
|
if (batch.IsAdditive)
|
|
{
|
|
if (renderPass == WbRenderPass.Opaque) continue;
|
|
}
|
|
else if (!batch.IsTransparent)
|
|
{
|
|
if (renderPass == WbRenderPass.Transparent) continue;
|
|
}
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:732-740:
|
|
var cullMode = batch.CullMode;
|
|
var groupIdx = (int)cullMode + (batch.IsAdditive ? 4 : 0);
|
|
if (!batchesByCullMode.TryGetValue(groupIdx, out var list))
|
|
{
|
|
list = new List<(ObjectRenderBatch, int, int)>();
|
|
batchesByCullMode[groupIdx] = list;
|
|
}
|
|
list.Add((batch, call.count, call.offset));
|
|
totalDraws++;
|
|
}
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:743:
|
|
if (totalDraws == 0) return;
|
|
|
|
// WB BaseObjectRenderManager.cs:745-759: resize buffers if needed.
|
|
if (totalDraws > _mdiCommandCapacity)
|
|
{
|
|
_mdiCommandCapacity = Math.Max(_mdiCommandCapacity * 2, totalDraws);
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
|
_gl.BufferData(GLEnum.DrawIndirectBuffer,
|
|
(nuint)(_mdiCommandCapacity * sizeof(DrawElementsIndirectCommand)), null, GLEnum.DynamicDraw);
|
|
|
|
_modernBatchCapacity = _mdiCommandCapacity;
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernBatchCapacity * sizeof(ModernBatchData)), null, GLEnum.DynamicDraw);
|
|
}
|
|
|
|
int uniqueInstanceCount = allInstances.Count;
|
|
if (uniqueInstanceCount > _modernInstanceCapacity)
|
|
{
|
|
_modernInstanceCapacity = Math.Max(_modernInstanceCapacity * 2, uniqueInstanceCount);
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernInstanceCapacity * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
|
|
|
// Phase U.3: keep the clip-slot buffer (binding=3) sized to the
|
|
// instance buffer so instanceClipSlot[BaseInstance + gl_InstanceID]
|
|
// is always in range.
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
|
|
if (_commands.Length < totalDraws)
|
|
Array.Resize(ref _commands, Math.Max(_commands.Length * 2, totalDraws));
|
|
if (_modernBatches.Length < totalDraws)
|
|
Array.Resize(ref _modernBatches, Math.Max(_modernBatches.Length * 2, totalDraws));
|
|
|
|
// WB BaseObjectRenderManager.cs:764-781: build commands array.
|
|
int cmdIndex = 0;
|
|
foreach (var group in batchesByCullMode)
|
|
{
|
|
foreach (var item in group.Value)
|
|
{
|
|
_modernBatches[cmdIndex] = new ModernBatchData
|
|
{
|
|
TextureHandle = item.batch.BindlessTextureHandle,
|
|
TextureIndex = (uint)item.batch.TextureIndex,
|
|
};
|
|
|
|
_commands[cmdIndex] = new DrawElementsIndirectCommand
|
|
{
|
|
Count = (uint)item.batch.IndexCount,
|
|
InstanceCount = (uint)item.instanceCount,
|
|
FirstIndex = item.batch.FirstIndex,
|
|
BaseVertex = (int)item.batch.BaseVertex,
|
|
BaseInstance = (uint)item.instanceOffset,
|
|
};
|
|
cmdIndex++;
|
|
}
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:784-805: upload (with orphaning).
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
|
_gl.BufferData(GLEnum.DrawIndirectBuffer,
|
|
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), null, GLEnum.DynamicDraw);
|
|
fixed (DrawElementsIndirectCommand* ptr = _commands)
|
|
{
|
|
_gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0,
|
|
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), ptr);
|
|
}
|
|
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
|
if (_gpuInstanceTransforms.Length < uniqueInstanceCount)
|
|
Array.Resize(ref _gpuInstanceTransforms, Math.Max(_gpuInstanceTransforms.Length * 2, uniqueInstanceCount));
|
|
for (int i = 0; i < uniqueInstanceCount; i++)
|
|
_gpuInstanceTransforms[i] = allInstances[i].Transform;
|
|
fixed (Matrix4x4* ptr = _gpuInstanceTransforms)
|
|
{
|
|
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
|
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), ptr);
|
|
}
|
|
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(totalDraws * sizeof(ModernBatchData)), null, GLEnum.DynamicDraw);
|
|
fixed (ModernBatchData* ptr = _modernBatches)
|
|
{
|
|
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
|
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
|
|
}
|
|
|
|
// 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
|
|
// 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)];
|
|
if (_cellIdToSlot is null)
|
|
{
|
|
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < uniqueInstanceCount; i++)
|
|
_clipSlotData[i] = _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
|
|
? (uint)slot : 0u;
|
|
}
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)(uniqueInstanceCount * sizeof(uint)), null, GLEnum.DynamicDraw);
|
|
fixed (uint* ptr = _clipSlotData)
|
|
{
|
|
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
|
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
|
|
}
|
|
|
|
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
|
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
|
if (globalVao == 0) return;
|
|
if (_currentVao != globalVao)
|
|
{
|
|
_gl.BindVertexArray(globalVao);
|
|
_currentVao = globalVao;
|
|
}
|
|
|
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 0, _modernInstanceBuffer);
|
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 1, _modernBatchBuffer);
|
|
// Phase U.3: per-instance clip slots (binding=3) + shared clip regions
|
|
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
|
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
|
BindClipRegionBinding2();
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
|
|
|
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
|
|
|
// WB BaseObjectRenderManager.cs:821-847: issue per-group multi-draw calls.
|
|
int currentDrawOffset = 0;
|
|
foreach (var group in batchesByCullMode)
|
|
{
|
|
var cullMode = (CullMode)(group.Key % 4);
|
|
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
|
|
// uniformly, but the room surfaces need to be visible from inside
|
|
// under acdream's current global winding state. Render cell polys
|
|
// double-sided while the architectural cause is isolated.
|
|
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
|
|
if (_currentCullMode != cullMode)
|
|
{
|
|
SetCullMode(cullMode);
|
|
}
|
|
|
|
bool isAdditive = group.Key >= 4;
|
|
if (isAdditive)
|
|
{
|
|
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
|
shader.SetInt("uRenderPass", (int)renderPass | 0x100);
|
|
}
|
|
else
|
|
{
|
|
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
shader.SetInt("uRenderPass", (int)renderPass);
|
|
}
|
|
|
|
shader.SetInt("uDrawIDOffset", currentDrawOffset);
|
|
int numDraws = group.Value.Count;
|
|
_gl.MultiDrawElementsIndirect(
|
|
PrimitiveType.Triangles,
|
|
DrawElementsType.UnsignedShort,
|
|
(void*)(currentDrawOffset * sizeof(DrawElementsIndirectCommand)),
|
|
(uint)numDraws,
|
|
(uint)sizeof(DrawElementsIndirectCommand));
|
|
|
|
currentDrawOffset += numDraws;
|
|
}
|
|
|
|
// Phase U.4: leave a clean opaque-default render state (mirrors WbDrawDispatcher's
|
|
// post-transparent restore) so a Transparent pass's Blend-on / DepthMask-off does
|
|
// not leak into particles or the next frame's draws.
|
|
_gl.Disable(EnableCap.Blend);
|
|
_gl.DepthMask(true);
|
|
|
|
// WB BaseObjectRenderManager.cs:845-847:
|
|
shader.SetInt("uDrawIDOffset", 0);
|
|
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SetCullMode
|
|
// Verbatim copy of WB BaseObjectRenderManager.cs:850-866.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private void SetCullMode(CullMode mode)
|
|
{
|
|
_currentCullMode = mode;
|
|
switch (mode)
|
|
{
|
|
case CullMode.None:
|
|
_gl.Disable(EnableCap.CullFace);
|
|
break;
|
|
case CullMode.Clockwise:
|
|
_gl.Enable(EnableCap.CullFace);
|
|
_gl.CullFace(TriangleFace.Front);
|
|
break;
|
|
case CullMode.CounterClockwise:
|
|
case CullMode.Landblock:
|
|
_gl.Enable(EnableCap.CullFace);
|
|
_gl.CullFace(TriangleFace.Back);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BindClipRegionBinding2 (Phase U.3)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared
|
|
/// <see cref="ClipFrame"/> buffer (<see cref="SetClipRegionSsbo"/>); otherwise
|
|
/// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so
|
|
/// the shader never reads an unbound SSBO.
|
|
/// </summary>
|
|
private void BindClipRegionBinding2()
|
|
{
|
|
if (_sharedClipRegionSsbo != 0)
|
|
{
|
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
|
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
|
|
return;
|
|
}
|
|
|
|
if (_fallbackClipRegionSsbo == 0)
|
|
{
|
|
_gl.GenBuffers(1, out _fallbackClipRegionSsbo);
|
|
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
|
var zero = new byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
|
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _fallbackClipRegionSsbo);
|
|
fixed (byte* p = zero)
|
|
{
|
|
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
|
(nuint)AcDream.App.Rendering.ClipFrame.CellClipStrideBytes, p, GLEnum.DynamicDraw);
|
|
}
|
|
}
|
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
|
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// List pool (GetPooledList)
|
|
// Copied from WB ObjectRenderManagerBase (pattern).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private List<InstanceData> GetPooledList()
|
|
{
|
|
// 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
|
|
// 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)
|
|
{
|
|
if (_poolIndex < _listPool.Count)
|
|
{
|
|
var list = _listPool[_poolIndex++];
|
|
list.Clear();
|
|
return list;
|
|
}
|
|
var fresh = new List<InstanceData>();
|
|
_listPool.Add(fresh);
|
|
_poolIndex++;
|
|
return fresh;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers: bounds computation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Computes a local-space AABB from a CellStruct's vertex array.
|
|
/// </summary>
|
|
private static WbBoundingBox ComputeLocalBoundsFromCellStruct(DatReaderWriter.Types.CellStruct cellStruct)
|
|
{
|
|
if (cellStruct.VertexArray is null || cellStruct.VertexArray.Vertices is null || cellStruct.VertexArray.Vertices.Count == 0)
|
|
return new WbBoundingBox(Vector3.Zero, Vector3.Zero);
|
|
|
|
var min = new Vector3(float.MaxValue);
|
|
var max = new Vector3(float.MinValue);
|
|
foreach (var v in cellStruct.VertexArray.Vertices.Values)
|
|
{
|
|
var p = v.Origin;
|
|
min = Vector3.Min(min, p);
|
|
max = Vector3.Max(max, p);
|
|
}
|
|
return new WbBoundingBox(min, max);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transforms a local AABB to a world AABB by transforming all 8 corners.
|
|
/// </summary>
|
|
private static WbBoundingBox TransformBoundingBox(WbBoundingBox local, Matrix4x4 transform)
|
|
{
|
|
if (local.Min == local.Max && local.Min == Vector3.Zero)
|
|
return local;
|
|
|
|
var min = local.Min;
|
|
var max = local.Max;
|
|
|
|
Span<Vector3> corners = stackalloc Vector3[8]
|
|
{
|
|
new Vector3(min.X, min.Y, min.Z),
|
|
new Vector3(max.X, min.Y, min.Z),
|
|
new Vector3(min.X, max.Y, min.Z),
|
|
new Vector3(max.X, max.Y, min.Z),
|
|
new Vector3(min.X, min.Y, max.Z),
|
|
new Vector3(max.X, min.Y, max.Z),
|
|
new Vector3(min.X, max.Y, max.Z),
|
|
new Vector3(max.X, max.Y, max.Z),
|
|
};
|
|
|
|
var wMin = new Vector3(float.MaxValue);
|
|
var wMax = new Vector3(float.MinValue);
|
|
foreach (var c in corners)
|
|
{
|
|
var w = Vector3.Transform(c, transform);
|
|
wMin = Vector3.Min(wMin, w);
|
|
wMax = Vector3.Max(wMax, w);
|
|
}
|
|
return new WbBoundingBox(wMin, wMax);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IDisposable
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public void Dispose()
|
|
{
|
|
if (IsDisposed) return;
|
|
IsDisposed = true;
|
|
|
|
if (_mdiCommandBuffer != 0) { _gl.DeleteBuffer(_mdiCommandBuffer); _mdiCommandBuffer = 0; }
|
|
if (_modernInstanceBuffer != 0){ _gl.DeleteBuffer(_modernInstanceBuffer); _modernInstanceBuffer = 0; }
|
|
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
|
|
}
|
|
}
|