feat(render): Phase A8 — indoor visibility + streaming fixes batch
Lands the working A8 indoor-rendering and streaming fixes accumulated this session. User has verified these visually to some degree (e.g. lifestone / translucent meshes confirmed fine under the FrontFace flip; bridge / wall / collision regressions confirmed fixed after travel); not every path has been exhaustively gated. The cellar-flap defect remains OPEN and will be solved the retail-faithful way via a dedicated brainstorm (see handoff docs). Rendering core (reviewed, high confidence): - EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of the 80B CPU InstanceData struct the shader never expected — fixes the transform/texture "explosion" for any draw with >1 instance (cells that dedupe to a shared cellGeomId). Real root cause. - WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into same-cull runs with absolute uDrawIDOffset per run). - EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) + WorldEntity.BuildingShellAnchorCellId so building shells scope to their dat-derived building cell instead of rendering everywhere. - RenderOutsideInAcdream (look into buildings from outside) + CollectVisiblePortalBuildings frustum cull of portal bounds. - Sky-when-inside-building + per-cell audit probe + GL-state probe. Streaming / perf (test-covered; not independently code-reviewed this session): - Near/far priority queues so near work wins over far; PromoteToNear carries full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids rebuilding the animated-lookup dict in the hot draw path. Fixes the bridge-not-appearing / missing-walls / broken-collision-after-travel regressions and improves post-transition FPS. Tooling + docs: - tools/A8CellAudit: offline dat cell/portal/building dumper (portals + buildings modes) — reproduces the cellar-flap investigation with no launch. - docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil double-duty finding + the WB-recursive design decision + brainstorm prompt), entity-taxonomy, replan, issue-78 visibility investigation. Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert provisional pos.w clamp, and the probe families are kept (env-var gated, zero cost when off) because the pending option-2 cellar-flap brainstorm needs them. Strip in the option-2 ship commit. Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8 visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e415bb3863
commit
5dc4140c11
38 changed files with 3965 additions and 277 deletions
|
|
@ -23,7 +23,6 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DatReaderWriter.Enums;
|
||||
|
|
@ -70,6 +69,10 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
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>();
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
|
|
@ -193,7 +196,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_gl.GenBuffers(1, out _modernInstanceBuffer);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(InstanceData)), null, GLEnum.DynamicDraw);
|
||||
(nuint)(_modernInstanceCapacity * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
||||
|
||||
// Per-batch data SSBO (binding=1)
|
||||
_gl.GenBuffers(1, out _modernBatchBuffer);
|
||||
|
|
@ -464,11 +467,30 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
/// 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)
|
||||
public void PrepareRenderBatches(
|
||||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraPosition,
|
||||
HashSet<uint>? filter = null,
|
||||
int? centerLbX = null,
|
||||
int? centerLbY = null,
|
||||
int? renderRadius = null)
|
||||
{
|
||||
// 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; }
|
||||
|
||||
|
|
@ -479,8 +501,19 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
// 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:
|
||||
|
|
@ -815,32 +848,13 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_gl.BindVertexArray(0);
|
||||
_currentVao = 0;
|
||||
|
||||
// Phase A8 (2026-05-28 visual-gate-#4 follow-up): NO cull-restore
|
||||
// at exit. The Landblock→None override can leave cull DISABLED
|
||||
// if the last batch was Landblock — and that's intentional: the
|
||||
// subsequent `dispatcher.Draw(IndoorPass)` call in
|
||||
// RenderInsideOutAcdream's Step 3 wants cull-off too, because
|
||||
// AC's cottage-shell GfxObj parts (the wooden floor planks +
|
||||
// wall slabs that the player walks on / through) have winding
|
||||
// that gets back-face-culled by the dispatcher's default
|
||||
// FrontFace=CCW. Letting cull stay off through IndoorPass
|
||||
// renders both shell and cell mesh double-sided, so floors are
|
||||
// visible from above (and inverted-front-facing wall slabs are
|
||||
// visible from inside the room). Step 4's
|
||||
// `gl.Enable(EnableCap.CullFace)` (line ~10768) + the cleanup
|
||||
// block's enable (line ~10870) re-establish cull-back before
|
||||
// LiveDynamic chars / NPCs / doors render — so those still
|
||||
// look solid (no see-through head). The static `_currentVao`
|
||||
// is reset because the next Render call's batch loop needs to
|
||||
// re-issue BindVertexArray regardless; `_currentCullMode` is
|
||||
// intentionally left at None so the cache matches actual GL
|
||||
// state until the next Render call's per-batch SetCullMode
|
||||
// either confirms or re-sets it.
|
||||
//
|
||||
// The retail-faithful long-term move is matching WB's
|
||||
// glFrontFace(CW) globally (GameScene.cs:843) so cull-back
|
||||
// selects the correct side for AC's polygon winding without
|
||||
// double-sided rendering — deferred until a wider audit.
|
||||
// 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;
|
||||
|
|
@ -932,7 +946,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_modernInstanceCapacity = Math.Max(_modernInstanceCapacity * 2, uniqueInstanceCount);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(InstanceData)), null, GLEnum.DynamicDraw);
|
||||
(nuint)(_modernInstanceCapacity * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
|
||||
|
|
@ -977,12 +991,15 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(uniqueInstanceCount * sizeof(InstanceData)), null, GLEnum.DynamicDraw);
|
||||
var instancesSpan = CollectionsMarshal.AsSpan(allInstances);
|
||||
fixed (InstanceData* ptr = instancesSpan)
|
||||
(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(InstanceData)), ptr);
|
||||
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
||||
|
|
@ -1014,23 +1031,10 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
foreach (var group in batchesByCullMode)
|
||||
{
|
||||
var cullMode = (CullMode)(group.Key % 4);
|
||||
// Phase A8 fix (2026-05-28 visual-gate-#3 evidence): override
|
||||
// CullMode.Landblock to None for cell-mesh batches. WB sets
|
||||
// glFrontFace(CW) globally (GameScene.cs:843) so its CullMode
|
||||
// mapping (Landblock→Back) culls the correct side; we set
|
||||
// glFrontFace(CCW) in WbDrawDispatcher (line 1056) so the
|
||||
// mapping would cull the OPPOSITE side, hiding cell floors.
|
||||
// Cell-mesh polys with CullMode.Landblock represent the floor +
|
||||
// walls + ceiling of a single room — they face different
|
||||
// directions but share one CullMode value, so a single cull
|
||||
// setting can't be correct for all of them. The retail-faithful
|
||||
// approach is double-sided rendering for cell polys (cull off),
|
||||
// matching what the cull-disable A/B diagnostic empirically
|
||||
// confirmed (floor visible with cull off in visual-gate-#3).
|
||||
// CullMode.Landblock is only ever assigned in this codebase by
|
||||
// PrepareCellStructMeshData (cell polys) — terrain has its own
|
||||
// renderer that doesn't go through this code path — so this
|
||||
// override is scoped exactly right.
|
||||
// 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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue