882 lines
40 KiB
C#
882 lines
40 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// App-layer port of the retail indoor render orchestration:
|
|
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
|
/// PView::DrawInside -> ConstructView -> DrawCells.
|
|
/// </summary>
|
|
internal sealed class RetailPViewRenderer
|
|
{
|
|
private readonly RenderSceneShadowRuntime _renderSceneShadow;
|
|
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
|
private readonly RetailPViewFrameResult _frameResultScratch = new();
|
|
|
|
private static readonly ClipViewSlice NoClipSlice =
|
|
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
|
|
private static readonly IReadOnlySet<uint> NoParticleOwners =
|
|
new HashSet<uint>();
|
|
|
|
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
|
|
|
|
// MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across
|
|
// frames instead of `new HashSet<uint>(pvFrame.OrderedVisibleCells)` every
|
|
// call. Every walk consumer reads it synchronously in this frame.
|
|
private readonly HashSet<uint> _drawableCellsScratch = new();
|
|
private readonly HashSet<uint> _visibleCellsScratch = new();
|
|
|
|
// FW3 visual-gate fix: the interior root's dynamics phase, invoked by
|
|
// the driver's clearInteriorDepth closure at the walk's pre-clear
|
|
// boundary (retail draws outside objects inside LScape::draw, before
|
|
// the clear+seals). Assigned per frame around the Replay, always
|
|
// cleared in finally.
|
|
private Action? _walkPreClearDynamics;
|
|
|
|
// Output-only Facility Hub staircase probe. This counter is consulted
|
|
// only while ACDREAM_PROBE_FACILITY_STAIRS=1 and never affects the walk.
|
|
private ulong _probeFacilityStairFrame;
|
|
private ulong _probeCathedralShellOrderFrame;
|
|
private string? _probeFacilityStairRootSignature;
|
|
private string? _probeCathedralStairRootSignature;
|
|
|
|
// FW6 allocation closeout: the walk's large event/view/route scratch,
|
|
// frame context, and one-cell leaf collections are renderer-lifetime
|
|
// owners. Only their frame-local bindings change. Before this cutover all
|
|
// three were constructed inside DrawInside and accounted for the measured
|
|
// ~1.5 MiB/frame dense-town tail.
|
|
private Walk.WalkFrameDriver? _walkFrameDriverScratch;
|
|
private Walk.WalkProductionFrameContext? _walkFrameContextScratch;
|
|
private WalkProductionLeafRenderer? _walkLeafRendererScratch;
|
|
private readonly Action _walkClearInteriorDepthAction;
|
|
private readonly Action _walkDrawExitSealsAction;
|
|
private RetailPViewPassExecutor? _activeWalkPasses;
|
|
private RetailPViewFrameInput? _activeWalkFrame;
|
|
private ClipFrameAssembly? _activeWalkClipAssembly;
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
internal RetailPViewRenderer(
|
|
RenderSceneShadowRuntime renderSceneShadow,
|
|
Walk.WalkBuildingRegistry walkBuildings,
|
|
Walk.WalkLandscapeAssembler walkLandscape,
|
|
CellVisibility walkCellRegistry,
|
|
ShadowObjectRegistry shadows,
|
|
Func<uint, uint?>? findParentLocalId = null)
|
|
{
|
|
_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,
|
|
shadows ?? throw new ArgumentNullException(nameof(shadows)),
|
|
findParentLocalId);
|
|
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
|
_walkDrawExitSealsAction = DrawWalkExitSeals;
|
|
}
|
|
|
|
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
|
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
|
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
|
// caller's per-building frustum pre-gate on aperture bounds (GameWindow's
|
|
// gather); seeds themselves are unbounded.
|
|
private const float OutdoorBuildingSeedDistance = float.PositiveInfinity;
|
|
|
|
internal RetailPViewFrameResult DrawInside(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ctx);
|
|
ArgumentNullException.ThrowIfNull(passes);
|
|
passes.BeginFrame();
|
|
// Campaign FW4: there is one production renderer and one visibility
|
|
// owner. The fake/legacy executor path was useful during the cutover,
|
|
// but retaining it here kept the retired PortalVisibilityBuilder and
|
|
// look-in seed machinery alive. Fail loudly if composition ever tries
|
|
// to reintroduce that split.
|
|
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
|
|
?? throw new InvalidOperationException(
|
|
"The retail frame walk requires RetailPViewPassExecutor.");
|
|
ClipFrameAssembly clipAssembly = passes.BeginWalkClipFrame(
|
|
ctx.RootCell.IsOutdoorNode,
|
|
_clipAssemblyScratch);
|
|
// FW4 slice 1: PrepareClipFrame (the one clip-region publication)
|
|
// moved BELOW the walk block — an interior-rooted walk frame
|
|
// re-derives the outside-view slices from the walk's own views
|
|
// first, so the appended slots join the same single publication.
|
|
|
|
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
|
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
|
// so every visible cell's shell has a prepared batch and seals — killing the grey
|
|
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
|
|
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
|
_drawableCellsScratch.Clear();
|
|
HashSet<uint> drawableCells = _drawableCellsScratch;
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
|
|
// Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/
|
|
// walkCameraCell exactly as the pre-FW3.4a pre-walk collection pass
|
|
// did, then drives WalkFrameDriver.Collect — a SINGLE RetailFrameWalk
|
|
// pass that both learns the flood/visited-cell set (needed below,
|
|
// BEFORE prepareCells is finalized, so EnvCellRenderer prepares
|
|
// batches for every shell the driver will draw later in this same
|
|
// DrawInside call) and records the walk's draw events for Replay
|
|
// further down, in DrawWalkDrivenStatics. The former SECOND walk pass
|
|
// (a dedicated set-collecting sink, run again through this same
|
|
// driver machinery just to submit) is gone — see WalkFrameDriver's
|
|
// own doc comment for the FW3.4 perf numbers that motivated this.
|
|
// _walkWorldData.BeginFrame precedes Collect deliberately: Collect's
|
|
// stream appends classify records immediately (WalkStaticStreamPopulator
|
|
// runs at append time, not at Replay time), so the world data must
|
|
// already be rebuilt for this frame before the walk starts.
|
|
Walk.WalkFrameDriver walkDriver;
|
|
{
|
|
Matrix4x4 view = ctx.CameraView;
|
|
var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33));
|
|
(int Width, int Height)? attachment = walkExecutor!.WalkAttachmentExtent;
|
|
// Fallback matches the FW3.2b-2 shadow probe's own comment: every
|
|
// screen projection shares the same constants, so this is only
|
|
// reached before the world pass has published its scope.
|
|
float viewportWidth = attachment?.Width ?? 1024f;
|
|
float viewportHeight = attachment?.Height ?? 720f;
|
|
if (_walkFrameContextScratch is null)
|
|
{
|
|
_walkFrameContextScratch = new Walk.WalkProductionFrameContext(
|
|
_walkCellRegistry,
|
|
_walkBuildings,
|
|
ctx.ViewerEyePos,
|
|
forward,
|
|
ctx.ViewProjection,
|
|
viewportWidth,
|
|
viewportHeight);
|
|
}
|
|
else
|
|
{
|
|
_walkFrameContextScratch.Reset(
|
|
ctx.ViewerEyePos,
|
|
forward,
|
|
ctx.ViewProjection,
|
|
viewportWidth,
|
|
viewportHeight);
|
|
}
|
|
Walk.WalkProductionFrameContext walkContext = _walkFrameContextScratch;
|
|
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
|
|
Walk.WalkLandscape walkLandscape = _walkLandscape.Landscape;
|
|
|
|
Walk.WalkCell? walkCameraCell = null;
|
|
if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100)
|
|
{
|
|
walkCameraCell = _walkCellRegistry!.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded)
|
|
? loaded?.Walk
|
|
: null;
|
|
if (walkCameraCell is null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"walk root=0x{ctx.ViewerCellId:X8}: the interior camera cell has "
|
|
+ "no committed walk data — the Campaign FW3.2b-2 static cutover "
|
|
+ "requires the walk registry to already hold the viewer's own cell "
|
|
+ "(fail-loud rule; a silently skipped root would leave the frame "
|
|
+ "with no static draws at all).");
|
|
}
|
|
}
|
|
|
|
if (ctx.RootCell.IsOutdoorNode && clipAssembly.OutsideViewSlices.Length != 1)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"walk static cutover: an outdoor root's clip assembly produced "
|
|
+ $"{clipAssembly.OutsideViewSlices.Length} outside-view slices, not the "
|
|
+ "expected 1 — the outdoor root draws through the full-screen default "
|
|
+ "view (retail set_default_view; the walk fans exactly its own 1), and "
|
|
+ "the outdoor slice data still comes from the assembler (plan §FW3 item "
|
|
+ "2c's pinned assumption; assert rather than silently coercing to 1).");
|
|
}
|
|
|
|
_walkWorldData!.BeginFrame(
|
|
_renderSceneShadow!.Query,
|
|
ctx.PlayerLandblockId ?? 0u,
|
|
ctx.RenderCenterLbX,
|
|
ctx.RenderCenterLbY);
|
|
|
|
_activeWalkPasses = walkExecutor;
|
|
_activeWalkFrame = ctx;
|
|
_activeWalkClipAssembly = clipAssembly;
|
|
if (_walkLeafRendererScratch is null)
|
|
{
|
|
_walkLeafRendererScratch = new WalkProductionLeafRenderer(
|
|
walkExecutor,
|
|
ctx,
|
|
clipAssembly,
|
|
_walkClearInteriorDepthAction,
|
|
_walkDrawExitSealsAction);
|
|
}
|
|
else
|
|
{
|
|
_walkLeafRendererScratch.Reset(
|
|
walkExecutor,
|
|
ctx,
|
|
clipAssembly,
|
|
_walkClearInteriorDepthAction,
|
|
_walkDrawExitSealsAction);
|
|
}
|
|
|
|
if (_walkFrameDriverScratch is null)
|
|
{
|
|
_walkFrameDriverScratch = new Walk.WalkFrameDriver(
|
|
walkExecutor.Dispatcher,
|
|
_walkLeafRendererScratch,
|
|
_walkWorldData,
|
|
clipFrame: clipAssembly.Frame);
|
|
}
|
|
else
|
|
{
|
|
_walkFrameDriverScratch.RebindFrame(
|
|
_walkLeafRendererScratch,
|
|
clipAssembly.Frame);
|
|
}
|
|
walkDriver = _walkFrameDriverScratch;
|
|
|
|
try
|
|
{
|
|
walkDriver.Collect(
|
|
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
|
|
ctx.ViewProjection, ctx.CameraWorldPosition);
|
|
}
|
|
catch
|
|
{
|
|
ClearWalkFrameBindings();
|
|
throw;
|
|
}
|
|
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
|
|
// come from THE WALK'S OWN outside_view — retail's one
|
|
// visibility structure. See ClipFrameAssembler.
|
|
// ReassembleOutsideViewFromWalk's doc comment for the boundary-
|
|
// frame desync (the stairwell/grass flash) this retires. The
|
|
// outdoor root keeps the assembler's single full-screen slice
|
|
// (asserted ==1 above; identical content by construction).
|
|
if (walkCameraCell is not null)
|
|
{
|
|
ClipFrameAssembler.ReassembleOutsideViewFromWalk(
|
|
clipAssembly,
|
|
_frameWalk.InteriorOutsideView,
|
|
viewportWidth,
|
|
viewportHeight);
|
|
}
|
|
|
|
// The walk's visited set replaces PortalVisibilityFrame's old
|
|
// OrderedVisibleCells side-channel for every production consumer.
|
|
_drawableCellsScratch.Clear();
|
|
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
|
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
|
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics
|
|
.ProbeCathedralShellOrderEnabled
|
|
&& ((ctx.ViewerCellId & 0xFFFF0000u) == 0xF4180000u
|
|
|| (ctx.PlayerCellId & 0xFFFF0000u) == 0xF4180000u))
|
|
{
|
|
_probeCathedralShellOrderFrame++;
|
|
walkDriver.TraceCathedralShellOrder(
|
|
_probeCathedralShellOrderFrame,
|
|
ctx.ViewerCellId,
|
|
ctx.PlayerCellId,
|
|
ctx.RootCell.CellId,
|
|
ctx.ViewerEyePos);
|
|
}
|
|
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
|
|
{
|
|
static int TargetMembership(IReadOnlyCollection<uint> cells)
|
|
{
|
|
const uint a = 0x8A02015Eu;
|
|
const uint b = 0x8A02015Fu;
|
|
const uint c = 0x8A0201C1u;
|
|
int mask = 0;
|
|
foreach (uint cell in cells)
|
|
{
|
|
if (cell == a) mask |= 1;
|
|
if (cell == b) mask |= 2;
|
|
if (cell == c) mask |= 4;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
_probeFacilityStairFrame++;
|
|
int floodMask = TargetMembership(walkDriver.InteriorFloodCells);
|
|
int visitedMask = TargetMembership(walkDriver.VisitedCells);
|
|
int turnMask = TargetMembership(walkDriver.LookInCells);
|
|
string signature = $"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:"
|
|
+ $"{ctx.RootCell.CellId:X8}:{floodMask}:{visitedMask}:{turnMask}:"
|
|
+ $"{walkDriver.InteriorFloodCells.Count}:{walkDriver.VisitedCells.Count}:"
|
|
+ $"{walkDriver.LookInCellTurns.Count}";
|
|
bool changed = !string.Equals(
|
|
_probeFacilityStairRootSignature,
|
|
signature,
|
|
StringComparison.Ordinal);
|
|
_probeFacilityStairRootSignature = signature;
|
|
if (changed)
|
|
{
|
|
static string DescribeMask(int mask) =>
|
|
$"15e={((mask & 1) != 0 ? 1 : 0)},"
|
|
+ $"15f={((mask & 2) != 0 ? 1 : 0)},"
|
|
+ $"1c1={((mask & 4) != 0 ? 1 : 0)}";
|
|
Console.WriteLine(
|
|
$"[facility-root] f={_probeFacilityStairFrame} changed={(changed ? 1 : 0)} "
|
|
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
|
|
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
|
|
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
|
|
+ $"rootFlood={walkDriver.InteriorFloodCells.Count}"
|
|
+ $"[{DescribeMask(floodMask)}] "
|
|
+ $"visited={walkDriver.VisitedCells.Count}"
|
|
+ $"[{DescribeMask(visitedMask)}] "
|
|
+ $"turns={walkDriver.LookInCellTurns.Count}"
|
|
+ $"[{DescribeMask(turnMask)}]");
|
|
}
|
|
|
|
static int CathedralMembership(IReadOnlyCollection<uint> cells)
|
|
{
|
|
const uint oldBuilding = 0xF4180107u;
|
|
const uint stairParent = 0xF4180112u;
|
|
int mask = 0;
|
|
foreach (uint cell in cells)
|
|
{
|
|
if (cell == oldBuilding) mask |= 1;
|
|
if (cell == stairParent) mask |= 2;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
static uint BuildingAnchor(Walk.WalkBuilding building)
|
|
{
|
|
foreach (Walk.WalkBldPortal portal in building.Portals)
|
|
{
|
|
if (portal.OtherCellId != 0xFFFFFFFFu)
|
|
return portal.OtherCellId;
|
|
}
|
|
return 0u;
|
|
}
|
|
|
|
int cathedralFlood = CathedralMembership(walkDriver.InteriorFloodCells);
|
|
int cathedralVisited = CathedralMembership(walkDriver.VisitedCells);
|
|
int cathedralLookIn = CathedralMembership(walkDriver.LookInCells);
|
|
bool bucket107 = _walkWorldData.StaticBucketContains(
|
|
0xF4180107u,
|
|
0x020009A2u);
|
|
bool bucket112 = _walkWorldData.StaticBucketContains(
|
|
0xF4180112u,
|
|
0x020009A2u);
|
|
string buildingAnchors = string.Join(",", walkDriver.VisitedBuildings
|
|
.ConvertAll(building => $"0x{BuildingAnchor(building):X8}"));
|
|
string cathedralSignature =
|
|
$"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:{ctx.RootCell.CellId:X8}:"
|
|
+ $"{cathedralFlood}:{cathedralVisited}:{cathedralLookIn}:"
|
|
+ $"{(bucket107 ? 1 : 0)}:{(bucket112 ? 1 : 0)}:{buildingAnchors}";
|
|
bool cathedralChanged = !string.Equals(
|
|
_probeCathedralStairRootSignature,
|
|
cathedralSignature,
|
|
StringComparison.Ordinal);
|
|
_probeCathedralStairRootSignature = cathedralSignature;
|
|
if (cathedralChanged
|
|
&& ((ctx.ViewerCellId & 0xFFFF0000u) == 0xF4180000u
|
|
|| (ctx.PlayerCellId & 0xFFFF0000u) == 0xF4180000u))
|
|
{
|
|
static string DescribeCathedralMask(int mask) =>
|
|
$"107={((mask & 1) != 0 ? 1 : 0)},"
|
|
+ $"112={((mask & 2) != 0 ? 1 : 0)}";
|
|
Console.WriteLine(
|
|
$"[cathedral-stair] f={_probeFacilityStairFrame} "
|
|
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
|
|
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
|
|
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
|
|
+ $"rootFlood=[{DescribeCathedralMask(cathedralFlood)}] "
|
|
+ $"visited=[{DescribeCathedralMask(cathedralVisited)}] "
|
|
+ $"lookIn=[{DescribeCathedralMask(cathedralLookIn)}] "
|
|
+ $"buckets=[107={(bucket107 ? 1 : 0)},112={(bucket112 ? 1 : 0)}] "
|
|
+ $"buildings=[{buildingAnchors}]");
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// FW4 slice 1: the ONE clip-region publication, after any walk
|
|
// reassembly so the walk-derived outside-view slots are included
|
|
// (moved from directly after AssembleClipFrame; the count is
|
|
// reservation metadata the RHI arm ignores).
|
|
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
|
passes.PrepareClipFrame(terrainUploadCount);
|
|
|
|
// Production prepares exactly the one walk's visited-cell set.
|
|
HashSet<uint> prepareCells = drawableCells;
|
|
|
|
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
|
|
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
|
|
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
|
// is built once per frame in GameWindow, player-anchored.)
|
|
|
|
passes.PrepareCellBatches(ctx, prepareCells);
|
|
|
|
{
|
|
RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts;
|
|
RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
|
|
RenderProjectionCounts sourceCounts = retainedCounts;
|
|
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
|
clipAssembly,
|
|
drawableCells,
|
|
_visibleCellsScratch,
|
|
counts,
|
|
sourceCounts,
|
|
diagnosticPartition: null);
|
|
passes.EmitDiagnostics(ctx, result);
|
|
|
|
// The one collected walk owns terrain, cell shells, buildings,
|
|
// statics, dynamics, particles, punches, and alpha barriers at
|
|
// their retail turns. Interior-root landscape services run at the
|
|
// pre-clear callback; outdoor roots have no clear and run after
|
|
// replay.
|
|
_walkPreClearDynamics = () =>
|
|
{
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly);
|
|
};
|
|
try
|
|
{
|
|
DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
{
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_walkPreClearDynamics = null;
|
|
ClearWalkFrameBindings();
|
|
}
|
|
|
|
// OUTDOOR root: the LScape-boundary alpha drain deferred from the
|
|
// landscape stage runs HERE, after punches, interior shells, cell
|
|
// objects, and the dynamics pass — the frame's complete opaque
|
|
// world. Retail's walk draws all of those before its boundary
|
|
// flush (LScape::draw includes every cell's objects,
|
|
// DrawSortCell 0x005A17C0), so this is the same one-list far→near
|
|
// composite over finished depth; draining at the stage end instead
|
|
// let every later opaque mesh overwrite the flames (#132).
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
passes.FlushLandscapeAlpha();
|
|
|
|
// Interior-cell UNATTACHED emitters (spell ground effects and
|
|
// swirls anchored in EnvCells) draw in this final world scope —
|
|
// the cells' walls and the seals already own the depth buffer, so
|
|
// one unclipped submission matches retail's cell-walk insertion.
|
|
// Outdoor-cell unattached emitters drew in the landscape stage.
|
|
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
|
|
|
private void ClearWalkInteriorDepth()
|
|
{
|
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active pass binding.");
|
|
|
|
// FW3 visual-gate fix (owner report: doors/candles invisible looking
|
|
// out; the crossing vanish): retail draws outside objects inside
|
|
// LScape::draw, before the clear+seals.
|
|
_walkPreClearDynamics?.Invoke();
|
|
passes.FlushLandscapeAlpha();
|
|
passes.ClearInteriorDepth();
|
|
}
|
|
|
|
private void DrawWalkExitSeals()
|
|
{
|
|
RetailPViewFrameInput frame = _activeWalkFrame
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active frame binding.");
|
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active pass binding.");
|
|
Walk.WalkFrameDriver driver = _walkFrameDriverScratch
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active driver binding.");
|
|
|
|
DrawWalkExitPortalMasks(frame, passes, driver);
|
|
}
|
|
|
|
private void ClearWalkFrameBindings()
|
|
{
|
|
_walkFrameDriverScratch?.AbortFrame();
|
|
_activeWalkPasses = null;
|
|
_activeWalkFrame = null;
|
|
_activeWalkClipAssembly = null;
|
|
}
|
|
|
|
/// <summary>Campaign FW3.2b-2 — THE PRODUCTION ROOTING; Campaign FW3.4a —
|
|
/// REPLAY ONLY. <paramref name="driver"/> already ran its Collect pass
|
|
/// earlier in <see cref="DrawInside"/> (before <c>PrepareCellBatches</c>);
|
|
/// this method's job is now just <see cref="Walk.WalkFrameDriver.Replay"/>
|
|
/// (terrain/sky, every building's shell + punch + look-in cell statics,
|
|
/// and the interior root's own flood shells + statics, with retail's
|
|
/// depth-clear/exit-seal turn — all in walk order, replacing
|
|
/// <see cref="DrawLandscapeThroughOutsideView"/>'s static half,
|
|
/// <see cref="DrawExitPortalMasks"/>'s old top-level call,
|
|
/// <see cref="DrawEnvCellShells"/>, and <see cref="DrawCellObjectLists"/>'s
|
|
/// static half for this frame) plus re-sourcing the particle owners the
|
|
/// routes it replaced used to ride, from the driver's own visited
|
|
/// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and
|
|
/// 4).</summary>
|
|
private void DrawWalkDrivenStatics(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
Walk.WalkFrameDriver driver)
|
|
{
|
|
var (frame, encoder) = passes.RequireWalkSubmission();
|
|
passes.SetWalkTerrainInViewLandcells(driver.VisitedLandscapeCellIds);
|
|
try
|
|
{
|
|
driver.Replay(frame, encoder);
|
|
}
|
|
finally
|
|
{
|
|
passes.SetWalkTerrainInViewLandcells(null);
|
|
}
|
|
|
|
// Landscape-stage static-owner particles (candles, the cathedral
|
|
// falls) submit AT THEIR OWN WALK TURNS inside Replay
|
|
// (WalkFrameEventKind.StaticParticles) for BOTH root kinds — the
|
|
// #132 positional invariant (e102fb36's user-verified rule ported
|
|
// walk-natively): each owner cell's emitters enter the one alpha
|
|
// queue at that cell's far-to-near position, so every nearer
|
|
// building's pre-punch AlphaBarrier drains the farther content
|
|
// against still-true depth before its punch stamps far-Z.
|
|
|
|
// Cell-stage particle owners for the interior root's OWN flood cells
|
|
// (non-look-in — the walk draws their statics too, via
|
|
// OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic
|
|
// route's cell-particle submission needs the same re-sourcing).
|
|
// These stay POST-replay: interior emitters draw in the final world
|
|
// scope where the cell walls already own depth (retail's cell-walk
|
|
// insertion). Look-in cells get their OWN per-cell union in
|
|
// DrawBuildingLookInDynamics so a static owner is never submitted
|
|
// twice.
|
|
_cellParticleOwnerScratch.Clear();
|
|
foreach (uint cellId in driver.VisitedCells)
|
|
{
|
|
if (driver.LookInCells.Contains(cellId))
|
|
continue;
|
|
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
|
}
|
|
if (_cellParticleOwnerScratch.Count > 0)
|
|
{
|
|
passes.DrawCellParticles(
|
|
ctx,
|
|
new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch));
|
|
}
|
|
}
|
|
|
|
private static void UnionRecordOwners(
|
|
Walk.WalkFrameStaticRecords records, HashSet<uint> destination)
|
|
{
|
|
foreach (RenderProjectionRecord record in records.Records)
|
|
{
|
|
if (record.Source.LocalEntityId != 0)
|
|
destination.Add(record.Source.LocalEntityId);
|
|
}
|
|
}
|
|
|
|
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
|
/// <see cref="DrawLandscapeThroughOutsideView"/> + <see cref="DrawBuildingLookIns"/>
|
|
/// split. The walk now draws every mesh and every attached emitter at its
|
|
/// own turn. This tail therefore carries only the two landscape services
|
|
/// that are not scene projections: ownerless outdoor-cell emitters and
|
|
/// weather. It deliberately submits no packed entity route.</summary>
|
|
private void DrawLandscapeDynamicsPhase(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
ClipFrameAssembly clipAssembly)
|
|
{
|
|
if (clipAssembly.OutsideViewSlices.Length == 0)
|
|
return;
|
|
|
|
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
|
|
// hasBuildingLookIns gate only existed to sequence this submission
|
|
// around the OLD static barrier drains, which no longer run here
|
|
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
|
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
|
|
|
// 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
|
|
// those records at OnLandscapeCellTurn.
|
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
|
{
|
|
passes.SetTerrainClip(slice.Planes);
|
|
passes.ClearClipRouting();
|
|
passes.DrawLandscapeSliceLate(
|
|
ctx,
|
|
new RetailPViewLandscapeLateSliceContext(
|
|
slice,
|
|
Array.Empty<WorldEntity>()));
|
|
}
|
|
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
}
|
|
|
|
/// <summary>Campaign FW4 slice 2 — the walk-flood seal draw. Retail's
|
|
/// <c>PView::DrawCells</c> stamps every exit portal of THE FLOOD'S OWN
|
|
/// cells (pc:432785-432786, reverse cell_draw_list far→near) — one
|
|
/// visibility structure decides the flood, the seals, and the terrain
|
|
/// views alike. The old apparatus's flood misses exit portals at the
|
|
/// #456 cathedral seam band (its never-drawn panel family), leaving
|
|
/// aperture depth unsealed after the interior clear; the end-of-frame
|
|
/// alpha drain (cell-owned emitters — retail's own timing) then
|
|
/// z-passes across the whole opening (the falls shine-through). Each
|
|
/// portal is stamped once per exact walk-owned view captured for that
|
|
/// flood cell, matching retail's <c>CEnvCell::setup_view</c> loop. The
|
|
/// legacy visibility assembly has no production role here.</summary>
|
|
private void DrawWalkExitPortalMasks(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
Walk.WalkFrameDriver driver)
|
|
{
|
|
List<uint> floodCells = driver.InteriorFloodCells;
|
|
for (int i = floodCells.Count - 1; i >= 0; i--)
|
|
{
|
|
uint cellId = floodCells[i];
|
|
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
|
|
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
|
{
|
|
passes.DrawExitPortalMask(
|
|
ctx,
|
|
cellId,
|
|
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static RenderFrameDiagnosticCounts WalkDiagnosticCounts(
|
|
RenderProjectionCounts source)
|
|
{
|
|
int dynamics = checked(
|
|
source.LiveDynamicRoot
|
|
+ source.ActiveAnimatedStatic
|
|
+ source.EquippedChild);
|
|
return new RenderFrameDiagnosticCounts(
|
|
source.OutdoorStatic,
|
|
source.IndoorCellStatic,
|
|
dynamics,
|
|
TransformCount: source.Total,
|
|
OpaqueClassificationCount: 0,
|
|
AlphaClassificationCount: 0,
|
|
LightSetCount: 0,
|
|
SelectionPartCount: 0,
|
|
RouteCandidateCount: source.Total,
|
|
EntityCandidateCount: source.Total,
|
|
MeshPartCount: 0);
|
|
}
|
|
|
|
}
|
|
|
|
public interface IRetailPViewCellSource
|
|
{
|
|
LoadedCell? Find(uint cellId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Typed execution seam for the GL passes ordered by
|
|
/// <see cref="RetailPViewRenderer"/>. Implementations execute the requested
|
|
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
|
/// All frame inputs and results are borrowed for the duration of the call.
|
|
/// </summary>
|
|
public sealed class RetailPViewFrameInput
|
|
{
|
|
public LoadedCell RootCell { get; private set; } = null!;
|
|
|
|
/// <summary>R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the
|
|
/// outdoor node. Null for interior roots. Grouped by BuildingId inside <see cref="DrawInside"/>.</summary>
|
|
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; private set; }
|
|
|
|
public Vector3 ViewerEyePos { get; private set; }
|
|
public Matrix4x4 ViewProjection { get; private set; }
|
|
public IRetailPViewCellSource Cells { get; private set; } = null!;
|
|
public ICamera Camera { get; private set; } = null!;
|
|
public Vector3 CameraWorldPosition { get; private set; }
|
|
public FrustumPlanes? Frustum { get; private set; }
|
|
public uint? PlayerLandblockId { get; private set; }
|
|
public HashSet<uint>? AnimatedEntityIds { get; private set; }
|
|
public int RenderCenterLbX { get; private set; }
|
|
public int RenderCenterLbY { get; private set; }
|
|
public int RenderRadius { get; private set; }
|
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
|
{ get; private set; } = Array.Empty<(uint, Vector3, Vector3,
|
|
IReadOnlyList<WorldEntity>,
|
|
IReadOnlyDictionary<uint, WorldEntity>?)>();
|
|
|
|
// Pass-presentation and diagnostic values consumed synchronously by the
|
|
// typed executor. This input is data-only and is never retained.
|
|
public bool RenderSky { get; private set; }
|
|
public bool RenderWeather { get; private set; }
|
|
public float DayFraction { get; private set; }
|
|
public DayGroupData? ActiveDayGroup { get; private set; }
|
|
public SkyKeyframe SkyKeyframe { get; private set; }
|
|
public bool EnvironOverrideActive { get; private set; }
|
|
public uint ViewerCellId { get; private set; }
|
|
public uint PlayerCellId { get; private set; }
|
|
public Vector3 PlayerViewPosition { get; private set; }
|
|
public Matrix4x4 CameraView { get; private set; }
|
|
public CameraCellResolution CameraCellResolution { get; private set; }
|
|
|
|
internal RetailPViewFrameInput Reset(
|
|
LoadedCell rootCell,
|
|
IReadOnlyList<LoadedCell>? nearbyBuildingCells,
|
|
Vector3 viewerEyePos,
|
|
Matrix4x4 viewProjection,
|
|
IRetailPViewCellSource cells,
|
|
ICamera camera,
|
|
Vector3 cameraWorldPosition,
|
|
FrustumPlanes? frustum,
|
|
uint? playerLandblockId,
|
|
HashSet<uint>? animatedEntityIds,
|
|
int renderCenterLbX,
|
|
int renderCenterLbY,
|
|
int renderRadius,
|
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
|
bool renderSky,
|
|
bool renderWeather,
|
|
float dayFraction,
|
|
DayGroupData? activeDayGroup,
|
|
SkyKeyframe skyKeyframe,
|
|
bool environOverrideActive,
|
|
uint viewerCellId,
|
|
uint playerCellId,
|
|
Vector3 playerViewPosition,
|
|
Matrix4x4 cameraView,
|
|
CameraCellResolution cameraCellResolution)
|
|
{
|
|
RootCell = rootCell;
|
|
NearbyBuildingCells = nearbyBuildingCells;
|
|
ViewerEyePos = viewerEyePos;
|
|
ViewProjection = viewProjection;
|
|
Cells = cells;
|
|
Camera = camera;
|
|
CameraWorldPosition = cameraWorldPosition;
|
|
Frustum = frustum;
|
|
PlayerLandblockId = playerLandblockId;
|
|
AnimatedEntityIds = animatedEntityIds;
|
|
RenderCenterLbX = renderCenterLbX;
|
|
RenderCenterLbY = renderCenterLbY;
|
|
RenderRadius = renderRadius;
|
|
LandblockEntries = landblockEntries;
|
|
RenderSky = renderSky;
|
|
RenderWeather = renderWeather;
|
|
DayFraction = dayFraction;
|
|
ActiveDayGroup = activeDayGroup;
|
|
SkyKeyframe = skyKeyframe;
|
|
EnvironOverrideActive = environOverrideActive;
|
|
ViewerCellId = viewerCellId;
|
|
PlayerCellId = playerCellId;
|
|
PlayerViewPosition = playerViewPosition;
|
|
CameraView = cameraView;
|
|
CameraCellResolution = cameraCellResolution;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Borrowed renderer scratch valid only until the next
|
|
/// <see cref="RetailPViewRenderer.DrawInside"/> call. Collections and nested
|
|
/// frame objects are deliberately reused to keep the render loop allocation
|
|
/// free; consumers must copy any state they need to retain asynchronously.
|
|
/// </summary>
|
|
public sealed class RetailPViewFrameResult
|
|
{
|
|
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
|
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The production retail walk's exact visited-cell set. This is the one
|
|
/// visibility answer consumed by EnvCell preparation, particles, lights,
|
|
/// and directional-shadow filtering.
|
|
/// </summary>
|
|
public HashSet<uint> VisibleCells { get; private set; } = null!;
|
|
|
|
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
|
|
internal RenderProjectionCounts SourceCounts { get; private set; }
|
|
internal InteriorEntityPartition.Result? DiagnosticPartition
|
|
{ get; private set; }
|
|
|
|
internal RetailPViewFrameResult Reset(
|
|
ClipFrameAssembly clipAssembly,
|
|
HashSet<uint> drawableCells,
|
|
HashSet<uint> visibleCells,
|
|
RenderFrameDiagnosticCounts diagnosticCounts,
|
|
RenderProjectionCounts sourceCounts,
|
|
InteriorEntityPartition.Result? diagnosticPartition)
|
|
{
|
|
ClipAssembly = clipAssembly;
|
|
DrawableCells = drawableCells;
|
|
VisibleCells = visibleCells;
|
|
DiagnosticCounts = diagnosticCounts;
|
|
SourceCounts = sourceCounts;
|
|
DiagnosticPartition = diagnosticPartition;
|
|
return this;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scene-particle owners for ONE unclipped landscape-stage submission (the
|
|
/// union of every outside slice's cone survivors). Mesh alpha for the same
|
|
/// owners is already queued by the entity routes; retail inserts each
|
|
/// emitter's polys into the single alpha list once, during its owner cell's
|
|
/// walk turn, with no portal-view clip.
|
|
/// </summary>
|
|
public readonly record struct RetailPViewLandscapeStaticParticleContext(
|
|
IReadOnlySet<uint> ParticleOwnerIds);
|
|
|
|
/// <summary>#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.</summary>
|
|
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
|
ClipViewSlice Slice,
|
|
IReadOnlyList<WorldEntity> Dynamics);
|
|
|
|
public readonly record struct RetailPViewCellSliceContext(
|
|
uint CellId,
|
|
ClipViewSlice Slice,
|
|
IReadOnlySet<uint> ParticleOwnerIds);
|