From 3af7d0048d94bc7ad81d0f8a659e1226aa322a40 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 23 Jun 2026 17:51:19 +0200 Subject: [PATCH] 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) --- .../Rendering/RetailPViewRenderer.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 4993f5c1..3a470c5a 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -23,6 +23,11 @@ public sealed class RetailPViewRenderer new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty()); private readonly HashSet _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 _shellBatch = new(); // R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame). private readonly Dictionary> _buildingGroups = new(); @@ -656,11 +661,28 @@ public sealed class RetailPViewRenderer // Per-cell opaque+transparent keeps the far→near transparent // compositing the per-cell loop already provided. 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)) { + if (!_envCells.CellHasTransparent(entry.CellId)) continue; _oneCell.Clear(); _oneCell.Add(entry.CellId); - _envCells.Render(WbRenderPass.Opaque, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell); } }