feat(render) Campaign FW1: the frame-walk composition root
RetailFrameWalk composes the ported machinery into retail frame shapes: SmartBox::RenderNormalMode rooting (camera-cell low word < 0x100), PView::DrawInside + the DrawCells event half (interior pview, the traces pv=009d4a80), LScape::draw (per-view visibility, blocks far-to-near, per-block cells far-to-near, buildings at their cell turn) and RenderDeviceD3D::DrawBuilding (BLD at entry before the degrade check; two-pass BSP portal walk per active view on the outdoor pview, the traces pv=009d4b08). WalkLandscape ports the block grid + draw_check_blocks/landcell_check visibility (192 m/24 m pitch, viewer-relative, union across views, never downgrade). Six composition tests: outdoor far-to-near emission, degrade-entry event, interior ov=0/ov=1 shapes, rooting, complete view unwind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f05b93b5b5
commit
4b401a08ed
3 changed files with 638 additions and 0 deletions
194
src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
Normal file
194
src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Walk;
|
||||
|
||||
/// <summary>The composition context: the cell/building halves plus the
|
||||
/// frame-level state retail keeps in globals (the CY near plane from
|
||||
/// <c>Render::update_viewpoint</c>, and the active-view installation the
|
||||
/// building clip consumes).</summary>
|
||||
public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
||||
{
|
||||
WalkPlane CyPlane { get; }
|
||||
|
||||
/// <summary><c>Render::set_view</c> at the frame level: install view
|
||||
/// <paramref name="index"/> of <paramref name="views"/> as the active
|
||||
/// clip context for subsequent building-polygon clips.</summary>
|
||||
void SetActiveView(WalkPortalView views, int index);
|
||||
|
||||
float ViewportWidth { get; }
|
||||
float ViewportHeight { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW1 — the frame walk root, composing the ported machinery into
|
||||
/// retail's frame shapes (model:
|
||||
/// docs/research/2026-08-30-fw-walk-pseudocode.md §1, §3-§5; oracle:
|
||||
/// docs/research/2026-08-30-fw-walk-oracle/README.md):
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>Rooting (<c>SmartBox::RenderNormalMode</c> @0x00453aa0): the
|
||||
/// CAMERA cell's low word < 0x100 → outdoor root (default full-screen
|
||||
/// view + the landscape walk); otherwise → <c>DrawInside</c>.</item>
|
||||
/// <item>Outdoor (<c>LScape::draw</c> @0x00506330): visibility per view,
|
||||
/// blocks far-to-near, per-block cells far-to-near, buildings at their
|
||||
/// cell's turn with the two-pass portal machinery (punches + look-ins on
|
||||
/// the OUTDOOR pview — the traces' <c>pv=009d4b08 ov=0</c>).</item>
|
||||
/// <item>Interior (<c>PView::DrawInside</c> @0x005a5860 +
|
||||
/// <c>DrawCells</c> @0x005a4840): the flood on the INTERIOR pview
|
||||
/// (<c>pv=009d4a80</c>), then the landscape drawn THROUGH the exit views
|
||||
/// when any survive (<c>ov>0</c>).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Two PView instances exist exactly as the live traces showed.
|
||||
/// </summary>
|
||||
public sealed class RetailFrameWalk
|
||||
{
|
||||
private readonly WalkPView _interiorPView = new();
|
||||
private readonly WalkPView _outdoorPView = new();
|
||||
private readonly WalkPortalView _defaultView = new();
|
||||
|
||||
public WalkPView InteriorPView => _interiorPView;
|
||||
public WalkPView OutdoorPView => _outdoorPView;
|
||||
|
||||
/// <summary>The per-frame root (<c>SmartBox::RenderNormalMode</c>).
|
||||
/// <paramref name="cameraCell"/> may be null only when the camera is
|
||||
/// outdoors.</summary>
|
||||
public void WalkFrame(
|
||||
uint cameraCellId, WalkCell? cameraCell, WalkLandscape landscape,
|
||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
if ((cameraCellId & 0xFFFF) < 0x100)
|
||||
{
|
||||
// Render::set_default_view @0x0054ef50: the full-screen quad view.
|
||||
_defaultView.ResetForPush();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
_defaultView, ctx.Rays, ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||||
DrawLandscape(landscape, _defaultView, ctx, sink);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawInside(
|
||||
cameraCell ?? throw new ArgumentNullException(nameof(cameraCell)),
|
||||
landscape, ctx, sink);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary><c>PView::DrawInside</c> + the event half of
|
||||
/// <c>DrawCells</c>. The geometry/object passes emit no walk events.</summary>
|
||||
public void DrawInside(
|
||||
WalkCell cell, WalkLandscape landscape,
|
||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
sink.Emit(WalkEvent.DrawInside(cell.CellId));
|
||||
cell.PushView();
|
||||
AddViews(cell.StabList, ctx);
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
cell.TopView, ctx.Rays, ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||||
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
|
||||
|
||||
EmitDrawCells(_interiorPView, sink);
|
||||
if (_interiorPView.OutsideView.ViewCount > 0)
|
||||
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
|
||||
|
||||
RemoveViews(cell.StabList, ctx);
|
||||
cell.PopView();
|
||||
}
|
||||
|
||||
/// <summary><c>LScape::draw</c>: visibility per active view, then blocks
|
||||
/// far-to-near, cells far-to-near, buildings at their cell's turn.</summary>
|
||||
public void DrawLandscape(
|
||||
WalkLandscape landscape, WalkPortalView activeViews,
|
||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
sink.Emit(WalkEvent.Landscape());
|
||||
landscape.CalcDrawOrder();
|
||||
landscape.CheckBlocks(ctx.CyPlane, activeViews);
|
||||
|
||||
for (int i = landscape.BlockDrawCount - 1; i >= 0; i--)
|
||||
{
|
||||
WalkLandBlock? block = landscape.Blocks[landscape.BlockDrawList[i]];
|
||||
if (block is null || block.InView == WalkBoundingType.Outside)
|
||||
continue;
|
||||
int cellCount = block.SideCellCount * block.SideCellCount;
|
||||
for (int k = 0; k < cellCount; k++)
|
||||
{
|
||||
int cellIndex = block.DrawArray[k];
|
||||
// DrawSortCell runs for in-view cells (alwaysDrawObjects
|
||||
// default 0); terrain (DrawLandCell) emits no walk event.
|
||||
if (block.CellInView[cellIndex] == WalkBoundingType.Outside)
|
||||
continue;
|
||||
if (block.CellBuildings[cellIndex] is WalkBuilding building)
|
||||
DrawBuilding(building, activeViews, ctx, sink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 at the walk
|
||||
/// level: the BLD event fires at ENTRY (before the degrade check); the
|
||||
/// portal pass walks the drawing BSP twice per active view (punch, then
|
||||
/// look-in + DrawCells on the outdoor pview); the shell pass emits no
|
||||
/// walk event.</summary>
|
||||
public void DrawBuilding(
|
||||
WalkBuilding building, WalkPortalView activeViews,
|
||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||||
if (!building.HasGeometry) return;
|
||||
|
||||
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||
var passSink = new PortalPassSink(sink);
|
||||
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||||
for (int v = 0; v < viewCount; v++)
|
||||
{
|
||||
ctx.SetActiveView(activeViews, v);
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
building.DrawingBsp, 1, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
building.DrawingBsp, 2, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitDrawCells(WalkPView pview, IWalkEventSink sink)
|
||||
{
|
||||
uint[] cells = new uint[pview.CellDrawList.Count];
|
||||
for (int i = 0; i < cells.Length; i++)
|
||||
cells[i] = pview.CellDrawList[i].CellId;
|
||||
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||||
}
|
||||
|
||||
private void AddViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||||
{
|
||||
foreach (uint id in stabList)
|
||||
ctx.GetVisible(id)?.PushView();
|
||||
}
|
||||
|
||||
private void RemoveViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||||
{
|
||||
foreach (uint id in stabList)
|
||||
ctx.GetVisible(id)?.PopView();
|
||||
}
|
||||
|
||||
private sealed class PortalPassSink(IWalkEventSink sink)
|
||||
: WalkBuildingPortals.IWalkPortalPassSink
|
||||
{
|
||||
public void OnPunch(WalkPolygon polygon)
|
||||
{
|
||||
// The far-Z punch is a depth-only GPU submission (FW2's ordered
|
||||
// stream); the oracle traces do not log it.
|
||||
}
|
||||
|
||||
public void OnDrawCells(WalkPView pview)
|
||||
{
|
||||
uint[] cells = new uint[pview.CellDrawList.Count];
|
||||
for (int i = 0; i < cells.Length; i++)
|
||||
cells[i] = pview.CellDrawList[i].CellId;
|
||||
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue