checkpoint(render): preserve pre-overhaul investigation state
This commit is contained in:
parent
e880860291
commit
b3b7d922f1
45 changed files with 3168 additions and 619 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/(f−n) 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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -295,7 +295,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "6214fc04936d92320594acb722e63f1cd1f7106dd4d0af60ed985abc2a50c4a9",
|
||||
"sourceSha256": "1df7e2009cda8f84ba3d84546bf58baf71d10fe53e696655b4ca7b8292a32fa5",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 (005A4A00–005A4ADE):
|
||||
// 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 (005A4ADE–005A4B2D): 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 & 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue