checkpoint(render): preserve pre-overhaul investigation state

This commit is contained in:
Erik 2026-09-01 18:04:24 +02:00
parent e880860291
commit b3b7d922f1
45 changed files with 3168 additions and 619 deletions

View file

@ -507,7 +507,8 @@ internal sealed class FrameRootCompositionPhase
?? throw new InvalidOperationException(
"The retail frame walk requires the landscape registry."),
d.CellVisibility,
d.PhysicsEngine.ShadowObjects),
d.PhysicsEngine.ShadowObjects,
live.EquippedChildren.FindParentLocalId),
retailPViewPassExecutor,
retailPViewPassExecutor),
retailPViewCells,

View file

@ -32,6 +32,7 @@
// silent std430/std140 drift can't reach the GPU.
using System;
using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
@ -200,6 +201,31 @@ public sealed class ClipFrame : IDisposable
return slot;
}
/// <summary>
/// Borrows the clip-space planes packed for one live slot. The walk uses
/// this to replay an EnvCell's exact captured <c>portal_view</c> while
/// stamping its exit polygons; it must not rebuild those views from the
/// legacy visibility assembly after the walk has already decided them.
/// </summary>
internal ReadOnlySpan<Vector4> GetSlotPlanes(uint slot)
{
if (slot >= (uint)_slotCount)
throw new ArgumentOutOfRangeException(nameof(slot));
int byteOffset = checked((int)slot * CellClipStrideBytes);
int count = checked((int)ReadUInt(_regionBytes, byteOffset));
if ((uint)count > MaxPlanes)
{
throw new InvalidOperationException(
$"Clip slot {slot} contains invalid plane count {count}.");
}
return MemoryMarshal.Cast<byte, Vector4>(
_regionBytes.AsSpan(
byteOffset + CellClipPlanesOffset,
count * sizeof(float) * 4));
}
/// <summary>
/// Set the terrain OutsideView clip region (the single region the terrain
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
@ -243,6 +269,12 @@ public sealed class ClipFrame : IDisposable
dst[offset + 3] = (byte)((value >> 24) & 0xFF);
}
private static uint ReadUInt(byte[] src, int offset) =>
(uint)(src[offset + 0]
| (src[offset + 1] << 8)
| (src[offset + 2] << 16)
| (src[offset + 3] << 24));
private static void WriteInt(byte[] dst, int offset, int value)
=> WriteUInt(dst, offset, unchecked((uint)value));

View file

@ -224,10 +224,12 @@ internal readonly record struct GpuDepthState(bool Test, bool Write, GpuCompareO
///
/// <para>Added at slice V6l. Core Vulkan 1.3 makes ALL of these dynamic
/// (<c>VK_DYNAMIC_STATE_STENCIL_OP</c>, <c>_COMPARE_MASK</c>, <c>_WRITE_MASK</c>,
/// <c>_REFERENCE</c>), and #117's portal punch changes every one of them between
/// its mark pass and its punch pass, so they live here as a pipeline DEFAULT and
/// on <see cref="IGpuPassEncoder.SetStencil"/> as the per-draw override —
/// exactly the split cull mode, front face and depth write already have.
/// <c>_REFERENCE</c>). They live here as a pipeline DEFAULT and on
/// <see cref="IGpuPassEncoder.SetStencil"/> as the per-draw override — exactly
/// the split cull mode, front face and depth write already have. The portal
/// renderer no longer consumes stencil after Campaign FW restored retail's
/// ordered one-pass depth punch; the facility remains for render packs and
/// future stencil users.
/// Whether the pipeline uses the stencil aspect at all is
/// <see cref="GpuPipelineDescription.StencilTest"/>, because that is also the
/// attachment intent.</para>

View file

@ -9,21 +9,11 @@ namespace AcDream.App.Rendering;
/// Campaign V slice V6l: the portal depth mask's RHI submission arm — V4g's
/// remaining half, and the reason the slice grew a stencil dimension.
///
/// <para>Plan §5.5.16 defect 2: this renderer's two-pass punch (#117) is built
/// on <c>glStencilFunc</c>/<c>glStencilOp</c>/<c>glStencilMask</c> and the pinned
/// <see cref="GpuPipelineDescription"/> carried no stencil state at all, so it
/// stayed raw GL and was invisible to the Vulkan arm. The reviewed amendment
/// puts the ENABLE and the attachment intent in the pipeline
/// (<see cref="GpuPipelineDescription.StencilTest"/>) and the per-draw compare,
/// ops, reference and masks on <see cref="IGpuPassEncoder.SetStencil"/>, because
/// core Vulkan 1.3 makes exactly that split dynamic.</para>
///
/// <para><b>Three pipelines, not one.</b> Depth COMPARE is not dynamic in the
/// contract (only depth write is), and the punch's two passes differ in it —
/// mark tests <c>LEQUAL</c> and writes no depth, punch tests <c>ALWAYS</c> and
/// writes. The seal is a third: <c>ALWAYS</c> + write, with no stencil at all.
/// All three write no colour, which is what retail's "COLOR-INVISIBLE triangle
/// fan" means.</para>
/// <para>Campaign FW restores retail's ordered frame walk, so this Vulkan arm
/// now records the exact retail state for both operations: one color-invisible
/// triangle fan, depth compare ALWAYS, depth write enabled, culling disabled.
/// The push-constant pass selector chooses true projected depth (seal) or
/// forced far-Z (punch). There is no stencil mark pass or depth bias.</para>
///
/// <para><b>Two other differences from the GL arm.</b> The fan is expanded to a
/// triangle LIST on the CPU, because <see cref="GpuPrimitiveTopology"/> has no
@ -39,9 +29,7 @@ public sealed partial class PortalDepthMaskRenderer
private readonly IGpuDevice? _device;
private readonly ICurrentGpuFrameSource? _frames;
private readonly IWorldPassScope? _scope;
private IGpuPipeline? _sealPipeline;
private IGpuPipeline? _punchMarkPipeline;
private IGpuPipeline? _punchWritePipeline;
private IGpuPipeline? _depthWritePipeline;
private bool _rhiFrameStarted;
/// <summary>One position per vertex — the only attribute <c>portal_depth.vert</c> reads.</summary>
@ -51,16 +39,8 @@ public sealed partial class PortalDepthMaskRenderer
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0)));
/// <summary>
/// The stencil reference the mark pass writes and the punch pass gates on.
/// Retail has no equivalent — the whole stencil pass is acdream's
/// z-buffered replacement for retail's painter's-order safety (#117) — so
/// the value is arbitrary and only has to agree with itself.
/// </summary>
private const uint PunchStencilReference = 1;
/// <summary>
/// The RHI arm's constructor. No GL context and no inline program: the three
/// pipelines compile <c>portal_depth</c> from the committed SPIR-V, and the
/// The RHI arm's constructor. No GL context and no inline program: the one
/// pipeline compiles <c>portal_depth</c> from the committed SPIR-V, and the
/// per-frame fan vertices come from the frame ring.
/// </summary>
internal PortalDepthMaskRenderer(
@ -76,53 +56,17 @@ public sealed partial class PortalDepthMaskRenderer
try
{
int samples = scope.SampleCount;
// SEAL: retail maxZ2, bit0 clear, data 0x00820e14 — depth ALWAYS at
// the polygon's true projected depth, no stencil. It runs
// immediately after the gated full depth clear, so there is no
// nearer content to stomp.
_sealPipeline = CreatePortalPipeline(
// Retail DrawPortalPolyInternal @0x0059BC90 uses the same depth
// state for seals and punches. maxZ2/maxZ1 only select the vertex
// depth written by the shader.
_depthWritePipeline = CreatePortalPipeline(
device,
"portal-depth-seal",
"portal-depth-write",
GpuCompareOp.Always,
depthWrite: true,
stencilTest: false,
GpuStencilState.Default,
samples);
// PUNCH pass A: mark stencil where the aperture fan passes a LEQUAL
// depth test at its (biased) true depth — i.e. where the aperture is
// actually visible against everything drawn so far.
_punchMarkPipeline = CreatePortalPipeline(
device,
"portal-depth-punch-mark",
GpuCompareOp.LessOrEqual,
depthWrite: false,
stencilTest: true,
GpuStencilState.Default with
{
Compare = GpuCompareOp.Always,
Fail = GpuStencilOp.Keep,
DepthFail = GpuStencilOp.Keep,
Pass = GpuStencilOp.Replace,
Reference = PunchStencilReference,
},
samples);
// PUNCH pass B: the far-Z write on marked pixels only, zeroing the
// stencil as it goes so the buffer is self-cleaning.
_punchWritePipeline = CreatePortalPipeline(
device,
"portal-depth-punch-write",
GpuCompareOp.Always,
depthWrite: true,
stencilTest: true,
GpuStencilState.Default with
{
Compare = GpuCompareOp.Equal,
Fail = GpuStencilOp.Keep,
DepthFail = GpuStencilOp.Keep,
Pass = GpuStencilOp.Zero,
Reference = PunchStencilReference,
},
samples);
}
catch
{
@ -202,38 +146,14 @@ public sealed partial class PortalDepthMaskRenderer
for (int i = 0; i < planeCount; i++)
clipPlanes[i] = planes[i];
if (!forceFarZ)
{
RecordPortalPass(
encoder,
_sealPipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 0,
depthBias: 0f);
return;
}
RecordPortalPass(
encoder,
_punchMarkPipeline!,
_depthWritePipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 0,
depthBias: PunchMarkDepthBias);
RecordPortalPass(
encoder,
_punchWritePipeline!,
clip,
vertices,
vertexCount,
in viewProjection,
renderPass: 1,
depthBias: 0f);
renderPass: forceFarZ ? 1 : 0);
}
private static void RecordPortalPass(
@ -243,8 +163,7 @@ public sealed partial class PortalDepthMaskRenderer
in GpuRingAllocation vertices,
int vertexCount,
in Matrix4x4 viewProjection,
int renderPass,
float depthBias)
int renderPass)
{
encoder.BindPipeline(pipeline);
encoder.SetPushConstants(new GpuPushConstants
@ -258,8 +177,8 @@ public sealed partial class PortalDepthMaskRenderer
LightDebug = 0,
TextureIndexA = 0,
TextureIndexB = 0,
ParamA = depthBias,
ParamB = PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters,
ParamA = 0f,
ParamB = 0f,
});
encoder.BindUniformBuffer(
ClipFrame.TerrainClipUboBinding,
@ -286,12 +205,8 @@ public sealed partial class PortalDepthMaskRenderer
catch (Exception error) { (failures ??= []).Add(error); }
}
Attempt(() => _sealPipeline?.Dispose());
_sealPipeline = null;
Attempt(() => _punchMarkPipeline?.Dispose());
_punchMarkPipeline = null;
Attempt(() => _punchWritePipeline?.Dispose());
_punchWritePipeline = null;
Attempt(() => _depthWritePipeline?.Dispose());
_depthWritePipeline = null;
_rhiFrameStarted = false;
if (failures is not null)

View file

@ -8,13 +8,12 @@ namespace AcDream.App.Rendering;
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// (Ghidra 0x0059bc90, pc:424490).
///
/// <para><b>Wired by T1 (BR-3, `579c8b0`):</b> seal on interior roots, punch
/// on outdoor / look-in roots, via <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>
/// (the <c>DrawExitPortalMasks</c> slice callback) — safe alongside the
/// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after
/// dynamics and erased the player; reverted 88be519). #117 (2026-06-11)
/// added the two-pass stencil depth gate on the punch side — see
/// <see cref="DrawDepthFan"/>.</para>
/// <para>Campaign FW's ordered frame walk restores the positional discipline
/// on which this primitive depends: farther landscape/static content is
/// submitted before a building punch, the punch precedes that building's
/// look-in cells, and nearer content repaints afterward. Consequently both
/// sides now use retail's one-pass depth operation directly; the former
/// stencil mark/bias approximation from #117 is intentionally gone.</para>
///
/// <para>Retail projects a portal polygon, software-clips it against the
/// installed portal view (<c>polyClipFinish</c>), and draws the survivor as a
@ -78,49 +77,6 @@ public sealed partial class PortalDepthMaskRenderer : IDisposable
}
/// <summary>
/// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the
/// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because
/// retail's outdoor pass is painter's-ordered far→near (anything nearer
/// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame
/// has no such order, so an unconditional far-Z punch erased the depth
/// of NEARER occluders (terrain hills, closer buildings) at aperture
/// pixels — doors/interiors painted through them (the T5 #117 report).
/// The z-buffer-correct equivalent: punch ONLY where the aperture
/// polygon itself wins a depth test at its true depth (two-pass
/// stencil below). The bias keeps the #108 case covered — terrain
/// hugging the door plane (centimeters in front of the aperture) must
/// still be punched; a hill or another house meters nearer must not.
/// </summary>
private const float PunchMarkDepthBias = 0.0005f;
/// <summary>
/// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b
/// spans ≈ b·d²/near meters of eye depth at eye distance d. With
/// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but
/// ~190 m at a landblock away: every hill/house in front of a distant
/// aperture passed the mark and got far-Z punched — door-shaped leaks
/// through occluders. Fix: cap the bias's EYE-SPACE span at
/// <see cref="PunchMarkBiasEyeCapMeters"/>. Below the ~10 m crossover
/// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins —
/// bit-identical to the T5-validated close-range behavior (#108 grass
/// coverage untouched); beyond it the punch can never reach an occluder
/// more than the cap in front of the aperture plane.
/// </summary>
public const float PunchMarkBiasEyeCapMeters = 0.5f;
/// <summary>Retail <c>Render::znear</c> = 0.1 (decomp :342173, re-landed
/// d4b5c71). The cap conversion below assumes the production camera near
/// plane; the small f/(fn) factor (~1.00002 at far 5000) is ignored.</summary>
public const float CameraNearPlaneMeters = 0.1f;
/// <summary>CPU mirror of the vertex-shader mark-bias expression (keep in
/// sync with <c>VertSrc</c>): the NDC bias applied at eye depth
/// <paramref name="eyeDepthMeters"/>.</summary>
public static float MarkBiasNdc(float eyeDepthMeters) =>
MathF.Min(PunchMarkDepthBias,
PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters
/ MathF.Max(eyeDepthMeters * eyeDepthMeters, 1e-6f));
/// <summary>
/// Draw one portal polygon as an invisible depth write, clipped to the
/// slice's clip-space half-planes. <paramref name="forceFarZ"/> selects
@ -130,13 +86,9 @@ public sealed partial class PortalDepthMaskRenderer : IDisposable
/// depth ALWAYS + true projected depth. It runs immediately after the
/// gated full depth clear, so there is no nearer content to stomp.</para>
///
/// <para><b>Punch</b> (outdoor root / look-in): two passes (#117).
/// Pass A marks stencil where the aperture fan passes a LEQUAL depth
/// test at its (biased) true depth — i.e. where the aperture is
/// actually visible against everything drawn so far. Pass B writes the
/// far-Z punch with depth ALWAYS but stencil-gated to the marked
/// pixels, and zeroes the stencil as it goes (self-cleaning). This is
/// the z-buffered equivalent of retail's painter's-order safety.</para>
/// <para><b>Punch</b> (outdoor root / look-in): one pass, retail-verbatim
/// — depth ALWAYS + far-Z. The ordered frame walk, not a visibility test
/// at the aperture plane, supplies retail's painter-order safety.</para>
/// </summary>
public void DrawDepthFan(
ReadOnlySpan<Vector3> worldVerts,

View file

@ -207,7 +207,6 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
private Action _drawExitSeals = null!;
private readonly HashSet<uint> _singleCellScratch = new();
private readonly List<uint> _singleCellListScratch = new();
private readonly Dictionary<uint, int> _singleCellClipScratch = new(1);
internal WalkProductionLeafRenderer(
RetailPViewPassExecutor passes,
@ -244,26 +243,32 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
public void DrawTerrainSlice(int sliceIndex) =>
_passes.DrawWalkTerrainSlice(_frame, _clipAssembly, sliceIndex);
public void DrawCellShell(uint cellId, uint clipSlot)
public void DrawCellShell(uint cellId)
{
_singleCellClipScratch.Clear();
_singleCellClipScratch.Add(cellId, checked((int)clipSlot));
_passes.SetCellShellClipRouting(_singleCellClipScratch);
try
// Output-gated binary discriminator for the moving wall-textured
// triangles at the 0xF4180107/0112 floating stairs. Missing walls are
// intentional while enabled: this identifies the visual carrier and
// must not become a production admission rule.
if (AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipFloatingStairCellShells
&& cellId is 0xF4180107u or 0xF4180112u)
{
_singleCellScratch.Clear();
_singleCellScratch.Add(cellId);
_passes.DrawOpaqueCellShells(_singleCellScratch);
if (_passes.CellHasTransparentShell(cellId))
{
_singleCellListScratch.Clear();
_singleCellListScratch.Add(cellId);
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
}
return;
}
finally
// Retail DrawEnvCell @0x0059F170 submits the prepared EnvCell mesh
// whole after stamping it drawn-this-frame. obj_view_set prepares
// legacy polygon/object tests, but the use_built_mesh branch calls
// D3DPolyRender::DrawMesh directly; it does not slice the shell mesh
// once per portal polygon.
_singleCellScratch.Clear();
_singleCellScratch.Add(cellId);
_passes.DrawOpaqueCellShells(_singleCellScratch);
if (_passes.CellHasTransparentShell(cellId))
{
_passes.SetCellShellClipRouting(null);
_singleCellListScratch.Clear();
_singleCellListScratch.Add(cellId);
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
}
}

View file

@ -345,8 +345,13 @@ internal sealed partial class RetailPViewPassExecutor :
public void DrawExitPortalMask(
RetailPViewFrameInput frame,
RetailPViewCellSliceContext context) =>
DrawPortalDepthWrite(context, frame, forceFarZ: frame.RootCell.IsOutdoorNode);
uint cellId,
ReadOnlySpan<Vector4> clipPlanes) =>
DrawPortalDepthWrite(
cellId,
clipPlanes,
frame,
forceFarZ: frame.RootCell.IsOutdoorNode);
public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame,
@ -422,7 +427,8 @@ internal sealed partial class RetailPViewPassExecutor :
frame.CameraCellResolution);
private void DrawPortalDepthWrite(
RetailPViewCellSliceContext context,
uint cellId,
ReadOnlySpan<Vector4> clipPlanes,
RetailPViewFrameInput frame,
bool forceFarZ,
int? onlyPortalIndex = null)
@ -432,7 +438,14 @@ internal sealed partial class RetailPViewPassExecutor :
// apertures stamp far depth (punch). The renderer owns that choice.
if (_portalDepthMask is null)
return;
LoadedCell? cell = frame.Cells.Find(context.CellId);
if (!forceFarZ
&& AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipFloatingStairSeals
&& cellId is 0xF4180107u or 0xF4180112u)
{
return;
}
LoadedCell? cell = frame.Cells.Find(cellId);
if (cell is null)
return;
@ -462,7 +475,7 @@ internal sealed partial class RetailPViewPassExecutor :
_diagnostics.EmitSeamMask(
RenderingDiagnostics.ProbeSeamDrawEnabled,
RenderingDiagnostics.SeamDrawTargetCells,
context.CellId,
cellId,
index,
forceFarZ,
world[..count]);
@ -470,7 +483,7 @@ internal sealed partial class RetailPViewPassExecutor :
_portalDepthMask.DrawDepthFan(
world[..count],
frame.ViewProjection,
context.Slice.Planes,
clipPlanes,
forceFarZ);
}
}

View file

@ -20,7 +20,6 @@ internal sealed class RetailPViewRenderer
private static readonly ClipViewSlice NoClipSlice =
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice };
private static readonly IReadOnlySet<uint> NoParticleOwners =
new HashSet<uint>();
@ -39,6 +38,13 @@ internal sealed class RetailPViewRenderer
// 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
@ -72,7 +78,8 @@ internal sealed class RetailPViewRenderer
Walk.WalkBuildingRegistry walkBuildings,
Walk.WalkLandscapeAssembler walkLandscape,
CellVisibility walkCellRegistry,
ShadowObjectRegistry shadows)
ShadowObjectRegistry shadows,
Func<uint, uint?>? findParentLocalId = null)
{
_renderSceneShadow = renderSceneShadow
?? throw new ArgumentNullException(nameof(renderSceneShadow));
@ -84,7 +91,8 @@ internal sealed class RetailPViewRenderer
?? throw new ArgumentNullException(nameof(walkCellRegistry));
_walkWorldData = new Walk.WalkProductionWorldData(
_walkBuildings,
shadows ?? throw new ArgumentNullException(nameof(shadows)));
shadows ?? throw new ArgumentNullException(nameof(shadows)),
findParentLocalId);
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
_walkDrawExitSealsAction = DrawWalkExitSeals;
}
@ -282,6 +290,134 @@ internal sealed class RetailPViewRenderer
_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
@ -391,14 +527,11 @@ internal sealed class RetailPViewRenderer
RetailPViewPassExecutor passes = _activeWalkPasses
?? throw new InvalidOperationException(
"The retained walk leaf has no active pass binding.");
ClipFrameAssembly clipAssembly = _activeWalkClipAssembly
?? throw new InvalidOperationException(
"The retained walk leaf has no active clip binding.");
Walk.WalkFrameDriver driver = _walkFrameDriverScratch
?? throw new InvalidOperationException(
"The retained walk leaf has no active driver binding.");
DrawWalkExitPortalMasks(frame, passes, clipAssembly, driver);
DrawWalkExitPortalMasks(frame, passes, driver);
}
private void ClearWalkFrameBindings()
@ -528,27 +661,27 @@ internal sealed class RetailPViewRenderer
/// #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). Per-cell
/// slice clips still come from the old assembly where present; a cell
/// the old apparatus missed seals unclipped (the depth fan is the exact
/// dat aperture polygon and z-tests, so over-coverage is benign).</summary>
/// 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,
ClipFrameAssembly clipAssembly,
Walk.WalkFrameDriver driver)
{
List<uint> floodCells = driver.InteriorFloodCells;
for (int i = floodCells.Count - 1; i >= 0; i--)
{
uint cellId = floodCells[i];
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
{
passes.DrawExitPortalMask(
ctx,
new RetailPViewCellSliceContext(
cellId,
slice,
NoParticleOwners));
cellId,
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
}
}
}
@ -573,18 +706,6 @@ internal sealed class RetailPViewRenderer
MeshPartCount: 0);
}
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

View file

@ -20,7 +20,6 @@
// selector. This shader's two passes ARE seal
// (retail maxZ2, true projected depth) and punch
// (retail maxZ1, far-plane z).
// uDepthBias / EyeCapN -> uParamA / uParamB.
layout(location = 0) in vec3 aPos;
@ -29,8 +28,6 @@ layout(location = 0) in vec3 aPos;
// the shared 96-byte push block (tools/ShaderCompiler/VulkanGlslPreamble.cs).
uniform mat4 uViewProjection;
uniform int uRenderPass; // 0 = seal (retail maxZ2), 1 = punch (retail maxZ1)
uniform float uParamA; // #117 mark-pass NDC bias toward the viewer
uniform float uParamB; // #129 eye-span cap x near plane
layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
int uTerrainClipCount;
@ -55,16 +52,5 @@ void main()
{
clipPos.z = clipPos.w * 0.99999988; // retail far-z punch constant (0x0059bc90 tail)
}
else if (uParamA > 0.0)
{
// #117 mark-pass bias, #129 eye-space cap. clipPos.w = eye depth d;
// an NDC bias b spans ~b*d*d/near meters of eye depth, so the
// constant-NDC form alone reached METERS at distance (door-shaped
// leaks through hills/houses). Keep in sync with
// PortalDepthMaskRenderer.MarkBiasNdc.
float biasNdc = min(uParamA, uParamB / max(clipPos.w * clipPos.w, 1e-6));
clipPos.z -= biasNdc * clipPos.w;
}
gl_Position = clipPos;
}

View file

@ -295,7 +295,7 @@
"stages": [
{
"stage": "vert",
"sourceSha256": "6214fc04936d92320594acb722e63f1cd1f7106dd4d0af60ed985abc2a50c4a9",
"sourceSha256": "1df7e2009cda8f84ba3d84546bf58baf71d10fe53e696655b4ca7b8292a32fa5",
"compiled": true
},
{

View file

@ -119,8 +119,10 @@ internal interface IWalkFrameLeafRenderer
/// draws ALL shells in reverse <c>cell_draw_list</c> order, then starts a
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
/// The ordinary interior-root flood and every building look-in flood use
/// this same two-pass discipline.</summary>
void DrawCellShell(uint cellId, uint clipSlot);
/// this same two-pass discipline. Retail's runtime <c>use_built_mesh</c>
/// branch submits the complete constructed shell; the cell's drawn stamp,
/// portal depth writes, and later depth-tested repaint own visibility.</summary>
void DrawCellShell(uint cellId);
/// <summary>One landscape cell's or building shell's static-owner
/// particle submission, at its own walk turn — see
@ -238,9 +240,10 @@ internal enum WalkFrameEventKind : byte
TerrainSlice,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the cell and
/// <see cref="WalkFrameEvent.IntArg"/> is the exact portal-view GPU clip
/// slot for this retail <c>DrawEnvCell</c> turn.</summary>
/// <see cref="WalkFrameEvent.CellId"/> is the cell. Retail's
/// <c>DrawEnvCell</c> marks the EnvCell drawn before submitting its built
/// mesh, so the shell is whole and frame-deduplicated; portal-view slices
/// are not shell geometry clips.</summary>
CellShell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> —
@ -308,6 +311,14 @@ internal interface IWalkLookInViewSource
in Vector3 center,
float radius,
bool testSphere);
/// <summary>Output-only description of the exact CY/edge-plane distances
/// used by one DrawObjCell turn. Kept behind the existing Facility probe;
/// production admission never consumes this text.</summary>
string DescribeLookInTurn(
int routeIndex,
in Vector3 center,
float radius) => "unavailable";
}
internal readonly record struct WalkLookInSlice(
@ -367,8 +378,8 @@ internal readonly struct WalkFrameEvent
internal static WalkFrameEvent TerrainSlice(int sliceIndex) =>
new(WalkFrameEventKind.TerrainSlice, sliceIndex, 0, 0f, null);
internal static WalkFrameEvent CellShell(uint cellId, uint clipSlot) =>
new(WalkFrameEventKind.CellShell, checked((int)clipSlot), cellId, 0f, null);
internal static WalkFrameEvent CellShell(uint cellId) =>
new(WalkFrameEventKind.CellShell, 0, cellId, 0f, null);
internal static WalkFrameEvent PunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
new(WalkFrameEventKind.PunchFan, activeViewIndex, 0, 0f, worldPolygon);
@ -505,12 +516,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private readonly List<int> _floodViewRouteScratch = new();
private WalkPlane _lookInCyPlane;
// Retail adds each physics object to every overlapped cell's shadow-part
// list, then CPhysicsPart::Get/SetDrawnThisFrame (0x0059F388) prevents
// those aliases from drawing more than once. The walk's outdoor buckets
// now carry the same multi-cell aliases, so retain the same frame guard.
private readonly HashSet<RenderProjectionId> _outdoorDrawnThisFrame = new();
private readonly HashSet<uint> _outdoorParticleOwnersDrawnThisFrame = new();
private readonly HashSet<uint> _cellShellsDrawnThisFrame = new();
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
@ -529,6 +536,52 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
/// alpha drain splats through (the cathedral falls shine-through).</summary>
internal List<uint> InteriorFloodCells { get; } = new();
/// <summary>Number of exact walk-owned portal-view slices that retail's
/// <c>CEnvCell::setup_view</c> installs while sealing one entry in
/// <see cref="InteriorFloodCells"/>. The matching route was captured at
/// that cell's shell turn and remains valid through synchronous replay.</summary>
internal int InteriorFloodViewSliceCountAt(int floodCellIndex)
{
int routeIndex = InteriorFloodViewRouteAt(floodCellIndex);
return _lookInTurns[routeIndex].SliceCount;
}
/// <summary>Borrows the clip-space half-planes for one exact walk-owned
/// portal-view slice. An empty span is the legitimate no-clip slot, not a
/// fallback to another visibility structure.</summary>
internal ReadOnlySpan<Vector4> InteriorFloodViewClipPlanesAt(
int floodCellIndex,
int sliceOffset)
{
int routeIndex = InteriorFloodViewRouteAt(floodCellIndex);
WalkLookInTurn turn = _lookInTurns[routeIndex];
if ((uint)sliceOffset >= (uint)turn.SliceCount)
throw new ArgumentOutOfRangeException(nameof(sliceOffset));
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (_clipFrame is null)
return ReadOnlySpan<Vector4>.Empty;
return _clipFrame.GetSlotPlanes(slice.ClipSlot);
}
private int InteriorFloodViewRouteAt(int floodCellIndex)
{
if ((uint)floodCellIndex >= (uint)InteriorFloodCells.Count
|| (uint)floodCellIndex >= (uint)_floodViewRouteScratch.Count)
{
throw new ArgumentOutOfRangeException(nameof(floodCellIndex));
}
int routeIndex = _floodViewRouteScratch[floodCellIndex];
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
{
throw new InvalidOperationException(
$"Interior flood cell 0x{InteriorFloodCells[floodCellIndex]:X8} "
+ "has no captured portal-view route.");
}
return routeIndex;
}
// Replay scratch for StaticParticles events (sequential replay — one
// reused set is safe).
private readonly HashSet<uint> _staticParticleOwnerScratch = new();
@ -624,8 +677,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_lookInPlanes.Clear();
_visibleClipSlotScratch.Clear();
_floodViewRouteScratch.Clear();
_outdoorDrawnThisFrame.Clear();
_dispatcher.EndWalkPartFrame();
_outdoorParticleOwnersDrawnThisFrame.Clear();
_cellShellsDrawnThisFrame.Clear();
_lookInCyPlane = default;
LookInCells.Clear();
VisitedBuildings.Clear();
@ -741,6 +795,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
+ "EndFrame (or let a thrown exception's cleanup run) before starting the next.");
}
_dispatcher.BeginWalkPartFrame();
_ctx = ctx;
_viewProjection = viewProjection;
_cameraWorldPosition = cameraWorldPosition;
@ -759,8 +814,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_lookInPlanes.Clear();
_visibleClipSlotScratch.Clear();
_lookInCyPlane = ctx.CyPlane;
_outdoorDrawnThisFrame.Clear();
_outdoorParticleOwnersDrawnThisFrame.Clear();
_cellShellsDrawnThisFrame.Clear();
LookInCells.Clear();
VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear();
@ -784,6 +839,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
}
finally
{
_dispatcher.EndWalkPartFrame();
_ctx = null;
_readyToReplay = true;
}
@ -854,7 +910,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_leafRenderer.DrawTerrainSlice(e.IntArg);
break;
case WalkFrameEventKind.CellShell:
_leafRenderer.DrawCellShell(e.CellId, checked((uint)e.IntArg));
_leafRenderer.DrawCellShell(e.CellId);
break;
case WalkFrameEventKind.PunchFan:
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
@ -920,6 +976,113 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
}
}
/// <summary>
/// Output-only Campaign FW cathedral trace. The event list described here
/// is the same list <see cref="Replay"/> immediately executes, so this
/// captures the actual clear/seal/punch/shell submission order without
/// inserting a second visibility walk or changing rendering behavior.
/// </summary>
internal void TraceCathedralShellOrder(
ulong frame,
uint viewerCellId,
uint playerCellId,
uint rootCellId,
Vector3 viewerEye)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralShellOrderEnabled
|| !_readyToReplay
|| (frame != 1 && frame % 5 != 0))
{
return;
}
static bool IsCathedralCell(uint cellId) =>
(cellId & 0xFFFF0000u) == 0xF4180000u
&& (cellId & 0xFFFFu) >= 0x0100u;
static bool IsFloatingStairCell(uint cellId) => cellId is
0xF4180104u or 0xF4180106u or 0xF4180107u
or 0xF4180112u or 0xF4180113u or 0xF4180114u;
var order = new System.Text.StringBuilder(1024);
order.Append("[cathedral-shell-order] f=").Append(frame)
.Append(" viewer=0x").Append(viewerCellId.ToString("X8"))
.Append(" player=0x").Append(playerCellId.ToString("X8"))
.Append(" root=0x").Append(rootCellId.ToString("X8"))
.Append(" eye=(").Append(viewerEye.X.ToString("F4"))
.Append(',').Append(viewerEye.Y.ToString("F4"))
.Append(',').Append(viewerEye.Z.ToString("F4")).Append(')')
.Append(" flood=[");
for (int i = 0; i < InteriorFloodCells.Count; i++)
{
if (i != 0)
order.Append(',');
order.Append("0x").Append(InteriorFloodCells[i].ToString("X8"));
}
order.Append("] lookIn=[");
for (int i = 0; i < LookInCellTurns.Count; i++)
{
if (i != 0)
order.Append(',');
order.Append("0x").Append(LookInCellTurns[i].ToString("X8"));
}
order.Append("] replay=");
var clips = new System.Text.StringBuilder(1024);
clips.Append("[cathedral-shell-clips] f=").Append(frame).Append(" slots=");
bool wroteClip = false;
for (int i = 0; i < _events.Count; i++)
{
WalkFrameEvent e = _events[i];
switch (e.Kind)
{
case WalkFrameEventKind.StreamMark:
order.Append('>').Append(i).Append(":M").Append(e.IntArg);
break;
case WalkFrameEventKind.AlphaSubmitMark:
order.Append('>').Append(i).Append(":AM").Append(e.IntArg);
break;
case WalkFrameEventKind.Sky:
order.Append('>').Append(i).Append(":SKY");
break;
case WalkFrameEventKind.TerrainSlice:
order.Append('>').Append(i).Append(":T").Append(e.IntArg);
break;
case WalkFrameEventKind.CellShell when IsCathedralCell(e.CellId):
order.Append('>').Append(i).Append(":S")
.Append((e.CellId & 0xFFFFu).ToString("X4"));
if (IsFloatingStairCell(e.CellId))
{
if (wroteClip)
clips.Append(';');
wroteClip = true;
clips.Append('S')
.Append((e.CellId & 0xFFFFu).ToString("X4"))
.Append("=whole-once");
}
break;
case WalkFrameEventKind.PunchFan:
order.Append('>').Append(i).Append(":P").Append(e.IntArg);
break;
case WalkFrameEventKind.AlphaBarrier:
order.Append('>').Append(i).Append(":AB");
break;
case WalkFrameEventKind.ClearInteriorDepth:
order.Append('>').Append(i).Append(":CLEAR");
break;
case WalkFrameEventKind.ExitSeals:
order.Append('>').Append(i).Append(":SEALS");
break;
}
}
Console.WriteLine(order.ToString());
Console.WriteLine(clips.ToString());
}
// ------------------------------------------------------------------
// IWalkEventSink
// ------------------------------------------------------------------
@ -1006,13 +1169,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_populator.PopulateOutdoorStatics(
_stream, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame,
_alphaSubmissions);
this, _landscapeViewRouteIndex,
alphaSubmissions: _alphaSubmissions);
_populator.PopulateCellDynamics(
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame,
_alphaSubmissions);
this, _landscapeViewRouteIndex,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
@ -1078,14 +1241,21 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// look-in flood appended (keeps every range single-stage).
MarkIfGrown();
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
_populator.PopulateCell(
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
bool skipSouthBuildingShell =
AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipSouthBuildingShell
&& WalkProductionWorldData.AnchorCellId(building) == 0xF4180112u;
if (!skipSouthBuildingShell)
{
MarkIfGrown();
MarkAlphaIfGrown();
_populator.PopulateCell(
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
MarkAlphaIfGrown();
}
}
// FW4 (the #132 positional invariant): the building's own shell
// emitters submit at the shell turn, after the shell content
@ -1104,6 +1274,22 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
ArgumentNullException.ThrowIfNull(polygon);
RequireOpenFrame();
// Output-gated binary discriminator for the cathedral's moving
// transparent seam. Cell 0xF4180112 belongs to a distinct authored
// building which is reached as a look-in while the camera remains in
// 0xF4180107/0108. Suppress only that building's far-Z punch so a
// visual gate can prove whether the apparent "beam" is the aperture
// depth write or the seven-part stair Setup behind it. Never enabled
// in normal production.
if (AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipStairBuildingPunch
&& Array.Exists(
building.Portals,
portal => portal.OtherCellId == 0xF4180112u))
{
return;
}
MarkIfGrown();
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
_events.Add(
@ -1115,12 +1301,28 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
ArgumentNullException.ThrowIfNull(cells);
RequireOpenFrame();
// PView::DrawCells @0x005a4840 advances m_nFrameStamp at 0x005a4886
// after LScape::draw + FlushAlphaList and before the depth clear. Its
// drawn-part AND DrawEnvCell dedupe is therefore per render stamp, not
// per presented frame: content admitted during the landscape must
// remain eligible for the interior-cell repaint after the clear. In
// Collect, every landscape candidate has been classified by this point
// and no interior-root candidate has, so re-arming both CPU-side stamp
// mirrors here is the exact boundary. Without the shell re-arm, color
// from a pre-clear building look-in survives while the root repaint is
// incorrectly suppressed, producing wall-textured bleed slabs.
MarkIfGrown();
if (_skyDrawnThisFrame)
{
_dispatcher.AdvanceWalkPartPassStamp();
_cellShellsDrawnThisFrame.Clear();
}
// PView::DrawCells @0x005a4840: the gated full depth clear
// (pc:432731-432732) then the exit-portal seals (pc:432785-432786) —
// both unconditional for an interior root's own flood, whether or
// not a landscape turn just ran (see this driver's type doc
// comment).
MarkIfGrown();
_events.Add(WalkFrameEvent.ClearInteriorDepth());
MarkIfGrown();
@ -1215,18 +1417,32 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_floodViewRouteScratch.Add(-1);
// PView::DrawCells @0x005A4840, loop 2 (005A4A00005A4ADE):
// cell_draw_list[count - 1] down to zero. DrawEnvCell loops the
// cell's live portal_view slices, so capture those slices here and
// replay the shell once per exact GPU clip slot.
// cell_draw_list[count - 1] down to zero. The loop calls setup_view +
// DrawEnvCell for each live view, but DrawEnvCell @0x0059F170 begins
// with GetDrawnThisFrame/SetDrawnThisFrame. Its built-mesh path then
// calls D3DPolyRender::DrawMesh directly: the EnvCell shell is drawn
// WHOLE on the first call and every later view/cross-flood call is a
// no-op for this render stamp. Portal-view clipping remains active for
// the following object-list loop; it is not a shell geometry clip.
for (int i = cells.Count - 1; i >= 0; i--)
{
int viewRouteIndex = CaptureCellViewRoute(cells[i]);
_floodViewRouteScratch[i] = viewRouteIndex;
IReadOnlyList<uint> clipSlots = VisibleClipSlotsInLookInTurn(
viewRouteIndex, default, 0f, testSphere: false);
MarkIfGrown();
for (int slotIndex = 0; slotIndex < clipSlots.Count; slotIndex++)
_events.Add(WalkFrameEvent.CellShell(cells[i], clipSlots[slotIndex]));
if (_cellShellsDrawnThisFrame.Add(cells[i]))
{
bool skipSouthLookInShell =
stage == WalkDrawStage.LookInStatic
&& ((AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipSouthLookInCellShells
&& cells[i] is 0xF4180112u or 0xF4180113u or 0xF4180114u)
|| AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipLookInShellCellId == cells[i]);
if (!skipSouthLookInShell)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.CellShell(cells[i]));
}
}
}
// Loop 3 (005A4ADE005A4B2D): restart at count - 1 and draw each
@ -1372,6 +1588,40 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
return _visibleClipSlotScratch;
}
public string DescribeLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return "route-missing";
WalkLookInTurn turn = _lookInTurns[routeIndex];
var description = new System.Text.StringBuilder(192);
float cyDistance = Vector3.Dot(_lookInCyPlane.Normal, center)
+ _lookInCyPlane.D;
description.Append("turnCell=0x")
.Append(turn.CellId.ToString("X8"))
.Append(" slices=").Append(turn.SliceCount)
.Append(" cy=").Append(cyDistance.ToString("F5"))
.Append(" cyMargin=").Append((cyDistance + radius).ToString("F5"));
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
description.Append(" slice[").Append(sliceOffset).Append("]=");
for (int planeOffset = 0; planeOffset < slice.PlaneCount; planeOffset++)
{
if (planeOffset != 0)
description.Append(',');
WalkPlane plane = _lookInPlanes[slice.PlaneStart + planeOffset];
float distance = Vector3.Dot(plane.Normal, center) + plane.D;
description.Append(distance.ToString("F5"));
}
}
return description.ToString();
}
/// <summary>Converts one retail pixel-space portal_view polygon into the
/// clip-space half-planes consumed by mesh_modern.vert. copy_view already
/// deduplicates and removes collinear vertices, so 3..8 points map

View file

@ -24,6 +24,12 @@ public sealed class WalkPView
{
private static int _masterTimestamp;
/// <summary>
/// Output-only correlation key for diagnostics emitted downstream from
/// the same frame walk. Production admission must never depend on it.
/// </summary>
internal static int MasterTimestampForDiagnostics => _masterTimestamp;
private readonly struct TodoEntry(WalkCell cell, float dist)
{
public readonly WalkCell Cell = cell;
@ -177,12 +183,28 @@ public sealed class WalkPView
for (int j = 0; j < cell.Portals.Length; j++)
{
ref WalkPortalFlags flags = ref top.PortalFlags[j];
if (!flags.Seen || flags.InView) continue;
ref WalkCellPortal portal = ref cell.Portals[j];
if (!flags.Seen || flags.InView)
{
EmitFacilityPortalProbe(
cell, j, viewIndex: -1, top, portal, flags, ctx,
projectedCount: -1, clippedCount: -1,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "flag-gate");
continue;
}
if (cell.CachedNeighbors[j] is null && portal.OtherCellId != 0xFFFFFFFFu)
{
cell.CachedNeighbors[j] = ctx.GetVisible(portal.OtherCellId);
if (cell.CachedNeighbors[j] is null) continue; // not loaded: silently dead
if (cell.CachedNeighbors[j] is null)
{
EmitFacilityPortalProbe(
cell, j, viewIndex: -1, top, portal, flags, ctx,
projectedCount: -1, clippedCount: -1,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "neighbor-missing");
continue; // not loaded: silently dead
}
}
anyLive = true;
}
@ -201,7 +223,17 @@ public sealed class WalkPView
cell, portal.PortalSide,
cell.PortalPolygons[portal.PolygonIndex],
doClip: true, ctx, _clipScratch);
if (n == 0) continue;
int projectedCount =
cell.PortalPolygons[portal.PolygonIndex].Vertices.Length;
if (n == 0)
{
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, clippedCount: 0,
neighborViewsBefore: -1, neighborViewsAfter: -1,
appended: false, result: "clip-empty");
continue;
}
if (portal.OtherCellId == 0xFFFFFFFFu)
{
@ -223,18 +255,125 @@ public sealed class WalkPView
{
n = OtherPortalClip(cell, j, n, ctx);
SetView(top, i); // restore after the far-frame excursion
if (n == 0) continue;
if (n == 0)
{
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, clippedCount: 0,
neighborViewsBefore: neighbor.TopView.ViewCount,
neighborViewsAfter: neighbor.TopView.ViewCount,
appended: false, result: "far-clip-empty");
continue;
}
}
int before = neighbor.TopView.ViewCount;
bool appended = false;
if (neighbor.NumView != 0)
WalkCopyView.Append(
appended = WalkCopyView.Append(
neighbor.TopView, _clipScratch.AsSpan(0, n),
ctx.Rays, ctx.WorldViewpoint);
EmitFacilityPortalProbe(
cell, j, i, top, portal, flags, ctx,
projectedCount, n, before, neighbor.TopView.ViewCount,
appended,
neighbor.NumView == 0 ? "neighbor-not-pushed"
: appended ? "view-appended" : "append-rejected");
}
}
}
return true;
}
/// <summary>
/// Output-only Facility Hub discriminator for the residual #177 stair
/// disappearance. It traces only portals entering cell 0x8A02015F so a
/// side-to-away camera turn can distinguish flag admission, homogeneous
/// portal clipping, and copy_view append without changing the walk.
/// </summary>
private void EmitFacilityPortalProbe(
WalkCell cell,
int portalIndex,
int viewIndex,
WalkPortalView top,
WalkCellPortal portal,
WalkPortalFlags flags,
IWalkFrameContext ctx,
int projectedCount,
int clippedCount,
int neighborViewsBefore,
int neighborViewsAfter,
bool appended,
string result)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled
|| portal.OtherCellId != 0x8A02015Fu
|| (cell.CellId & 0xFFFF0000u) != 0x8A020000u)
{
return;
}
string activeBounds = "none";
if ((uint)viewIndex < (uint)top.ViewCount)
{
WalkViewPoly active = top.View.Polys[viewIndex];
activeBounds = $"({active.XMin:F2},{active.YMin:F2})-"
+ $"({active.XMax:F2},{active.YMax:F2})";
}
string projectedBounds = DescribeScreenBounds(
_projectScratch.AsSpan(0, Math.Max(projectedCount, 0)));
string clippedBounds = DescribeScreenBounds(
_clipScratch.AsSpan(0, Math.Max(clippedCount, 0)));
Console.WriteLine(
$"[facility-portal] frame={_masterTimestamp} "
+ $"from=0x{cell.CellId:X8} portal={portalIndex} "
+ $"to=0x{portal.OtherCellId:X8} back={portal.OtherPortalId} "
+ $"view={viewIndex}/{top.ViewCount} startFlags="
+ $"seen:{(flags.Seen ? 1 : 0)},in:{(flags.InView ? 1 : 0)} "
+ $"side={portal.PortalSide} exact={(portal.ExactMatch ? 1 : 0)} "
+ $"eye=({ctx.WorldViewpoint.X:F6},{ctx.WorldViewpoint.Y:F6},"
+ $"{ctx.WorldViewpoint.Z:F6}) active={activeBounds} "
+ $"projected={projectedCount}:{projectedBounds} "
+ $"clipped={clippedCount}:{clippedBounds} "
+ $"neighborViews={neighborViewsBefore}->{neighborViewsAfter} "
+ $"append={(appended ? 1 : 0)} result={result}");
}
private static string DescribeScreenBounds(ReadOnlySpan<WalkScreenPoint> points)
{
if (points.IsEmpty)
return "none";
float xmin = float.PositiveInfinity;
float xmax = float.NegativeInfinity;
float ymin = float.PositiveInfinity;
float ymax = float.NegativeInfinity;
float wmin = float.PositiveInfinity;
float wmax = float.NegativeInfinity;
int divided = 0;
for (int index = 0; index < points.Length; index++)
{
ref readonly WalkScreenPoint point = ref points[index];
wmin = MathF.Min(wmin, point.W);
wmax = MathF.Max(wmax, point.W);
if (point.W < WalkScreenClip.MinW)
continue;
float x = point.X / point.W;
float y = point.Y / point.W;
xmin = MathF.Min(xmin, x);
xmax = MathF.Max(xmax, x);
ymin = MathF.Min(ymin, y);
ymax = MathF.Max(ymax, y);
divided++;
}
return divided == 0
? $"behind(w={wmin:F4}..{wmax:F4})"
: $"({xmin:F2},{ymin:F2})-({xmax:F2},{ymax:F2});"
+ $"w={wmin:F4}..{wmax:F4};front={divided}";
}
// ------------------------------------------------------------------
// OtherPortalClip @0x005a5400 — the double clip for non-exact_match
// portals: snapshot the near-clipped poly as a temp view, then re-clip

View file

@ -1,5 +1,6 @@
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Physics;
@ -12,10 +13,18 @@ namespace AcDream.App.Rendering.Walk;
/// <see cref="BeginFrame"/>:
///
/// <list type="bullet">
/// <item>Cell statics — <see cref="RenderSceneQuery.CopyCellStaticsTo"/> on
/// demand, one arena segment per distinct cell per frame (a cell can be
/// visited once by the root flood OR once per admitting look-in portal; the
/// per-frame cache keeps the copy single).</item>
/// <item>Cell statics — ONE <see cref="RenderSceneQuery.CopyIndexTo"/> sweep
/// over authored indoor statics, bucketed into every cell crossed by the
/// object's visual parts. This is retail's <c>CPartArray::AddPartsShadow</c>
/// render index, which deliberately includes non-colliding decorations and
/// is distinct from the physics shadow-object index.</item>
/// <item>Live dynamics — ONE global dynamic-index sweep, bucketed into every
/// cell in the object's retained physics <c>CELLARRAY</c>. Retail feeds that
/// exact array to <c>CPhysicsObj::add_shadows_to_cells</c>, which calls
/// <c>CPartArray::AddPartsShadow</c> for every member cell. A creature crossing
/// a stair portal must therefore remain drawable from both the feet cell and
/// the head cell; indexing only by its authored parent makes individual body
/// parts disappear at the portal edge.</item>
/// <item>Outdoor statics — ONE <see cref="RenderSceneQuery.CopyIndexTo"/>
/// sweep bucketed by landscape cell id
/// (<c>(lb &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> from the record's
@ -24,10 +33,10 @@ namespace AcDream.App.Rendering.Walk;
/// shell turn, retail <c>CPhysicsPart::Draw(parts, 0)</c> @0x0059f331, not
/// at the cell's <c>DrawObjCell</c> turn).</item>
/// <item>Building shells — the same sweep's <c>IsBuildingShell</c> records
/// bucketed by <c>Source.BuildingShellAnchorCellId</c>; a
/// <see cref="WalkBuilding"/> maps to its anchor via its first
/// non-exit portal's destination (the SAME rule
/// <c>LandblockLoader</c> used to author the anchor).</item>
/// bucketed by <c>Source.BuildingShellAnchorCellId</c> for portal-bearing
/// buildings. Portal-less buildings have no interior anchor; retail still
/// draws them at their landscape position-cell turn, so those records use
/// <c>Source.EffectCellId</c>, matching <see cref="WalkBuilding.PositionCellId"/>.</item>
/// </list>
///
/// The tuple landblock id handed to the classifier is the frame's player
@ -56,6 +65,7 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
private readonly WalkBuildingRegistry _buildings;
private readonly ShadowObjectRegistry _shadows;
private readonly Func<uint, uint?> _findParentLocalId;
private RenderSceneQuery _scene;
private uint _tupleLandblockId;
private int _renderCenterLbX;
@ -63,16 +73,22 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellDynamicCache = new();
private readonly Dictionary<string, string> _facilityShadowProbeSignatures = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _indoorByCell = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _indoorDynamicsByCell = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorByCell = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorDynamicsByCell = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorDynamicsMaterialized = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _shellMaterialized = new();
private readonly Dictionary<RenderProjectionId, StaticRenderCellCacheEntry>
_staticRenderCellCache = new();
private ulong _staticRenderCellIndexRevision = ulong.MaxValue;
private RenderSceneGeneration _staticRenderCellGeneration;
private RenderProjectionRecord[] _indoorSweepScratch = new RenderProjectionRecord[256];
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
private RenderProjectionRecord[] _dynamicSweepScratch = new RenderProjectionRecord[256];
private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
private RenderProjectionRecord[] _cellDynamicScratch = new RenderProjectionRecord[256];
// Campaign FW3.4a: the per-frame, grow-only materialization arena — see
// this type's own doc comment.
@ -81,12 +97,16 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
internal WalkProductionWorldData(
WalkBuildingRegistry buildings,
ShadowObjectRegistry shadows)
ShadowObjectRegistry shadows,
Func<uint, uint?>? findParentLocalId = null)
{
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
_findParentLocalId = findParentLocalId ?? NoParentLocalId;
}
private static uint? NoParentLocalId(uint _) => null;
/// <summary>Rebuilds the frame's outdoor/shell buckets and clears the
/// per-cell cache. Call once per frame before the driver runs.
/// <paramref name="renderCenterLbX"/>/<paramref name="renderCenterLbY"/>
@ -112,6 +132,10 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
_outdoorDynamicsMaterialized.Clear();
_shellMaterialized.Clear();
_arenaLength = 0;
foreach (List<RenderProjectionRecord> bucket in _indoorByCell.Values)
bucket.Clear();
foreach (List<RenderProjectionRecord> bucket in _indoorDynamicsByCell.Values)
bucket.Clear();
foreach (List<RenderProjectionRecord> bucket in _outdoorByCell.Values)
bucket.Clear();
foreach (List<RenderProjectionRecord> bucket in _outdoorDynamicsByCell.Values)
@ -119,23 +143,56 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
foreach (List<RenderProjectionRecord> bucket in _shellsByAnchor.Values)
bucket.Clear();
if (_staticRenderCellIndexRevision != scene.IndexRevision
|| _staticRenderCellGeneration != scene.Generation)
{
_staticRenderCellCache.Clear();
_staticRenderCellIndexRevision = scene.IndexRevision;
_staticRenderCellGeneration = scene.Generation;
}
// Retail CEnvCell::init_static_objects does not leave an object solely
// in its authored parent cell. add_obj_to_cell ->
// calc_cross_cells_static -> CPartArray::AddPartsShadow registers all
// visual parts in every crossed cell, including parts with no physics
// BSP. Build that render-only index before the walk starts.
int required = _scene.IndexCounts.For(RenderSceneIndex.IndoorCellStatic);
if (required > _indoorSweepScratch.Length)
{
_indoorSweepScratch = new RenderProjectionRecord[
Math.Max(required, _indoorSweepScratch.Length * 2)];
}
int count = _scene.CopyIndexTo(
RenderSceneIndex.IndoorCellStatic,
_indoorSweepScratch);
for (int i = 0; i < count; i++)
{
ref readonly RenderProjectionRecord record = ref _indoorSweepScratch[i];
IReadOnlyList<uint> renderCells = ResolveStaticRenderCells(in record);
BucketIndoorRecord(
in record,
renderCells,
_indoorByCell,
_outdoorByCell);
}
// CopyIndexTo THROWS on an undersized destination (ArchRenderScene
// validates up front — the first connected gate run of the FW3.2b-2
// cutover crashed on exactly this at Aerlinthe's 5,040 outdoor
// statics), so presize from the query's own index counts.
int required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorStatic);
required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorStatic);
if (required > _sweepScratch.Length)
{
_sweepScratch = new RenderProjectionRecord[
Math.Max(required, _sweepScratch.Length * 2)];
}
int count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
for (int i = 0; i < count; i++)
{
ref readonly RenderProjectionRecord record = ref _sweepScratch[i];
if (record.EntityPayload.IsBuildingShell)
{
uint anchor = record.Source.BuildingShellAnchorCellId;
uint anchor = BuildingShellBucketCellId(in record);
if (!_shellsByAnchor.TryGetValue(anchor, out List<RenderProjectionRecord>? shells))
_shellsByAnchor[anchor] = shells = new List<RenderProjectionRecord>();
shells.Add(record);
@ -149,27 +206,260 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
_renderCenterLbY);
}
required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorDynamic);
// Retail CPhysicsObj::add_shadows_to_cells installs every PartArray in
// every cell from calc_cross_cells' retained CELLARRAY. The scene's
// parent-cell dictionary cannot represent that membership, so consume
// the global dynamic index once and rebuild both indoor and outdoor
// render buckets from ShadowObjectRegistry's exact retained array.
required = _scene.IndexCounts.For(RenderSceneIndex.Dynamic);
if (required > _dynamicSweepScratch.Length)
{
_dynamicSweepScratch = new RenderProjectionRecord[
Math.Max(required, _dynamicSweepScratch.Length * 2)];
}
count = _scene.CopyIndexTo(
RenderSceneIndex.OutdoorDynamic,
RenderSceneIndex.Dynamic,
_dynamicSweepScratch);
for (int i = 0; i < count; i++)
{
ref readonly RenderProjectionRecord record = ref _dynamicSweepScratch[i];
BucketOutdoorRecord(
IReadOnlyList<uint> renderCells = ResolveDynamicRenderCells(
in record,
_shadows.GetOwnerCells(record.Source.LocalEntityId),
_shadows.GetOwnerCells,
_findParentLocalId);
BucketDynamicRecord(
in record,
renderCells,
_indoorDynamicsByCell,
_outdoorDynamicsByCell,
_renderCenterLbX,
_renderCenterLbY);
}
}
/// <summary>
/// Retail <c>CPhysicsObj::add_shadows_to_cells</c> recursively passes the
/// root object's same CELLARRAY to every child in its CHILDLIST. Attached
/// projections deliberately own no independent collision rows in acdream,
/// so walk their accepted parent chain until the root's retained array is
/// found. Ordinary dynamics continue to consume only their own rows.
/// </summary>
internal static IReadOnlyList<uint> ResolveDynamicRenderCells(
in RenderProjectionRecord record,
Func<uint, IReadOnlyList<uint>> getOwnerCells,
Func<uint, uint?> findParentLocalId)
{
ArgumentNullException.ThrowIfNull(getOwnerCells);
ArgumentNullException.ThrowIfNull(findParentLocalId);
uint current = record.Source.LocalEntityId;
IReadOnlyList<uint> cells = getOwnerCells(current);
if (cells.Count > 0
|| record.EntityPayload.CasterIdentity
!= RenderCasterIdentityKind.EquippedChild)
{
return cells;
}
// ParentAttachmentState rejects cycles. Keep a hard bound here so a
// corrupted diagnostic callback still cannot stall a render frame.
for (int depth = 0; depth < 64; depth++)
{
uint? parent = findParentLocalId(current);
if (parent is not { } parentId
|| parentId == 0u
|| parentId == current)
{
break;
}
current = parentId;
cells = getOwnerCells(current);
if (cells.Count > 0)
return cells;
}
return Array.Empty<uint>();
}
private IReadOnlyList<uint> ResolveStaticRenderCells(
in RenderProjectionRecord record)
{
if (_staticRenderCellCache.TryGetValue(
record.Id,
out StaticRenderCellCacheEntry cached)
&& cached.ParentCellId == record.Source.ParentCellId
&& cached.TransformFingerprint == record.Source.TransformFingerprint
&& cached.GeometryFingerprint == record.Source.GeometryFingerprint)
{
return cached.Cells;
}
IReadOnlyList<uint> collisionCells =
_shadows.GetOwnerCells(record.Source.LocalEntityId);
IReadOnlyList<uint> cells = Array.Empty<uint>();
int visualPartCount = 0;
bool hasPhysicsBsp = false;
bool primitiveSetup = false;
PhysicsDataCache? cache = _shadows.DataCache;
if (cache is not null
&& record.EntityPayload.MeshRefs is { Count: > 0 } meshRefs)
{
List<ShadowShape> visualParts =
ShadowShapeBuilder.FromStaticRenderParts(
meshRefs,
cache.GetGfxObj,
cache.GetVisualBounds,
out hasPhysicsBsp);
visualPartCount = visualParts.Count;
// A primitive-only Setup takes retail's cylsphere/sorting-sphere
// calc_cross_cells_static arm. Its collision registration already
// carries that exact cell set. BSP-bearing objects and pure visual
// objects take the per-visual-part box walk instead.
primitiveSetup =
(record.Source.SourceId & 0xFF000000u) == 0x02000000u
&& !hasPhysicsBsp
&& collisionCells.Count > 0;
if (primitiveSetup)
{
cells = collisionCells;
}
else if (visualParts.Count > 0)
{
cells = _shadows.ComputeStaticRenderCells(
record.Source.ParentCellId,
record.Transform.Position,
record.Transform.Rotation,
visualParts);
}
}
if (cells.Count == 0)
cells = collisionCells.Count > 0
? collisionCells
: record.Source.ParentCellId != 0u
? new[] { record.Source.ParentCellId }
: Array.Empty<uint>();
uint[] snapshot = cells as uint[] ?? cells.ToArray();
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled
&& record.Source.SourceId == 0x020009A2u)
{
static string DescribeCells(IReadOnlyList<uint> source) =>
"[" + string.Join(",", source.Select(id => $"0x{id:X8}")) + "]";
Console.WriteLine(
$"[cathedral-static-index] source=0x{record.Source.SourceId:X8} "
+ $"local=0x{record.Source.LocalEntityId:X8} "
+ $"parent=0x{record.Source.ParentCellId:X8} "
+ $"parts={visualPartCount} hasPhysicsBsp={(hasPhysicsBsp ? 1 : 0)} "
+ $"primitiveSetup={(primitiveSetup ? 1 : 0)} "
+ $"collision={DescribeCells(collisionCells)} "
+ $"render={DescribeCells(snapshot)}");
}
_staticRenderCellCache[record.Id] = new StaticRenderCellCacheEntry(
record.Source.ParentCellId,
record.Source.TransformFingerprint,
record.Source.GeometryFingerprint,
snapshot);
return snapshot;
}
internal bool StaticBucketContains(uint cellId, uint sourceId) =>
_indoorByCell.TryGetValue(
cellId,
out List<RenderProjectionRecord>? records)
&& records.Exists(record => record.Source.SourceId == sourceId);
/// <summary>
/// Buckets one authored indoor static into the render cell lists produced
/// by retail's cross-cell PartArray walk. Outdoor cells are routed to the
/// landscape turn because a visual part may cross an exit portal.
/// </summary>
internal static void BucketIndoorRecord(
in RenderProjectionRecord record,
IReadOnlyList<uint> renderCells,
Dictionary<uint, List<RenderProjectionRecord>> indoorBuckets,
Dictionary<uint, List<RenderProjectionRecord>> outdoorBuckets)
{
ArgumentNullException.ThrowIfNull(renderCells);
ArgumentNullException.ThrowIfNull(indoorBuckets);
ArgumentNullException.ThrowIfNull(outdoorBuckets);
bool added = false;
for (int i = 0; i < renderCells.Count; i++)
{
uint cellId = renderCells[i];
uint low = cellId & 0xFFFFu;
if (low is >= 1u and <= 64u)
{
AddToBucket(in record, cellId, outdoorBuckets);
added = true;
}
else if (low >= 0x100u)
{
AddToBucket(in record, cellId, indoorBuckets);
added = true;
}
}
if (!added && record.Source.ParentCellId != 0u)
AddToBucket(in record, record.Source.ParentCellId, indoorBuckets);
}
/// <summary>
/// Installs one live PartArray into every cell in retail's retained
/// <c>CELLARRAY</c>. Interior and landscape memberships can coexist while
/// crossing a building exit. If the collision owner is not registered yet
/// (or the object is a visual-only effect), the authored interior parent
/// or outdoor root-position cell remains the conservative fallback.
/// </summary>
internal static void BucketDynamicRecord(
in RenderProjectionRecord record,
IReadOnlyList<uint> shadowCells,
Dictionary<uint, List<RenderProjectionRecord>> indoorBuckets,
Dictionary<uint, List<RenderProjectionRecord>> outdoorBuckets,
int renderCenterLbX,
int renderCenterLbY)
{
ArgumentNullException.ThrowIfNull(shadowCells);
ArgumentNullException.ThrowIfNull(indoorBuckets);
ArgumentNullException.ThrowIfNull(outdoorBuckets);
bool added = false;
for (int i = 0; i < shadowCells.Count; i++)
{
uint cellId = shadowCells[i];
uint low = cellId & 0xFFFFu;
if (low is >= 1u and <= 64u)
{
AddToBucket(in record, cellId, outdoorBuckets);
added = true;
}
else if (low >= 0x100u)
{
AddToBucket(in record, cellId, indoorBuckets);
added = true;
}
}
if (added)
return;
uint parentLow = record.Source.ParentCellId & 0xFFFFu;
if (record.Source.ParentCellId != 0u && parentLow >= 0x100u)
{
AddToBucket(in record, record.Source.ParentCellId, indoorBuckets);
return;
}
uint outdoorCell = LandscapeCellId(
record.Transform.Position,
renderCenterLbX,
renderCenterLbY);
AddToBucket(in record, outdoorCell, outdoorBuckets);
}
/// <summary>
/// Installs one outdoor object's render shadow in every outdoor cell of
/// its authoritative physics <c>CELLARRAY</c>. This is retail's
@ -252,19 +542,13 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
// Same up-front-validation contract as CopyIndexTo: presize from the
// query's own count rather than probing with an undersized span.
int required = _scene.GetCellStaticCount(cellId);
if (required > _cellScratch.Length)
{
_cellScratch = new RenderProjectionRecord[
Math.Max(required, _cellScratch.Length * 2)];
}
int count = _scene.CopyCellStaticsTo(cellId, _cellScratch);
WalkFrameStaticRecords records = count == 0
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
: new WalkFrameStaticRecords(
AppendToArena(_cellScratch.AsSpan(0, count)), _tupleLandblockId);
WalkFrameStaticRecords records =
_indoorByCell.TryGetValue(cellId, out List<RenderProjectionRecord>? bucket)
&& bucket.Count > 0
? new WalkFrameStaticRecords(
AppendToArena(CollectionsMarshal.AsSpan(bucket)), _tupleLandblockId)
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
EmitFacilityShadowProbe(records.Records, cellId, "static");
_cellCache[cellId] = records;
return records;
}
@ -273,21 +557,81 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
int required = _scene.GetCellDynamicCount(cellId);
if (required > _cellDynamicScratch.Length)
{
_cellDynamicScratch = new RenderProjectionRecord[
Math.Max(required, _cellDynamicScratch.Length * 2)];
}
int count = _scene.CopyCellDynamicsTo(cellId, _cellDynamicScratch);
WalkFrameStaticRecords records = count == 0
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
: new WalkFrameStaticRecords(
AppendToArena(_cellDynamicScratch.AsSpan(0, count)), _tupleLandblockId);
WalkFrameStaticRecords records =
_indoorDynamicsByCell.TryGetValue(
cellId,
out List<RenderProjectionRecord>? bucket)
&& bucket.Count > 0
? new WalkFrameStaticRecords(
AppendToArena(CollectionsMarshal.AsSpan(bucket)), _tupleLandblockId)
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
EmitFacilityShadowProbe(records.Records, cellId, "dynamic");
_cellDynamicCache[cellId] = records;
return records;
}
/// <summary>
/// Facility Hub discriminator for retail's cross-cell render-shadow path.
/// The scene query is currently keyed by authored parent cell, while retail
/// also appends each object's parts to every cell in its physics CELLARRAY
/// through CPhysicsObj::add_shadows_to_cells / CPartArray::AddPartsShadow.
/// This probe prints the authoritative physics owner set without changing
/// admission, so a correction is made only if the live staircase/player
/// registration proves that path is populated.
/// </summary>
private void EmitFacilityShadowProbe(
ReadOnlySpan<RenderProjectionRecord> records,
uint queriedCellId,
string route)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
for (int recordIndex = 0; recordIndex < records.Length; recordIndex++)
{
ref readonly RenderProjectionRecord record = ref records[recordIndex];
bool targetPlayer = record.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer;
bool targetStair = record.Source.SourceId == 0x02000623u;
bool targetCathedralStair =
record.Source.SourceId == 0x020009A2u;
if (!targetStair && !targetCathedralStair && !targetPlayer)
continue;
IReadOnlyList<uint> ownerCells =
_shadows.GetOwnerCells(record.Source.LocalEntityId);
var cells = new StringBuilder(ownerCells.Count * 11 + 2);
cells.Append('[');
for (int cellIndex = 0; cellIndex < ownerCells.Count; cellIndex++)
{
if (cellIndex != 0)
cells.Append(',');
cells.Append("0x").Append(ownerCells[cellIndex].ToString("X8"));
}
cells.Append(']');
string key = $"{route}:{queriedCellId:X8}:"
+ $"{record.Source.LocalEntityId:X8}";
string signature = $"{record.Source.ParentCellId:X8}:{cells}";
if (_facilityShadowProbeSignatures.TryGetValue(key, out string? prior)
&& string.Equals(prior, signature, StringComparison.Ordinal))
{
continue;
}
_facilityShadowProbeSignatures[key] = signature;
Console.WriteLine(
$"[facility-shadow] route={route} "
+ $"kind={(targetPlayer ? "player" : targetCathedralStair ? "cathedral-stair" : "stair")} "
+ $"guid=0x{record.Source.ServerGuid:X8} "
+ $"local=0x{record.Source.LocalEntityId:X8} "
+ $"query=0x{queriedCellId:X8} "
+ $"parent=0x{record.Source.ParentCellId:X8} "
+ $"owners={cells}");
}
}
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId)
{
if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached))
@ -325,7 +669,7 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
{
uint anchor = AnchorCellId(building);
uint anchor = BuildingShellBucketCellId(building);
if (anchor == 0)
return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached))
@ -382,6 +726,30 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
return 0;
}
/// <summary>
/// Selects the retained-scene bucket for a shell record. Portal-bearing
/// buildings use their authored EnvCell anchor. A portal-less building has
/// no such anchor, but it is still a real <c>CBuildingObj</c>; retail's
/// <c>DrawSortCell</c> reaches it through the outdoor cell containing its
/// placement, represented by <c>EffectCellId</c> on the retained record.
/// </summary>
internal static uint BuildingShellBucketCellId(in RenderProjectionRecord record)
=> record.Source.BuildingShellAnchorCellId != 0
? record.Source.BuildingShellAnchorCellId
: record.Source.EffectCellId;
/// <summary>
/// Resolves the same bucket from the walk-side building. The authored
/// interior anchor wins when present; otherwise the landscape assembler's
/// exact position cell is the shell turn that retail uses.
/// </summary>
internal static uint BuildingShellBucketCellId(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
uint anchor = AnchorCellId(building);
return anchor != 0 ? anchor : building.PositionCellId;
}
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building)
{
if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry))
@ -391,4 +759,10 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
}
return entry.WorldTransform;
}
private readonly record struct StaticRenderCellCacheEntry(
uint ParentCellId,
RenderSceneHash128 TransformFingerprint,
RenderSceneHash128 GeometryFingerprint,
uint[] Cells);
}

View file

@ -110,7 +110,8 @@ internal sealed class WalkStaticStreamPopulator
ClassifyAndAppend(
stream, WalkDrawStage.OutdoorStatic, cellId, in records[i],
tupleLandblockId, cameraWorldPosition, viewProjection,
liveDynamic: false, views, viewRouteIndex, alphaSubmissions);
liveDynamic: false, views, viewRouteIndex,
alphaSubmissions: alphaSubmissions);
}
}
@ -142,7 +143,7 @@ internal sealed class WalkStaticStreamPopulator
liveDynamic: true,
lookInViews,
lookInRouteIndex,
alphaSubmissions);
alphaSubmissions: alphaSubmissions);
}
}
@ -169,17 +170,25 @@ internal sealed class WalkStaticStreamPopulator
liveDynamic,
lookInViews,
lookInRouteIndex,
cellId);
cellId,
diagnosticViewProjection: viewProjection);
for (int i = 0; i < _batchScratch.Count; i++)
{
WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[i];
if (batch.IsOpaque)
{
int commandIndex = stream.Count;
stream.Append(new OrderedDrawCommand(
batch.Key, batch.Transform, stage, cellId, batch.ClipSlot,
batch.Lights, batch.IndoorFlag, batch.Alpha,
batch.SelectionLighting, batch.DetailCategory));
_dispatcher.ProbeFacilityStairCommandAppended(
commandIndex,
record.Source.LocalEntityId,
cellId,
stage,
in batch);
}
else
{

View file

@ -0,0 +1,171 @@
using System.Text;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Rendering;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Temporary issue-177 live probe. The classifier-side facility trace proves
/// whether the stair GfxObj survives the retail drawing-sphere test; this
/// companion trace follows that same exact object through the ordered stream,
/// ring upload, and final indirect draw call. It is inert unless
/// <c>ACDREAM_PROBE_FACILITY_STAIRS=1</c>.
/// </summary>
public sealed partial class WbDrawDispatcher
{
private const uint FacilityStairSubmissionLocalId = 0x4F41806Cu;
private readonly List<FacilityStairSubmissionCommand> _facilityStairSubmissionCommands = new(4);
private readonly HashSet<int> _facilityStairSubmittedCommands = new();
private int _facilityStairSubmissionFrame = -1;
private bool _facilityStairPrepareSeen;
private bool _facilityStairUploadCompleted;
private int _facilityStairPreparedCount;
private int _facilityStairSubmissionRangeFirst = -1;
private int _facilityStairSubmissionRangeCount;
private int _facilityStairSubmissionRunFirst = -1;
private int _facilityStairSubmissionRunCount;
private readonly record struct FacilityStairSubmissionCommand(
int CommandIndex,
uint CellId,
WalkDrawStage Stage,
System.Numerics.Vector3 Translation,
GroupKey Key);
internal void BeginFacilityStairSubmissionProbeFrame()
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
_facilityStairSubmissionCommands.Clear();
_facilityStairSubmittedCommands.Clear();
_facilityStairPrepareSeen = false;
_facilityStairUploadCompleted = false;
_facilityStairPreparedCount = 0;
_facilityStairSubmissionRangeFirst = -1;
_facilityStairSubmissionRangeCount = 0;
_facilityStairSubmissionRunFirst = -1;
_facilityStairSubmissionRunCount = 0;
_facilityStairSubmissionFrame = WalkPView.MasterTimestampForDiagnostics;
}
internal void ProbeFacilityStairCommandAppended(
int commandIndex,
uint localEntityId,
uint cellId,
WalkDrawStage stage,
in WalkClassifiedBatch batch)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled
|| localEntityId != FacilityStairSubmissionLocalId)
{
return;
}
_facilityStairSubmissionCommands.Add(new FacilityStairSubmissionCommand(
commandIndex,
cellId,
stage,
batch.Transform.Translation,
batch.Key));
}
private void ProbeFacilityStairPrepareStarted(int streamCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
int frame = WalkPView.MasterTimestampForDiagnostics;
_facilityStairPrepareSeen = true;
_facilityStairPreparedCount = streamCount;
EmitFacilityStairSubmissionProbe("prepare");
}
private void ProbeFacilityStairUploadCompleted(int preparedCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
_facilityStairUploadCompleted = true;
_facilityStairPreparedCount = preparedCount;
EmitFacilityStairSubmissionProbe("upload");
}
private void ProbeFacilityStairRunSubmitted(
int rangeFirst,
int rangeCount,
int runFirst,
int runCount)
{
if (!RenderingDiagnostics.ProbeFacilityStairsEnabled
|| _facilityStairSubmissionCommands.Count == 0)
{
return;
}
int runEnd = runFirst + runCount;
int submittedBefore = _facilityStairSubmittedCommands.Count;
for (int i = 0; i < _facilityStairSubmissionCommands.Count; i++)
{
int commandIndex = _facilityStairSubmissionCommands[i].CommandIndex;
if (commandIndex < runFirst || commandIndex >= runEnd)
continue;
_facilityStairSubmittedCommands.Add(commandIndex);
_facilityStairSubmissionRangeFirst = rangeFirst;
_facilityStairSubmissionRangeCount = rangeCount;
_facilityStairSubmissionRunFirst = runFirst;
_facilityStairSubmissionRunCount = runCount;
}
if (_facilityStairSubmittedCommands.Count != submittedBefore)
EmitFacilityStairSubmissionProbe("submit");
}
private void EmitFacilityStairSubmissionProbe(string phase)
{
if (_facilityStairSubmissionFrame < 0
|| _facilityStairSubmissionCommands.Count == 0)
{
return;
}
var commandDetails = new StringBuilder();
for (int i = 0; i < _facilityStairSubmissionCommands.Count; i++)
{
FacilityStairSubmissionCommand command = _facilityStairSubmissionCommands[i];
if (i != 0)
commandDetails.Append(';');
commandDetails.Append(command.CommandIndex)
.Append(':').Append(command.Stage)
.Append(":0x").Append(command.CellId.ToString("X8"))
.Append(":(")
.Append(command.Translation.X.ToString("F4"))
.Append(',')
.Append(command.Translation.Y.ToString("F4"))
.Append(',')
.Append(command.Translation.Z.ToString("F4"))
.Append(')')
.Append(':').Append(command.Key.CullMode)
.Append(':').Append(command.Key.FirstIndex)
.Append('+').Append(command.Key.IndexCount)
.Append(':').Append(command.Key.BaseVertex);
}
Console.WriteLine(
$"[facility-stair-path] phase={phase} frame={_facilityStairSubmissionFrame} "
+ $"local=0x{FacilityStairSubmissionLocalId:X8} "
+ $"commands={_facilityStairSubmissionCommands.Count} "
+ $"detail={commandDetails} "
+ $"prepare={(_facilityStairPrepareSeen ? 1 : 0)} "
+ $"preparedCount={_facilityStairPreparedCount} "
+ $"uploaded={(_facilityStairUploadCompleted ? 1 : 0)} "
+ $"submitted={_facilityStairSubmittedCommands.Count}/"
+ $"{_facilityStairSubmissionCommands.Count} "
+ $"range={_facilityStairSubmissionRangeFirst}+"
+ $"{_facilityStairSubmissionRangeCount} "
+ $"run={_facilityStairSubmissionRunFirst}+"
+ $"{_facilityStairSubmissionRunCount}");
}
}

View file

@ -355,6 +355,7 @@ public sealed unsafe partial class WbDrawDispatcher
_orderedStream = stream;
_orderedFrame = frame;
_orderedPreparedCount = 0;
ProbeFacilityStairPrepareStarted(stream.Count);
// Fail loud before any GPU work: a PortalPunch command has no
// submission path.
@ -436,6 +437,7 @@ public sealed unsafe partial class WbDrawDispatcher
checked((uint)(count * DrawCommandStride)));
_orderedPreparedCount = count;
ProbeFacilityStairUploadCompleted(count);
}
/// <summary>
@ -572,6 +574,11 @@ public sealed unsafe partial class WbDrawDispatcher
DrawIndirectRangeRhi(
encoder, ref pushConstants, commandBuffer, commandBase,
run.FirstCommand, run.CommandCount, _orderedDrawCullModes);
ProbeFacilityStairRunSubmitted(
firstCommand,
commandCount,
run.FirstCommand,
run.CommandCount);
}
}

View file

@ -55,6 +55,86 @@ namespace AcDream.App.Rendering.Wb;
/// </summary>
public sealed partial class WbDrawDispatcher
{
private static readonly IReadOnlyList<uint> RetailWholeMeshSlot = new uint[] { 0u };
private readonly HashSet<WalkDrawnPartKey> _walkDrawnParts = new();
private bool _walkPartFrameActive;
private readonly Dictionary<string, string> _facilityStairProbeSignatures = new();
private Dictionary<uint, FacilityStairProbeSnapshot> _facilityStairsCurrent = new();
private Dictionary<uint, FacilityStairProbeSnapshot> _facilityStairsPrevious = new();
private int _facilityStairsCurrentFrame = -1;
private int _facilityStairsPreviousFrame = -1;
private readonly record struct FacilityStairProbeSnapshot(
uint CellId,
int RouteIndex,
bool Admitted,
bool ScreenIntersect,
string ScreenBounds,
Vector3 WorldCenter);
private readonly record struct WalkDrawnPartKey(
RenderProjectionId ProjectionId,
int PartIndex);
/// <summary>
/// Opens retail's per-part drawn-stamp scope. A CPartArray may be present
/// in several cells from its retained CELLARRAY; CPhysicsPart::Draw stamps
/// only a part that actually passed the current cell's portal test. That
/// lets a rejected head/hair part retry through another crossed cell while
/// preventing already-admitted torso parts from being submitted twice in
/// one retail draw pass. PView::DrawCells advances the stamp between its
/// landscape and interior-cell passes; <see cref="AdvanceWalkPartPassStamp"/>
/// reproduces that mid-frame boundary.
/// </summary>
internal void BeginWalkPartFrame()
{
if (_walkPartFrameActive)
{
throw new InvalidOperationException(
"A walk part frame was opened before the previous scope closed.");
}
_walkDrawnParts.Clear();
BeginFacilityStairSubmissionProbeFrame();
_walkPartFrameActive = true;
}
/// <summary>
/// Re-arms retail's per-part drawn stamp at the mid-DrawCells pass
/// boundary. Retail increments RenderDevice::m_nFrameStamp at 0x005A4886
/// after LScape::draw + FlushAlphaList and before the interior depth clear,
/// so a shadow part drawn through the landscape may draw once more when
/// the interior cell list repaints after that clear.
/// </summary>
internal void AdvanceWalkPartPassStamp()
{
if (!_walkPartFrameActive)
{
throw new InvalidOperationException(
"The walk part pass stamp cannot advance outside an active frame.");
}
_walkDrawnParts.Clear();
}
internal void EndWalkPartFrame()
{
_walkPartFrameActive = false;
_walkDrawnParts.Clear();
}
private bool TryStampWalkPart(
in RenderProjectionRecord projection,
int partIndex) =>
!_walkPartFrameActive
// RenderDeviceD3D::DrawMeshInternal @0x0059F360 explicitly bypasses
// Get/SetDrawnThisFrame when IsPartOfPlayerObj is true. The local
// player must repaint at every crossed-cell turn so later wall/depth
// ordering cannot leave only the parts admitted by an earlier cell.
|| projection.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer
|| _walkDrawnParts.Add(new WalkDrawnPartKey(projection.Id, partIndex));
/// <summary>
/// One walk-classified (entity, part, batch) draw candidate — exactly
/// <c>OrderedDrawCommand</c>'s per-instance field set (minus
@ -188,7 +268,8 @@ public sealed partial class WbDrawDispatcher
bool liveDynamic = false,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
uint lookInCellId = 0)
uint lookInCellId = 0,
Matrix4x4 diagnosticViewProjection = default)
{
ArgumentNullException.ThrowIfNull(batches);
ArgumentNullException.ThrowIfNull(selectionParts);
@ -270,6 +351,17 @@ public sealed partial class WbDrawDispatcher
ObjectRenderData? partData = _meshAdapter.TryGetRenderData(gfxObjId);
if (partData is null)
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled
&& entity.LocalEntityId == 0x4F41806Cu
&& gfxObjId == 0x01001FE8u)
{
Console.WriteLine(
$"[facility-part] frame={WalkPView.MasterTimestampForDiagnostics} "
+ $"local=0x{entity.LocalEntityId:X8} "
+ $"cell=0x{lookInCellId:X8} gfx=0x{gfxObjId:X8} "
+ $"part={setupPartIndex} route={lookInRouteIndex} "
+ "decision=render-data-miss");
}
if (_missRequested.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
continue;
@ -282,21 +374,48 @@ public sealed partial class WbDrawDispatcher
(uint)setupPartIndex)
: 1f;
if (opacity <= 0f)
{
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)gfxObjId, setupPartIndex,
partData, default, diagnosticViewProjection,
default, 0f, hasSphere: false,
opacity, "opacity-zero", lookInViews);
continue;
}
Matrix4x4 restPose = partTransform * meshRef.PartTransform;
Matrix4x4 model = restPose * entity.RootWorld;
int selectionPartIndex = unchecked((partIndex << 16) | (setupPartIndex & 0xFFFF));
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
lookInViews, lookInRouteIndex, partData, model);
if (partClipSlots is { Count: 0 })
lookInViews,
lookInRouteIndex,
partData,
model,
out Vector3 sphereCenter,
out float sphereRadius,
out bool hasSphere);
bool visible = partClipSlots is not { Count: 0 };
bool firstAdmission = visible
&& TryStampWalkPart(in projection, selectionPartIndex);
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)gfxObjId, setupPartIndex,
partData, model, diagnosticViewProjection,
sphereCenter, sphereRadius, hasSphere,
opacity,
!visible
? "sphere-reject"
: firstAdmission ? "admit" : "already-drawn",
lookInViews);
if (!firstAdmission)
continue;
EmitClassifiedBatches(
partData, model, in entity, meshRef, paletteIdentity,
entityHasCutoutSubset, slot, lights, indoor, selectionLighting,
detailCategory, opacity, partClipSlots, batches);
entityHasCutoutSubset, slot, lights, indoor,
selectionLighting, detailCategory, opacity,
partClipSlots, batches);
selectionParts.Add(new WalkClassifiedSelectionPart(
entity.ServerGuid, entity.LocalEntityId, selectionPartIndex,
(uint)gfxObjId, model));
@ -311,17 +430,45 @@ public sealed partial class WbDrawDispatcher
(uint)partIndex)
: 1f;
if (opacity <= 0f)
{
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)meshRef.GfxObjId, partIndex,
renderData, default, diagnosticViewProjection,
default, 0f, hasSphere: false,
opacity, "opacity-zero", lookInViews);
continue;
}
Matrix4x4 model = meshRef.PartTransform * entity.RootWorld;
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
lookInViews, lookInRouteIndex, renderData, model);
if (partClipSlots is { Count: 0 })
lookInViews,
lookInRouteIndex,
renderData,
model,
out Vector3 sphereCenter,
out float sphereRadius,
out bool hasSphere);
bool visible = partClipSlots is not { Count: 0 };
bool firstAdmission = visible
&& TryStampWalkPart(in projection, partIndex);
EmitFacilityStairPartProbe(
in projection, in entity, lookInCellId,
lookInRouteIndex, (uint)meshRef.GfxObjId, partIndex,
renderData, model, diagnosticViewProjection,
sphereCenter, sphereRadius, hasSphere,
opacity,
!visible
? "sphere-reject"
: firstAdmission ? "admit" : "already-drawn",
lookInViews);
if (!firstAdmission)
continue;
EmitClassifiedBatches(
renderData, model, in entity, meshRef, paletteIdentity,
entityHasCutoutSubsetOverride: null, slot, lights, indoor,
selectionLighting, detailCategory, opacity, partClipSlots, batches);
selectionLighting, detailCategory, opacity,
partClipSlots, batches);
selectionParts.Add(new WalkClassifiedSelectionPart(
entity.ServerGuid, entity.LocalEntityId, partIndex,
(uint)meshRef.GfxObjId, model));
@ -381,25 +528,250 @@ public sealed partial class WbDrawDispatcher
IWalkLookInViewSource? lookInViews,
int routeIndex,
ObjectRenderData renderData,
Matrix4x4 localToWorld)
Matrix4x4 localToWorld,
out Vector3 sphereCenter,
out float sphereRadius,
out bool hasSphere)
{
sphereCenter = default;
sphereRadius = 0f;
hasSphere = false;
if (lookInViews is null)
return null;
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
{
return lookInViews.VisibleClipSlotsInLookInTurn(
IReadOnlyList<uint> admittedViews = lookInViews.VisibleClipSlotsInLookInTurn(
routeIndex,
Vector3.Zero,
radius: 0f,
testSphere: false);
return admittedViews.Count == 0
? admittedViews
: RetailWholeMeshSlot;
}
TransformDrawingSphere(sphere, localToWorld, out Vector3 center, out float radius);
return lookInViews.VisibleClipSlotsInLookInTurn(
hasSphere = true;
TransformDrawingSphere(sphere, localToWorld, out sphereCenter, out sphereRadius);
IReadOnlyList<uint> visibleViews = lookInViews.VisibleClipSlotsInLookInTurn(
routeIndex,
in center,
radius,
in sphereCenter,
sphereRadius,
testSphere: true);
// RenderDeviceD3D::DrawMesh @0x005A0860 tests the authored drawing
// sphere against each active PortalList view. Once any view admits
// it, DrawMeshInternal @0x0059F360 submits the complete constructed
// mesh. The portal polygon is not forwarded as a GPU mesh clip.
return visibleViews.Count == 0
? visibleViews
: RetailWholeMeshSlot;
}
private void EmitFacilityStairPartProbe(
in RenderProjectionRecord projection,
in RenderInstanceCandidate entity,
uint cellId,
int routeIndex,
uint gfxObjId,
int partIndex,
ObjectRenderData renderData,
Matrix4x4 localToWorld,
Matrix4x4 viewProjection,
Vector3 sphereCenter,
float sphereRadius,
bool hasSphere,
float opacity,
string decision,
IWalkLookInViewSource? lookInViews)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
bool targetStairCell = cellId is 0x8A02015Eu or 0x8A02015Fu;
bool targetPlayerCell = targetStairCell || cellId == 0x8A0201C1u;
bool targetStair = targetStairCell && gfxObjId == 0x010000DEu;
bool targetPlayer = targetPlayerCell
&& projection.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer;
// Static render projections carry the local render identity here;
// SourceId is not guaranteed to retain the Setup DID after the
// landblock projection has been materialized.
// Cathedral floating stair Setup 0x020009A2 is seven independent
// visual parts (0x01001FE8 + six 0x01001FE6 slabs). Tracing only the
// first GfxObj hid the actual camera-angle failure on the six visible
// slabs, so keep the probe keyed to the complete PartArray owner.
bool targetCathedralRamp = entity.LocalEntityId == 0x4F41806Cu;
if (!targetStair && !targetPlayer && !targetCathedralRamp)
return;
int frame = WalkPView.MasterTimestampForDiagnostics;
string screenBounds = "unavailable";
bool screenIntersect = false;
if (targetStair || targetCathedralRamp)
{
AdvanceFacilityStairProbeFrame(frame);
screenIntersect = TryProjectFacilityStairBounds(
renderData,
localToWorld,
viewProjection,
out screenBounds);
var snapshot = new FacilityStairProbeSnapshot(
cellId,
routeIndex,
string.Equals(decision, "admit", StringComparison.Ordinal),
screenIntersect,
screenBounds,
hasSphere ? sphereCenter : localToWorld.Translation);
if (!_facilityStairsCurrent.TryGetValue(
entity.LocalEntityId,
out FacilityStairProbeSnapshot existing)
|| (!existing.Admitted && snapshot.Admitted))
{
_facilityStairsCurrent[entity.LocalEntityId] = snapshot;
}
}
string key = $"{entity.ServerGuid:X8}:{entity.LocalEntityId:X8}:"
+ $"{cellId:X8}:{gfxObjId:X8}:{partIndex}";
// A route index is allocated afresh every frame. Including it in the
// signature made a stationary cathedral trace print at frame rate and
// obscure the useful admit/reject transition. Cell + part are already
// in the key; emit again only when that route's decision changes.
string signature = targetCathedralRamp
? $"{decision}:{(hasSphere ? 1 : 0)}:"
+ $"{(opacity <= 0f ? 0 : opacity < 1f ? 1 : 2)}"
: $"{routeIndex}:{decision}:{(hasSphere ? 1 : 0)}:"
+ $"{(opacity <= 0f ? 0 : opacity < 1f ? 1 : 2)}";
if (_facilityStairProbeSignatures.TryGetValue(key, out string? prior)
&& string.Equals(prior, signature, StringComparison.Ordinal))
{
return;
}
_facilityStairProbeSignatures[key] = signature;
string authored = renderData.SelectionSphere is { } sourceSphere
? $" authored=({sourceSphere.Origin.X:F4},{sourceSphere.Origin.Y:F4},"
+ $"{sourceSphere.Origin.Z:F4};r={sourceSphere.Radius:F4})"
: " authored=none";
string world = hasSphere
? $" world=({sphereCenter.X:F4},{sphereCenter.Y:F4},"
+ $"{sphereCenter.Z:F4};r={sphereRadius:F4})"
: " world=none";
string cone = targetCathedralRamp && hasSphere && lookInViews is not null
? " cone={" + lookInViews.DescribeLookInTurn(
routeIndex, in sphereCenter, sphereRadius) + "}"
: string.Empty;
Console.WriteLine(
$"[facility-part] frame={frame} "
+ $"kind={projection.EntityPayload.CasterIdentity} "
+ $"guid=0x{entity.ServerGuid:X8} local=0x{entity.LocalEntityId:X8} "
+ $"cell=0x{cellId:X8} parent=0x{entity.ParentCellId:X8} "
+ $"gfx=0x{gfxObjId:X8} part={partIndex} route={routeIndex} "
+ $"opacity={opacity:F4} decision={decision} "
+ $"screen={(screenIntersect ? 1 : 0)} ndc={screenBounds}"
+ authored + world + cone);
}
private void AdvanceFacilityStairProbeFrame(int frame)
{
if (frame == _facilityStairsCurrentFrame)
return;
if (_facilityStairsCurrentFrame >= 0)
{
if (_facilityStairsPreviousFrame >= 0)
{
foreach ((uint localId, FacilityStairProbeSnapshot previous)
in _facilityStairsPrevious)
{
if (!_facilityStairsCurrent.TryGetValue(localId, out _))
{
Console.WriteLine(
$"[facility-stair-drop] frame={_facilityStairsCurrentFrame} "
+ $"previousFrame={_facilityStairsPreviousFrame} "
+ $"local=0x{localId:X8} cell=0x{previous.CellId:X8} "
+ $"route={previous.RouteIndex} admitted={(previous.Admitted ? 1 : 0)} "
+ $"screen={(previous.ScreenIntersect ? 1 : 0)} "
+ $"lastNdc={previous.ScreenBounds} "
+ $"world=({previous.WorldCenter.X:F4},{previous.WorldCenter.Y:F4},"
+ $"{previous.WorldCenter.Z:F4}) reason=cell-not-walked");
}
}
foreach ((uint localId, FacilityStairProbeSnapshot current)
in _facilityStairsCurrent)
{
if (_facilityStairsPrevious.TryGetValue(
localId,
out FacilityStairProbeSnapshot previous)
&& current.Admitted != previous.Admitted)
{
Console.WriteLine(
$"[facility-stair-decision] frame={_facilityStairsCurrentFrame} "
+ $"local=0x{localId:X8} cell=0x{current.CellId:X8} "
+ $"admitted={(previous.Admitted ? 1 : 0)}->"
+ $"{(current.Admitted ? 1 : 0)} "
+ $"screen={(current.ScreenIntersect ? 1 : 0)} "
+ $"ndc={current.ScreenBounds}");
}
}
}
Dictionary<uint, FacilityStairProbeSnapshot> swap =
_facilityStairsPrevious;
_facilityStairsPrevious = _facilityStairsCurrent;
_facilityStairsCurrent = swap;
_facilityStairsCurrent.Clear();
_facilityStairsPreviousFrame = _facilityStairsCurrentFrame;
}
_facilityStairsCurrentFrame = frame;
}
private static bool TryProjectFacilityStairBounds(
ObjectRenderData renderData,
Matrix4x4 localToWorld,
Matrix4x4 viewProjection,
out string bounds)
{
Vector3 min = renderData.BoundingBox.Min;
Vector3 max = renderData.BoundingBox.Max;
float minX = float.PositiveInfinity;
float minY = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float maxY = float.NegativeInfinity;
int projected = 0;
for (int corner = 0; corner < 8; corner++)
{
var local = new Vector3(
(corner & 1) == 0 ? min.X : max.X,
(corner & 2) == 0 ? min.Y : max.Y,
(corner & 4) == 0 ? min.Z : max.Z);
Vector3 world = Vector3.Transform(local, localToWorld);
Vector4 clip = Vector4.Transform(new Vector4(world, 1f), viewProjection);
if (!float.IsFinite(clip.W) || clip.W <= 0.0001f)
continue;
float x = clip.X / clip.W;
float y = clip.Y / clip.W;
if (!float.IsFinite(x) || !float.IsFinite(y))
continue;
minX = MathF.Min(minX, x);
minY = MathF.Min(minY, y);
maxX = MathF.Max(maxX, x);
maxY = MathF.Max(maxY, y);
projected++;
}
if (projected == 0)
{
bounds = "behind";
return false;
}
bounds = $"({minX:F3},{minY:F3})-({maxX:F3},{maxY:F3});corners={projected}";
return maxX >= -1f && minX <= 1f && maxY >= -1f && minY <= 1f;
}
internal static bool LookInDrawingSphereVisible(

View file

@ -463,8 +463,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
private readonly IWorldRenderRangeSource _ranges;
private readonly SkyPesFrameController? _skyPes;
private readonly Func<bool> _persistentDaylight;
private readonly HashSet<uint> _visibleCells = [];
private bool _visibleCellsValid;
public RuntimeWorldFrameEnvironmentPreparation(
RuntimeOptions options,
@ -513,9 +511,12 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
UpdateSunFromSky(landscapeLighting, roots.PlayerInsideCell);
_lighting.UpdateViewerLight(roots.PlayerViewPosition);
_lighting.Tick(camera.Position);
_lighting.BuildPointLightSnapshot(
roots.PlayerViewPosition,
_visibleCellsValid ? _visibleCells : null);
// Retail collects point lights from CEnvCell::visible_cell_table, which
// is the resident EnvCell registry populated at activation time. It is
// not the camera's per-frame portal walk. Feeding last frame's visited
// cells here made the Facility Hub room jump between five lights and
// the cell-less viewer light when zoom changed the camera root cell.
_lighting.BuildPointLightSnapshot(roots.PlayerViewPosition);
_dispatcher?.SetSceneLights(_lighting.PointSnapshot);
_environmentCells?.SetPointSnapshot(_lighting.PointSnapshot);
@ -540,15 +541,12 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
_lightingUbo?.Upload(ubo);
}
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
{
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
ArgumentNullException.ThrowIfNull(drawableCells);
_visibleCells.Clear();
_visibleCells.UnionWith(drawableCells);
_visibleCellsValid = true;
}
public void ClearDrawableCells() => _visibleCellsValid = false;
public void ClearDrawableCells()
{
}
private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell)
{