perf(pview): batch EnvCell shell opaque pass + skip empty transparent

Dense-town FPS root cause: DrawEnvCellShells called the heavy per-frame
EnvCellRenderer.Render once PER cell x opaque+transparent (~94 calls/frame,
24.75ms = 75% of GPU at Arwic). Batch opaque into one Render(Opaque, allCells)
(z-buffer order; per-instance CellId-keyed lighting => safe) + skip the
transparent Render for opaque-only cells, keep far->near for the rest.

Measured (Arwic, same facing, profiler on): cells 94 calls/24.75ms -> 1 call/
0.37ms; frame 34ms/29fps -> ~10ms/100fps p50.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-23 17:51:19 +02:00
parent f72f7ce1f4
commit 3af7d0048d

View file

@ -23,6 +23,11 @@ public sealed class RetailPViewRenderer
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>()); new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
private readonly HashSet<uint> _oneCell = new(1); private readonly HashSet<uint> _oneCell = new(1);
// Shell-batch scratch: all of a pass's cells collected for ONE batched
// opaque Render call (instead of one heavy Render per cell). Reused across
// frames + across look-in buildings. Spec:
// docs/superpowers/specs/2026-06-23-envcell-shell-batching-design.md
private readonly HashSet<uint> _shellBatch = new();
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame). // R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new(); private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
@ -656,11 +661,28 @@ public sealed class RetailPViewRenderer
// Per-cell opaque+transparent keeps the far→near transparent // Per-cell opaque+transparent keeps the far→near transparent
// compositing the per-cell loop already provided. // compositing the per-cell loop already provided.
UseIndoorMembershipOnlyRouting(); UseIndoorMembershipOnlyRouting();
// Opaque: ONE batched Render for all shell cells (was one heavy per-frame
// Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
// Arwic). Opaque needs no draw order (z-buffer), and lighting is
// per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI-
// Internal), so cross-cell batching is visually identical. The filtered
// Render path already groups all cells' instances into one MDI.
_shellBatch.Clear();
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
_shellBatch.Add(entry.CellId);
if (_shellBatch.Count > 0)
_envCells.Render(WbRenderPass.Opaque, _shellBatch);
// Transparent: far→near order matters for compositing, so keep these
// per-cell in ShellPass order — but skip cells with no transparent
// geometry (most are opaque-only walls/floors), removing the bulk of the
// per-cell transparent Render calls.
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
{ {
if (!_envCells.CellHasTransparent(entry.CellId)) continue;
_oneCell.Clear(); _oneCell.Clear();
_oneCell.Add(entry.CellId); _oneCell.Add(entry.CellId);
_envCells.Render(WbRenderPass.Opaque, _oneCell);
_envCells.Render(WbRenderPass.Transparent, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell);
} }
} }