diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 5a9cb8bd..faf1a558 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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 _outdoorRootNoCells = new(0); - private readonly HashSet _exteriorPortalLandblocks = new(); - private readonly List _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 diff --git a/src/AcDream.App/Rendering/InteriorRenderer.cs b/src/AcDream.App/Rendering/InteriorRenderer.cs deleted file mode 100644 index 5de0ffc6..00000000 --- a/src/AcDream.App/Rendering/InteriorRenderer.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System.Collections.Generic; -using System.Numerics; -using AcDream.App.Rendering.Wb; -using AcDream.Core.World; - -namespace AcDream.App.Rendering; - -/// Per-frame inputs for one flood. -public sealed class InteriorRenderContext -{ - /// Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame. - public required IReadOnlyList OrderedVisibleCells { get; init; } - - /// The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys = - /// the GameWindow envCellShellFilter). A cell may appear in but - /// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the - /// membership filter; supplies the draw ORDER. - public required IReadOnlySet DrawableCells { get; init; } - - /// Per-cell portal_view slots, in the same order retail setup_view(cell, i) - /// selects them inside PView::DrawCells. - public required IReadOnlyDictionary CellClipSlots { get; init; } - - public required int OutdoorSlot { get; init; } - public required bool OutdoorVisible { get; init; } - - /// The 3-bucket entity split (). Only ByCell + - /// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door - /// step (clipped to OutsideView). - public required InteriorEntityPartition.Result Partition { get; init; } - - public required ICamera Camera { get; init; } - public required FrustumPlanes? Frustum { get; init; } - - /// 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. - public required uint? PlayerLandblockId { get; init; } - - public required HashSet? AnimatedEntityIds { get; init; } -} - -/// -/// 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. -/// -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 _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(); - return true; - } - - slots = System.Array.Empty(); - 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 bucket, HashSet? visibleCellIds) - => DrawEntityBucket( - ctx.Camera, - ctx.Frustum, - ctx.PlayerLandblockId, - ctx.AnimatedEntityIds, - bucket, - visibleCellIds); - - public void DrawEntityBucket( - ICamera camera, - FrustumPlanes? frustum, - uint? playerLandblockId, - HashSet? animatedEntityIds, - IReadOnlyList bucket, - HashSet? 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)bucket, - (IReadOnlyDictionary?)null); - - _entities.Draw( - camera, - new[] { entry }, - frustum, - neverCullLandblockId: playerLandblockId, - visibleCellIds: visibleCellIds, - animatedEntityIds: animatedEntityIds); - } -} diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 2b297da0..a85b6d2c 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -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(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? EmitDiagnostics { get; init; } } -public sealed class RetailPViewPortalDrawContext : IRetailPViewCellDrawContext -{ - public required IEnumerable CandidateCells { get; init; } - public required Vector3 ViewerEyePos { get; init; } - public required Matrix4x4 ViewProjection { get; init; } - public required Func 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? 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 Entities, - IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; } - public required Action SetTerrainClipUbo { get; init; } - public Action? DrawExitPortalMasks { get; init; } - public Action? DrawCellParticles { get; init; } - public Action? EmitDiagnostics { get; init; } -} - public sealed class RetailPViewFrameResult { public required PortalVisibilityFrame PortalFrame { get; init; }