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

View file

@ -1,164 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
/// <summary>Per-frame inputs for one <see cref="InteriorRenderer.DrawInside"/> flood.</summary>
public sealed class InteriorRenderContext
{
/// <summary>Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame.</summary>
public required IReadOnlyList<uint> OrderedVisibleCells { get; init; }
/// <summary>The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys =
/// the GameWindow envCellShellFilter). A cell may appear in <see cref="OrderedVisibleCells"/> but
/// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the
/// membership filter; <see cref="OrderedVisibleCells"/> supplies the draw ORDER.</summary>
public required IReadOnlySet<uint> DrawableCells { get; init; }
/// <summary>Per-cell portal_view slots, in the same order retail setup_view(cell, i)
/// selects them inside PView::DrawCells.</summary>
public required IReadOnlyDictionary<uint, int[]> CellClipSlots { get; init; }
public required int OutdoorSlot { get; init; }
public required bool OutdoorVisible { get; init; }
/// <summary>The 3-bucket entity split (<see cref="InteriorEntityPartition"/>). Only ByCell +
/// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door
/// step (clipped to OutsideView).</summary>
public required InteriorEntityPartition.Result Partition { get; init; }
public required ICamera Camera { get; init; }
public required FrustumPlanes? Frustum { get; init; }
/// <summary>The full FFFF-suffixed landblock id of the player. Used as BOTH the synthetic
/// per-cell entry id AND neverCullLandblockId so the degenerate (zero) synthetic AABB is never
/// landblock-culled — per-entity frustum culling inside Draw still applies.</summary>
public required uint? PlayerLandblockId { get; init; }
public required HashSet<uint>? AnimatedEntityIds { get; init; }
}
/// <summary>
/// The interior render flood, matching retail PView::DrawCells @ 0x005a4840:
/// after the caller handles outside_view terrain + the depth-only clear, DrawCells
/// walks cell_draw_list from the end back to zero in separate stages: cell shells,
/// then each cell's object_list. The transparent shell pass is split out because
/// the modern renderer batches opaque/transparent surfaces separately.
/// </summary>
public sealed class InteriorRenderer
{
private readonly EnvCellRenderer _envCells;
private readonly WbDrawDispatcher _entities;
// Reused single-cell filter set — cleared + repopulated per cell to avoid per-frame allocs.
private readonly HashSet<uint> _oneCell = new(1);
public InteriorRenderer(EnvCellRenderer envCells, WbDrawDispatcher entities)
{
_envCells = envCells;
_entities = entities;
}
public void DrawInside(InteriorRenderContext ctx)
{
// Retail Loop 2: DrawEnvCell for each drawable cell, farthest-to-nearest
// (cell_draw_list[cell_draw_num - 1] down to 0).
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
ApplyMembershipOnlyRouting();
_envCells.Render(WbRenderPass.Opaque, _oneCell);
}
// Retail Loop 3: Render::PortalList = cell->portal_view; DrawObjCellForDummies(cell).
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
if (ctx.Partition.ByCell.TryGetValue(cellId, out var cellEntities) && cellEntities.Count > 0)
{
ApplyMembershipOnlyRouting();
DrawEntityBucket(ctx, cellEntities, visibleCellIds: _oneCell);
}
}
// Modern split of DrawEnvCell's transparent/additive batches, same reverse cell order.
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
ApplyMembershipOnlyRouting();
_envCells.Render(WbRenderPass.Transparent, _oneCell);
}
}
private bool TryBeginCell(InteriorRenderContext ctx, uint cellId, out int[] slots)
{
if (ctx.DrawableCells.Contains(cellId))
{
ctx.CellClipSlots.TryGetValue(cellId, out slots!);
slots ??= System.Array.Empty<int>();
return true;
}
slots = System.Array.Empty<int>();
return false;
}
private void ApplyMembershipOnlyRouting()
{
// PView membership controls which cell shell/object bucket is visited.
// Do not turn the 2D portal view into gl_ClipDistance for indoor meshes:
// that slices avatars and shell triangles at stairs/doorways instead of
// matching retail's DrawMesh view-check-then-draw behavior.
_envCells.SetClipRouting(null);
_entities.ClearClipRouting();
}
// Draws one bucket of entities via the existing dispatcher, scoped to a synthetic single-entry
// landblock list. visibleCellIds gates which entities pass the cell-membership walk (a single-cell
// set for per-cell objects; null only for fallback/outdoor buckets where clip-slot routing owns cull).
// The clip slot per entity comes from the SetClipRouting the caller installed (cellIdToSlot +
// outdoorSlot + outdoorVisible) via ResolveEntitySlot.
private void DrawEntityBucket(
InteriorRenderContext ctx, IReadOnlyList<WorldEntity> bucket, HashSet<uint>? visibleCellIds)
=> DrawEntityBucket(
ctx.Camera,
ctx.Frustum,
ctx.PlayerLandblockId,
ctx.AnimatedEntityIds,
bucket,
visibleCellIds);
public void DrawEntityBucket(
ICamera camera,
FrustumPlanes? frustum,
uint? playerLandblockId,
HashSet<uint>? animatedEntityIds,
IReadOnlyList<WorldEntity> bucket,
HashSet<uint>? visibleCellIds)
{
// LandblockId == neverCullLandblockId (PlayerLandblockId) ⇒ the degenerate (zero) AABB is
// never landblock-frustum-culled; per-entity AABB culling inside Draw still applies.
uint lbId = playerLandblockId ?? 0u;
var entry = (lbId, Vector3.Zero, Vector3.Zero,
(IReadOnlyList<WorldEntity>)bucket,
(IReadOnlyDictionary<uint, WorldEntity>?)null);
_entities.Draw(
camera,
new[] { entry },
frustum,
neverCullLandblockId: playerLandblockId,
visibleCellIds: visibleCellIds,
animatedEntityIds: animatedEntityIds);
}
}

View file

@ -186,62 +186,6 @@ public sealed class RetailPViewRenderer
}
}
public RetailPViewFrameResult? DrawPortal(RetailPViewPortalDrawContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
var pvFrame = PortalVisibilityBuilder.BuildFromExterior(
ctx.CandidateCells,
ctx.ViewerEyePos,
ctx.CellLookup,
ctx.ViewProjection,
ctx.MaxSeedDistance);
if (pvFrame.OrderedVisibleCells.Count == 0)
{
RestoreNoClip(ctx.SetTerrainClipUbo);
return null;
}
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
UploadClipFrame(ctx.SetTerrainClipUbo);
var drawableCells = new HashSet<uint>(clipAssembly.CellIdToSlot.Keys);
UseIndoorMembershipOnlyRouting();
_envCells.PrepareRenderBatches(
ctx.ViewProjection,
ctx.CameraWorldPosition,
filter: drawableCells,
centerLbX: ctx.RenderCenterLbX,
centerLbY: ctx.RenderCenterLbY,
renderRadius: ctx.RenderRadius);
var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries);
var result = new RetailPViewFrameResult
{
PortalFrame = pvFrame,
ClipAssembly = clipAssembly,
DrawableCells = drawableCells,
Partition = partition,
};
ctx.EmitDiagnostics?.Invoke(result);
// T1: look-in order — punch the apertures, then interior cells WHOLE,
// then the looked-into building's per-cell statics. Dynamics are NOT
// drawn here: they belong exclusively to the frame's single last
// entity pass (the outdoor root's DrawDynamicsLast), which prevents
// double-draws of entities inside looked-into buildings.
var viewcone = ViewconeCuller.Build(clipAssembly, ctx.ViewProjection);
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(pvFrame);
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
RestoreNoClip(ctx.SetTerrainClipUbo);
return result;
}
private void DrawLandscapeThroughOutsideView(
RetailPViewDrawContext ctx,
ClipFrameAssembly clipAssembly,
@ -644,30 +588,6 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
}
public sealed class RetailPViewPortalDrawContext : IRetailPViewCellDrawContext
{
public required IEnumerable<LoadedCell> CandidateCells { get; init; }
public required Vector3 ViewerEyePos { get; init; }
public required Matrix4x4 ViewProjection { get; init; }
public required Func<uint, LoadedCell?> CellLookup { get; init; }
public required ICamera Camera { get; init; }
public required Vector3 CameraWorldPosition { get; init; }
public required FrustumPlanes? Frustum { get; init; }
public required uint? PlayerLandblockId { get; init; }
public required HashSet<uint>? AnimatedEntityIds { get; init; }
public required int RenderCenterLbX { get; init; }
public required int RenderCenterLbY { get; init; }
public required int RenderRadius { get; init; }
public required float MaxSeedDistance { get; init; }
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
public required Action<uint> SetTerrainClipUbo { get; init; }
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
}
public sealed class RetailPViewFrameResult
{
public required PortalVisibilityFrame PortalFrame { get; init; }