T4 (BR-6): ONE visibility gate - ACME BFS deleted from the frame, legacy second render path deleted

The one-gate rule (feedback_render_one_gate) is now structural:

- The per-frame ACME BFS (CellVisibility.ComputeVisibilityFromRoot) is
  GONE from the frame. Its only production consumer was the
  cameraInsideCell boolean - which is exactly 'viewerRoot is not null'
  (the TryGetCell that produced viewerRoot already proves cells are
  loaded; ComputeVisibilityFromRoot returned null iff root was null).
  A full second visibility computation ran every frame to derive a
  boolean we already had. The method + its tests remain as quarantined
  non-production code (dual-live-visibility-computations, confirmed).

- The clipRoot==null mini-pipeline is DELETED (legacy-outdoor-branch-
  remnant, adjusted-confirmed): the outdoor partition draw, the
  Chebyshev look-in gather, the DrawPortal invocation and the dynamics
  fallback. clipRoot is null only when NO viewer cell exists (pre-login,
  fly/debug cameras, transient gaps) - those frames draw flat through
  the dispatcher; every normal outdoor frame is the outdoor node.

- DELETED with it: InteriorRenderer (class file - its only caller was
  the legacy branch), RetailPViewRenderer.DrawPortal +
  RetailPViewPortalDrawContext (the look-in product; outdoor-root frames
  flood buildings via MergeNearbyBuildingFloods inside DrawInside),
  the _exteriorPortal*/_outdoorRootNoCells fields.

Per frame there is now exactly ONE visibility computation
(PortalVisibilityBuilder) and ONE render path (DrawInside).

Suites: build green, App 226 green, Core baseline (1398 + 4 pre-existing
#99-era).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 13:03:06 +02:00
parent a6aec8c32f
commit 4a307d33b5
3 changed files with 20 additions and 360 deletions

View file

@ -169,7 +169,6 @@ public sealed class GameWindow : IDisposable
// R1 (render redesign): the per-cell DrawInside flood + its per-frame entity partition.
// _interiorRenderer is constructed once both renderers exist; _interiorPartition is rebuilt
// each frame on an indoor root (null on the outdoor root).
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
@ -182,9 +181,6 @@ public sealed class GameWindow : IDisposable
// three renderers so each re-binds binding=2 immediately before its own draw.
// U.4 replaces the NoClip() frame with one built from the portal-visibility result.
private ClipFrame? _clipFrame;
private readonly HashSet<uint> _outdoorRootNoCells = new(0);
private readonly HashSet<uint> _exteriorPortalLandblocks = new();
private readonly List<LoadedCell> _exteriorPortalCandidateCells = new();
// Phase 3 (render unification, 2026-06-07): the synthetic outdoor cell node — the outdoor
// world as a flood-graph cell (spec 2026-06-07-render-unification-outdoor-as-cell). Rebuilt
@ -1840,8 +1836,6 @@ public sealed class GameWindow : IDisposable
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
_envCellRenderer.Initialize(_meshShader!);
// R1: the per-cell DrawInside flood. Both renderers exist here (just constructed).
_interiorRenderer = new AcDream.App.Rendering.InteriorRenderer(_envCellRenderer!, _wbDrawDispatcher!);
_clipFrame ??= ClipFrame.NoClip();
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
@ -7314,8 +7308,13 @@ public sealed class GameWindow : IDisposable
LoadedCell? viewerRoot = null;
if (viewerCellId != 0u && _cellVisibility.TryGetCell(viewerCellId, out var viewerRegCell))
viewerRoot = viewerRegCell;
var visibility = _cellVisibility.ComputeVisibilityFromRoot(viewerRoot, viewerEyePos);
bool cameraInsideCell = visibility?.CameraCell is not null;
// T4 (BR-6): the per-frame ACME BFS (ComputeVisibilityFromRoot) is
// DELETED from the frame — it ran a full second visibility
// computation whose only production consumer was this boolean,
// which is exactly "the viewer root resolved to a loaded interior
// cell" (TryGetCell above already proves cells are loaded). The
// PView flood is the ONE visibility gate (feedback_render_one_gate).
bool cameraInsideCell = viewerRoot is not null;
// Retail render routing is owned by the collided camera/viewer cell.
// The player cell still owns lighting state, but it must not force an
@ -7732,114 +7731,19 @@ public sealed class GameWindow : IDisposable
}
else
{
if (_interiorRenderer is not null)
{
_outdoorRootNoCells.Clear();
var outdoorPartition = AcDream.App.Rendering.InteriorEntityPartition.Partition(
_outdoorRootNoCells, _worldState.LandblockEntries);
sigOutdoorRootObjectCount = outdoorPartition.OutdoorStatic.Count;
// T1: static world first (shells + scenery)…
if (outdoorPartition.OutdoorStatic.Count > 0)
{
_interiorRenderer.DrawEntityBucket(
camera,
frustum,
playerLb,
animatedIds,
outdoorPartition.OutdoorStatic,
visibleCellIds: null);
}
_exteriorPortalLandblocks.Clear();
_exteriorPortalCandidateCells.Clear();
// FPS (2026-06-07): the outdoor look-in (DrawPortal -> BuildFromExterior) seeds only
// from exit portals within MaxSeedDistance (48 m) of the camera. A landblock is 192 m,
// so any cell that could seed is in the player's landblock or an immediate neighbour;
// cells further out are already discarded by BuildFromExterior's per-portal cutoff.
// Iterating EVERY cell in EVERY loaded landblock (near radius 4 = up to 81 LBs) just to
// discard them is an O(all loaded cells) sweep every outdoor frame — the cause of the
// "FPS drops as soon as I look out" report. Restrict candidates to the 1-ring around the
// player (Chebyshev <= 1 in landblock grid). No behaviour change: the excluded cells are
// all > 48 m away and were already culled by the seed-distance cutoff.
int playerGridX = playerLb.HasValue ? (int)((playerLb.Value >> 24) & 0xFFu) : -1;
int playerGridY = playerLb.HasValue ? (int)((playerLb.Value >> 16) & 0xFFu) : -1;
foreach (var entry in _worldState.LandblockEntries)
{
uint lbPrefix = (entry.LandblockId >> 16) & 0xFFFFu;
if (playerLb.HasValue)
{
int gX = (int)((lbPrefix >> 8) & 0xFFu);
int gY = (int)(lbPrefix & 0xFFu);
if (Math.Max(Math.Abs(gX - playerGridX), Math.Abs(gY - playerGridY)) > 1)
continue;
}
if (!_exteriorPortalLandblocks.Add(lbPrefix))
continue;
foreach (var cell in _cellVisibility.GetCellsForLandblock(lbPrefix))
_exteriorPortalCandidateCells.Add(cell);
}
if (_exteriorPortalCandidateCells.Count > 0 && _retailPViewRenderer is not null)
{
var portalResult = _retailPViewRenderer.DrawPortal(
new AcDream.App.Rendering.RetailPViewPortalDrawContext
{
CandidateCells = _exteriorPortalCandidateCells,
ViewerEyePos = viewerEyePos,
ViewProjection = envCellViewProj,
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
Camera = camera,
CameraWorldPosition = camPos,
Frustum = frustum,
PlayerLandblockId = playerLb,
AnimatedEntityIds = animatedIds,
RenderCenterLbX = renderCenterLbX,
RenderCenterLbY = renderCenterLbY,
RenderRadius = _nearRadius,
MaxSeedDistance = 48f,
LandblockEntries = _worldState.LandblockEntries,
SetTerrainClipUbo = uboId => _terrain?.SetClipUbo(uboId),
// T1: look-in — PUNCH building entry apertures to far-Z so
// the flooded interior shows through the doorway. Safe:
// dynamics draw after this whole block.
DrawExitPortalMasks = sliceCtx =>
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
forceFarZ: true),
});
if (portalResult is not null)
{
sigOutdoorPortalDrawn = true;
sigExteriorPvFrame = portalResult.PortalFrame;
sigExteriorClipAssembly = portalResult.ClipAssembly;
sigExteriorDrawableCells = portalResult.DrawableCells;
sigExteriorPartition = portalResult.Partition;
}
}
// T1: …then ALL dynamics last (after the look-in punched +
// drew interiors), depth-tested, never hard-clipped.
if (outdoorPartition.Dynamics.Count > 0)
{
sigLiveDynamicDrawnCount = outdoorPartition.Dynamics.Count;
_interiorRenderer.DrawEntityBucket(
camera,
frustum,
playerLb,
animatedIds,
outdoorPartition.Dynamics,
visibleCellIds: null);
}
}
else
{
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
neverCullLandblockId: playerLb,
visibleCellIds: null,
animatedEntityIds: animatedIds);
}
// T4 (BR-6): the old clipRoot==null mini-pipeline (outdoor
// partition + Chebyshev look-in gather + DrawPortal + dynamics
// fallback) is DELETED — it was the SECOND render path the
// one-gate rule forbids (legacy-outdoor-branch-remnant,
// adjusted-confirmed). clipRoot is null only when NO viewer
// cell exists at all (pre-login, fly/debug cameras, transient
// streaming gaps — the outdoor node covers every normal outdoor
// frame): draw the world flat through the dispatcher; floods
// resume the moment a viewer cell resolves.
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
neverCullLandblockId: playerLb,
visibleCellIds: null,
animatedEntityIds: animatedIds);
}
// Phase U.3: close the world-geometry clip bracket opened above. From here down the