diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 782c5374..aa6af66b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -192,7 +192,6 @@ public sealed class GameWindow : IDisposable // yet rooted; the clipRoot flip + OutsideView terrain integration is the cutover step). private LoadedCell? _outdoorNode; private readonly List _outdoorNodeBuildingCells = new(); - private readonly HashSet _outdoorNodeSeenLbs = new(); private readonly HashSet _outdoorSceneParticleEntityIds = new(); private readonly HashSet _visibleSceneParticleEntityIds = new(); private string? _lastRenderSignature; @@ -7464,26 +7463,38 @@ public sealed class GameWindow : IDisposable if (viewerRoot is null && viewerCellId != 0u) { _outdoorNodeBuildingCells.Clear(); - _outdoorNodeSeenLbs.Clear(); - int onGridX = playerLb.HasValue ? (int)((playerLb.Value >> 24) & 0xFFu) : -1; - int onGridY = playerLb.HasValue ? (int)((playerLb.Value >> 16) & 0xFFu) : -1; - foreach (var onEntry in _worldState.LandblockEntries) + // T2 (BR-4): draw-driven flood gating. Retail floods a building's + // interior exactly when its shell DRAWS and an aperture survives + // the view (DrawBuilding Ghidra 0x0059f2a0: per-view viewconeCheck + // → portal-BSP walk → ConstructView's GetClip; NO distance + // constant anywhere on the chain). Port: a per-BUILDING frustum + // pre-gate on the aperture bounds (Building.PortalBounds — the + // tight equivalent of the shell viewconeCheck for FLOOD purposes), + // replacing the old Chebyshev≤1 landblock cell-sweep; the 48 m + // seed cap dies with it (RetailPViewRenderer seeds at ∞). The + // per-portal admission stays BuildFromExterior's screen clip + // (empty clip = no seed) — retail's GetClip-vs-view gate. + // Per-building iteration is also the FPS fix the 2026-06-07 + // Chebyshev hack approximated: dozens of AABB tests instead of an + // O(all loaded cells) portal sweep. + foreach (var registry in _buildingRegistries.Values) { - uint onLb = (onEntry.LandblockId >> 16) & 0xFFFFu; - if (playerLb.HasValue) + foreach (var b in registry.All()) { - int gX = (int)((onLb >> 8) & 0xFFu); - int gY = (int)(onLb & 0xFFu); - if (Math.Max(Math.Abs(gX - onGridX), Math.Abs(gY - onGridY)) > 1) continue; + if (b.HasPortalBounds + && !AcDream.App.Rendering.FrustumCuller.IsAabbVisible( + frustum, b.PortalBounds.Min, b.PortalBounds.Max)) + continue; + + foreach (uint cid in b.EnvCellIds) + if (_cellVisibility.TryGetCell(cid, out var bc) && bc is not null) + _outdoorNodeBuildingCells.Add(bc); } - if (!_outdoorNodeSeenLbs.Add(onLb)) continue; - foreach (var onCell in _cellVisibility.GetCellsForLandblock(onLb)) - _outdoorNodeBuildingCells.Add(onCell); } _outdoorNode = AcDream.App.Rendering.OutdoorCellNode.Build(viewerCellId); if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) Console.WriteLine(System.FormattableString.Invariant( - $"[outdoor-node] cell=0x{viewerCellId:X8} nearbyCells={_outdoorNodeBuildingCells.Count} (R-A2 per-building floods)")); + $"[outdoor-node] cell=0x{viewerCellId:X8} nearbyCells={_outdoorNodeBuildingCells.Count} (T2 frustum-gated per-building floods)")); } uint playerCellId = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u; diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 93b045bc..e2faaf6a 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -26,7 +26,13 @@ public sealed class RetailPViewRenderer // R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame). private readonly Dictionary> _buildingGroups = new(); - private const float OutdoorBuildingSeedDistance = 48f; + + // T2 (BR-4): retail has NO distance constant on the flood-admission chain + // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + + // GetClip + GetVisible only). The old 48 m seed cap is replaced by the + // caller's per-building frustum pre-gate on aperture bounds (GameWindow's + // gather); seeds themselves are unbounded. + private const float OutdoorBuildingSeedDistance = float.PositiveInfinity; public RetailPViewRenderer( GL gl,