From 1b59eae4289a3b41e0ebc2e0244163905b56a8d9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 30 Aug 2026 14:40:24 +0200 Subject: [PATCH] feat(render) Campaign FW3.2b-2 step 1: the production leaf adapter Additive, nothing invokes it yet (the cutover flip is step 2): - WalkProductionLeafRenderer + RetailPViewPassExecutor.WalkLeaf: the walk driver's leaf turns over the SAME executor renderers the packed path uses today - DrawWalkSky (the per-slice sky block looped under one driver turn), DrawWalkTerrainSlice (the terrain block of DrawLandscapeSlice), per-cell shells via EnvCellRenderer, DrawWalkPunchFan (PortalDepthMaskRenderer far-Z, +ShellDrawLiftZ matching today's DrawPortalDepthWrite until FW3.3 retires it), the alpha barrier via FlushLandscapeAlphaFartherThan, and caller-supplied clear/seal actions (the renderer owns the pass scope). The cutover changes ORDER, never leaf mechanics. - Punch fans now carry the ACTIVE VIEW INDEX end to end (retail pins building_view = Render::portal_view_num @0x0059f3bf for the whole two-pass walk; the fan clips by that view's slice planes): PortalPassSink.ActiveViewIndex -> IWalkEventSink.OnPunchGeometry -> IWalkFrameLeafRenderer.DrawPunchFan. - The FW3.2b-2 rooting design is recorded in the plan (dual-compute split, LookInObject route filtered to dynamics, consumer re-pointing, gate list). Suites: full Release build 0 warnings; Walk lane green; hermetic green. Co-Authored-By: Claude Fable 5 --- .../RetailPViewPassExecutor.WalkLeaf.cs | 202 ++++++++++++++++++ .../Rendering/RetailPViewPassExecutor.cs | 2 +- .../Rendering/Walk/RetailFrameWalk.cs | 11 +- src/AcDream.App/Rendering/Walk/WalkEvents.cs | 10 +- .../Rendering/Walk/WalkFrameDriver.cs | 13 +- .../Rendering/Walk/WalkFrameDriverTests.cs | 6 +- 6 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs new file mode 100644 index 00000000..47407e10 --- /dev/null +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs @@ -0,0 +1,202 @@ +using System.Numerics; +using AcDream.App.Rendering.Walk; +using AcDream.Core.Vfx; + +namespace AcDream.App.Rendering; + +/// Campaign FW3.2b-2 (step 1): the walk driver's leaf draws over +/// this executor's renderers. Same leaf mechanics as the packed slice +/// methods — the cutover changes ORDER only. +internal sealed partial class RetailPViewPassExecutor +{ + /// The walk's single sky turn: retail draws GameSky once + /// inside LScape::draw, clipped by the active views; looping + /// today's per-slice scissor + terrain-clip block reproduces today's + /// pixels while the driver sees ONE turn. + internal void DrawWalkSky( + RetailPViewFrameInput frame, ClipFrameAssembly clipAssembly) + { + if (!frame.RenderSky) + return; + ReadOnlySpan slices = clipAssembly.OutsideViewSlices; + for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++) + { + ClipViewSlice slice = slices[sliceIndex]; + bool scissor = BeginDoorwayScissor(slice.NdcAabb); + _surface.BindTerrainClip(); + EnableClipDistances(); + _sky?.RenderSky( + frame.Camera, + frame.CameraWorldPosition, + frame.DayFraction, + frame.ActiveDayGroup, + frame.SkyKeyframe, + frame.EnvironOverrideActive); + DisableClipDistances(); + if (_particles is not null && _particleRenderer is not null) + { + _particleRenderer.Draw( + frame.Camera, + frame.CameraWorldPosition, + ParticleRenderPass.SkyPreScene); + } + if (scissor) + _surface.EndScissor(); + } + } + + /// The walk's per-slice terrain turn — the terrain block of + /// DrawLandscapeSlice (scissor + terrain clip + slice planes + + /// NDC AABB), without the sky/entity halves the walk owns separately. + internal void DrawWalkTerrainSlice( + RetailPViewFrameInput frame, ClipFrameAssembly clipAssembly, int sliceIndex) + { + ReadOnlySpan slices = clipAssembly.OutsideViewSlices; + if ((uint)sliceIndex >= (uint)slices.Length) + { + throw new ArgumentOutOfRangeException( + nameof(sliceIndex), sliceIndex, + $"walk terrain turn: slice {sliceIndex} of {slices.Length}"); + } + ClipViewSlice slice = slices[sliceIndex]; + bool scissor = BeginDoorwayScissor(slice.NdcAabb); + _surface.BindTerrainClip(); + EnableClipDistances(); + _terrainDiagnostics.Begin(); + _terrain?.Draw( + frame.Camera, + frame.Frustum, + neverCullLandblockId: frame.PlayerLandblockId, + clipPlanes: slice.Planes, + ndcClipAabb: slice.NdcAabb); + _terrainDiagnostics.Complete(); + DisableClipDistances(); + if (scissor) + _surface.EndScissor(); + } + + /// The walk's punch-fan turn — DrawPortalPolyInternal + /// @0x0059bc90's far-Z punch through PortalDepthMaskRenderer, + /// clipped by the pinned view's slice planes (retail + /// building_view @0x0059f3bf). The +ShellDrawLiftZ + /// matches today's DrawPortalDepthWrite and retires with + /// FW3.3. + internal void DrawWalkPunchFan( + RetailPViewFrameInput frame, + ClipFrameAssembly clipAssembly, + WalkPolygon worldPolygon, + int activeViewIndex) + { + if (_portalDepthMask is null) + return; + Vector3[] vertices = worldPolygon.Vertices; + if (vertices.Length < 3) + return; + ReadOnlySpan slices = clipAssembly.OutsideViewSlices; + ReadOnlySpan planes = (uint)activeViewIndex < (uint)slices.Length + ? slices[activeViewIndex].Planes + : default; + + Span world = stackalloc Vector3[32]; + int count = Math.Min(vertices.Length, world.Length); + for (int vertex = 0; vertex < count; vertex++) + { + world[vertex] = vertices[vertex]; + world[vertex].Z += PortalVisibilityBuilder.ShellDrawLiftZ; + } + _portalDepthMask.DrawDepthFan( + world[..count], + frame.ViewProjection, + planes, + forceFarZ: true); + } +} + +/// +/// Campaign FW stage FW3.2b-2 (step 1, additive): the production +/// over this executor's leaf renderers. +/// Constructed per frame by the FW3.2b-2 rooting (plan §FW3 "FW3.2b-2 — +/// the production rooting"); nothing invokes it until the static cutover +/// flips. Each member maps a turn onto the +/// SAME leaf calls the packed path uses today, so the cutover changes +/// ORDER, never the leaf mechanics: +/// +/// +/// → the sky block of DrawLandscapeSlice, +/// looped over the active slices internally (retail draws GameSky once +/// inside LScape::draw clipped by the active views; the per-slice +/// scissor+clip here reproduces today's pixels while the driver still sees +/// ONE sky turn). +/// → the terrain block of +/// DrawLandscapeSlice (scissor + terrain clip + slice planes). +/// EnvCellRenderer opaque + +/// transparent-ordered for ONE cell (retail DrawEnvCell +/// @0x0059f170 draws per cell at its flood turn). +/// PortalDepthMaskRenderer.DrawDepthFan +/// with forceFarZ, clipped by the pinned view's slice planes +/// (retail building_view @0x0059f3bf). The +ShellDrawLiftZ +/// on the fan verts matches today's DrawPortalDepthWrite and +/// retires with FW3.3. +/// / → +/// caller-supplied actions (the renderer owns the pass scope and the +/// root-flood seal iteration; the adapter only provides the turns). +/// FlushLandscapeAlphaFartherThan +/// (the DrawBuilding barrier; retail's flush-all +/// FlushAlphaList(0f) @0x0059f30b is the FW4 adjudication +/// candidate recorded on the driver's interface). +/// +/// +internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer +{ + private readonly RetailPViewPassExecutor _passes; + private readonly RetailPViewFrameInput _frame; + private readonly ClipFrameAssembly _clipAssembly; + private readonly Action _clearInteriorDepth; + private readonly Action _drawExitSeals; + private readonly HashSet _singleCellScratch = new(); + private readonly List _singleCellListScratch = new(); + + internal WalkProductionLeafRenderer( + RetailPViewPassExecutor passes, + RetailPViewFrameInput frame, + ClipFrameAssembly clipAssembly, + Action clearInteriorDepth, + Action drawExitSeals) + { + _passes = passes ?? throw new ArgumentNullException(nameof(passes)); + _frame = frame ?? throw new ArgumentNullException(nameof(frame)); + _clipAssembly = clipAssembly; + _clearInteriorDepth = clearInteriorDepth + ?? throw new ArgumentNullException(nameof(clearInteriorDepth)); + _drawExitSeals = drawExitSeals + ?? throw new ArgumentNullException(nameof(drawExitSeals)); + } + + public void DrawSky() => _passes.DrawWalkSky(_frame, _clipAssembly); + + public void DrawTerrainSlice(int sliceIndex) => + _passes.DrawWalkTerrainSlice(_frame, _clipAssembly, sliceIndex); + + public void DrawCellShell(uint cellId) + { + _singleCellScratch.Clear(); + _singleCellScratch.Add(cellId); + _passes.DrawOpaqueCellShells(_singleCellScratch); + if (_passes.CellHasTransparentShell(cellId)) + { + _singleCellListScratch.Clear(); + _singleCellListScratch.Add(cellId); + _passes.DrawTransparentCellShellsOrdered(_singleCellListScratch); + } + } + + public void ClearInteriorDepth() => _clearInteriorDepth(); + + public void DrawExitSeals() => _drawExitSeals(); + + public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) => + _passes.DrawWalkPunchFan(_frame, _clipAssembly, worldPolygon, activeViewIndex); + + public void AlphaBarrier(float viewerDistance) => + _passes.FlushLandscapeAlphaFartherThan(viewerDistance); +} diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 037c9f17..9477567c 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -114,7 +114,7 @@ internal interface IRenderFrameEntityPassExecutor /// is written once; the four places it touches graphics state directly are owned /// by . /// -internal sealed class RetailPViewPassExecutor : +internal sealed partial class RetailPViewPassExecutor : IRetailPViewPassExecutor, IRenderFrameEntityPassExecutor, IOutdoorSceneParticleOwnerSource diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs index 767431dd..11a13f3f 100644 --- a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs +++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs @@ -212,6 +212,10 @@ public sealed class RetailFrameWalk Vector3 viewpoint = ctx.ViewpointInBuilding(building); for (int v = 0; v < viewCount; v++) { + // Retail pins building_view = the CURRENT view for the whole + // two-pass walk (DrawMeshInternal @0x0059f3bf) — the punch fans + // it emits clip against THAT view, so the sink carries the index. + passSink.ActiveViewIndex = v; ctx.SetActiveView(activeViews, v); WalkBuildingPortals.BuildDrawPortalsOnly( bsp, 1, viewpoint, @@ -257,6 +261,11 @@ public sealed class RetailFrameWalk private sealed class PortalPassSink(WalkBuilding building, IWalkEventSink sink) : WalkBuildingPortals.IWalkPortalPassSink { + /// The view the two-pass walk is currently pinned to + /// (retail building_view = Render::portal_view_num + /// @0x0059f3bf) — punch fans clip against THIS view's slice. + public int ActiveViewIndex; + public void OnPunch(WalkPolygon polygon) { // The far-Z punch is a depth-only GPU submission (FW2's ordered @@ -264,7 +273,7 @@ public sealed class RetailFrameWalk // FW3.2b-1): forward to the richer sink so a driver can flush + // draw the punch fan — building-local space, world transform is // the driver's job (WalkProductionFrameContext-style lookup). - sink.OnPunchGeometry(building, polygon); + sink.OnPunchGeometry(building, polygon, ActiveViewIndex); } public void OnDrawCells(WalkPView pview) diff --git a/src/AcDream.App/Rendering/Walk/WalkEvents.cs b/src/AcDream.App/Rendering/Walk/WalkEvents.cs index 854710a9..53517ffb 100644 --- a/src/AcDream.App/Rendering/Walk/WalkEvents.cs +++ b/src/AcDream.App/Rendering/Walk/WalkEvents.cs @@ -124,7 +124,15 @@ public interface IWalkEventSink /// stayed a no-op through FW1/FW2 for exactly that reason). Default /// no-op. /// - void OnPunchGeometry(WalkBuilding building, WalkPolygon polygon) { } + /// The building whose portal walk emitted the + /// punch. + /// The portal polygon, building-local. + /// The active-view index the two-pass walk + /// is pinned to (retail building_view @0x0059f3bf) — the punch + /// fan clips against THIS view's slice planes in production. + void OnPunchGeometry( + WalkBuilding building, WalkPolygon polygon, int activeViewIndex) + { } /// /// Fires once per interior root, at the point PView::DrawCells diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs index 6815f0c7..d7c4d28f 100644 --- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs +++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs @@ -152,8 +152,11 @@ internal interface IWalkFrameLeafRenderer /// the punch (a preceding cell's/building's contents — never this /// building's OWN shell, which retail draws only after the whole portal /// walk completes; see 's type doc comment) - /// reaches the GPU first. - void DrawPunchFan(WalkPolygon worldPolygon); + /// reaches the GPU first. is the view + /// the emitting two-pass walk was pinned to (retail + /// building_view = Render::portal_view_num @0x0059f3bf) — + /// production clips the fan by that view's slice planes. + void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex); /// RetailAlphaQueue.FlushFartherThan's DrawBuilding /// barrier — retail's own call site is @@ -424,7 +427,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection); } - void IWalkEventSink.OnPunchGeometry(WalkBuilding building, WalkPolygon polygon) + void IWalkEventSink.OnPunchGeometry( + WalkBuilding building, WalkPolygon polygon, int activeViewIndex) { ArgumentNullException.ThrowIfNull(building); ArgumentNullException.ThrowIfNull(polygon); @@ -432,7 +436,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink FlushIfNonEmpty(); Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building); - _leafRenderer.DrawPunchFan(TransformToWorld(polygon, worldTransform)); + _leafRenderer.DrawPunchFan( + TransformToWorld(polygon, worldTransform), activeViewIndex); } void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList cells) diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs index c6bdfdba..3045f37e 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs @@ -53,10 +53,10 @@ public sealed class WalkFrameDriverTests public void DrawExitSeals() => log.Add("SEALS"); - public void DrawPunchFan(WalkPolygon worldPolygon) + public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { Punches.Add(worldPolygon); - log.Add($"PUNCH:{worldPolygon.Vertices.Length}"); + log.Add($"PUNCH:{worldPolygon.Vertices.Length}@v{activeViewIndex}"); } public void AlphaBarrier(float viewerDistance) => log.Add($"ALPHA:{viewerDistance:F2}"); @@ -406,7 +406,7 @@ public sealed class WalkFrameDriverTests driver.EndFrame(); Assert.Equal( - new[] { "ALPHA:12.50", "PUNCH:4", "SHELL:00000104", "FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell" }, + new[] { "ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104", "FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell" }, log); // The punch polygon reached the leaf renderer in WORLD space: the