From 69e69408e809e763526560b4851494c1f2afa9b7 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 31 Aug 2026 05:27:25 +0200 Subject: [PATCH] refactor(render): delete legacy pview execution machinery --- .../Composition/FrameRootComposition.cs | 13 +- .../Rendering/RetailPViewPassExecutor.cs | 237 +- .../Rendering/RetailPViewRenderer.cs | 2022 +---------------- .../Rendering/Walk/WalkProductionWorldData.cs | 3 +- .../Rendering/WorldSceneRenderer.cs | 4 +- .../Rendering/BuildingGroupScratchTests.cs | 252 -- .../Rendering/HouseExitWalkReplayTests.cs | 500 ---- .../Rendering/RetailPViewPassExecutorTests.cs | 642 +----- .../Rendering/WorldSceneRendererTests.cs | 12 +- 9 files changed, 58 insertions(+), 3627 deletions(-) delete mode 100644 tests/AcDream.App.Tests/Rendering/BuildingGroupScratchTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/HouseExitWalkReplayTests.cs diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 07cba262..03ad8e2f 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -494,13 +494,18 @@ internal sealed class FrameRootCompositionPhase d.ParticleVisibility, new WorldScenePViewRenderer( new RetailPViewRenderer( - currentRenderSceneOracle, - live.RenderSceneShadow, + live.RenderSceneShadow + ?? throw new InvalidOperationException( + "The retail frame walk requires the retained render scene."), // Campaign FW3.2b-2: the walk's production world // data, published/retired with each landblock by the // render publisher (FW3.1). - live.LandblockPipeline.RenderPublisher?.WalkBuildings, - live.LandblockPipeline.RenderPublisher?.WalkLandscape, + live.LandblockPipeline.RenderPublisher?.WalkBuildings + ?? throw new InvalidOperationException( + "The retail frame walk requires the building registry."), + live.LandblockPipeline.RenderPublisher?.WalkLandscape + ?? throw new InvalidOperationException( + "The retail frame walk requires the landscape registry."), d.CellVisibility), retailPViewPassExecutor, retailPViewPassExecutor), diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 851525d1..17f70b50 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -86,37 +86,12 @@ internal interface IOutdoorSceneParticleOwnerSource IReadOnlySet OutdoorSceneParticleEntityIds { get; } } -internal readonly record struct RenderFrameEntityDrawRequest( - RenderFrameView View, - RenderFrameCandidateRoute Route, - int RouteIndex, - uint CellId, - uint TupleLandblockId); - -internal interface IRenderFrameEntityPassExecutor -{ - void BeginEntityFrame(in RenderFrameView view); - - bool DrawEntityRoute( - ICamera camera, - in RenderFrameView view, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId, - uint tupleLandblockId); - - void CompleteEntityFrame(in RenderFrameView view); - - void AbortEntityFrame(); -} - /// /// Campaign V slice V6j: backend-neutral. The order it implements is retail's and /// is written once; the four places it touches graphics state directly are owned /// by . /// internal sealed partial class RetailPViewPassExecutor : - IRetailPViewPassExecutor, IOutdoorSceneParticleOwnerSource { private readonly IWorldPassSurface _surface; @@ -134,15 +109,9 @@ internal sealed partial class RetailPViewPassExecutor : private readonly TerrainDrawDiagnosticsController _terrainDiagnostics; private readonly RetailPViewParticleClassifications _particleClassifications = new(); private readonly HashSet _noSceneParticleEntityIds = []; - private readonly Dictionary _singleCellClipRouting = new(1); - private readonly Dictionary _noCellClipRouting = new(0); - - // ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counters. - // They distinguish the walk-owned far-Z portal punches from the retired - // legacy look-in punch route. RetailPViewRenderer samples them for the - // diagnostic comparison after the walk-turn look-in routes have replayed. + // ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counter for + // the walk-owned far-Z portal punches. internal int WalkLookInPunchCountThisFrame { get; private set; } - internal int LegacyLookInPunchCountThisFrame { get; private set; } /// /// Borrowed until the next late landscape pass. The outdoor-root post-world @@ -190,7 +159,6 @@ internal sealed partial class RetailPViewPassExecutor : { AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre"; WalkLookInPunchCountThisFrame = 0; - LegacyLookInPunchCountThisFrame = 0; } } @@ -242,11 +210,6 @@ internal sealed partial class RetailPViewPassExecutor : ClipFrameAssembly reuseAssembly) => ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly); - public void AppendLookInClipFrames( - IReadOnlyList lookInFrames, - ClipFrameAssembly assembly) => - ClipFrameAssembler.AppendLookInFrames(_clipFrame, lookInFrames, assembly); - public void PrepareClipFrame(int terrainUploadCount) => _surface.PrepareClipFrame(terrainUploadCount); @@ -263,24 +226,6 @@ internal sealed partial class RetailPViewPassExecutor : _entities.ClearClipRouting(); } - public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice) - { - _singleCellClipRouting.Clear(); - _singleCellClipRouting.Add(cellId, slice.Slot); - _envCells.SetClipRouting(_singleCellClipRouting); - // Retail DrawMesh only viewcone-checks an object's sphere under the - // installed PortalList and then draws the mesh whole. Hard clipping the - // object here slices a stationary player when the chase camera crosses - // into the opposite cathedral cell while the player remains behind. - _entities.ClearClipRouting(); - } - - private void UseOutdoorPortalViewRouting(ClipViewSlice slice) => - _entities.SetClipRouting( - _noCellClipRouting, - outdoorSlot: slice.Slot, - outdoorVisible: true); - public void PrepareCellBatches( RetailPViewFrameInput frame, HashSet visibleCellIds) => @@ -298,112 +243,9 @@ internal sealed partial class RetailPViewPassExecutor : public bool CellHasTransparentShell(uint cellId) => _envCells.CellHasTransparent(cellId); - public void DrawTransparentCellShells(HashSet cellIds) => - _envCells.Render(WbRenderPass.Transparent, cellIds); - public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => _envCells.RenderTransparentOrdered(cellIds); - public void DrawEntityBucket( - RetailPViewFrameInput frame, - IReadOnlyList entities, - HashSet? visibleCellIds) - { - uint landblockId = frame.PlayerLandblockId ?? 0u; - var entry = ( - landblockId, - Vector3.Zero, - Vector3.Zero, - entities, - (IReadOnlyDictionary?)null); - - _entities.Draw( - frame.Camera, - new[] { entry }, - frame.Frustum, - neverCullLandblockId: frame.PlayerLandblockId, - visibleCellIds: visibleCellIds, - animatedEntityIds: frame.AnimatedEntityIds); - } - - public void EmitClipRouteProbe( - ClipFrameAssembly clipAssembly, - ClipViewSlice slice, - int sliceIndex) => - _diagnostics.EmitClipRouteProbe( - RenderingDiagnostics.ProbeClipRouteEnabled, - _clipFrame, - clipAssembly, - slice, - sliceIndex); - - public void DrawLandscapeSlice( - RetailPViewFrameInput frame, - RetailPViewLandscapeSliceContext context) - { - ClipViewSlice slice = context.Slice; - bool scissor = BeginDoorwayScissor(slice.NdcAabb); - _diagnostics.EmitClipRouteScissorProbe( - RenderingDiagnostics.ProbeClipRouteEnabled, - scissor, - slice.NdcAabb); - - _surface.BindTerrainClip(); - EnableClipDistances(); - if (frame.RenderSky) - { - _sky?.RenderSky( - frame.Camera, - frame.CameraWorldPosition, - frame.DayFraction, - frame.ActiveDayGroup, - frame.SkyKeyframe, - frame.EnvironOverrideActive); - } - - DisableClipDistances(); - if (frame.RenderSky && _particles is not null && _particleRenderer is not null) - { - _particleRenderer.Draw( - frame.Camera, - frame.CameraWorldPosition, - ParticleRenderPass.SkyPreScene); - } - - EnableClipDistances(); - _terrainDiagnostics.Begin(); - _terrain?.Draw( - frame.Camera, - frame.Frustum, - neverCullLandblockId: frame.PlayerLandblockId, - clipPlanes: slice.Planes, - ndcClipAabb: slice.NdcAabb); - _terrainDiagnostics.Complete(); - - DisableClipDistances(); - if (context.OutdoorEntities.Count > 0) - { - var sceneryEntry = ( - frame.PlayerLandblockId ?? 0u, - Vector3.Zero, - Vector3.Zero, - context.OutdoorEntities, - (IReadOnlyDictionary?)null); - _entities.Draw( - frame.Camera, - new[] { sceneryEntry }, - frame.Frustum, - neverCullLandblockId: frame.PlayerLandblockId, - visibleCellIds: null, - animatedEntityIds: frame.AnimatedEntityIds); - } - - if (scissor) - _surface.EndScissor(); - _entities.ClearClipRouting(); - DisableClipDistances(); - } - public void DrawLandscapeSliceLate( RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) @@ -503,38 +345,6 @@ internal sealed partial class RetailPViewPassExecutor : DisableClipDistances(); } - public void DrawLandscapeBuildingShellSlice( - RetailPViewFrameInput frame, - RetailPViewLandscapeBuildingShellSliceContext context) - { - UseOutdoorPortalViewRouting(context.Slice); - bool scissor = BeginDoorwayScissor(context.Slice.NdcAabb); - _surface.BindTerrainClip(); - DisableClipDistances(); - - if (context.BuildingShells.Count > 0) - { - var buildingEntry = ( - frame.PlayerLandblockId ?? 0u, - Vector3.Zero, - Vector3.Zero, - context.BuildingShells, - (IReadOnlyDictionary?)null); - _entities.Draw( - frame.Camera, - new[] { buildingEntry }, - frame.Frustum, - neverCullLandblockId: frame.PlayerLandblockId, - visibleCellIds: null, - animatedEntityIds: frame.AnimatedEntityIds); - } - - if (scissor) - _surface.EndScissor(); - _entities.ClearClipRouting(); - DisableClipDistances(); - } - public void ClearInteriorDepth() { if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) @@ -547,23 +357,6 @@ internal sealed partial class RetailPViewPassExecutor : RetailPViewCellSliceContext context) => DrawPortalDepthWrite(context, frame, forceFarZ: frame.RootCell.IsOutdoorNode); - public void DrawLookInPortalPunch( - RetailPViewFrameInput frame, - RetailPViewCellSliceContext context, - int portalIndex) - { - if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) - { - LegacyLookInPunchCountThisFrame++; - Console.WriteLine( - $"[lookin-punch] source=legacy viewer=0x{frame.ViewerCellId:X8} " - + $"root=0x{frame.RootCell.CellId:X8} cell=0x{context.CellId:X8} " - + $"portal={portalIndex} planes={context.Slice.Planes.Length} " - + $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}"); - } - DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex); - } - public void DrawUnattachedSceneParticles( RetailPViewFrameInput frame, bool outdoorCells) @@ -643,9 +436,6 @@ internal sealed partial class RetailPViewPassExecutor : public void FlushLandscapeAlpha() => _alpha.Flush(); - public void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - _alpha.FlushFartherThan(minViewerDistance); - public void DrawCellParticles( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) @@ -676,29 +466,6 @@ internal sealed partial class RetailPViewPassExecutor : DisableClipDistances(); } - public void DrawDynamicsParticles( - RetailPViewFrameInput frame, - IReadOnlySet ownerIds) - { - if (_particles is null || _particleRenderer is null || ownerIds.Count == 0) - return; - - HashSet dynamics = _particleClassifications.Dynamics; - dynamics.Clear(); - dynamics.UnionWith(ownerIds); - if (dynamics.Count == 0) - return; - - DisableClipDistances(); - ProbeWalkParticleRoute("dyn-owners", dynamics); - _particleRenderer.DrawForOwners( - frame.Camera, - frame.CameraWorldPosition, - ParticleRenderPass.Scene, - dynamics); - DisableClipDistances(); - } - public void EmitDiagnostics( RetailPViewFrameInput frame, RetailPViewFrameResult result) => diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 1d9fe0b2..d1bdba89 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -11,14 +11,11 @@ namespace AcDream.App.Rendering; /// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside -> /// PView::DrawInside -> ConstructView -> DrawCells. /// -public sealed class RetailPViewRenderer +internal sealed class RetailPViewRenderer { - private readonly InteriorEntityPartition.IObserver? _partitionObserver; - private readonly ICurrentRenderPViewObserver? _candidateObserver; - private readonly RenderSceneShadowRuntime? _renderSceneShadow; + private readonly RenderSceneShadowRuntime _renderSceneShadow; private readonly PortalVisibilityFrame _mainPortalFrameScratch = new(); private readonly ClipFrameAssembly _clipAssemblyScratch = new(); - private readonly ViewconeCuller _viewconeScratch = new(); private readonly RetailPViewFrameResult _frameResultScratch = new(); private static readonly ClipViewSlice NoClipSlice = @@ -27,74 +24,12 @@ public sealed class RetailPViewRenderer private static readonly IReadOnlySet NoParticleOwners = new HashSet(); - // Frame unions for the once-per-frame particle submissions (retail: one - // unclipped alpha-list insertion per emitter; occlusion by depth at the - // flush). Per-slice owner culls still run — these accumulate their union. - private readonly HashSet _staticParticleUnionScratch = new(); - private readonly HashSet _cellParticleUnionScratch = new(); - - // Every cell drawn as a building look-in this frame. Retail marks each - // drawn non-player part for the frame (DrawMeshInternal @0x0059F360, - // GetDrawnThisFrame), so an object whose cell drew with a look-in cannot - // draw again in a later pass; dynamics-last consults this set to honor - // the same drawn-once contract. - private readonly HashSet _lookInCellIds = new(); - - 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(); - // Transparent shell cells retain IndoorDrawPlan's far-to-near order. The - // EnvCell renderer uploads shared instance/command data once, then issues - // range-addressed MDI calls in this exact order. - private readonly List _orderedTransparentShellCells = new(); - - // R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame). - private readonly BuildingGroupScratch _buildingGroups = new(); - private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new(); - - // #124: per-building look-in frames under an INTERIOR root, drawn as a - // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the - // main frame (see DrawInside). Rebuilt each interior-root frame. - private readonly List _lookInFrames = new(); - private readonly Stack _lookInFramePool = new(); - private readonly HashSet _lookInPrepareScratch = new(); - - // #131/#132: landscape scene-particle owner survivors. With building - // look-ins, static owners use the pre-building alpha barrier and the late - // phase contains only outside-stage dynamics; otherwise the late phase - // carries both sets. - private readonly HashSet _lateParticleOwnerScratch = new(); private readonly HashSet _cellParticleOwnerScratch = new(); - private readonly HashSet _dynamicParticleOwnerScratch = new(); - - // The walk's TRUE root flood as a set, rebuilt per frame for the - // DynamicLast stage gate (the stage-set split — synthesis plan step 4). - private readonly HashSet _rootFloodSetScratch = new(); - - // MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/ - // Dynamics), reused across frames instead of `new`ing a Result (a Dictionary - // + 2 Lists, plus one List per visible cell) every DrawInside - // call. See InteriorEntityPartition.Partition(Result, ...) — clears in - // place and reuses each cell's list across frames when the cell stays - // visible. - // Slice G4: this is now a diagnostic/fallback oracle only. Normal - // production consumes the retained RenderFrameView routes directly. - private readonly InteriorEntityPartition.Result _partitionResult = new(); // MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across // frames instead of `new HashSet(pvFrame.OrderedVisibleCells)` every - // call. Every consumer (DrawEntityBucket, DrawExitPortalMasks, - // DrawCellObjectLists, RetailPViewFrameResult.DrawableCells) reads it - // synchronously within the same frame it was built. + // call. Every walk consumer reads it synchronously in this frame. private readonly HashSet _drawableCellsScratch = new(); - private readonly RetailPViewScratchRetention _scratchRetention = new(); - - public RetailPViewRenderer() - { - } // FW3 visual-gate fix: the interior root's dynamics phase, invoked by // the driver's clearInteriorDepth closure at the walk's pre-clear @@ -109,43 +44,36 @@ public sealed class RetailPViewRenderer private bool? _probeWalkRootPrevOutdoor; private int _probeWalkRootFramesLeft; private ulong _probeWalkRootFrame; - private string? _probeLookInState; - - // The parked remote player's authoritative parent cell for the current - // cathedral FW4 investigation. This is diagnostic scope only: it changes - // no visibility or draw decision. - private const uint ProbeCathedralRemoteCellId = 0xF4180112u; // Campaign FW3.2b-2: the walk's production world-data registries // (published/retired by LandblockRenderPublisher) plus the per-frame // driver state. Null until the composition passes them; the static // cutover requires all three. - private readonly Walk.WalkBuildingRegistry? _walkBuildings; - private readonly Walk.WalkLandscapeAssembler? _walkLandscape; - private readonly CellVisibility? _walkCellRegistry; + private readonly Walk.WalkBuildingRegistry _walkBuildings; + private readonly Walk.WalkLandscapeAssembler _walkLandscape; + private readonly CellVisibility _walkCellRegistry; // Campaign FW3.2b-2: the production IWalkFrameWorldData over the retained // scene — owned here (not just inside the driver) because DrawInside also // reads it directly to re-source particle owners for the routes the walk // now draws (plan §FW3 item 4). Non-null exactly when _walkBuildings is. - private readonly Walk.WalkProductionWorldData? _walkWorldData; + private readonly Walk.WalkProductionWorldData _walkWorldData; internal RetailPViewRenderer( - InteriorEntityPartition.IObserver? partitionObserver, - RenderSceneShadowRuntime? renderSceneShadow = null, - Walk.WalkBuildingRegistry? walkBuildings = null, - Walk.WalkLandscapeAssembler? walkLandscape = null, - CellVisibility? walkCellRegistry = null) + RenderSceneShadowRuntime renderSceneShadow, + Walk.WalkBuildingRegistry walkBuildings, + Walk.WalkLandscapeAssembler walkLandscape, + CellVisibility walkCellRegistry) { - _walkBuildings = walkBuildings; - _walkLandscape = walkLandscape; - _walkCellRegistry = walkCellRegistry; - _walkWorldData = walkBuildings is not null - ? new Walk.WalkProductionWorldData(walkBuildings) - : null; - _partitionObserver = partitionObserver; - _candidateObserver = partitionObserver as ICurrentRenderPViewObserver; - _renderSceneShadow = renderSceneShadow; + _renderSceneShadow = renderSceneShadow + ?? throw new ArgumentNullException(nameof(renderSceneShadow)); + _walkBuildings = walkBuildings + ?? throw new ArgumentNullException(nameof(walkBuildings)); + _walkLandscape = walkLandscape + ?? throw new ArgumentNullException(nameof(walkLandscape)); + _walkCellRegistry = walkCellRegistry + ?? throw new ArgumentNullException(nameof(walkCellRegistry)); + _walkWorldData = new Walk.WalkProductionWorldData(_walkBuildings); } // T2 (BR-4): retail has NO distance constant on the flood-admission chain @@ -155,9 +83,9 @@ public sealed class RetailPViewRenderer // gather); seeds themselves are unbounded. private const float OutdoorBuildingSeedDistance = float.PositiveInfinity; - public RetailPViewFrameResult DrawInside( + internal RetailPViewFrameResult DrawInside( RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes) + RetailPViewPassExecutor passes) { ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(passes); @@ -170,17 +98,6 @@ public sealed class RetailPViewRenderer RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor ?? throw new InvalidOperationException( "The retail frame walk requires RetailPViewPassExecutor."); - if (_renderSceneShadow is null - || _walkBuildings is null - || _walkLandscape is null - || _walkCellRegistry is null - || _walkWorldData is null) - { - throw new InvalidOperationException( - "The retail frame walk requires the retained scene and all " - + "committed walk registries."); - } - // Compatibility carrier only. Production visibility is populated // exclusively by RetailFrameWalk below; this frame supplies the clip // assembler's outdoor full-screen seed until that carrier is removed. @@ -414,8 +331,6 @@ public sealed class RetailPViewRenderer passes.PrepareCellBatches(ctx, prepareCells); - _candidateObserver?.BeginPViewFrame(); - try { RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts; RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts); @@ -435,15 +350,13 @@ public sealed class RetailPViewRenderer // their retail turns. Interior-root landscape services run at the // pre-clear callback; outdoor roots have no clear and run after // replay. - Walk.WalkFrameDriver capturedDriver = walkDriver!; _walkPreClearDynamics = () => { passes.UseIndoorMembershipOnlyRouting(); DrawLandscapeDynamicsPhase( ctx, passes, - clipAssembly, - capturedDriver); + clipAssembly); }; try { @@ -454,8 +367,7 @@ public sealed class RetailPViewRenderer DrawLandscapeDynamicsPhase( ctx, passes, - clipAssembly, - walkDriver!); + clipAssembly); } } finally @@ -481,602 +393,12 @@ public sealed class RetailPViewRenderer // Outdoor-cell unattached emitters drew in the landscape stage. passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false); - _candidateObserver?.CompletePViewFrame(); return result; } - catch - { - _candidateObserver?.AbortPViewFrame(); - throw; - } } - // R-A2: group the nearby building cells by BuildingId and run one per-building flood per group - // (retail's per-building ConstructView(CBldPortal)), merging each small view into the frame. The - // grouping dict contains only this frame's keys; lists are pooled across frames. - private void MergeNearbyBuildingFloods(RetailPViewFrameInput ctx, PortalVisibilityFrame pvFrame) - { - RebuildBuildingGroups(ctx.NearbyBuildingCells!); - - foreach (var group in _buildingGroups.Values) - { - if (group.Count == 0) - continue; - var buildingFrame = PortalVisibilityBuilder.ConstructViewBuilding( - group, - ctx.ViewerEyePos, - ctx.Cells.Find, - ctx.ViewProjection, - OutdoorBuildingSeedDistance, - reuseFrame: _outdoorBuildingFrameScratch); - MergeBuildingFrame(pvFrame, buildingFrame); - } - } - - // T2 (BR-4): merge a per-building flood's cells + views into the frame as a - // UNION. Retail accumulates EVERY clipped portal polygon as a new view_poly - // on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0; - // a cell visible through two apertures holds two views, all consumed - // downstream). The old first-wins (`ContainsKey -> continue`) dropped the - // second building flood's views whenever a cell was already in the frame — - // the multiview-loss-first-wins divergence (a named #109 suspect: per-frame - // winner flips between apertures). CellView.Add dedups exact/collinear - // re-emissions (the dac8f6a CanonicalKey), so unioning is convergent. - // OutsideView is NOT merged — the outdoor root already seeds full-screen - // terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView - // empty (it stops at exit portals once inside the building). - private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src) - { - foreach (uint cellId in src.OrderedVisibleCells) - { - if (!src.CellViews.TryGetValue(cellId, out var srcView)) - continue; - - if (!target.CellViews.TryGetValue(cellId, out var existing)) - { - existing = target.RentCellView(); - target.CellViews[cellId] = existing; - target.OrderedVisibleCells.Add(cellId); - } - - // Copy the view polygons into storage owned by the target frame. - // Source building frames are reused immediately for the next - // building flood, so retaining their CellView reference would - // alias pooled scratch and mutate the merged main view. - foreach (var p in srcView.Polygons) - existing.Add(target.CopyPolygon(p.Vertices)); - } - } - - // #124: per-building look-in floods for an INTERIOR root, seeded clipped - // against the OutsideView (retail: GetClip runs under the INSTALLED view — - // the accumulated doorway region — so a far building floods only within the - // doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip - // 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own - // building self-excludes via the seed eye-side test. - private void BuildInteriorRootLookIns(RetailPViewFrameInput ctx, PortalVisibilityFrame pvFrame) - { - RebuildBuildingGroups(ctx.NearbyBuildingCells!); - - foreach (var group in _buildingGroups.Values) - { - if (group.Count == 0) - continue; - PortalVisibilityFrame frameScratch = _lookInFramePool.Count != 0 - ? _lookInFramePool.Pop() - : new PortalVisibilityFrame(); - var frame = PortalVisibilityBuilder.ConstructViewBuilding( - group, ctx.ViewerEyePos, ctx.Cells.Find, ctx.ViewProjection, - OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons, - reuseFrame: frameScratch); - LoadedCell sourceCell = group[0]; - frame.SourceBuildingKey = sourceCell.BuildingId ?? sourceCell.CellId; - frame.SourceBuildingLandblockId = sourceCell.CellId & 0xFFFF0000u; - if (frame.OrderedVisibleCells.Count > 0) - _lookInFrames.Add(frame); - else - ReturnLookInFrame(frame); - } - } - - /// - /// Conservative barrier drain threshold for one look-in frame: the viewer - /// distance to the frame's nearest anchor-cell ORIGIN. Cell origins sit - /// inside the building, so this over-estimates the building's - /// nearest-point distance and under-drains; anything conservatively - /// retained still composites correctly at the later depth-tested drains. - /// Retail needs no threshold — its far→near walk guarantees only farther - /// content is queued when DrawBuilding flushes (@0x0059F2A0). Returns 0 - /// (full drain, today's behavior) when no cell resolves. - /// - /// - /// The pre-punch barrier threshold for : - /// the nearest drawable cell whose exit-portal mask is about to write - /// far-Z. Every queued alpha entry at or beyond it must drain first - /// (retail DrawBuilding @0x0059F2A0's FlushAlphaList(0f) before the - /// portal-only pass), because after the punch those entries would z-pass - /// across aperture pixels whose true depth no longer exists. No punched - /// cells → MaxValue → the partial drain retains everything. - /// - internal static float ExitPortalMaskBarrierDistance( - PortalVisibilityFrame frame, - HashSet drawableCells, - IRetailPViewCellSource cells, - Vector3 viewerPosition) - { - float best = float.PositiveInfinity; - for (int i = 0; i < frame.OrderedVisibleCells.Count; i++) - { - uint cellId = frame.OrderedVisibleCells[i]; - if (!drawableCells.Contains(cellId)) - continue; - LoadedCell? cell = cells.Find(cellId); - if (cell is null) - continue; - float distance = Vector3.Distance( - cell.WorldTransform.Translation, - viewerPosition); - if (distance < best) - best = distance; - } - - return float.IsFinite(best) ? best : float.MaxValue; - } - - internal static float LookInBarrierDrainDistance( - PortalVisibilityFrame frame, - IRetailPViewCellSource cells, - Vector3 viewerPosition) - { - float best = float.PositiveInfinity; - for (int i = 0; i < frame.OrderedVisibleCells.Count; i++) - { - LoadedCell? cell = cells.Find(frame.OrderedVisibleCells[i]); - if (cell is null) - continue; - float distance = Vector3.Distance( - cell.WorldTransform.Translation, - viewerPosition); - if (distance < best) - best = distance; - } - - return float.IsFinite(best) ? best : 0f; - } - - private void RecycleLookInFrames() - { - for (int i = 0; i < _lookInFrames.Count; i++) - ReturnLookInFrame(_lookInFrames[i]); - _scratchRetention.ClearFrameBuffers( - _lookInFrames, - _lookInPrepareScratch, - _drawableCellsScratch, - _shellBatch, - _orderedTransparentShellCells); - } - - private void ReturnLookInFrame(PortalVisibilityFrame frame) - { - frame.ResetForBuild(); - if (_lookInFramePool.Count < RetailPViewScratchRetention.MaxRetainedLookInFrames) - _lookInFramePool.Push(frame); - } - - private void RebuildBuildingGroups(IReadOnlyList nearbyCells) - => _buildingGroups.Rebuild(nearbyCells); - - private void ResetBuildingGroups() - => _buildingGroups.Reset(); - - // #124: draw the interior-root look-ins INSIDE the landscape stage — - // retail's placement (LScape::draw → DrawBlock → DrawSortCell → - // DrawBuilding runs as the FIRST call of DrawCells' outside-view branch, - // pc:432719, before the depth clear + seals). Per building: punch ALL - // apertures first (retail finishes build_draw_portals_only pass 1 — the - // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods), - // then draw the flooded cells' shells + statics far→near (the nested - // DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is - // empty by construction — PView ctor draw_landscape=0 — so no recursive - // landscape/clear/seal). Retail CEnvCell::setup_view installs every cell's - // nested portal_view before DrawEnvCell, while DrawMesh iterates that same - // PortalList for cell objects. Preserve that per-slice gate here; drawing a - // nested cell whole lets its floor, details, and emitters escape the authored - // aperture even when the outer depth choreography is otherwise correct. - private void DrawBuildingLookIns( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - ClipFrameAssembly clipAssembly, - InteriorEntityPartition.Result? partition, - ViewconeCuller viewcone, - IRenderFrameEntityPassExecutor? frameEntityPasses, - in RenderFrameView frameView) - { - if (_lookInFrames.Count == 0) - return; - - int outsideSliceCount = clipAssembly.OutsideViewSlices.Length; - int lookInRouteIndex = 0; - for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++) - { - PortalVisibilityFrame frame = _lookInFrames[frameIndex]; - - // Retail enters DrawBuilding once per building and drains the - // alpha accumulated by the preceding building before punching the - // next building's portals — and because retail's far→near walk - // has only inserted FARTHER content by then, that drain can never - // composite an emitter nearer than this building - // (FlushAlphaList(0f) @0x0059F2A0 under the walk; AP-236). - // The first building uses the pre-look-in barrier in - // DrawLandscapeThroughOutsideView. - if (frameIndex > 0) - { - passes.FlushLandscapeAlphaFartherThan( - LookInBarrierDrainDistance( - frame, - ctx.Cells, - ctx.CameraWorldPosition)); - } - - // Pass 1: far-Z punch every aperture of this building. - foreach (ExteriorPortalSeed seed in frame.ExteriorSeedPortals) - { - foreach (var poly in seed.View.Polygons) - { - var cps = ClipPlaneSet.From(poly); - if (cps.IsNothingVisible) - continue; - passes.DrawLookInPortalPunch(ctx, new RetailPViewCellSliceContext( - seed.CellId, - new ClipViewSlice( - 0, - new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY), - cps.PlaneArray), - NoParticleOwners), - seed.PortalIndex); - } - } - - // Pass 2: shells + objects, far→near, once per portal_view slice. - for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--) - { - uint cellId = frame.OrderedVisibleCells[i]; - var clipKey = new LookInClipCell(frameIndex, cellId); - if (!clipAssembly.LookInCellToViewSlices.TryGetValue( - clipKey, - out ClipViewSlice[]? cellSlices) - || cellSlices.Length == 0) - { - continue; - } - - _cellStaticScratch.Clear(); - if (partition is not null - && partition.ByCell.TryGetValue(cellId, out var bucket)) - { - _cellStaticScratch.AddRange(bucket); - } - - // #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the - // Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE - // under an interior root — DrawDynamicsLast viewcone-culls - // them (the main cone has no entries for look-in cells), and - // post-clear they would z-fail against the root's seal anyway - // (the #118 lesson). Retail draws a look-in cell's objects - // inside the NESTED DrawCells (DrawObjCellForDummies, - // pc:432878+), i.e. right here in the landscape stage. - // No double-draw: dynamics-last keeps culling them (their - // cell is absent from the main cone), and their emitters ride - // the DrawCellParticles call below, not DrawDynamicsParticles - // (which only sees dynamics-last cone survivors). - if (partition is not null) - { - foreach (var e in partition.Dynamics) - if (e.ParentCellId == cellId) - _cellStaticScratch.Add(e); - } - - bool cellDrewObjects = false; - _cellParticleUnionScratch.Clear(); - foreach (ClipViewSlice slice in cellSlices) - { - int routeIndex = lookInRouteIndex++; - passes.UseCellPortalViewRouting(cellId, slice); - _oneCell.Clear(); - _oneCell.Add(cellId); - passes.DrawOpaqueCellShells(_oneCell); - if (passes.CellHasTransparentShell(cellId)) - passes.DrawTransparentCellShells(_oneCell); - - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.Replace( - _cellParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.LookInObject, - routeIndex, - cellId); - } - else - { - ReplaceOwnerIds( - _cellParticleOwnerScratch, - _cellStaticScratch); - } - - if (frameEntityPasses is not null - || _cellStaticScratch.Count > 0) - { - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.LookInObject, - routeIndex, - cellId, - _cellStaticScratch); - DrawEntityRouteOrLegacy( - ctx, - passes, - frameEntityPasses, - in frameView, - RenderFrameCandidateRoute.LookInObject, - routeIndex, - cellId, - _cellStaticScratch, - _oneCell); - - cellDrewObjects = true; - _cellParticleUnionScratch.UnionWith( - _cellParticleOwnerScratch); - } - } - - // The nested DrawCells object pass includes emitters: ONE - // unclipped submission per look-in cell (retail draws a - // particle during its cell's walk turn; the cell walls own - // occlusion by depth at alpha playback — never a view clip). - if (cellDrewObjects) - { - passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext( - cellId, NoClipSlice, _cellParticleUnionScratch)); - } - } - - // The ordinary exterior building shell is clipped by the outer - // outside_view, not by the nested cell PortalList. - passes.UseIndoorMembershipOnlyRouting(); - - // Retail's ordinary shell pass immediately follows this same - // building's portal-only pass. Pair by the shell's authored - // anchor EnvCell; never let an unrelated building repaint a - // look-in merely because both happen to be nearby. - int sliceIndex = 0; - _staticParticleUnionScratch.Clear(); - foreach (ClipViewSlice slice in clipAssembly.OutsideViewSlices) - { - int shellRouteIndex = LookInBuildingShellRouteIndex( - frameIndex, - outsideSliceCount, - sliceIndex); - _buildingShellScratch.Clear(); - if (partition is not null) - { - foreach (WorldEntity entity in partition.OutdoorStatic) - { - if (!entity.IsBuildingShell - || FindLookInFrameIndex( - entity.BuildingShellAnchorCellId ?? 0, - _lookInFrames, - ctx.Cells) != frameIndex) - { - continue; - } - - EntitySphere(entity, out Vector3 center, out float radius); - if (viewcone.SphereVisibleInOutsideSlice( - sliceIndex, - center, - radius)) - { - _buildingShellScratch.Add(entity); - } - } - } - - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.LandscapeBuildingShell, - shellRouteIndex, - 0, - _buildingShellScratch); - bool hasPackedShell = frameEntityPasses is not null - && HasExactRoute( - in frameView, - RenderFrameCandidateRoute.LandscapeBuildingShell, - shellRouteIndex, - 0); - if (hasPackedShell || _buildingShellScratch.Count > 0) - { - RenderFrameEntityDrawRequest? shellDraw = - frameEntityPasses is null - ? null - : new RenderFrameEntityDrawRequest( - frameView, - RenderFrameCandidateRoute.LandscapeBuildingShell, - shellRouteIndex, - 0, - ctx.PlayerLandblockId ?? 0); - passes.DrawLandscapeBuildingShellSlice( - ctx, - new RetailPViewLandscapeBuildingShellSliceContext( - slice, - _buildingShellScratch) - { - EntityDraw = shellDraw, - }); - - _lateParticleOwnerScratch.Clear(); - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.Replace( - _lateParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.LandscapeBuildingShell, - shellRouteIndex, - 0); - } - else - { - ReplaceOwnerIds( - _lateParticleOwnerScratch, - _buildingShellScratch); - } - _staticParticleUnionScratch.UnionWith( - _lateParticleOwnerScratch); - } - sliceIndex++; - } - - // ONE unclipped submission for this look-in frame's shell-route - // owners (retail: one alpha-list insertion per emitter, - // depth-occluded at the flush — never re-drawn per outside view). - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); - } - } - - /// Campaign FW3.2b-2 flip apparatus (the I5 dual-shadow - /// pattern): drive the production walk over the FW3.1 registries with a - /// set-collecting sink and print one [walk-shadow] line per frame - /// whose visited cells diverge from the old path's - /// (main flood ∪ look-ins). Divergence is - /// EXPECTED where the walk's retail model deliberately differs from the - /// old builder — the probe's value is proving the production data - /// pipeline live and QUANTIFYING the difference for the flip review. A - /// probe exception prints loudly and never kills the frame (it is the - /// probe's own signal, not a production fault). - private void RunWalkShadowProbe( - RetailPViewFrameInput ctx, HashSet oldPathCells) - { - if (_walkBuildings is null || _walkLandscape is null - || _walkCellRegistry is null) - { - return; - } - try - { - // Forward = -(view column 3): System.Numerics CreateLookAt's - // zaxis is eye-target (backward). Viewport: the walk's SETS are - // viewport-scale-tolerant (every screen projection shares the - // same constants), so the shadow pins retail's capture size; the - // flip itself will use the real attachment extent. - Matrix4x4 view = ctx.CameraView; - var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33)); - var context = new Walk.WalkProductionFrameContext( - _walkCellRegistry, - _walkBuildings, - ctx.ViewerEyePos, - forward, - ctx.ViewProjection, - viewportWidth: 1024f, - viewportHeight: 720f); - Walk.WalkLandscape landscape = _walkLandscape.Landscape; - _walkLandscape.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos); - Walk.WalkCell? cameraCell = null; - if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100) - { - cameraCell = _walkCellRegistry.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded) - ? loaded?.Walk - : null; - if (cameraCell is null) - { - Console.WriteLine( - $"[walk-shadow] root={ctx.ViewerCellId:x8} interior camera cell has no walk data"); - return; - } - } - var sink = new WalkVisitedSetCollector(); - _frameWalk.WalkFrame( - ctx.ViewerCellId, cameraCell, landscape, context, sink); - - int onlyWalk = 0; - foreach (uint id in sink.Cells) - if (!oldPathCells.Contains(id)) - onlyWalk++; - int onlyOld = 0; - foreach (uint id in oldPathCells) - if (!sink.Cells.Contains(id)) - onlyOld++; - if (onlyWalk != 0 || onlyOld != 0) - { - Console.WriteLine( - $"[walk-shadow] root={ctx.ViewerCellId:x8} walkCells={sink.Cells.Count} " - + $"oldCells={oldPathCells.Count} onlyWalk={onlyWalk} onlyOld={onlyOld} " - + $"walkBuildings={sink.BuildingCount} landscapeTurns={sink.LandscapeCount}"); - } - } - catch (Exception failure) - { - Console.WriteLine($"[walk-shadow] PROBE FAULT root={ctx.ViewerCellId:x8}: {failure}"); - } - } - - // Campaign FW3.2b-2: the one RetailFrameWalk instance shared by the - // diagnostic shadow probe and the real Collect run (WalkFrameDriver.Collect, - // driven from DrawInside's walkActive block) — WalkFrame calls are never - // concurrent/re-entrant within a single-threaded render loop, so one - // shared instance is safe and avoids re-allocating the walk's own PView - // scratch per call site. Campaign FW3.4a retired the THIRD role this - // field used to serve (a dedicated pre-walk collection pass) — Collect - // now gathers the same visited sets itself, on WalkFrameDriver, as a - // side effect of the one walk it already runs. private readonly Walk.RetailFrameWalk _frameWalk = new(); - /// Campaign FW3.2b-2 (the I5 dual-shadow pattern): an - /// events-only that collects the SETS a - /// driven run would touch, without doing any leaf drawing itself. Used - /// ONLY by now — Campaign FW3.4a moved - /// the production role (the walk's flood cell set for the - /// prepareCells union, the visited building list and - /// landscape-cell turn ids for particle re-sourcing) onto - /// itself, which gathers the same sets - /// as a side effect of the one walk - /// already runs, instead of a second dedicated pass. - private sealed class WalkVisitedSetCollector : Walk.IWalkEventSink - { - public readonly HashSet Cells = new(); - public readonly List Buildings = new(); - public readonly HashSet LandscapeCellIds = new(); - - public int BuildingCount => Buildings.Count; - public int LandscapeCount => LandscapeCellIds.Count; - - public void Reset() - { - Cells.Clear(); - Buildings.Clear(); - LandscapeCellIds.Clear(); - } - - public void Emit(in Walk.WalkEvent walkEvent) - { - switch (walkEvent.Kind) - { - case Walk.WalkEventKind.DrawInside: - Cells.Add(walkEvent.CellId); - break; - case Walk.WalkEventKind.DrawCells: - foreach (uint id in walkEvent.Cells) - Cells.Add(id); - break; - } - } - - public void OnLandscapeCellTurn(uint cellId) => LandscapeCellIds.Add(cellId); - - public void OnBuildingTurn(Walk.WalkBuilding building) => Buildings.Add(building); - } - /// Campaign FW3.2b-2 — THE PRODUCTION ROOTING; Campaign FW3.4a — /// REPLAY ONLY. already ran its Collect pass /// earlier in (before PrepareCellBatches); @@ -1158,9 +480,8 @@ public sealed class RetailPViewRenderer /// weather. It deliberately submits no packed entity route. private void DrawLandscapeDynamicsPhase( RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - ClipFrameAssembly clipAssembly, - Walk.WalkFrameDriver walkDriver) + RetailPViewPassExecutor passes, + ClipFrameAssembly clipAssembly) { if (clipAssembly.OutsideViewSlices.Length == 0) return; @@ -1171,11 +492,6 @@ public sealed class RetailPViewRenderer // (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn). passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); - // Trace-only now. Look-in dynamics no longer have a late production - // phase: WalkFrameDriver replays each packed route at the walk's own - // per-cell DrawCells turn, before the enclosing building shell. - ProbeBuildingLookInFrames(ctx, passes, walkDriver); - // GameSky's weather pass still runs through each active landscape // view so doorway scissor/clip state matches the old executor. The // dynamics collection is intentionally empty: the walk already drew @@ -1194,492 +510,6 @@ public sealed class RetailPViewRenderer passes.UseIndoorMembershipOnlyRouting(); } - /// ACDREAM_PROBE_WALK_ROOT companion for FW4's surviving - /// through-wall dynamic. It compares the now-diagnostic-only legacy - /// look-in frames with the production walk's exact look-in set and reports - /// the two punch producers separately. Print-only; never participates in - /// admission. - private void ProbeBuildingLookInFrames( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - Walk.WalkFrameDriver walkDriver) - { - if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) - return; - - uint[] walkLookInCells = walkDriver.LookInCells - .OrderBy(cellId => cellId) - .ToArray(); - bool legacyHasTarget = _lookInFrames.Any( - frame => frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId)); - bool walkHasTarget = Array.IndexOf( - walkLookInCells, ProbeCathedralRemoteCellId) >= 0; - string legacyShape = string.Join( - ";", - _lookInFrames.Select((frame, index) => - $"{index}:b{frame.SourceBuildingKey:x8}/lb{frame.SourceBuildingLandblockId:x8}" - + $"/s{frame.ExteriorSeedPortals.Count}/c[" - + string.Join(",", frame.OrderedVisibleCells.Select(cell => cell.ToString("x8"))) - + "]")); - string walkShape = string.Join(",", walkLookInCells.Select(cell => cell.ToString("x8"))); - int walkPunches = passes is RetailPViewPassExecutor concrete - ? concrete.WalkLookInPunchCountThisFrame - : -1; - int legacyPunches = passes is RetailPViewPassExecutor concreteLegacy - ? concreteLegacy.LegacyLookInPunchCountThisFrame - : -1; - string state = - $"viewer={ctx.ViewerCellId:x8}|root={ctx.RootCell.CellId:x8}" - + $"|phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}" - + $"|legacy={legacyShape}|walk={walkShape}" - + $"|p={walkPunches}/{legacyPunches}"; - bool emit = state != _probeLookInState || _probeWalkRootFrame % 30 == 0; - _probeLookInState = state; - if (!emit) - return; - - Console.WriteLine( - $"[lookin-frame] f={_probeWalkRootFrame} " - + $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} " - + $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase} " - + $"legacyFrames={_lookInFrames.Count} legacy112={(legacyHasTarget ? 1 : 0)} " - + $"walkLookIn={walkLookInCells.Length} walk112={(walkHasTarget ? 1 : 0)} " - + $"punches=walk:{walkPunches},legacy:{legacyPunches}"); - for (int index = 0; index < _lookInFrames.Count; index++) - { - PortalVisibilityFrame frame = _lookInFrames[index]; - Console.WriteLine( - $"[lookin-frame] f={_probeWalkRootFrame} index={index} " - + $"building=0x{frame.SourceBuildingKey:X8} " - + $"landblock=0x{frame.SourceBuildingLandblockId:X8} " - + $"seeds={frame.ExteriorSeedPortals.Count} " - + $"has112={(frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId) ? 1 : 0)} " - + $"cells=[{string.Join(",", frame.OrderedVisibleCells.Select(cell => $"0x{cell:X8}"))}]"); - } - Console.WriteLine( - $"[lookin-frame] f={_probeWalkRootFrame} walkCells=[" - + string.Join(",", walkLookInCells.Select(cell => $"0x{cell:X8}")) - + "]"); - } - - private void DrawLandscapeThroughOutsideView( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - ClipFrameAssembly clipAssembly, - InteriorEntityPartition.Result? partition, - ViewconeCuller viewcone, - IRenderFrameEntityPassExecutor? frameEntityPasses, - in RenderFrameView frameView) - { - if (clipAssembly.OutsideViewSlices.Length == 0) - return; - - // #131/#132: retail drains the remaining landscape alpha after - // LScape::draw (DrawCells pc:432720), while each DrawBuilding is also - // an earlier alpha barrier before its portal traversal (pc:427954). - // Our dispatcher batches outdoor content, so the stage is split into: - // EARLY sky/terrain/static meshes; an optional pre-look-in static-alpha - // barrier; building look-ins; then LATE outside-stage dynamics, - // remaining particles, and weather; followed by the outer flush. - int probeSliceIndex = 0; - foreach (var slice in clipAssembly.OutsideViewSlices) - { - passes.SetTerrainClip(slice.Planes); - // T3 (BR-5): entities are never hard-clipped — retail viewcone- - // CHECKS each mesh's sphere against the view (Ghidra 0x0054c250) - // and draws it whole. The old per-slice entity clip routing - // (gl_ClipDistance via SetClipRouting) is replaced by the sphere - // pre-filter below; terrain/sky keep their per-slice plane clip. - passes.ClearClipRouting(); - if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled) - passes.EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex); - - _outdoorStaticScratch.Clear(); - if (partition is not null) - { - foreach (var e in partition.OutdoorStatic) - { - if (e.IsBuildingShell - && FindLookInFrameIndex( - e.BuildingShellAnchorCellId ?? 0, - _lookInFrames, - ctx.Cells) >= 0) - { - continue; - } - EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInOutsideSlice( - probeSliceIndex, - c, - r)) - { - _outdoorStaticScratch.Add(e); - } - } - } - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.LandscapeOutdoorStatic, - probeSliceIndex, - 0, - _outdoorStaticScratch); - RenderFrameEntityDrawRequest? entityDraw = - frameEntityPasses is null - ? null - : new RenderFrameEntityDrawRequest( - frameView, - RenderFrameCandidateRoute.LandscapeOutdoorStatic, - probeSliceIndex, - 0, - ctx.PlayerLandblockId ?? 0); - probeSliceIndex++; - passes.DrawLandscapeSlice( - ctx, - new RetailPViewLandscapeSliceContext( - slice, - _outdoorStaticScratch) - { - EntityDraw = entityDraw, - }); - } - - // Retail DrawBuilding flushes every alpha submission accumulated before - // the building immediately before its portal-only traversal - // (RenderDeviceD3D::DrawBuilding pc:427954-427956). That barrier is - // essential at open-air seams: foliage and static emitters encountered - // before the building must not be flushed after the look-in cell floor - // and repaint it. Our outdoor statics are one retained batch rather than - // retail's BSP-by-building walk, so use one barrier before the first - // look-in; DrawBuildingLookIns adds the corresponding barrier between - // each later building pair. Submit the early static owners' particles - // into the same alpha queue first; their mesh alpha was already - // submitted by the EARLY entity route above. - bool hasBuildingLookIns = _lookInFrames.Count > 0; - if (hasBuildingLookIns) - { - // Ownerless OUTDOOR-cell emitters cannot ride an entity route. - // Retail inserts each one into the single alpha list once, during - // its cell's landscape walk turn, with no portal-view clip; the - // interior-cell ownerless emitters submit in the final world - // scope instead (see DrawDynamicsLast). - passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); - - _staticParticleUnionScratch.Clear(); - int outsideSliceTotal = clipAssembly.OutsideViewSlices.Length; - for (int barrierSliceIndex = 0; - barrierSliceIndex < outsideSliceTotal; - barrierSliceIndex++) - { - _lateParticleOwnerScratch.Clear(); - if (partition is not null) - { - foreach (var e in partition.OutdoorStatic) - { - if (e.IsBuildingShell - && FindLookInFrameIndex( - e.BuildingShellAnchorCellId ?? 0, - _lookInFrames, - ctx.Cells) >= 0) - { - continue; - } - EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInOutsideSlice( - barrierSliceIndex, - c, - r)) - { - _lateParticleOwnerScratch.Add(e.Id); - } - } - } - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.Replace( - _lateParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.LandscapeOutdoorStatic, - barrierSliceIndex, - 0); - } - _staticParticleUnionScratch.UnionWith( - _lateParticleOwnerScratch); - } - - // ONE unclipped submission for the union of every slice's cone - // survivors, then retail's pre-building barrier drain. Under - // retail's far→near walk, DrawBuilding's FlushAlphaList(0f) - // @0x0059F2A0 can only ever flush content from cells FARTHER - // than the building it precedes — a nearer emitter (the Holtburg - // candle in front of a door) has not been inserted yet and - // composites at a later flush, after that building's opaques. - // Drain the far prefix only; nearer entries stay queued for the - // DrawCells-boundary flush, which runs after the late dynamics - // (AP-236 retirement). - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); - passes.FlushLandscapeAlphaFartherThan( - LookInBarrierDrainDistance( - _lookInFrames[0], - ctx.Cells, - ctx.CameraWorldPosition)); - } - - // #124: far-building look-ins draw HERE — still inside the landscape - // stage (their punches mark against the terrain/exterior depth just - // drawn), strictly BEFORE the outer depth clear + seals below, matching - // retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785). - DrawBuildingLookIns( - ctx, - passes, - clipAssembly, - partition, - viewcone, - frameEntityPasses, - in frameView); - - // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn - // pre-clear so the seal protects their aperture pixels; AFTER the - // look-ins so a translucent portal mesh blends over a far interior - // instead of being overpainted). The scene-particle owners (statics + - // dynamics cone survivors) accumulate across the slices and submit - // ONCE, unclipped, after the loop. - _staticParticleUnionScratch.Clear(); - probeSliceIndex = 0; - foreach (var slice in clipAssembly.OutsideViewSlices) - { - passes.SetTerrainClip(slice.Planes); - passes.ClearClipRouting(); - - _outdoorStaticScratch.Clear(); // late: dynamics survivors - _lateParticleOwnerScratch.Clear(); // late: dynamics, plus statics without look-ins - if (!hasBuildingLookIns && partition is not null) - { - foreach (var e in partition.OutdoorStatic) - { - EntitySphere(e, out var c, out float r); - bool ownerPass = viewcone.SphereVisibleInOutsideSlice( - probeSliceIndex, - c, - r); - if (ownerPass) - _lateParticleOwnerScratch.Add(e.Id); - } - } - foreach (var e in _outsideStageDynamics) - { - EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r)) - { - _outdoorStaticScratch.Add(e); - // Particles emit in the stage matching the PARENT CELL: - // an INTERIOR dynamic whose sphere merely straddles an - // exit-portal plane keeps its mesh in both stages (#118) - // but its particles belong to the final pass — draining - // them at the pre-clear boundary lets the interior stage - // repaint over them except on seal-protected aperture - // pixels (the cathedral middle-cell spell-star cut). - if (!InteriorEntityPartition.IsIndoorCellId(e.ParentCellId)) - _lateParticleOwnerScratch.Add(e.Id); - } - } - if (frameEntityPasses is not null) - { - if (hasBuildingLookIns) - { - _lateParticleOwnerScratch.Clear(); - } - else - { - RenderFrameRouteOwnerSelector.Replace( - _lateParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.LandscapeOutdoorStatic, - probeSliceIndex, - 0); - } - RenderFrameRouteOwnerSelector.Union( - _lateParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.LandscapeOutsideDynamic, - probeSliceIndex, - 0); - } - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.LandscapeOutsideDynamic, - probeSliceIndex, - 0, - _outdoorStaticScratch); - RenderFrameEntityDrawRequest? entityDraw = - frameEntityPasses is null - ? null - : new RenderFrameEntityDrawRequest( - frameView, - RenderFrameCandidateRoute.LandscapeOutsideDynamic, - probeSliceIndex, - 0, - ctx.PlayerLandblockId ?? 0); - probeSliceIndex++; - _staticParticleUnionScratch.UnionWith(_lateParticleOwnerScratch); - passes.DrawLandscapeSliceLate( - ctx, - new RetailPViewLandscapeLateSliceContext( - slice, - _outdoorStaticScratch) - { - EntityDraw = entityDraw, - }); - } - - // ONE unclipped submission for every late-stage particle owner — - // OUTDOOR-parented outside-stage dynamics' emitters plus, without - // look-ins, the outdoor statics' emitters (retail: one alpha-list - // insertion per emitter during the landscape walk; per-slice - // re-submission with clip slots was the direction-dependent - // disappearance class). Interior-parented straddlers appear in BOTH - // the LandscapeOutsideDynamic and DynamicLast routes; their particles - // emit only in the final pass, so remove them here. - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.ExceptRoute( - _staticParticleUnionScratch, - in frameView, - RenderFrameCandidateRoute.DynamicLast); - } - if (_staticParticleUnionScratch.Count > 0) - { - passes.DrawLandscapeStaticParticles( - ctx, - new RetailPViewLandscapeStaticParticleContext( - _staticParticleUnionScratch)); - _staticParticleUnionScratch.Clear(); - } - - // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, - // campfires, ground effects anchored at a position) have no owner id - // to ride any of the id-filtered particle passes. OUTDOOR-cell ones - // submit ONCE in the landscape stage, unclipped — retail inserts each - // particle into the single alpha list during its owner cell's walk - // turn (ShouldDrawParticles @0x0050FE60 gates by cell + distance; - // FlushAlphaList @0x0059D2E0 depth-tests at composition). The former - // once-per-outside-slice submission with that slice's clip slot cut - // effects at aperture boundaries and drew NOTHING when no outside - // slice was in view. Interior-cell unattached emitters submit in the - // final world scope (DrawDynamicsLast) — in the landscape stage the - // upcoming depth clear + interior repaint would erase them. - if (!hasBuildingLookIns) - passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); - - // Retail PView::DrawCells 0x005A4872 drains the landscape alpha list - // immediately after LScape::draw and before the optional depth clear. - // The queue remains active for the post-clear/final-world scope. - // - // Only an INTERIOR root drains here: its full depth clear follows, and - // a flame drained after that clear would z-pass through every interior - // wall. An OUTDOOR root has no depth clear (retail gates it on - // portalsDrawnCount, pc:432731), and retail's LScape::draw walk has - // already drawn every building interior and every cell object via - // DrawSortCell 0x005A17C0 before that boundary — while our outdoor - // frame draws punches, interior shells, cell objects, and ALL dynamics - // (doors, creatures, NPCs) after this point. Draining here painted the - // flames first and let each of those later opaque meshes overwrite - // them (#132: "the door draws over the candle"); the outdoor drain - // therefore runs after DrawDynamicsLast, where world depth is complete - // and the one far-to-near list composites over everything, exactly as - // retail's boundary flush does relative to its finished walk. - if (!ctx.RootCell.IsOutdoorNode) - passes.FlushLandscapeAlpha(); - - // T1: retail clears the FULL depth buffer ONCE between the outside - // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — - // Clear gated on portalsDrawnCount; exact gate semantics is a plan - // open question, staged as "any outside slice drawn"), then re-stamps - // every outside-leading portal's TRUE depth (the seals, - // DrawExitPortalMasks). Replaces the old per-slice scissored AABB - // clear (wrong shape, no seal after it). - if (clipAssembly.OutsideViewSlices.Length > 0 && !ctx.RootCell.IsOutdoorNode) - passes.ClearInteriorDepth(); - - passes.UseIndoorMembershipOnlyRouting(); - } - - internal static int LookInBuildingShellRouteIndex( - int frameIndex, - int outsideSliceCount, - int sliceIndex) => - checked((frameIndex * outsideSliceCount) + sliceIndex); - - internal static int FindLookInFrameIndex( - uint buildingShellAnchorCellId, - IReadOnlyList lookInFrames, - IRetailPViewCellSource cells) - { - if (buildingShellAnchorCellId == 0) - return -1; - - LoadedCell? anchorCell = cells.Find(buildingShellAnchorCellId); - if (anchorCell is null) - return -1; - - uint buildingKey = anchorCell.BuildingId ?? anchorCell.CellId; - uint landblockId = anchorCell.CellId & 0xFFFF0000u; - for (int frameIndex = 0; frameIndex < lookInFrames.Count; frameIndex++) - { - PortalVisibilityFrame frame = lookInFrames[frameIndex]; - if (frame.SourceBuildingKey == buildingKey - && frame.SourceBuildingLandblockId == landblockId) - { - return frameIndex; - } - } - - return -1; - } - - private static bool HasExactRoute( - in RenderFrameView view, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId) - { - foreach (RenderFrameCandidateRange range in view.RouteRanges) - { - if (range.Route == route - && range.RouteIndex == routeIndex - && range.CellId == cellId - && range.Count > 0) - { - return true; - } - } - return false; - } - - private void DrawExitPortalMasks( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - PortalVisibilityFrame pvFrame, - ClipFrameAssembly clipAssembly, - HashSet drawableCells) - { - for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) - { - uint cellId = pvFrame.OrderedVisibleCells[i]; - if (!drawableCells.Contains(cellId)) - continue; - - foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) - passes.DrawExitPortalMask( - ctx, - new RetailPViewCellSliceContext( - cellId, - slice, - NoParticleOwners)); - } - } - /// Campaign FW4 slice 2 — the walk-flood seal draw. Retail's /// PView::DrawCells stamps every exit portal of THE FLOOD'S OWN /// cells (pc:432785-432786, reverse cell_draw_list far→near) — one @@ -1695,7 +525,7 @@ public sealed class RetailPViewRenderer /// dat aperture polygon and z-tests, so over-coverage is benign). private void DrawWalkExitPortalMasks( RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, + RetailPViewPassExecutor passes, ClipFrameAssembly clipAssembly, Walk.WalkFrameDriver driver) { @@ -1713,416 +543,6 @@ public sealed class RetailPViewRenderer } } - private void DrawEnvCellShells( - IRetailPViewPassExecutor passes, - PortalVisibilityFrame pvFrame) - { - // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's - // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once. - // Retail NEVER clips cell geometry: the production path is the - // prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the - // planeMask=0xffffffff legacy submit means skip-all-edges), and - // aperture exactness comes from the punch/seal depth writes + the - // z-buffer + this order. The former gl_ClipDistance chop - // (927fd8f/9ce335e, #114) is deleted with this rewrite. - // Per-cell opaque+transparent keeps the far→near transparent - // compositing the per-cell loop already provided. - passes.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) - passes.DrawOpaqueCellShells(_shellBatch); - - // Transparent: far-to-near order matters for compositing. The ordered - // list retains ShellPass cell boundaries while EnvCellRenderer shares - // one instance/command/light upload across the complete pass. - _orderedTransparentShellCells.Clear(); - foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) - { - if (passes.CellHasTransparentShell(entry.CellId)) - _orderedTransparentShellCells.Add(entry.CellId); - } - if (_orderedTransparentShellCells.Count > 0) - passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells); - } - - // T1: the frame's single LAST entity pass — ALL server-spawned dynamics - // (player, NPCs, doors, items), indoor or out, drawn after the static - // world + punches + interior cells. Depth-tested, never hard-clipped - // (retail draws objects per cell AFTER cells and viewcone-culls them — - // PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is - // T3). Drawing dynamics last is what makes the aperture punch safe. - // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its - // cell's views; outdoor/unresolved vs the outside views (pass-all under - // the outdoor root's full-screen outside view). A dynamic in a NON-flooded - // room culls HERE — retail never reaches an object whose cell is not in - // the draw list; the partition keeps routing it so the CULL (not the - // visibility set) drops it, exactly retail's shape. - private void DrawDynamicsLast( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - InteriorEntityPartition.Result? partition, - ViewconeCuller viewcone, - bool rootIsOutdoor, - IRenderFrameEntityPassExecutor? frameEntityPasses, - in RenderFrameView frameView) - { - if (partition is null) - { - RenderFrameRouteOwnerSelector.Replace( - _dynamicParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.DynamicLast, - 0, - 0); - - passes.UseIndoorMembershipOnlyRouting(); - DrawEntityRouteOrLegacy( - ctx, - passes, - frameEntityPasses, - in frameView, - RenderFrameCandidateRoute.DynamicLast, - 0, - 0, - Array.Empty(), - visibleCellIds: null); - - // Particles emit exactly once, in the stage matching the parent - // cell. Pure-outdoor dynamics are absent from the DynamicLast - // route (they draw only in the outside stage), and interior - // straddlers — present in BOTH routes — emit their particles - // HERE so the interior stage cannot repaint over them; the late - // landscape submission excludes DynamicLast owners for the same - // reason. - if (_dynamicParticleOwnerScratch.Count > 0) - { - passes.DrawDynamicsParticles( - ctx, - _dynamicParticleOwnerScratch); - } - return; - } - - if (partition.Dynamics.Count == 0 - && frameEntityPasses is null) - return; - - _dynamicsScratch.Clear(); - foreach (var e in partition.Dynamics) - { - EntitySphere(e, out var c, out float r); - bool indoor = InteriorEntityPartition.IsIndoorCellId(e.ParentCellId); - // TEMP (#138-B): trace the avatar's survival through this cull. - bool isProbePlayer = AcDream.App.Streaming.EntityVanishProbe.Enabled - && AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0 - && e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid; - // #118: under an interior root, outdoor-classified dynamics drew in - // the outside stage (pre-clear, seal-protected) — retail draws them - // via LScape::draw's per-landcell DrawSortCell, never in the - // post-seal cell-object epilogue (PView::DrawCells pc:432719 vs - // pc:432878). Drawing them here instead z-fails them against the - // seal. Indoor dynamics (incl. exit-portal straddlers, which drew - // in BOTH stages) stay — this pass is retail's loop C. - if (!rootIsOutdoor && !indoor) - { - if (isProbePlayer) - AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange( - $"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)"); - continue; - } - // Drawn-once (retail DrawMeshInternal @0x0059F360 marks every - // non-player part for the frame): a dynamic whose cell drew as a - // building LOOK-IN already rendered with that cell inside the - // landscape stage (#131). Redrawing it here would land AFTER the - // boundary alpha drain and overpaint nearer flames — the Holtburg - // door repainting the candle in front of it. - if (indoor && _lookInCellIds.Contains(e.ParentCellId!.Value)) - continue; - bool visible = indoor - ? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r) - : viewcone.SphereVisibleOutside(c, r); - if (isProbePlayer) - AcDream.App.Streaming.EntityVanishProbe.LogPlayerDynOnChange( - $"cell=0x{(e.ParentCellId ?? 0):X8} indoor={indoor} rootOutdoor={rootIsOutdoor} viewcone={visible} -> {(visible ? "DRAWN" : "CULLED(viewcone)")}"); - if (visible) - _dynamicsScratch.Add(e); - } - - if (_dynamicsScratch.Count == 0 - && frameEntityPasses is null) - return; - - if (_dynamicsScratch.Count > 0) - { - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.DynamicLast, - 0, - 0, - _dynamicsScratch); - } - passes.UseIndoorMembershipOnlyRouting(); - DrawEntityRouteOrLegacy( - ctx, - passes, - frameEntityPasses, - in frameView, - RenderFrameCandidateRoute.DynamicLast, - 0, - 0, - _dynamicsScratch, - visibleCellIds: null); - - // #121: dynamics' attached emitters (portal swirls, creature effects) - // gate through the SAME cone-surviving owner set as their meshes — - // retail draws emitters with the owner object. Before this callback, - // dynamics' emitters fell through EVERY particle filter under the pview - // path (the landscape slice carries outdoor statics + #118 outside- - // stage dynamics; the cell callback carries cell statics; T4 deleted - // the old clipRoot==null global pass from normal frames) — all world - // portals went invisible. Outside-stage dynamics are excluded here: - // their emitters already drew in the landscape slice (alpha-blended - // particles must not double-draw, unlike the depth-idempotent meshes). - if (frameEntityPasses is not null) - { - // Parent-cell stage split: every DynamicLast owner emits its - // particles here. Pure-outdoor dynamics are absent from this - // route (outside stage only), and interior straddlers — whose - // meshes drew in both stages — must emit HERE so the interior - // stage cannot repaint over them (matches the production - // partition-null path above). - RenderFrameRouteOwnerSelector.Replace( - _dynamicParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.DynamicLast, - 0, - 0); - } - else - { - _dynamicParticleOwnerScratch.Clear(); - // Interior-parented dynamics — INCLUDING exit-portal straddlers - // whose mesh also drew in the outside stage — emit particles in - // this final pass; outdoor-parented ones emitted in the late - // landscape submission (parent-cell stage split). - foreach (var e in _dynamicsScratch) - if (InteriorEntityPartition.IsIndoorCellId(e.ParentCellId)) - _dynamicParticleOwnerScratch.Add(e.Id); - } - if (_dynamicParticleOwnerScratch.Count > 0) - passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch); - } - - private void DrawCellObjectLists( - RetailPViewFrameInput ctx, - IRetailPViewPassExecutor passes, - PortalVisibilityFrame pvFrame, - ClipFrameAssembly clipAssembly, - HashSet drawableCells, - InteriorEntityPartition.Result? partition, - ViewconeCuller viewcone, - IRenderFrameEntityPassExecutor? frameEntityPasses, - in RenderFrameView frameView) - { - if (partition is null) - { - RenderFrameRouteOwnerSelector.Replace( - _cellParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.CellStatic, - 0, - 0); - - passes.UseIndoorMembershipOnlyRouting(); - DrawEntityRouteOrLegacy( - ctx, - passes, - frameEntityPasses, - in frameView, - RenderFrameCandidateRoute.CellStatic, - 0, - 0, - Array.Empty(), - visibleCellIds: null); - passes.DrawCellParticles( - ctx, - new RetailPViewCellSliceContext( - 0u, - NoClipSlice, - _cellParticleOwnerScratch)); - return; - } - - // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — - // dynamics moved to DrawDynamicsLast. Far→near with the cells, after - // the shells (retail DrawCells epilogue: PortalList = cell's views → - // DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is - // tested against ITS CELL's views (retail viewconeCheck) — the - // statics-through-walls fix: a static whose sphere is outside every - // view of its cell no longer paints through the wall (the cottage - // phantom staircase's draw path). - // Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching): - // the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic - // (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs + - // full state setup). Collapse them into ONE cross-cell batched draw — the - // shipped cells-shell batching pattern applied to cell OBJECTS. Two loops - // preserve the statics-before-particles depth order: loop 1 culls + - // accumulates every cell's survivors and draws them once; loop 2 runs the - // per-cell particle passes AFTER the statics own the depth buffer (particles - // depth-test but write no depth). The dispatcher sorts opaque front-to-back - // and transparent back-to-front by group distance (WbDrawDispatcher.cs: - // 1469-1470), so cross-cell batching composites correctly — equal-or-better - // than the old per-cell-bucketed order. visibleCellIds = the union of cells, - // so the dispatcher admits exactly the same survivor set. - - // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells. - _allCellStatics.Clear(); - _cellObjCells.Clear(); - for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) - { - uint cellId = pvFrame.OrderedVisibleCells[i]; - if (!drawableCells.Contains(cellId)) - continue; - - if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0) - continue; - - int survivorsBefore = _allCellStatics.Count; - foreach (var e in bucket) - { - EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInCell(cellId, c, r)) - _allCellStatics.Add(e); - } - int survivors = _allCellStatics.Count - survivorsBefore; - if (survivors > 0) - _cellObjCells.Add(cellId); - } - - // ONE batched static-object draw for every visible cell (was N per-cell - // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics - // draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against - // ITS cell's views above (the statics-through-walls fix is preserved by the - // cull; only the draw is batched). - if (frameEntityPasses is not null - || _allCellStatics.Count > 0) - { - _candidateObserver?.ObservePViewBucket( - CurrentRenderPViewRoute.CellStatic, - 0, - 0, - _allCellStatics); - passes.UseIndoorMembershipOnlyRouting(); - DrawEntityRouteOrLegacy( - ctx, - passes, - frameEntityPasses, - in frameView, - RenderFrameCandidateRoute.CellStatic, - 0, - 0, - _allCellStatics, - _cellObjCells); - } - - // Cell-particle pass — consolidated across ALL visible cells into ONE - // draw. Was per-cell, and each call re-walked the ENTIRE live particle set - // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every - // live emitter), i.e. O(cells × particles) — the dense-town cellobjects - // sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION - // (= _allCellStatics, already accumulated above for the batched draw) draws - // EXACTLY the same emitters: the callback gates on owner id (the cone- - // surviving set), the renderer sorts globally back-to-front, and the per- - // cell slice was never used for clipping (the scissor gate was deleted in - // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after - // the batched static draw so emitters depth-test against the statics now in - // the buffer (the statics-before-particles order). cellId/slice are unused - // by the particle pass — pass NoClipSlice + the union owner list. This also - // drops the per-cell BuildDrawList allocations (N → 1). - if (frameEntityPasses is not null - || _allCellStatics.Count > 0) - { - if (frameEntityPasses is not null) - { - RenderFrameRouteOwnerSelector.Replace( - _cellParticleOwnerScratch, - in frameView, - RenderFrameCandidateRoute.CellStatic, - 0, - 0); - } - else - { - ReplaceOwnerIds( - _cellParticleOwnerScratch, - _allCellStatics); - } - passes.DrawCellParticles( - ctx, - new RetailPViewCellSliceContext( - 0u, - NoClipSlice, - _cellParticleOwnerScratch)); - } - } - - private static void DrawEntityRouteOrLegacy( - RetailPViewFrameInput frame, - IRetailPViewPassExecutor passes, - IRenderFrameEntityPassExecutor? frameEntityPasses, - in RenderFrameView frameView, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId, - IReadOnlyList legacyEntities, - HashSet? visibleCellIds) - { - if (frameEntityPasses is not null) - { - frameEntityPasses.DrawEntityRoute( - frame.Camera, - in frameView, - route, - routeIndex, - cellId, - frame.PlayerLandblockId ?? 0); - return; - } - - passes.DrawEntityBucket( - frame, - legacyEntities, - visibleCellIds); - } - - // T3 scratch lists (render thread only; cleared per use). - private readonly List _outdoorStaticScratch = new(); - private readonly List _buildingShellScratch = new(); - private readonly List _cellStaticScratch = new(); - private readonly List _dynamicsScratch = new(); - // #118: dynamics assigned to the OUTSIDE stage this frame (interior roots - // only) — outdoor-classified + exit-portal straddlers. Cleared per frame. - private readonly List _outsideStageDynamics = new(); - // Dense-town FPS iteration-1 (cellobject batching): all visible cells' - // viewcone-surviving statics accumulated for ONE batched DrawEntityBucket, - // plus the union of their cell ids for the dispatcher's visibleCellIds gate. - // Cleared at the top of DrawCellObjectLists. - private readonly List _allCellStatics = new(); - private readonly HashSet _cellObjCells = new(); - - private bool LegacyPartitionDiagnosticsEnabled => - _partitionObserver is not null - || AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled - || AcDream.App.Streaming.EntityVanishProbe.Enabled; - private static RenderFrameDiagnosticCounts WalkDiagnosticCounts( RenderProjectionCounts source) { @@ -2144,132 +564,18 @@ public sealed class RetailPViewRenderer MeshPartCount: 0); } - internal static RenderFrameDiagnosticCounts LegacyDiagnosticCounts( - InteriorEntityPartition.Result partition) - { - int cellStaticCount = 0; - foreach (List bucket in partition.ByCell.Values) - cellStaticCount = checked(cellStaticCount + bucket.Count); - - return new RenderFrameDiagnosticCounts( - partition.OutdoorStatic.Count, - cellStaticCount, - partition.Dynamics.Count, - TransformCount: 0, - OpaqueClassificationCount: 0, - AlphaClassificationCount: 0, - LightSetCount: 0, - SelectionPartCount: 0, - RouteCandidateCount: - checked( - partition.OutdoorStatic.Count - + cellStaticCount - + partition.Dynamics.Count), - EntityCandidateCount: 0, - MeshPartCount: 0); - } - - internal static RenderProjectionCounts LegacySourceCounts( - InteriorEntityPartition.Result partition) - { - int cellStaticCount = 0; - foreach (List bucket in partition.ByCell.Values) - cellStaticCount = checked(cellStaticCount + bucket.Count); - int total = checked( - partition.OutdoorStatic.Count - + cellStaticCount - + partition.Dynamics.Count); - return new RenderProjectionCounts( - total, - partition.OutdoorStatic.Count, - cellStaticCount, - partition.Dynamics.Count, - ActiveAnimatedStatic: 0, - EquippedChild: 0); - } - - private static void ReplaceOwnerIds( - HashSet destination, - IReadOnlyList entities) - { - destination.Clear(); - for (int index = 0; index < entities.Count; index++) - { - uint localEntityId = entities[index].Id; - if (localEntityId != 0) - destination.Add(localEntityId); - } - } - - /// - /// #118 stage assignment for a dynamic under an INTERIOR root: does it draw - /// in the OUTSIDE (landscape) stage — before the gated depth clear and the - /// exit-portal seals — like retail's per-landcell object draw - /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at - /// the top of PView::DrawCells pc:432719)? - /// - /// True for outdoor-classified dynamics (their fragments lie beyond the - /// door plane and would z-fail the seal in the last pass), and for INDOOR - /// dynamics whose sphere straddles an exit-portal plane of their flood- - /// visible cell — retail draws an object once per overlapped shadow cell - /// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in - /// both stages and neither half clips at the plane. Pure — also driven - /// headlessly by HouseExitWalkReplayTests as the ordering contract. - /// - public static bool DynamicDrawsInOutsideStage( - uint? parentCellId, - Vector3 sphereCenter, - float sphereRadius, - HashSet drawableCells, - IRetailPViewCellSource cells) - { - if (!InteriorEntityPartition.IsIndoorCellId(parentCellId)) - return true; - - uint cellId = parentCellId!.Value; - if (!drawableCells.Contains(cellId)) - return false; // not in the flood — the last-pass cone cull owns it - var cell = cells.Find(cellId); - if (cell is null) - return false; - - var localC = Vector3.Transform(sphereCenter, cell.InverseWorldTransform); - int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count); - for (int i = 0; i < n; i++) - { - if (cell.Portals[i].OtherCellId != 0xFFFF) - continue; - var plane = cell.ClipPlanes[i]; - if (plane.Normal.LengthSquared() < 1e-8f) - continue; - float dist = Vector3.Dot(plane.Normal, localC) + plane.D; - if (MathF.Abs(dist) < sphereRadius) - return true; // sphere straddles the exit-portal plane - } - return false; - } - - // Conservative bounding sphere from the entity's cached AABB — the same - // bounds source the dispatcher's frustum cull uses. - private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius) - { - if (e.AabbDirty) - e.RefreshAabb(); - center = (e.AabbMin + e.AabbMax) * 0.5f; - radius = (e.AabbMax - e.AabbMin).Length() * 0.5f; - } - private static ClipViewSlice[] GetCellSlicesOrNoClip( ClipFrameAssembly clipAssembly, uint cellId) { if (clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices) && slices.Length > 0) + { return slices; + } return NoClipSlices; } - } public interface IRetailPViewCellSource @@ -2283,240 +589,6 @@ public interface IRetailPViewCellSource /// pass only; visibility construction and draw ordering remain renderer-owned. /// All frame inputs and results are borrowed for the duration of the call. /// -public interface IRetailPViewPassExecutor -{ - void AbortFrame(); - void BeginFrame(); - ClipFrameAssembly AssembleClipFrame( - PortalVisibilityFrame portalFrame, - ClipFrameAssembly reuseAssembly); - void AppendLookInClipFrames( - IReadOnlyList lookInFrames, - ClipFrameAssembly assembly); - void PrepareClipFrame(int terrainUploadCount); - void SetTerrainClip(ReadOnlySpan planes); - void ClearClipRouting(); - void UseIndoorMembershipOnlyRouting(); - void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice); - void PrepareCellBatches( - RetailPViewFrameInput frame, - HashSet visibleCellIds); - void DrawOpaqueCellShells(HashSet cellIds); - bool CellHasTransparentShell(uint cellId); - void DrawTransparentCellShells(HashSet cellIds); - void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds); - void DrawEntityBucket( - RetailPViewFrameInput frame, - IReadOnlyList entities, - HashSet? visibleCellIds); - void EmitClipRouteProbe( - ClipFrameAssembly clipAssembly, - ClipViewSlice slice, - int sliceIndex); - void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context); - void DrawLandscapeStaticParticles( - RetailPViewFrameInput frame, - RetailPViewLandscapeStaticParticleContext context); - void DrawLandscapeBuildingShellSlice( - RetailPViewFrameInput frame, - RetailPViewLandscapeBuildingShellSliceContext context); - void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context); - void ClearInteriorDepth(); - void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context); - void DrawLookInPortalPunch( - RetailPViewFrameInput frame, - RetailPViewCellSliceContext context, - int portalIndex); - /// - /// One unclipped submission for every renderable UNATTACHED emitter whose - /// owner cell matches the scope: outdoor landcells in the landscape stage, - /// interior EnvCells in the final world scope. Retail inserts each such - /// particle into the single alpha list during its owner cell's walk turn - /// and never clips it to a portal view. - /// - void DrawUnattachedSceneParticles( - RetailPViewFrameInput frame, - bool outdoorCells); - void FlushLandscapeAlpha(); - - /// - /// Pre/inter-building barrier drain: composites only the queued alpha at - /// or beyond and retains nearer - /// entries for the later boundary flush — retail's far→near walk outcome - /// (DrawBuilding's FlushAlphaList(0f) @0x0059F2A0 can only ever flush - /// content from cells farther than that building; AP-236). The default - /// falls back to a full flush so non-production executors keep today's - /// behavior until they opt in. - /// - void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - FlushLandscapeAlpha(); - void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context); - void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlySet ownerIds); - void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result); -} - -/// -/// Capacity policy for renderer-owned, one-frame scratch. Normal warmed frames -/// retain their storage; a pathological visibility spike is released at the -/// next frame boundary instead of becoming permanent process memory. -/// -internal sealed class RetailPViewScratchRetention -{ - internal const int MaxRetainedLookInFrames = 32; - internal const int MaxRetainedCellItems = 512; - internal const int CapacityTrimIdleFrames = 120; - - private int _lookInFramesUnderusedFrames; - private int _lookInPrepareUnderusedFrames; - private int _drawableCellsUnderusedFrames; - private int _shellBatchUnderusedFrames; - private int _orderedTransparentUnderusedFrames; - - internal void ClearFrameBuffers( - List lookInFrames, - HashSet lookInPrepare, - HashSet drawableCells, - HashSet shellBatch, - List orderedTransparentShellCells) - { - ClearCold( - lookInFrames, - MaxRetainedLookInFrames, - ref _lookInFramesUnderusedFrames); - ClearCold( - lookInPrepare, - MaxRetainedCellItems, - ref _lookInPrepareUnderusedFrames); - ClearCold( - drawableCells, - MaxRetainedCellItems, - ref _drawableCellsUnderusedFrames); - ClearCold(shellBatch, MaxRetainedCellItems, ref _shellBatchUnderusedFrames); - ClearCold( - orderedTransparentShellCells, - MaxRetainedCellItems, - ref _orderedTransparentUnderusedFrames); - } - - private static void ClearCold( - List values, - int maximumRetainedCapacity, - ref int underusedFrames) - { - int usedCount = values.Count; - int capacity = values.Capacity; - values.Clear(); - if (!ShouldTrim( - capacity, - usedCount, - maximumRetainedCapacity, - ref underusedFrames)) - return; - - if (capacity > maximumRetainedCapacity) - values.Capacity = 0; - } - - private static void ClearCold( - HashSet values, - int maximumRetainedCapacity, - ref int underusedFrames) - { - int usedCount = values.Count; - int capacity = values.EnsureCapacity(0); - values.Clear(); - if (!ShouldTrim( - capacity, - usedCount, - maximumRetainedCapacity, - ref underusedFrames)) - return; - - if (capacity > maximumRetainedCapacity) - values.TrimExcess(); - } - - private static bool ShouldTrim( - int capacity, - int usedCount, - int maximumRetainedCapacity, - ref int underusedFrames) - { - if (capacity <= maximumRetainedCapacity || (long)usedCount * 2L > capacity) - { - underusedFrames = 0; - return false; - } - - underusedFrames++; - if (underusedFrames < CapacityTrimIdleFrames) - return false; - - underusedFrames = 0; - return true; - } -} - -/// -/// Frame-scoped grouping for retail's per-building portal floods. Active keys -/// are rebuilt in nearby-cell encounter order every frame; the value lists are -/// retained through a bounded pool so travelling through the world cannot turn -/// every historical building id into permanent memory or per-frame scan work. -/// -internal sealed class BuildingGroupScratch -{ - internal const int MaxRetainedGroups = 256; - internal const int MaxRetainedCellsPerGroup = 256; - - private readonly Dictionary> _active = new(); - private readonly Stack> _listPool = new(); - - internal Dictionary>.ValueCollection Values => _active.Values; - internal IReadOnlyDictionary> Groups => _active; - internal int ActiveGroupCount => _active.Count; - internal int RetainedListCount => _listPool.Count; - internal int MapCapacity => _active.EnsureCapacity(0); - - internal void Rebuild(IReadOnlyList nearbyCells) - { - ArgumentNullException.ThrowIfNull(nearbyCells); - Reset(); - - for (int i = 0; i < nearbyCells.Count; i++) - { - LoadedCell cell = nearbyCells[i]; - // R-A2 seam behavior: an unstamped cell still gets a singleton - // entrance flood keyed by CellId. - uint groupKey = cell.BuildingId ?? cell.CellId; - if (!_active.TryGetValue(groupKey, out List? group)) - { - group = _listPool.Count != 0 - ? _listPool.Pop() - : new List(); - _active.Add(groupKey, group); - } - group.Add(cell); - } - } - - internal void Reset() - { - foreach (List group in _active.Values) - { - group.Clear(); - if (group.Capacity <= MaxRetainedCellsPerGroup - && _listPool.Count < MaxRetainedGroups) - { - _listPool.Push(group); - } - } - - _active.Clear(); - if (_active.EnsureCapacity(0) > MaxRetainedGroups) - _active.TrimExcess(); - } -} - public sealed class RetailPViewFrameInput { public LoadedCell RootCell { get; private set; } = null!; @@ -2658,28 +730,6 @@ public sealed class RetailPViewFrameResult return this; } - internal RetailPViewFrameResult Reset( - PortalVisibilityFrame portalFrame, - ClipFrameAssembly clipAssembly, - HashSet drawableCells, - InteriorEntityPartition.Result diagnosticPartition) => - Reset( - portalFrame, - clipAssembly, - drawableCells, - drawableCells, - RetailPViewRenderer.LegacyDiagnosticCounts( - diagnosticPartition), - RetailPViewRenderer.LegacySourceCounts( - diagnosticPartition), - diagnosticPartition); -} - -public readonly record struct RetailPViewLandscapeSliceContext( - ClipViewSlice Slice, - IReadOnlyList OutdoorEntities) -{ - internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } /// @@ -2692,24 +742,12 @@ public readonly record struct RetailPViewLandscapeSliceContext( public readonly record struct RetailPViewLandscapeStaticParticleContext( IReadOnlySet ParticleOwnerIds); -/// Retail DrawBuilding's ordinary exterior-shell pass, issued after -/// the same building's portal-only look-in traversal. -public readonly record struct RetailPViewLandscapeBuildingShellSliceContext( - ClipViewSlice Slice, - IReadOnlyList BuildingShells) -{ - internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } -} - /// #131/#132: the late landscape phase's per-slice payload — /// outside-stage dynamics to mesh-draw, plus the particle owners not already /// submitted at a pre-building barrier. public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, - IReadOnlyList Dynamics) -{ - internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } -} + IReadOnlyList Dynamics); public readonly record struct RetailPViewCellSliceContext( uint CellId, diff --git a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs index 1d91e3ec..46e63070 100644 --- a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs +++ b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs @@ -30,8 +30,7 @@ namespace AcDream.App.Rendering.Walk; /// /// /// The tuple landblock id handed to the classifier is the frame's player -/// landblock — the packed path's own convention for every -/// RenderFrameEntityDrawRequest. +/// landblock, matching the production walk's retained-scene query convention. /// /// Campaign FW3.4a: , , /// and used to materialize their result diff --git a/src/AcDream.App/Rendering/WorldSceneRenderer.cs b/src/AcDream.App/Rendering/WorldSceneRenderer.cs index cfad2750..ac81298f 100644 --- a/src/AcDream.App/Rendering/WorldSceneRenderer.cs +++ b/src/AcDream.App/Rendering/WorldSceneRenderer.cs @@ -41,12 +41,12 @@ internal interface IPreparedWorldSceneFramePhase : IWorldSceneFramePhase internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer { private readonly RetailPViewRenderer _renderer; - private readonly IRetailPViewPassExecutor _passes; + private readonly RetailPViewPassExecutor _passes; private readonly IOutdoorSceneParticleOwnerSource _particles; public WorldScenePViewRenderer( RetailPViewRenderer renderer, - IRetailPViewPassExecutor passes, + RetailPViewPassExecutor passes, IOutdoorSceneParticleOwnerSource particles) { _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); diff --git a/tests/AcDream.App.Tests/Rendering/BuildingGroupScratchTests.cs b/tests/AcDream.App.Tests/Rendering/BuildingGroupScratchTests.cs deleted file mode 100644 index c256ca62..00000000 --- a/tests/AcDream.App.Tests/Rendering/BuildingGroupScratchTests.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AcDream.App.Rendering; -using Xunit; - -namespace AcDream.App.Tests.Rendering; - -public sealed class BuildingGroupScratchTests -{ - [Fact] - public void Rebuild_DropsHistoricalKeys_BoundsRetention_AndPreservesEncounterOrder() - { - var scratch = new BuildingGroupScratch(); - int pathologicalCount = BuildingGroupScratch.MaxRetainedGroups * 4; - var pathological = new List(pathologicalCount); - for (int i = 0; i < pathologicalCount; i++) - { - pathological.Add(new LoadedCell - { - CellId = (uint)i + 1u, - BuildingId = 0x1000_0000u + (uint)i, - }); - } - - scratch.Rebuild(pathological); - Assert.Equal(pathologicalCount, scratch.ActiveGroupCount); - Assert.True(scratch.MapCapacity > BuildingGroupScratch.MaxRetainedGroups); - - var firstA = new LoadedCell { CellId = 0xAA01u, BuildingId = 0x20u }; - var second = new LoadedCell { CellId = 0xBB01u, BuildingId = 0x10u }; - var firstB = new LoadedCell { CellId = 0xAA02u, BuildingId = 0x20u }; - var unstamped = new LoadedCell { CellId = 0xCC01u }; - scratch.Rebuild([firstA, second, firstB, unstamped]); - - Assert.Equal(3, scratch.ActiveGroupCount); - Assert.Equal( - new uint[] { 0x20u, 0x10u, unstamped.CellId }, - scratch.Groups.Keys.ToArray()); - Assert.Equal(new[] { firstA, firstB }, scratch.Groups[0x20u]); - Assert.Equal(new[] { second }, scratch.Groups[0x10u]); - Assert.Equal(new[] { unstamped }, scratch.Groups[unstamped.CellId]); - Assert.DoesNotContain( - scratch.Groups.Keys, - key => key >= 0x1000_0000u); - Assert.InRange( - scratch.RetainedListCount, - 0, - BuildingGroupScratch.MaxRetainedGroups); - Assert.InRange( - scratch.MapCapacity, - 0, - BuildingGroupScratch.MaxRetainedGroups); - - scratch.Reset(); - - Assert.Equal(0, scratch.ActiveGroupCount); - Assert.Empty(scratch.Groups); - Assert.InRange( - scratch.RetainedListCount, - 0, - BuildingGroupScratch.MaxRetainedGroups); - Assert.InRange( - scratch.MapCapacity, - 0, - BuildingGroupScratch.MaxRetainedGroups); - } - - [Fact] - public void Reset_DoesNotRetainPathologicalGroupBackingArray() - { - var scratch = new BuildingGroupScratch(); - var oversizedGroup = new List( - BuildingGroupScratch.MaxRetainedCellsPerGroup * 2); - for (int i = 0; - i < BuildingGroupScratch.MaxRetainedCellsPerGroup * 2; - i++) - { - oversizedGroup.Add(new LoadedCell - { - CellId = (uint)i + 1u, - BuildingId = 0x42u, - }); - } - - scratch.Rebuild(oversizedGroup); - Assert.Single(scratch.Groups); - Assert.True( - scratch.Groups[0x42u].Capacity - > BuildingGroupScratch.MaxRetainedCellsPerGroup); - - scratch.Reset(); - - Assert.Equal(0, scratch.RetainedListCount); - Assert.Empty(scratch.Groups); - } -} - -public sealed class RetailPViewScratchRetentionTests -{ - [Fact] - public void ClearFrameBuffers_DropsPathologicalHighWater_AfterIdleHysteresis() - { - var retention = new RetailPViewScratchRetention(); - int pathologicalCellCount = - RetailPViewScratchRetention.MaxRetainedCellItems * 4; - int pathologicalFrameCount = - RetailPViewScratchRetention.MaxRetainedLookInFrames * 4; - var lookInFrames = new List(pathologicalFrameCount); - var lookInPrepare = new HashSet(); - var drawableCells = new HashSet(); - var shellBatch = new HashSet(); - var orderedTransparent = new List(pathologicalCellCount); - - for (int i = 0; i < pathologicalFrameCount; i++) - lookInFrames.Add(new PortalVisibilityFrame()); - for (int i = 0; i < pathologicalCellCount; i++) - { - uint id = (uint)i; - lookInPrepare.Add(id); - drawableCells.Add(id); - shellBatch.Add(id); - orderedTransparent.Add(id); - } - - Assert.True( - lookInFrames.Capacity - > RetailPViewScratchRetention.MaxRetainedLookInFrames); - Assert.True( - drawableCells.EnsureCapacity(0) - > RetailPViewScratchRetention.MaxRetainedCellItems); - - retention.ClearFrameBuffers( - lookInFrames, - lookInPrepare, - drawableCells, - shellBatch, - orderedTransparent); - - Assert.Empty(lookInFrames); - Assert.Empty(lookInPrepare); - Assert.Empty(drawableCells); - Assert.Empty(shellBatch); - Assert.Empty(orderedTransparent); - Assert.True( - lookInFrames.Capacity - > RetailPViewScratchRetention.MaxRetainedLookInFrames); - - for (int i = 0; i < RetailPViewScratchRetention.CapacityTrimIdleFrames; i++) - { - retention.ClearFrameBuffers( - lookInFrames, - lookInPrepare, - drawableCells, - shellBatch, - orderedTransparent); - } - - Assert.InRange( - lookInFrames.Capacity, - 0, - RetailPViewScratchRetention.MaxRetainedLookInFrames); - Assert.InRange( - lookInPrepare.EnsureCapacity(0), - 0, - RetailPViewScratchRetention.MaxRetainedCellItems); - Assert.InRange( - drawableCells.EnsureCapacity(0), - 0, - RetailPViewScratchRetention.MaxRetainedCellItems); - Assert.InRange( - shellBatch.EnsureCapacity(0), - 0, - RetailPViewScratchRetention.MaxRetainedCellItems); - Assert.InRange( - orderedTransparent.Capacity, - 0, - RetailPViewScratchRetention.MaxRetainedCellItems); - } - - [Fact] - public void ClearFrameBuffers_PreservesNormalWarmCapacity() - { - var retention = new RetailPViewScratchRetention(); - var lookInFrames = new List(8); - var lookInPrepare = new HashSet(); - var drawableCells = new HashSet(); - var shellBatch = new HashSet(); - var orderedTransparent = new List(64); - for (uint id = 0; id < 64; id++) - { - lookInPrepare.Add(id); - drawableCells.Add(id); - shellBatch.Add(id); - orderedTransparent.Add(id); - } - int lookInCapacity = lookInFrames.Capacity; - int prepareCapacity = lookInPrepare.EnsureCapacity(0); - int drawableCapacity = drawableCells.EnsureCapacity(0); - int shellCapacity = shellBatch.EnsureCapacity(0); - int transparentCapacity = orderedTransparent.Capacity; - - retention.ClearFrameBuffers( - lookInFrames, - lookInPrepare, - drawableCells, - shellBatch, - orderedTransparent); - - Assert.Equal(lookInCapacity, lookInFrames.Capacity); - Assert.Equal(prepareCapacity, lookInPrepare.EnsureCapacity(0)); - Assert.Equal(drawableCapacity, drawableCells.EnsureCapacity(0)); - Assert.Equal(shellCapacity, shellBatch.EnsureCapacity(0)); - Assert.Equal(transparentCapacity, orderedTransparent.Capacity); - } - - [Fact] - public void ClearFrameBuffers_RecurringLargeWorkingSet_PreservesWarmCapacity() - { - const int recurringCount = - RetailPViewScratchRetention.MaxRetainedCellItems + 163; - var retention = new RetailPViewScratchRetention(); - var lookInFrames = new List(); - var lookInPrepare = new HashSet(); - var drawableCells = new HashSet(); - var shellBatch = new HashSet(); - var orderedTransparent = new List(); - - for (int iteration = 0; - iteration < RetailPViewScratchRetention.CapacityTrimIdleFrames + 5; - iteration++) - { - for (int i = 0; i < recurringCount; i++) - { - uint id = (uint)i; - drawableCells.Add(id); - orderedTransparent.Add(id); - } - - int drawableCapacity = drawableCells.EnsureCapacity(0); - int transparentCapacity = orderedTransparent.Capacity; - retention.ClearFrameBuffers( - lookInFrames, - lookInPrepare, - drawableCells, - shellBatch, - orderedTransparent); - - Assert.Equal(drawableCapacity, drawableCells.EnsureCapacity(0)); - Assert.Equal(transparentCapacity, orderedTransparent.Capacity); - } - } -} diff --git a/tests/AcDream.App.Tests/Rendering/HouseExitWalkReplayTests.cs b/tests/AcDream.App.Tests/Rendering/HouseExitWalkReplayTests.cs deleted file mode 100644 index cd4d42d4..00000000 --- a/tests/AcDream.App.Tests/Rendering/HouseExitWalkReplayTests.cs +++ /dev/null @@ -1,500 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using AcDream.App.Rendering; -using DatReaderWriter; -using DatReaderWriter.Options; -using Xunit; -using Xunit.Abstractions; - -namespace AcDream.App.Tests.Rendering; - -/// -/// #118 exit-walk harness (handoff 2026-06-11 §5): the character is clipped and then -/// vanishes for a moment when exiting a house — the window where the player is just -/// outside the door while the collided viewer (camera) is still indoors. -/// -/// Per step of a deterministic eye+player path crossing the exit doorway of the -/// Holtburg corner building (cell 0xA9B40170, dat-loaded via the CornerFloodReplay -/// fixture loader), this drives the PRODUCTION decision stack headlessly: -/// viewer-cell resolution (healthy-sweep model, see below) → -/// → -/// → the exact DrawDynamicsLast visibility predicate -/// (RetailPViewRenderer.cs:375-401), PLUS the depth relationship DrawDynamicsLast is -/// subject to: under an INTERIOR root the exit-portal SEAL stamps the door fan at TRUE -/// depth (RetailPViewPassExecutor.DrawPortalDepthWrite, forceFarZ=false) after the full -/// depth clear, and dynamics draw depth-tested AFTER it. -/// -/// The four candidates this pins (handoff §5 + this session's read): -/// 1. eye/cell incoherence under damping — EXONERATED BY READ for clean exits: -/// RetailChaseCamera publishes (Position, ViewerCellId) from the SAME SweepEye call -/// (RetailChaseCamera.cs:188-203), published==damped when nothing collides (an open -/// doorway), and GameWindow updates the camera (≈:6889) BEFORE the visibility read -/// (≈:7361) in the same frame. -/// 2. exit-portal side test culling at an ε-outside eye → OutsideView EMPTY → -/// SphereVisibleOutside culls ALL outdoor dynamics. Quantified by the stale-root -/// diagnostic below (the healthy walk should never produce the incoherent pair). -/// 3. doorway-aperture cone tightness → tested per step by the predicate replica. -/// 4. (new, this session) SEAL-DEPTH vs dynamics-last ordering: a player whose -/// fragments lie BEYOND the door plane z-fails against the seal across the whole -/// aperture → invisible while fully outside (and clipped at the plane while -/// straddling). Tested per step by the CPU depth check (same viewProj math the GPU -/// consumes). -/// -/// Healthy-sweep model: the corner-seal replay (b21bb28) + the camera read above prove -/// the sweep resolves the eye's ACTUAL cell same-frame, so the harness derives the -/// viewer cell geometrically: door-plane side decides indoor/outdoor; AABB containment -/// (smallest containing volume) picks the interior cell. The outdoor root is -/// exactly as GameWindow builds it (full-screen -/// OutsideView ⇒ outdoor dynamics trivially cone-pass). -/// -[Collection(CameraDiagnosticsCollection.Name)] -[Trait("Lane", "InstalledDat")] -public class HouseExitWalkReplayTests -{ - private readonly ITestOutputHelper _out; - public HouseExitWalkReplayTests(ITestOutputHelper output) => _out = output; - - private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u; - - // Production humanoid entity sphere (RetailPViewRenderer.EntitySphere: AABB center + - // half-diagonal). A ~1.8 m × 0.6 m character AABB gives center ≈ feet+0.9, r ≈ 1.0. - private const float PlayerSphereRadius = 1.0f; - private static readonly Vector3 PlayerSphereCenterOffset = new(0f, 0f, 0.9f); - - private sealed record ExitDoor( - int PortalIndex, - Vector3[] WorldVerts, - Vector3 WorldCentroid, - Vector3 OutwardNormal, // unit, world space, pointing OUT of the building - float FloorZ); - - private sealed record WalkStep( - int Index, - float WalkS, // raw walk parameter (feet travel along the XY exit direction) - float CenterS, // signed distance of the SPHERE CENTER to the door plane (the production quantity) - float EyeS, // signed distance of the published eye - Vector3 PlayerFeet, - Vector3 Eye, - uint RootCellId, // 0 = outdoor root - uint PlayerParentCellId, - bool ConeVisible, - bool OutsideStage, // production stage assignment (DynamicDrawsInOutsideStage) - bool DepthCheckApplies, - bool DepthPass, - int OutsidePolys, - int FloodCells); - - // ── fixture / geometry ────────────────────────────────────────────── - - private static ExitDoor FindExitDoor(LoadedCell cell) - { - int best = -1; - float bestMinZ = float.MaxValue; - for (int i = 0; i < cell.Portals.Count; i++) - { - if (cell.Portals[i].OtherCellId != 0xFFFF) continue; - if (i >= cell.PortalPolygons.Count) continue; - var poly = cell.PortalPolygons[i]; - if (poly is null || poly.Length < 3) continue; - - float minZ = float.MaxValue; - foreach (var v in poly) - minZ = MathF.Min(minZ, Vector3.Transform(v, cell.WorldTransform).Z); - // a DOOR reaches the floor; a window doesn't — pick the lowest-silled exit portal - if (minZ < bestMinZ) { bestMinZ = minZ; best = i; } - } - Assert.True(best >= 0, $"cell 0x{cell.CellId:X8} has no exit portal (OtherCellId==0xFFFF)"); - - var local = cell.PortalPolygons[best]; - var world = new Vector3[local.Length]; - var centroid = Vector3.Zero; - float floorZ = float.MaxValue; - for (int v = 0; v < local.Length; v++) - { - world[v] = Vector3.Transform(local[v], cell.WorldTransform); - centroid += world[v]; - floorZ = MathF.Min(floorZ, world[v].Z); - } - centroid /= local.Length; - - // Outward = away from the cell interior. ClipPlanes[i].InsideSide encodes which - // side the cell centroid is on (CornerFloodReplayTests.LoadCell): InsideSide==0 - // ⇒ interior satisfies dot ≥ 0 ⇒ outward is −Normal; InsideSide==1 ⇒ +Normal. - var plane = cell.ClipPlanes[best]; - var outwardLocal = plane.InsideSide == 0 ? -plane.Normal : plane.Normal; - var outwardWorld = Vector3.Normalize(Vector3.TransformNormal(outwardLocal, cell.WorldTransform)); - - return new ExitDoor(best, world, centroid, outwardWorld, floorZ); - } - - private static float SignedSide(ExitDoor door, Vector3 p) - => Vector3.Dot(door.OutwardNormal, p - door.WorldCentroid); - - private static uint? ResolveInteriorCellByAabb( - Dictionary cells, Vector3 worldPoint, float margin = 0.05f) - { - uint? best = null; - float bestVolume = float.MaxValue; - foreach (var (id, cell) in cells) - { - var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform); - var min = cell.LocalBoundsMin - new Vector3(margin); - var max = cell.LocalBoundsMax + new Vector3(margin); - if (local.X < min.X || local.Y < min.Y || local.Z < min.Z - || local.X > max.X || local.Y > max.Y || local.Z > max.Z) - continue; - var ext = cell.LocalBoundsMax - cell.LocalBoundsMin; - float volume = MathF.Max(ext.X, 1e-3f) * MathF.Max(ext.Y, 1e-3f) * MathF.Max(ext.Z, 1e-3f); - if (volume < bestVolume) { bestVolume = volume; best = id; } - } - return best; - } - - // Outdoor landcell id for a landblock-local position (dat EnvCell positions are - // landblock-local). Only the (low word < 0x100) classification is load-bearing for - // the DrawDynamicsLast predicate; the exact landcell is kept honest anyway. - private static uint OutdoorLandcellId(Vector3 landblockLocalPos) - { - int cx = Math.Clamp((int)MathF.Floor(landblockLocalPos.X / 24f), 0, 7); - int cy = Math.Clamp((int)MathF.Floor(landblockLocalPos.Y / 24f), 0, 7); - return CornerFloodReplayTests.Landblock | (uint)(cx * 8 + cy + 1); - } - - // Exact replica of RetailPViewRenderer.DrawDynamicsLast's visibility predicate - // (RetailPViewRenderer.cs:386-391). Keep in lockstep with production. - private static bool DynamicsConeVisible(ViewconeCuller cone, uint parentCellId, Vector3 c, float r) - { - bool indoor = (parentCellId & 0xFFFFu) >= 0x0100u && (parentCellId & 0xFFFFu) != 0xFFFFu; - return indoor - ? cone.SphereVisibleInCell(parentCellId, c, r) - : cone.SphereVisibleOutside(c, r); - } - - // ── the walk ──────────────────────────────────────────────────────── - - private List? RunExitWalk() - { - var datDir = CornerFloodReplayTests.ResolveDatDir(); - if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return null; } - - using var dats = new DatCollection(datDir, DatAccessType.Read); - var cells = CornerFloodReplayTests.LoadBuilding(dats); - Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; - - var exitCell = cells[ExitCellId]; - var door = FindExitDoor(exitCell); - _out.WriteLine(FormattableString.Invariant( - $"exit door: cell=0x{ExitCellId:X8} portal[{door.PortalIndex}] centroid=({door.WorldCentroid.X:F2},{door.WorldCentroid.Y:F2},{door.WorldCentroid.Z:F2}) outward=({door.OutwardNormal.X:F2},{door.OutwardNormal.Y:F2},{door.OutwardNormal.Z:F2}) floorZ={door.FloorZ:F2} verts={door.WorldVerts.Length}")); - - // Walk straight out through the door centroid along the outward normal's XY - // projection (doors are vertical; assert so). - var out2d = Vector3.Normalize(new Vector3(door.OutwardNormal.X, door.OutwardNormal.Y, 0f)); - Assert.True(MathF.Abs(door.OutwardNormal.Z) < 0.3f, "exit door is not vertical — walk path invalid"); - - float yaw = MathF.Atan2(out2d.Y, out2d.X); - const float dt = 1f / 60f; - const float stepLen = 0.02f; // 2 cm per frame = 1.2 m/s walk - const float sStart = -1.2f, sEnd = 3.0f; // door plane at s=0 - var velocity = out2d * (stepLen / dt); - - var camera = new RetailChaseCamera { Aspect = 1280f / 720f }; - - Vector3 FeetAt(float s) => new( - door.WorldCentroid.X + out2d.X * s, - door.WorldCentroid.Y + out2d.Y * s, - door.FloorZ); - - // Warm the damped boom to convergence at the start pose (retail's convergence - // snap freezes it once the lerp step is sub-epsilon). - for (int i = 0; i < 240; i++) - camera.Update(FeetAt(sStart), yaw, Vector3.Zero, isOnGround: true, Vector3.UnitZ, dt); - - var clipFrame = ClipFrame.NoClip(); - var steps = new List(); - int stepCount = (int)MathF.Round((sEnd - sStart) / stepLen); - - for (int i = 0; i <= stepCount; i++) - { - float s = sStart + i * stepLen; - var feet = FeetAt(s); - camera.Update(feet, yaw, velocity, isOnGround: true, Vector3.UnitZ, dt); - var eye = camera.Position; - var viewProj = camera.View * camera.Projection; - - float eyeS = SignedSide(door, eye); - var sphereC = feet + PlayerSphereCenterOffset; - float playerS = SignedSide(door, sphereC); - - // membership model: the controller's pick is center point-in-cell (physics - // digest, P1) — the player's ParentCellId flips when the sphere CENTER - // crosses the door plane. - uint playerCell = playerS <= 0f - ? ExitCellId - : OutdoorLandcellId(feet); - - // healthy-sweep viewer resolution: plane side decides indoor/outdoor; - // AABB containment picks the interior cell the eye is in. - uint rootCellId = 0u; - LoadedCell? root = null; - if (eyeS <= 0f) - { - var contained = ResolveInteriorCellByAabb(cells, eye); - Assert.True(contained.HasValue, - FormattableString.Invariant( - $"step {i}: eye=({eye.X:F2},{eye.Y:F2},{eye.Z:F2}) (eyeS={eyeS:F2}) is inside the door plane but no loaded cell AABB contains it — adjust the walk")); - rootCellId = contained.Value; - root = cells[rootCellId]; - } - else - { - root = OutdoorCellNode.Build(OutdoorLandcellId(eye)); - } - - var pv = PortalVisibilityBuilder.Build(root, eye, lookup, viewProj); - var asm = ClipFrameAssembler.Assemble(clipFrame, pv); - var cone = ViewconeCuller.Build(asm, viewProj); - - bool coneVisible = DynamicsConeVisible(cone, playerCell, sphereC, PlayerSphereRadius); - - // Production stage assignment (#118 fix): an outside-stage dynamic draws - // BEFORE the depth clear + seal (retail LScape::draw → DrawSortCell), so - // the seal PROTECTS its pixels instead of z-killing them. - var drawable = new HashSet(pv.OrderedVisibleCells); - bool outsideStage = rootCellId != 0u - && RetailPViewRenderer.DynamicDrawsInOutsideStage( - playerCell, - sphereC, - PlayerSphereRadius, - drawable, - new TestCellSource(lookup)); - - // Candidate 4: the seal depth check. Applies when the root is INTERIOR - // (ClearDepthForInterior + the TRUE-depth seal run, GameWindow:7719-7729), - // the player sphere center lies BEYOND the door plane on the ray from the - // eye, AND the player draws in the post-seal last pass (not outside-stage). - bool depthApplies = false, depthPass = true; - if (rootCellId != 0u && coneVisible && playerS > 0f && eyeS < 0f && !outsideStage) - { - depthApplies = TrySealDepthCheck(door, eye, sphereC, viewProj, out depthPass); - } - - steps.Add(new WalkStep( - i, s, playerS, eyeS, feet, eye, rootCellId, playerCell, coneVisible, outsideStage, - depthApplies, depthPass, - pv.OutsideView.Polygons.Count, pv.OrderedVisibleCells.Count)); - } - - return steps; - } - - /// - /// CPU model of the depth relationship at the player-sphere-center pixel: - /// the SEAL (door fan at true depth, drawn after the full depth clear and before - /// dynamics — PortalDepthMaskRenderer, forceFarZ=false) vs the player fragment. - /// Returns true (out: pass) when the player's NDC depth at that pixel is ≤ the - /// seal's. Returns false (check N/A) when the eye→center ray misses the door fan - /// (the seal doesn't cover that pixel; depth there is the cleared far plane). - /// - private static bool TrySealDepthCheck( - ExitDoor door, Vector3 eye, Vector3 sphereCenter, in Matrix4x4 viewProj, out bool depthPass) - { - depthPass = true; - - float dEye = Vector3.Dot(door.OutwardNormal, eye - door.WorldCentroid); - float dC = Vector3.Dot(door.OutwardNormal, sphereCenter - door.WorldCentroid); - if (dEye >= 0f || dC <= 0f) return false; // plane not between eye and center - float t = dEye / (dEye - dC); - var hit = eye + (sphereCenter - eye) * t; - - // is the ray-plane hit inside the door polygon? (2D point-in-polygon in the - // plane's basis — the seal only stamps pixels inside the fan) - if (!PointInPolygon(door, hit)) return false; - - var pc = Vector4.Transform(new Vector4(sphereCenter, 1f), viewProj); - var hc = Vector4.Transform(new Vector4(hit, 1f), viewProj); - if (pc.W <= 1e-6f || hc.W <= 1e-6f) return false; // behind the eye — no pixel - - float playerZ = pc.Z / pc.W; - float sealZ = hc.Z / hc.W; - // GL depth test default LESS/LEQUAL: smaller NDC z = nearer = passes. - depthPass = playerZ <= sealZ + 1e-4f; - return true; - } - - private static bool PointInPolygon(ExitDoor door, Vector3 worldPoint) - { - // build a 2D basis in the door plane - var n = door.OutwardNormal; - var u = Vector3.Normalize(Vector3.Cross(n, Vector3.UnitZ)); - if (u.LengthSquared() < 1e-6f) u = Vector3.UnitX; - var v = Vector3.Cross(n, u); - - Vector2 Project(Vector3 p) => new( - Vector3.Dot(p - door.WorldCentroid, u), - Vector3.Dot(p - door.WorldCentroid, v)); - - var pt = Project(worldPoint); - bool inside = false; - for (int i = 0, j = door.WorldVerts.Length - 1; i < door.WorldVerts.Length; j = i++) - { - var a = Project(door.WorldVerts[i]); - var b = Project(door.WorldVerts[j]); - if ((a.Y > pt.Y) != (b.Y > pt.Y) - && pt.X < (b.X - a.X) * (pt.Y - a.Y) / (b.Y - a.Y) + a.X) - inside = !inside; - } - return inside; - } - - private void DumpSteps(IEnumerable steps, Func? filter = null) - { - foreach (var st in steps) - { - if (filter is not null && !filter(st)) continue; - string root = st.RootCellId == 0 ? "OUTDOOR " : FormattableString.Invariant($"0x{st.RootCellId:X8}"); - string cone = st.ConeVisible ? "VIS " : "CULL"; - string stage = st.OutsideStage ? "outside" : "last "; - string depth = st.DepthCheckApplies ? (st.DepthPass ? "pass" : "FAIL") : "n/a "; - _out.WriteLine(FormattableString.Invariant( - $"step={st.Index,3} s={st.WalkS,6:F2} cS={st.CenterS,6:F2} eyeS={st.EyeS,6:F2} root={root} pCell=0x{st.PlayerParentCellId:X8} cone={cone} stage={stage} depth={depth} outPolys={st.OutsidePolys} flood={st.FloodCells}")); - } - } - - // ── the pins ──────────────────────────────────────────────────────── - - /// - /// Candidates 1–3 (cone level): per step of the exit walk, the player sphere must - /// survive the exact DrawDynamicsLast cone predicate. A failing step pins the - /// side-test / cone-tightness / incoherence family with its exact geometry. - /// - [Fact] - public void ExitWalk_PlayerStaysConeVisible_EveryStep() - { - var steps = RunExitWalk(); - if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); - - var failures = steps.FindAll(s => !s.ConeVisible); - if (failures.Count > 0) - { - _out.WriteLine($"--- {failures.Count} cone-CULLED steps ---"); - DumpSteps(failures); - } - Assert.True(failures.Count == 0, - $"{failures.Count}/{steps.Count} steps cone-cull the player (first at step {(failures.Count > 0 ? failures[0].Index : -1)}) — see output"); - } - - /// - /// Candidate 4 (depth level): on every step where the root is interior and the - /// cone admits the outdoor player, the player's fragments must also SURVIVE the - /// exit-portal SEAL's depth — otherwise DrawDynamicsLast paints nothing (the - /// vanish) and a straddling body is cut at the door plane (the clip). - /// - [Fact] - public void ExitWalk_PlayerSurvivesSealDepth_WhenConeVisible() - { - var steps = RunExitWalk(); - if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); - - var applicable = steps.FindAll(s => s.DepthCheckApplies); - _out.WriteLine($"depth check applies on {applicable.Count}/{steps.Count} steps"); - var failures = applicable.FindAll(s => !s.DepthPass); - if (failures.Count > 0) - { - _out.WriteLine($"--- {failures.Count} seal-depth-FAILED steps ---"); - DumpSteps(failures); - } - Assert.True(failures.Count == 0, - $"{failures.Count}/{applicable.Count} applicable steps z-fail the player against the exit-portal seal — " + - "dynamics drawn after the TRUE-depth seal cannot appear beyond the door plane (see output)"); - } - - /// - /// The straddle ("clipped") phase: while the player sphere crosses the door - /// plane under an interior root, it must be assigned to the OUTSIDE stage - /// (drawn pre-seal — for indoor-classified straddlers that is retail's - /// per-overlapped-shadow-cell dual draw, DrawBlock pc:430056-430064), or the - /// beyond-plane body half is cut at the plane by the seal. - /// - [Fact] - public void ExitWalk_StraddlingPlayerDrawsInOutsideStage() - { - var steps = RunExitWalk(); - if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); - - var straddling = steps.FindAll(s => - s.RootCellId != 0u && MathF.Abs(s.CenterS) < PlayerSphereRadius); - Assert.True(straddling.Count > 0, "walk produced no interior-root straddle steps — geometry changed?"); - var failures = straddling.FindAll(s => !s.OutsideStage); - if (failures.Count > 0) - { - _out.WriteLine($"--- {failures.Count} straddle steps NOT outside-stage ---"); - DumpSteps(failures); - } - Assert.True(failures.Count == 0, - $"{failures.Count}/{straddling.Count} straddle steps are not outside-stage-assigned — the seal clips the body at the door plane"); - } - - /// Full per-step table for the handoff doc. - [Fact] - [Trait("Purpose", "Diagnostic")] - public void Diagnostic_ExitWalk_PerStepTable() - { - var steps = RunExitWalk(); - if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); - DumpSteps(steps); - // transition summary - int firstOutPlayer = steps.FindIndex(s => s.CenterS > 0f); - int firstOutRoot = steps.FindIndex(s => s.RootCellId == 0u); - _out.WriteLine(FormattableString.Invariant( - $"player center exits at step {firstOutPlayer}; viewer root flips outdoor at step {firstOutRoot}; interior-root/outdoor-player window = {Math.Max(0, firstOutRoot - firstOutPlayer)} steps")); - } - - /// - /// Candidate 2 quantifier (synthetic): the INCOHERENT (root=interior, eye outside) - /// pair the healthy sweep never produces. Documents how the flood degrades if the - /// root ever lagged the eye: within PortalSideEpsilon the exit portal still - /// traverses; beyond it the side test culls and OutsideView goes EMPTY (which would - /// cull ALL outdoor content — terrain included, not just the player). - /// - [Fact] - [Trait("Purpose", "Diagnostic")] - public void Diagnostic_StaleRootWindow_EyeJustOutside() - { - var datDir = CornerFloodReplayTests.ResolveDatDir(); - if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); } - - using var dats = new DatCollection(datDir, DatAccessType.Read); - var cells = CornerFloodReplayTests.LoadBuilding(dats); - Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; - var exitCell = cells[ExitCellId]; - var door = FindExitDoor(exitCell); - - var out2d = Vector3.Normalize(new Vector3(door.OutwardNormal.X, door.OutwardNormal.Y, 0f)); - var eyeBase = new Vector3(door.WorldCentroid.X, door.WorldCentroid.Y, door.FloorZ + 1.8f); - var playerC = eyeBase + out2d * 1.5f; // player just outside, in front of the eye - - var clipFrame = ClipFrame.NoClip(); - foreach (float d in new[] { 0.005f, 0.02f, 0.05f, 0.10f, 0.25f }) - { - var eye = eyeBase + out2d * d; - var view = Matrix4x4.CreateLookAt(eye, eye + out2d, Vector3.UnitZ); - var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1280f / 720f, 0.1f, 5000f); - var viewProj = view * proj; - - var pv = PortalVisibilityBuilder.Build(exitCell, eye, lookup, viewProj); - var asm = ClipFrameAssembler.Assemble(clipFrame, pv); - var cone = ViewconeCuller.Build(asm, viewProj); - bool playerVisible = cone.SphereVisibleOutside(playerC, PlayerSphereRadius); - - _out.WriteLine(FormattableString.Invariant( - $"eye {d * 100,5:F1} cm OUTSIDE + root still 0x{ExitCellId:X8}: outPolys={pv.OutsideView.Polygons.Count} flood={pv.OrderedVisibleCells.Count} playerConeVisible={playerVisible}")); - } - } - - private sealed class TestCellSource(Func find) : - IRetailPViewCellSource - { - public LoadedCell? Find(uint cellId) => find(cellId); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index dbabad28..c4f8c5c3 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -1,140 +1,17 @@ -using System.Numerics; using System.Reflection; using AcDream.App.Composition; using AcDream.App.Rendering; -using AcDream.App.Rendering.Scene; using AcDream.App.Tests.Architecture; -using AcDream.Core.World; namespace AcDream.App.Tests.Rendering; public sealed class RetailPViewPassExecutorTests { - [Fact] - public void DrawInside_outdoor_executes_the_real_typed_sequence_without_interior_clear() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - LoadedCell root = OutdoorCellNode.Build(0xA9B40000u); - - renderer.DrawInside(Frame(root), executor); - - Assert.Equal( - [ - "begin", - "assemble", - "append-look-in-clips", - // FW4 slice 1: the clip-region publication moved below the - // walk block (a walk-rooted interior frame re-derives the - // outside-view slices from the walk's own views first, and - // the appended slots must join the same single publication). - "indoor-routing", - "prepare-clip:3", - "prepare-cells", - "diagnostics", - "terrain-clip", - "clear-routing", - "landscape-early", - "terrain-clip", - "clear-routing", - "landscape-late", - "unattached-particles-outdoor", - // #132: an OUTDOOR root does NOT drain at the stage boundary. - // The far prefix drains at the pre-punch barrier (retail - // DrawBuilding @0x0059F2A0 flushes before its portal-only - // far-Z pass), and the full drain runs after the dynamics - // pass, where the frame's opaque world depth is complete. - "indoor-routing", - "indoor-routing", - "landscape-alpha-farther", - "exit-mask", - "indoor-routing", - "opaque-shells", - "landscape-alpha", - "unattached-particles-interior", - ], - executor.Operations); - } - - [Fact] - public void DrawInside_interior_exit_flushes_before_clear_and_resets_borrowed_result() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - LoadedCell outdoor = OutdoorCellNode.Build(0xA9B40000u); - RetailPViewFrameResult borrowed = renderer.DrawInside(Frame(outdoor), executor); - Assert.Contains(outdoor.CellId, borrowed.DrawableCells); - - executor.Operations.Clear(); - LoadedCell interior = InteriorWithExit(0xA9B40100u); - RetailPViewFrameResult reused = renderer.DrawInside(Frame(interior), executor); - - Assert.Same(borrowed, reused); - Assert.DoesNotContain(outdoor.CellId, reused.DrawableCells); - Assert.Contains(interior.CellId, reused.DrawableCells); - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "landscape-early", - "landscape-late", - "unattached-particles-outdoor", - "landscape-alpha", - "interior-depth-clear", - "indoor-routing", - "exit-mask"); - } - - [Fact] - public void DrawInside_interior_without_an_outside_slice_skips_landscape_and_depth_clear() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - var root = new LoadedCell - { - CellId = 0xA9B40100u, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - }; - - renderer.DrawInside(Frame(root), executor); - - Assert.DoesNotContain("terrain-clip", executor.Operations); - Assert.DoesNotContain("landscape-early", executor.Operations); - Assert.DoesNotContain("landscape-late", executor.Operations); - Assert.DoesNotContain("landscape-alpha", executor.Operations); - Assert.DoesNotContain("interior-depth-clear", executor.Operations); - } - - [Fact] - public void DrawInside_interior_without_an_outside_slice_still_draws_interior_unattached_particles() - { - // Repro (Sanctuary middle cell, looking north): spell ground effects - // vanished whenever no exit portal was in view, because unattached - // emitters submitted once PER outside slice under that slice's - // hardware clip slot — zero slices meant zero submissions. Retail - // draws such an emitter during its owner cell's walk turn - // (ShouldDrawParticles @0x0050FE60) and never clips it to a view. - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - var root = new LoadedCell - { - CellId = 0xA9B40100u, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - }; - - renderer.DrawInside(Frame(root), executor); - - Assert.Contains("unattached-particles-interior", executor.Operations); - Assert.DoesNotContain( - "unattached-particles-outdoor", - executor.Operations); - } - [Fact] public void Particle_classifications_reset_before_an_empty_following_frame() { var classifications = new RetailPViewParticleClassifications(); - classifications.ReplaceOutdoor([Entity(7u)]); + classifications.ReplaceOutdoor(new HashSet { 7u }); classifications.Visible.Add(8u); classifications.Dynamics.Add(9u); @@ -145,159 +22,6 @@ public sealed class RetailPViewPassExecutorTests Assert.Empty(classifications.Dynamics); } - [Fact] - public void DrawInside_real_fixtures_reach_entity_particle_and_transparent_shell_operations() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor { HasTransparentShells = true }; - LoadedCell interior = InteriorWithExit(0xA9B40100u); - WorldEntity cellStatic = Entity(10u, parentCellId: interior.CellId); - WorldEntity dynamic = Entity( - 11u, - serverGuid: 0x80000011u, - parentCellId: interior.CellId); - - renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor); - - Assert.Contains("opaque-shells", executor.Operations); - Assert.Contains("transparent-shells-ordered", executor.Operations); - Assert.Contains("entity-bucket", executor.Operations); - Assert.Contains("cell-particles", executor.Operations); - Assert.Contains("dynamics-particles", executor.Operations); - } - - [Fact] - public void DrawInside_referee_records_the_exact_routed_entity_buckets() - { - var oracle = new CurrentRenderSceneOracle(); - var renderer = new RetailPViewRenderer(oracle); - using var executor = new RecordingExecutor(); - LoadedCell interior = InteriorWithExit(0xA9B40100u); - WorldEntity cellStatic = Entity(20u, parentCellId: interior.CellId); - WorldEntity dynamic = Entity( - 21u, - serverGuid: 0x80000021u, - parentCellId: interior.CellId); - - renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor); - - Assert.Equal(1uL, oracle.Snapshot.CompletedPViewFrameSequence); - Assert.Equal(2, oracle.Snapshot.PViewCandidateCount); - Assert.Collection( - oracle.PViewCandidates, - candidate => - { - Assert.Equal(CurrentRenderPViewRoute.CellStatic, candidate.Route); - Assert.Equal(cellStatic.Id, candidate.Projection.EntityId); - }, - candidate => - { - Assert.Equal(CurrentRenderPViewRoute.DynamicLast, candidate.Route); - Assert.Equal(dynamic.Id, candidate.Projection.EntityId); - }); - } - - [Fact] - public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - LoadedCell root = InteriorWithExit(0xA9B40100u); - root.BuildingId = 1u; - LoadedCell[] building = NearbyTwoCellBuilding(); - - renderer.DrawInside( - Frame( - root, - nearbyBuildingCells: building, - additionalCells: building), - executor); - - Assert.Contains("look-in-punch", executor.Operations); - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "landscape-early", - "unattached-particles-outdoor", - "landscape-static-particles", - "landscape-alpha-farther", - "look-in-punch", - "landscape-late", - "landscape-alpha", - "interior-depth-clear"); - } - - [Fact] - public void DrawInside_repaints_the_exterior_building_shell_after_its_look_in() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - LoadedCell root = InteriorWithExit(0xA9B40100u); - root.BuildingId = 1u; - LoadedCell[] building = NearbyTwoCellBuilding(); - WorldEntity exteriorShell = Entity( - 0x700u, - isBuildingShell: true, - buildingShellAnchorCellId: building[0].CellId); - - renderer.DrawInside( - Frame( - root, - [exteriorShell], - nearbyBuildingCells: building, - additionalCells: building), - executor); - - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "landscape-early", - "look-in-punch", - "landscape-building-shell", - "landscape-late"); - } - - [Fact] - public void DrawInside_pairs_each_look_in_with_only_its_own_shell_and_alpha_barrier() - { - var renderer = new RetailPViewRenderer(); - using var executor = new RecordingExecutor(); - LoadedCell root = InteriorWithExit(0xA9B40100u); - root.BuildingId = 1u; - LoadedCell[] first = NearbyTwoCellBuilding(); - LoadedCell[] second = NearbyTwoCellBuilding( - 0xA9B40172u, - 0xA9B40173u, - buildingId: 3u); - LoadedCell[] buildings = [.. first, .. second]; - WorldEntity[] shells = - [ - Entity( - 0x700u, - isBuildingShell: true, - buildingShellAnchorCellId: first[0].CellId), - Entity( - 0x701u, - isBuildingShell: true, - buildingShellAnchorCellId: second[0].CellId), - ]; - - renderer.DrawInside( - Frame( - root, - shells, - nearbyBuildingCells: buildings, - additionalCells: buildings), - executor); - - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "look-in-punch", - "landscape-building-shell", - "landscape-static-particles", - "landscape-alpha-farther", - "look-in-punch", - "landscape-building-shell"); - } - [Fact] public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner() { @@ -330,7 +54,8 @@ public sealed class RetailPViewPassExecutorTests nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0); MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod( - nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!; + "DrawWalkTerrainSlice", + BindingFlags.Instance | BindingFlags.NonPublic)!; IReadOnlyList landscapeCalls = CompiledCallGraph.Read(landscape); int diagnosticsBegin = RequiredCallIndex( landscapeCalls, @@ -350,7 +75,7 @@ public sealed class RetailPViewPassExecutorTests } [Fact] - public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag() + public void Frame_composition_constructs_one_walk_executor() { MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod( "ComposeCore", @@ -377,368 +102,15 @@ public sealed class RetailPViewPassExecutorTests && call.Target.Name == ".ctor"); } - private static RetailPViewFrameInput Frame( - LoadedCell root, - IReadOnlyList? entities = null, - IReadOnlyList? nearbyBuildingCells = null, - IReadOnlyList? additionalCells = null) - { - var cells = new Dictionary { [root.CellId] = root }; - if (additionalCells is not null) - { - foreach (LoadedCell cell in additionalCells) - cells[cell.CellId] = cell; - } - - var entries = new List<( - uint LandblockId, - Vector3 AabbMin, - Vector3 AabbMax, - IReadOnlyList Entities, - IReadOnlyDictionary? AnimatedById)>(); - if (entities is not null) - { - entries.Add(( - root.CellId & 0xFFFF0000u, - Vector3.Zero, - Vector3.Zero, - entities, - null)); - } - - return new RetailPViewFrameInput().Reset( - root, - nearbyBuildingCells, - Vector3.Zero, - TestCamera.ViewProjection, - new DictionaryCellSource(cells), - new TestCamera(), - Vector3.Zero, - frustum: null, - playerLandblockId: root.CellId & 0xFFFF0000u, - animatedEntityIds: null, - renderCenterLbX: 0, - renderCenterLbY: 0, - renderRadius: 1, - landblockEntries: entries, - renderSky: true, - renderWeather: true, - dayFraction: 0f, - activeDayGroup: null, - skyKeyframe: default, - environOverrideActive: false, - viewerCellId: root.CellId, - playerCellId: root.CellId, - playerViewPosition: Vector3.Zero, - cameraView: TestCamera.ViewMatrix, - cameraCellResolution: CameraCellResolution.None); - } - - private static LoadedCell InteriorWithExit(uint cellId) - { - var cell = new LoadedCell - { - CellId = cellId, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - Portals = [new CellPortalInfo(0xFFFF, 0, 0, 0)], - }; - cell.PortalPolygons.Add( - [ - new Vector3(-1f, -1f, -2f), - new Vector3(1f, -1f, -2f), - new Vector3(1f, 1f, -2f), - new Vector3(-1f, 1f, -2f), - ]); - return cell; - } - - private static LoadedCell[] NearbyTwoCellBuilding( - uint vestibuleId = 0xA9B40170u, - uint roomId = 0xA9B40171u, - uint buildingId = 2u) - { - var vestibule = new LoadedCell - { - CellId = vestibuleId, - BuildingId = buildingId, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - Portals = - [ - new CellPortalInfo(0xFFFF, 0, 0, 0), - new CellPortalInfo((ushort)(roomId & 0xFFFFu), 1, 0, 0), - ], - ClipPlanes = - [ - new PortalClipPlane - { - Normal = new Vector3(0, 0, 1), - D = 3f, - InsideSide = 1, - }, - ], - }; - vestibule.PortalPolygons.Add( - [ - new Vector3(-0.5f, -0.5f, -2f), - new Vector3(0.5f, -0.5f, -2f), - new Vector3(0.5f, 0.5f, -2f), - new Vector3(-0.5f, 0.5f, -2f), - ]); - vestibule.PortalPolygons.Add( - [ - new Vector3(-0.6f, -0.6f, -4f), - new Vector3(0.6f, -0.6f, -4f), - new Vector3(0.6f, 0.6f, -4f), - new Vector3(-0.6f, 0.6f, -4f), - ]); - - var room = new LoadedCell - { - CellId = roomId, - BuildingId = buildingId, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - Portals = - [ - new CellPortalInfo( - (ushort)(vestibuleId & 0xFFFFu), - 0, - 0, - 1), - ], - }; - room.PortalPolygons.Add( - [ - new Vector3(-0.6f, -0.6f, -4f), - new Vector3(0.6f, -0.6f, -4f), - new Vector3(0.6f, 0.6f, -4f), - new Vector3(-0.6f, 0.6f, -4f), - ]); - return [vestibule, room]; - } - - private static WorldEntity Entity( - uint id, - uint serverGuid = 0, - uint? parentCellId = null, - bool isBuildingShell = false, - uint? buildingShellAnchorCellId = null) => new() - { - Id = id, - ServerGuid = serverGuid, - SourceGfxObjOrSetupId = 0, - Position = Vector3.Zero, - Rotation = Quaternion.Identity, - MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)], - ParentCellId = parentCellId, - IsBuildingShell = isBuildingShell, - BuildingShellAnchorCellId = buildingShellAnchorCellId, - }; - - private static void AssertAppearsInOrder(string source, params string[] needles) - { - int cursor = -1; - foreach (string needle in needles) - { - int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); - Assert.True(next > cursor, $"Missing or out-of-order fragment: {needle}"); - cursor = next; - } - } - private static int RequiredCallIndex( IReadOnlyList calls, Type declaringType, string methodName) { int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName); - Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}"); + Assert.True( + index >= 0, + $"Expected call to {declaringType.Name}.{methodName}."); return index; } - - private sealed class DictionaryCellSource( - IReadOnlyDictionary cells) : IRetailPViewCellSource - { - public LoadedCell? Find(uint cellId) => - cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null; - } - - private sealed class TestCamera : ICamera - { - public static Matrix4x4 ViewMatrix { get; } = Matrix4x4.CreateLookAt( - Vector3.Zero, - new Vector3(0, 0, -1), - Vector3.UnitY); - - public static Matrix4x4 ViewProjection { get; } = ViewMatrix - * Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f); - - public Matrix4x4 View => ViewMatrix; - public Matrix4x4 Projection { get; } = - Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f); - public float Aspect { get; set; } = 1f; - } - - private sealed class RecordingExecutor : - IRetailPViewPassExecutor, - IRenderFrameEntityPassExecutor, - IDisposable - { - public void AbortFrame() => Operations.Add("abort"); - - private readonly ClipFrame _clipFrame = ClipFrame.NoClip(); - - public List Operations { get; } = []; - public bool HasTransparentShells { get; init; } - - public void BeginFrame() => Operations.Add("begin"); - public ClipFrameAssembly AssembleClipFrame( - PortalVisibilityFrame portalFrame, - ClipFrameAssembly reuseAssembly) - { - Operations.Add("assemble"); - return ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly); - } - - public void AppendLookInClipFrames( - IReadOnlyList lookInFrames, - ClipFrameAssembly assembly) - { - Operations.Add("append-look-in-clips"); - ClipFrameAssembler.AppendLookInFrames( - _clipFrame, - lookInFrames, - assembly); - } - - public void PrepareClipFrame(int terrainUploadCount) => - Operations.Add($"prepare-clip:{terrainUploadCount}"); - - public void SetTerrainClip(ReadOnlySpan planes) => - Operations.Add("terrain-clip"); - - public void ClearClipRouting() => Operations.Add("clear-routing"); - public void UseIndoorMembershipOnlyRouting() => Operations.Add("indoor-routing"); - public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice) => - Operations.Add($"cell-portal-routing:{cellId:X8}:{slice.Slot}"); - public void PrepareCellBatches(RetailPViewFrameInput frame, HashSet visibleCellIds) => - Operations.Add("prepare-cells"); - public void DrawOpaqueCellShells(HashSet cellIds) => Operations.Add("opaque-shells"); - public bool CellHasTransparentShell(uint cellId) => HasTransparentShells; - public void DrawTransparentCellShells(HashSet cellIds) => Operations.Add("transparent-shells"); - public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => - Operations.Add("transparent-shells-ordered"); - public void DrawEntityBucket( - RetailPViewFrameInput frame, - IReadOnlyList entities, - HashSet? visibleCellIds) => Operations.Add("entity-bucket"); - public void EmitClipRouteProbe( - ClipFrameAssembly clipAssembly, - ClipViewSlice slice, - int sliceIndex) => Operations.Add("clip-probe"); - public void DrawLandscapeSlice( - RetailPViewFrameInput frame, - RetailPViewLandscapeSliceContext context) - { - Operations.Add("landscape-early"); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView view = request.View; - DrawEntityRoute( - frame.Camera, - in view, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - } - - public void DrawLandscapeSliceLate( - RetailPViewFrameInput frame, - RetailPViewLandscapeLateSliceContext context) - { - Operations.Add("landscape-late"); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView view = request.View; - DrawEntityRoute( - frame.Camera, - in view, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - } - - public void DrawLandscapeStaticParticles( - RetailPViewFrameInput frame, - RetailPViewLandscapeStaticParticleContext context) => - Operations.Add("landscape-static-particles"); - public void DrawLandscapeBuildingShellSlice( - RetailPViewFrameInput frame, - RetailPViewLandscapeBuildingShellSliceContext context) - { - Operations.Add("landscape-building-shell"); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView view = request.View; - DrawEntityRoute( - frame.Camera, - in view, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - } - public void ClearInteriorDepth() => Operations.Add("interior-depth-clear"); - public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask"); - public void DrawLookInPortalPunch( - RetailPViewFrameInput frame, - RetailPViewCellSliceContext context, - int portalIndex) => Operations.Add("look-in-punch"); - public void DrawUnattachedSceneParticles( - RetailPViewFrameInput frame, - bool outdoorCells) => Operations.Add( - outdoorCells - ? "unattached-particles-outdoor" - : "unattached-particles-interior"); - public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha"); - public void FlushLandscapeAlphaFartherThan(float minViewerDistance) => - Operations.Add("landscape-alpha-farther"); - public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles"); - public void DrawDynamicsParticles( - RetailPViewFrameInput frame, - IReadOnlySet ownerIds) => - Operations.Add("dynamics-particles"); - public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics"); - - public void BeginEntityFrame(in RenderFrameView view) => - Operations.Add("entity-frame-begin"); - - public bool DrawEntityRoute( - ICamera camera, - in RenderFrameView view, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId, - uint tupleLandblockId) - { - Operations.Add( - $"entity-route:{route}:{routeIndex}:{cellId:X8}"); - return true; - } - - public void CompleteEntityFrame(in RenderFrameView view) => - Operations.Add("entity-frame-complete"); - - public void AbortEntityFrame() => - Operations.Add("entity-frame-abort"); - - public void Dispose() => _clipFrame.Dispose(); - } } diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index ed7400f3..e4825f7a 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -738,15 +738,14 @@ public sealed class WorldSceneRendererTests _calls = calls; // Distinct flood-only vs in-view sets: 0x01010003 is a look-in // cell that is drawn but never part of the main flood. - var interiorPartition = new InteriorEntityPartition.Result(); _interiorResult = new RetailPViewFrameResult().Reset( new PortalVisibilityFrame(), new ClipFrameAssembly(), [0x01010001u], [0x01010001u, 0x01010003u], - RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition), - RetailPViewRenderer.LegacySourceCounts(interiorPartition), - interiorPartition); + default, + default, + diagnosticPartition: null); var outdoorPortalFrame = new PortalVisibilityFrame(); outdoorPortalFrame.OutsideView.Add(new ViewPolygon( [ @@ -759,7 +758,10 @@ public sealed class WorldSceneRendererTests outdoorPortalFrame, ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame), [], - new InteriorEntityPartition.Result()); + [], + default, + default, + diagnosticPartition: null); } public IReadOnlySet OutdoorSceneParticleEntityIds { get; } =