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