refactor(rendering): delete superseded visibility probes
Delete the callerless portal-BFS research graph and spent renderer probe families while retaining the production RetailFrameWalk path, terrain diagnostics, membership invariant, and walk transcript. Mutation evidence (all restored): 1. Restored PortalVisibilityBuilder type -> AppAssembly_ContainsNoSupersededPortalGraphTypes first failed Assert.Empty with AcDream.App.Rendering.PortalVisibilityBuilder. 2. Restored ACDREAM_PROBE_FACILITY_STAIRS -> ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof first failed Assert.Empty on RenderingDiagnostics.cs. 3. Added a second RetailFrameWalk field -> WalkFrameOwners_AreUnique first failed Assert.Single with _frameWalk and _mutatedSecondFrameWalk. 4. Added OrderBy to OrderedStream -> OrderedWalkStream_HasNoCrossStreamReorder first failed Assert.DoesNotContain on OrderBy(. 5. Added IDatReaderWriter parameter -> FrameTimeWalkOwners_HaveNoRawDatDependency first failed Assert.Empty on RetailFrameWalk.MutatedRawDatParameter.
This commit is contained in:
parent
b77989c323
commit
bf53e2ad6e
82 changed files with 451 additions and 10108 deletions
|
|
@ -358,15 +358,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
{
|
||||
// Campaign V slice V6j: the world scene is composed on BOTH arms.
|
||||
// Every renderer below now exists on Vulkan too; what forks is one
|
||||
// pass surface, one state restorer, one GL-state reader, and the
|
||||
// three renderers that stay raw GL until their own slices — sky,
|
||||
// particles and the portal depth mask, which the executors already
|
||||
// accept as absent.
|
||||
WorldRenderDiagnostics worldRenderDiagnostics =
|
||||
host.WorldRenderDiagnostics
|
||||
?? new WorldRenderDiagnostics(
|
||||
NullRenderGlStateReader.Instance,
|
||||
d.RenderDiagnosticLog);
|
||||
new(d.RenderDiagnosticLog);
|
||||
IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
|
||||
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
|
||||
IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
|
||||
|
|
@ -445,10 +438,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
live.ParticleRenderer,
|
||||
live.PortalDepthMask,
|
||||
d.RetailAlphaQueue,
|
||||
worldRenderDiagnostics,
|
||||
terrainDrawDiagnostics);
|
||||
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
|
||||
worldRenderDiagnostics,
|
||||
new RuntimeWorldScenePViewDiagnosticSource(
|
||||
d.PlayerController,
|
||||
d.PhysicsEngine,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ internal sealed record HostInputCameraResult(
|
|||
IRenderFrameSlotSource FrameSlots,
|
||||
IGpuDevice GpuDevice,
|
||||
GpuDeviceFrameLifetime GpuFrameLifetime,
|
||||
WorldRenderDiagnostics? WorldRenderDiagnostics,
|
||||
SilkKeyboardSource? KeyboardSource,
|
||||
SilkMouseSource? MouseSource,
|
||||
IMouseLookCursor? MouseLookCursor,
|
||||
|
|
@ -86,11 +85,6 @@ internal interface IHostInputCameraCompositionFactory
|
|||
GpuFrameFlightController? frameFlights,
|
||||
IGpuDevice device);
|
||||
|
||||
/// <summary>The raw-GL state tripwire, or null on a backend that has no GL state.</summary>
|
||||
WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
|
||||
GameWindowGraphics graphics,
|
||||
IRenderFrameDiagnosticLog log);
|
||||
|
||||
SilkKeyboardSource CreateKeyboardSource(
|
||||
IKeyboard keyboard,
|
||||
HostQuiescenceGate quiescence);
|
||||
|
|
@ -250,11 +244,6 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||
_publication.PublishGpuFrameLifetime(gpuFrameLifetime);
|
||||
|
||||
WorldRenderDiagnostics? diagnostics =
|
||||
_factory.CreateWorldRenderDiagnostics(
|
||||
graphics,
|
||||
_dependencies.RenderDiagnosticLog);
|
||||
|
||||
IKeyboard? firstKeyboard = input.Keyboards.FirstOrDefault();
|
||||
IMouse? firstMouse = input.Mice.FirstOrDefault();
|
||||
SilkKeyboardSource? keyboard = null;
|
||||
|
|
@ -356,7 +345,6 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
frameSlots,
|
||||
gpuDevice,
|
||||
gpuFrameLifetime,
|
||||
diagnostics,
|
||||
keyboard,
|
||||
mouse,
|
||||
cursor,
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ namespace AcDream.App.Composition;
|
|||
///
|
||||
/// <para><b>What is absent, and why.</b> There is no GL fence ring: the RHI
|
||||
/// device owns its own frames-in-flight through a timeline semaphore, so
|
||||
/// retirement and slot indexing come from the device instead. There is no
|
||||
/// <see cref="WorldRenderDiagnostics"/>: it was a raw-GL state tripwire, and
|
||||
/// Vulkan has no global state for it to watch. Both nulls are read by the
|
||||
/// phases that would otherwise consume them.</para>
|
||||
/// retirement and slot indexing come from the device instead.</para>
|
||||
/// </summary>
|
||||
internal sealed class VulkanHostInputCameraCompositionFactory
|
||||
: IHostInputCameraCompositionFactory
|
||||
|
|
@ -57,10 +54,6 @@ internal sealed class VulkanHostInputCameraCompositionFactory
|
|||
IGpuDevice device) =>
|
||||
new VulkanRenderFrameSlotSource(RequireContext(graphics).Device);
|
||||
|
||||
public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
|
||||
GameWindowGraphics graphics,
|
||||
IRenderFrameDiagnosticLog log) => null;
|
||||
|
||||
public SilkKeyboardSource CreateKeyboardSource(
|
||||
IKeyboard keyboard,
|
||||
HostQuiescenceGate quiescence) =>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ public sealed class LoadedCell
|
|||
/// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially
|
||||
/// visible from this cell, precomputed by the AC content tools. Refreshed only at
|
||||
/// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878).
|
||||
/// PortalVisibilityBuilder grounds set membership in it so a brittle per-frame
|
||||
/// portal-side test can't drop a potentially-visible cell from the visible set.
|
||||
/// The frame walk uses it to ground authored potentially-visible membership.
|
||||
/// Empty when the dat carried no stab list (degenerate / old cell).
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> VisibleCells = System.Array.Empty<uint>();
|
||||
|
|
@ -105,9 +104,8 @@ public sealed class LoadedCell
|
|||
|
||||
/// <summary>
|
||||
/// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by
|
||||
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a flood-graph cell whose
|
||||
/// shell is the landscape. <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView
|
||||
/// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell).
|
||||
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a walk root whose
|
||||
/// shell is the landscape. The frame assembler seeds its outside view full-screen.
|
||||
/// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but
|
||||
/// test fixtures use low ids for interior cells, so keying on the id would misfire.
|
||||
/// </summary>
|
||||
|
|
@ -168,10 +166,8 @@ public struct PortalClipPlane
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked
|
||||
/// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained
|
||||
/// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that
|
||||
/// still prints <see cref="LastCameraCellResolution"/> (always None post-Stage 3).
|
||||
/// Historical result describing which branch of the former camera-cell resolver ran.
|
||||
/// The retained physics-membership route always reports <see cref="None"/>.
|
||||
/// </summary>
|
||||
public enum CameraCellResolution
|
||||
{
|
||||
|
|
@ -219,7 +215,7 @@ public sealed class CellVisibility
|
|||
/// <summary>
|
||||
/// Stage 3 (2026-06-02): always <see cref="CameraCellResolution.None"/> — the FindCameraCell
|
||||
/// AABB grace-frame resolver was deleted; the physics membership answer is the sole root.
|
||||
/// Retained for the [flap-cam] probe log line in GameWindow.cs.
|
||||
/// Retained as a stable diagnostic result for existing frame records.
|
||||
/// </summary>
|
||||
public CameraCellResolution LastCameraCellResolution { get; private set; } = CameraCellResolution.None;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// ClipFrameAssembler.cs
|
||||
//
|
||||
// Retail view assembly policy. The production frame walk supplies its
|
||||
// outside_view directly; the legacy PortalVisibilityBuilder overload remains
|
||||
// only for isolated research/replay tests. Each visible polygon is packed as
|
||||
// an individual GPU clip slot.
|
||||
// outside_view directly. Each visible polygon is packed as an individual GPU
|
||||
// clip slot.
|
||||
//
|
||||
// outside_view landscape slices
|
||||
// reverse cell_draw_list exit masks
|
||||
|
|
@ -19,34 +18,10 @@ using System.Numerics;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 4 fix round 2 (L2): retired from the walk's own clip assembly —
|
||||
/// <see cref="ClipFrameAssembly"/> no longer tracks a terrain clip mode at
|
||||
/// all, because the walk draws the sky, terrain and weather unclipped, one
|
||||
/// call each, matching retail (<c>LScape::draw</c> never installs a view
|
||||
/// before any of the three). The type stays only because
|
||||
/// <see cref="WorldSceneRenderer"/>'s separate flat-world safety path still
|
||||
/// uses it for its own unrelated "did the flat terrain draw this frame"
|
||||
/// diagnostic flag (<c>Planes</c> = drew, <c>Skip</c> = the PView walk ran
|
||||
/// instead). S3 landing hygiene (H4) deleted the <c>Scissor</c> member: no
|
||||
/// producer ever wrote it (<see cref="WorldSceneRenderer"/> only ever
|
||||
/// assigns <c>Planes</c> or <c>Skip</c>), because there is no GPU scissor
|
||||
/// consumer left anywhere in this contract, walk or flat.
|
||||
/// </summary>
|
||||
public enum TerrainClipMode
|
||||
{
|
||||
/// <summary>The flat-world path drew terrain this frame.</summary>
|
||||
Planes,
|
||||
|
||||
/// <summary>The PView walk ran instead of the flat-world path this frame;
|
||||
/// flat terrain was not drawn.</summary>
|
||||
Skip,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is
|
||||
/// retained for diagnostics (<see cref="WorldRenderDiagnostics"/>) only — no
|
||||
/// GPU pass reads it; there is no scissor consumer left in the walk.
|
||||
/// retained as part of the exact outside-view reconstruction proof — no GPU
|
||||
/// pass reads it; there is no scissor consumer left in the walk.
|
||||
///
|
||||
/// <para>S3 review fix round 1 (F1): <paramref name="NothingVisible"/>
|
||||
/// distinguishes the "this walk view's own polygon collapsed" case (retail
|
||||
|
|
@ -95,7 +70,7 @@ public sealed class ClipFrameAssembly
|
|||
public Dictionary<uint, int> PerCellPlaneCounts { get; } = new();
|
||||
public int ScissorFallbacks { get; internal set; }
|
||||
|
||||
// The assembly is frame-scoped. An owner that passes it back to Assemble may reuse the
|
||||
// The assembly is frame-scoped. An owner may pass it back to reuse the
|
||||
// dictionaries, per-cell arrays, and construction list after the prior frame is consumed.
|
||||
// Exact-length pools keep the public array API honest: Length is always the live slice count.
|
||||
private readonly Dictionary<int, Stack<ClipViewSlice[]>> _sliceArraysByLength = new();
|
||||
|
|
@ -279,8 +254,7 @@ public sealed class ClipFrameAssembly
|
|||
public static class ClipFrameAssembler
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts one production walk assembly without constructing a parallel
|
||||
/// PortalVisibilityFrame. Outdoor roots begin with retail's full-screen
|
||||
/// Starts the one production walk assembly. Outdoor roots begin with retail's full-screen
|
||||
/// default view; interior roots begin empty and are populated from
|
||||
/// <see cref="Walk.RetailFrameWalk.InteriorOutsideView"/> after Collect.
|
||||
/// </summary>
|
||||
|
|
@ -321,109 +295,6 @@ public static class ClipFrameAssembler
|
|||
return assembly;
|
||||
}
|
||||
|
||||
public static ClipFrameAssembly Assemble(
|
||||
ClipFrame frame,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly? reuseAssembly = null)
|
||||
{
|
||||
System.ArgumentNullException.ThrowIfNull(frame);
|
||||
System.ArgumentNullException.ThrowIfNull(pvFrame);
|
||||
|
||||
frame.Reset();
|
||||
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
|
||||
assembly.Reset(frame);
|
||||
|
||||
Dictionary<uint, int> cellIdToSlot = assembly.CellIdToSlot;
|
||||
Dictionary<uint, int[]> cellIdToViewSlots = assembly.CellIdToViewSlots;
|
||||
Dictionary<uint, ClipViewSlice[]> cellIdToViewSlices = assembly.CellIdToViewSlices;
|
||||
Dictionary<uint, int> perCellPlaneCounts = assembly.PerCellPlaneCounts;
|
||||
int scissorFallbacks = 0;
|
||||
|
||||
foreach (uint cellId in pvFrame.OrderedVisibleCells)
|
||||
{
|
||||
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
|
||||
continue;
|
||||
|
||||
List<ClipViewSlice> slices = assembly.SliceScratch;
|
||||
slices.Clear();
|
||||
int maxPlaneCount = 0;
|
||||
|
||||
foreach (var poly in view.Polygons)
|
||||
{
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
|
||||
int slot;
|
||||
Vector4[] planes;
|
||||
if (cps.Count > 0)
|
||||
{
|
||||
planes = cps.PlaneArray;
|
||||
slot = frame.AppendSlot(planes);
|
||||
if (cps.Count > maxPlaneCount)
|
||||
maxPlaneCount = cps.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
planes = System.Array.Empty<Vector4>();
|
||||
slot = 0;
|
||||
scissorFallbacks++;
|
||||
}
|
||||
|
||||
slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
||||
}
|
||||
|
||||
if (slices.Count == 0)
|
||||
continue;
|
||||
|
||||
ClipViewSlice[] sliceArray = assembly.CopySlices(slices);
|
||||
cellIdToViewSlices[cellId] = sliceArray;
|
||||
cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray);
|
||||
cellIdToSlot[cellId] = sliceArray[0].Slot;
|
||||
perCellPlaneCounts[cellId] = maxPlaneCount;
|
||||
}
|
||||
|
||||
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
|
||||
outsideSlicesList.Clear();
|
||||
int outsideMaxPlaneCount = 0;
|
||||
bool outsideHasScissorFallback = false;
|
||||
|
||||
foreach (var poly in pvFrame.OutsideView.Polygons)
|
||||
{
|
||||
AppendOutsideSlice(
|
||||
frame,
|
||||
poly,
|
||||
outsideSlicesList,
|
||||
ref outsideMaxPlaneCount,
|
||||
ref outsideHasScissorFallback,
|
||||
ref scissorFallbacks);
|
||||
}
|
||||
|
||||
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
|
||||
bool outdoorVisible = outsideViewSlices.Length > 0;
|
||||
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
|
||||
|
||||
Vector4 outsideViewNdcAabb = outdoorVisible
|
||||
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
|
||||
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
|
||||
: Vector4.Zero;
|
||||
|
||||
assembly.SetOutsideViewSlices(outsideViewSlices);
|
||||
assembly.OutdoorSlot = outdoorSlot;
|
||||
assembly.OutdoorVisible = outdoorVisible;
|
||||
assembly.HasOutsideView = outdoorVisible;
|
||||
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
|
||||
// S3 chunk 4 fix round 2 (L2): equivalent to the deleted
|
||||
// terrainMode == TerrainClipMode.Planes gate without needing
|
||||
// TerrainClipMode at all — a scissor-fallback slice can only exist
|
||||
// when outdoorVisible is already true (both branches above append a
|
||||
// slice), so this reduces to the same three cases (no slices -> 0;
|
||||
// any scissor fallback -> 0; all-planes -> outsideMaxPlaneCount).
|
||||
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
|
||||
assembly.ScissorFallbacks = scissorFallbacks;
|
||||
return assembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW4 slice 1 — the interior root's outside-view cutover.
|
||||
/// Replaces the assembly's outside-view block (slices, terrain mode,
|
||||
|
|
@ -433,13 +304,8 @@ public static class ClipFrameAssembler
|
|||
/// exactly ONE visibility structure per frame: <c>LScape::draw</c>'s
|
||||
/// terrain clip, the punch fans' <c>building_view</c> planes, and the
|
||||
/// landscape turn's view count all read the views the walk itself
|
||||
/// installed. Feeding these from the old <c>PortalVisibilityBuilder</c>
|
||||
/// assembly let the two systems desynchronize on camera-transition
|
||||
/// boundary frames — terrain splashed through stale/fat old-apparatus
|
||||
/// exit views over interior pixels the walk's flood never repainted
|
||||
/// (the FW3 visual-gate stairwell/grass flash, probe-pinned
|
||||
/// 2026-08-30), and punch fans indexed the old slice array with walk
|
||||
/// view indices.
|
||||
/// installed. This keeps terrain, punch fans, and landscape turns on
|
||||
/// the same frame-owned walk views during camera transitions.
|
||||
///
|
||||
/// The walk stores view vertices as PIXEL screen points
|
||||
/// (<c>copy_view</c>'s post-divide viewport coordinates, origin
|
||||
|
|
@ -463,8 +329,7 @@ public static class ClipFrameAssembler
|
|||
/// <see cref="ClipFrameAssembly.OutsideViewSlices"/> by that index. The
|
||||
/// shared <see cref="AppendOutsideSlice"/> helper SKIPS a view whose
|
||||
/// polygon collapses (<see cref="ClipPlaneSet.IsNothingVisible"/>) —
|
||||
/// correct for <see cref="Assemble"/>'s "empty regions are omitted
|
||||
/// entirely" legacy policy, but wrong here: skipping would make the
|
||||
/// unsuitable here because skipping would make the
|
||||
/// slice array SHORTER than the view list, so every later view's fan
|
||||
/// would read the WRONG view's planes (off by however many views
|
||||
/// collapsed before it). A collapsed view instead gets its own slice
|
||||
|
|
@ -564,22 +429,14 @@ public static class ClipFrameAssembler
|
|||
assembly.OutdoorVisible = outdoorVisible;
|
||||
assembly.HasOutsideView = outdoorVisible;
|
||||
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
|
||||
// S3 chunk 4 fix round 2 (L2): see Assemble's matching comment — the
|
||||
// same reduction applies here.
|
||||
// Any scissor fallback makes the aggregate plane count zero.
|
||||
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
|
||||
assembly.ScissorFallbacks = scissorFallbacks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 4 fix round 2 (L4): the ONE place a single outside_view
|
||||
/// <see cref="ViewPolygon"/> becomes an appended clip slot plus its
|
||||
/// <see cref="ClipViewSlice"/> — both <see cref="Assemble"/>'s
|
||||
/// outside_view loop and <see cref="ReassembleOutsideViewFromWalk"/>'s
|
||||
/// outside_view loop call this instead of each carrying its own copy of
|
||||
/// the plane/scissor-fallback bookkeeping (a prior round's three-lens
|
||||
/// review found the duplication let a CPU/GPU equivalence pin exercise
|
||||
/// one copy while production ran the other — see <c>ClipFrameLayoutTests</c>'
|
||||
/// punch-fan pin). A polygon entirely outside every plane
|
||||
/// Converts one outside_view <see cref="ViewPolygon"/> into an appended
|
||||
/// clip slot and <see cref="ClipViewSlice"/>. A polygon entirely outside every plane
|
||||
/// (<see cref="ClipPlaneSet.IsNothingVisible"/>) appends nothing and
|
||||
/// returns false; the caller's own loop simply moves to the next
|
||||
/// polygon either way, so the return value only matters to a caller that
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// gl_ClipDistance, or a signal that the region OVER-includes (draws
|
||||
// unclipped) because it cannot be expressed as one convex plane set.
|
||||
//
|
||||
// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
|
||||
// This is the bridge between the frame walk's 2D NDC view polygons and
|
||||
// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
|
||||
// Pure System.Numerics math; NO GL. The shader consumes each plane as
|
||||
// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep)
|
||||
|
|
@ -186,8 +186,7 @@ public readonly struct ClipPlaneSet
|
|||
new(Array.Empty<Vector4>(), isPlaneOverflow: true, isNothingVisible: false);
|
||||
|
||||
/// <summary>
|
||||
/// Return the polygon wound CCW with collinear vertices removed. The PortalVisibilityBuilder
|
||||
/// already EnsureCcw's its output, but From() is a public entry point that must be robust to
|
||||
/// Return the polygon wound CCW with collinear vertices removed. From() must be robust to
|
||||
/// either winding (e.g. a hand-built CellView), so we normalize here too.
|
||||
/// </summary>
|
||||
private static int NormalizeAndMerge(ReadOnlySpan<Vector2> input, Span<Vector2> points)
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
// IndoorDrawPlan.cs
|
||||
//
|
||||
// Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840):
|
||||
// the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a
|
||||
// non-empty view is included — there is NO "drawable" filter. Dropping cells without a
|
||||
// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed).
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList<ViewPolygon> Slices);
|
||||
|
||||
public static class IndoorDrawPlan
|
||||
{
|
||||
/// <summary>Reverse OrderedVisibleCells (far→near), each visible cell with its view
|
||||
/// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped
|
||||
/// (they are not actually visible); no other cell is ever dropped.</summary>
|
||||
public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
|
||||
{
|
||||
var result = new List<CellDrawEntry>(frame.OrderedVisibleCells.Count);
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
if (!frame.CellViews.TryGetValue(cellId, out var view) || view.IsEmpty)
|
||||
continue;
|
||||
result.Add(new CellDrawEntry(cellId, view.Polygons));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,15 +6,15 @@ namespace AcDream.App.Rendering;
|
|||
/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors.
|
||||
/// Retail roots every in-world frame at <c>viewer_cell</c> (SmartBox::RenderNormalMode →
|
||||
/// DrawInside(viewer_cell), decomp:92635); when outdoors that is a <c>CLandCell</c>. acdream models it
|
||||
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> (so
|
||||
/// <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
|
||||
/// as the root's shell) and <see cref="LoadedCell.SeenOutside"/>.
|
||||
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> and
|
||||
/// <see cref="LoadedCell.SeenOutside"/>. <see cref="ClipFrameAssembler.BeginWalkFrame"/> seeds the
|
||||
/// full-screen outside view for this root.
|
||||
///
|
||||
/// <para>R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail
|
||||
/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the
|
||||
/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827).
|
||||
/// acdream issues those via <see cref="PortalVisibilityBuilder.ConstructViewBuilding"/> per nearby
|
||||
/// building inside <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
|
||||
/// acdream issues those through the retained frame walk per nearby building inside
|
||||
/// <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
|
||||
/// buildings from one root through reverse portals, coupling their interior membership to a single
|
||||
/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap.</para>
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -71,24 +71,6 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
|||
|
||||
Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius);
|
||||
|
||||
// [flap-sweep] camera-collision probe (ACDREAM_PROBE_FLAP), paired with the
|
||||
// builder's [flap]/[flap-cam]. start = the pivot-seated start cell (vs cell = the
|
||||
// player feet cell); ok = the sweep found a valid position (find_valid_position != 0).
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
||||
{
|
||||
var cp = _physics.DataCache?.GetCellStruct(startCell);
|
||||
string bsp = cp?.FlatPhysicsBsp is null
|
||||
? "noflatbsp"
|
||||
: (cp.FlatPhysicsBsp.RootIndex < 0 ? "noroot" : "ok");
|
||||
float desiredBack = Vector3.Distance(pivot, desiredEye);
|
||||
float eyeBack = Vector3.Distance(pivot, eye);
|
||||
System.Console.WriteLine(
|
||||
$"[flap-sweep] cell=0x{cellId:X8} start=0x{startCell:X8} ok={r.Ok} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " +
|
||||
$"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " +
|
||||
$"in=({desiredEye.X:F6},{desiredEye.Y:F6},{desiredEye.Z:F6}) out=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) " +
|
||||
$"viewerCell=0x{r.CellId:X8} collNormValid={r.CollisionNormalValid}");
|
||||
}
|
||||
|
||||
// success: set_viewer(curr_pos, 0); viewer_cell = sphere_path.curr_cell (pc:92870-92871).
|
||||
// Graph-tracked, no AABB/grace.
|
||||
if (r.Ok) return new CameraSweepResult(eye, r.CellId);
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ public static class PortalProjection
|
|||
}
|
||||
|
||||
/// <summary>Project a cell-local polygon to NDC, preserving the projected winding of
|
||||
/// the input (NOT normalized to CCW). The caller (PortalVisibilityBuilder) is responsible
|
||||
/// the input (NOT normalized to CCW). The frame-walk caller is responsible
|
||||
/// for feeding camera-facing portal polygons (via the portal-side test) so the result is
|
||||
/// CCW for the CCW-only <see cref="ScreenPolygonClip"/>. Returns fewer than 3 verts when
|
||||
/// the polygon is entirely behind the camera / degenerate.</summary>
|
||||
|
|
|
|||
|
|
@ -189,8 +189,8 @@ public sealed class CellView
|
|||
{
|
||||
if (p.IsEmpty) return false;
|
||||
|
||||
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build
|
||||
// re-queues a cell every time its CellView GROWS, so the flood only terminates when Add
|
||||
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). A caller may
|
||||
// re-queue a cell every time its CellView grows, so the flood only terminates when Add
|
||||
// recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns
|
||||
// float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman +
|
||||
// EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -62,7 +62,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
private readonly ParticleRenderer? _particleRenderer;
|
||||
private readonly PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private readonly RetailAlphaQueue _alpha;
|
||||
private readonly WorldRenderDiagnostics _diagnostics;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||
private readonly EnvCellAlphaDrawSource _envCellClipAlphaSource;
|
||||
|
|
@ -85,7 +84,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
ParticleRenderer? particleRenderer,
|
||||
PortalDepthMaskRenderer? portalDepthMask,
|
||||
RetailAlphaQueue alpha,
|
||||
WorldRenderDiagnostics diagnostics,
|
||||
TerrainDrawDiagnosticsController terrainDiagnostics)
|
||||
{
|
||||
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
|
||||
|
|
@ -100,7 +98,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
_particleRenderer = particleRenderer;
|
||||
_portalDepthMask = portalDepthMask;
|
||||
_alpha = alpha ?? throw new ArgumentNullException(nameof(alpha));
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_terrainDiagnostics = terrainDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
||||
_envCellClipAlphaSource = new EnvCellAlphaDrawSource(
|
||||
|
|
@ -663,20 +660,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
clipSlot: 0);
|
||||
}
|
||||
|
||||
public void EmitDiagnostics(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewFrameResult result) =>
|
||||
_diagnostics.EmitRetailPViewDiagnostics(
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled,
|
||||
RenderingDiagnostics.ProbeFlapEnabled,
|
||||
result,
|
||||
frame.RootCell,
|
||||
frame.ViewerCellId,
|
||||
frame.PlayerCellId,
|
||||
frame.CameraWorldPosition,
|
||||
frame.PlayerViewPosition,
|
||||
frame.CameraCellResolution);
|
||||
|
||||
/// <summary>Retail <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
||||
/// @0x0059BC90. Main interior roots stamp true depth (seal); outdoor and
|
||||
/// look-in apertures stamp far depth (punch). The renderer owns that
|
||||
|
|
@ -737,14 +720,6 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
cell.WorldTransform);
|
||||
}
|
||||
|
||||
_diagnostics.EmitSeamMask(
|
||||
RenderingDiagnostics.ProbeSeamDrawEnabled,
|
||||
RenderingDiagnostics.SeamDrawTargetCells,
|
||||
cellId,
|
||||
index,
|
||||
forceFarZ,
|
||||
world[..count]);
|
||||
|
||||
// S4-c1 fix round 1 F2: retail increments portalsDrawnCount
|
||||
// (0x59BD70-0x59BD74) BEFORE polyClipFinish runs (0x59BDB0) —
|
||||
// the counter records accepted ATTEMPTS, not successful GPU
|
||||
|
|
|
|||
|
|
@ -32,12 +32,6 @@ 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 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
|
||||
|
|
@ -108,11 +102,8 @@ internal sealed class RetailPViewRenderer
|
|||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
ArgumentNullException.ThrowIfNull(passes);
|
||||
passes.BeginFrame();
|
||||
// Campaign FW4: there is one production renderer and one visibility
|
||||
// owner. The fake/legacy executor path was useful during the cutover,
|
||||
// but retaining it here kept the retired PortalVisibilityBuilder and
|
||||
// look-in seed machinery alive. Fail loudly if composition ever tries
|
||||
// to reintroduce that split.
|
||||
// There is one production renderer and one frame-walk owner. Fail
|
||||
// loudly if composition attempts to split that path.
|
||||
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
|
||||
?? throw new InvalidOperationException(
|
||||
"The retail frame walk requires RetailPViewPassExecutor.");
|
||||
|
|
@ -294,128 +285,13 @@ internal sealed class RetailPViewRenderer
|
|||
viewportHeight);
|
||||
}
|
||||
|
||||
// The walk's visited set replaces PortalVisibilityFrame's old
|
||||
// OrderedVisibleCells side-channel for every production consumer.
|
||||
// The walk's visited set feeds every production consumer.
|
||||
_drawableCellsScratch.Clear();
|
||||
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
||||
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
||||
_visibleLandscapeCellsScratch.Clear();
|
||||
_visibleLandscapeCellsScratch.UnionWith(walkDriver.VisitedLandscapeCellIds);
|
||||
|
||||
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
|
||||
|
|
@ -448,8 +324,6 @@ internal sealed class RetailPViewRenderer
|
|||
counts,
|
||||
sourceCounts,
|
||||
diagnosticPartition: null);
|
||||
passes.EmitDiagnostics(ctx, result);
|
||||
|
||||
// The one collected walk owns terrain, cell shells, buildings,
|
||||
// statics, dynamics, particles, punches, and alpha barriers at
|
||||
// their retail turns. Interior-root landscape services run at the
|
||||
|
|
|
|||
|
|
@ -1,207 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
|
||||
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
|
||||
/// view by a bounding-sphere test against the view's edge planes, never
|
||||
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
|
||||
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
|
||||
/// the object's drawing sphere against the installed view's plane set;
|
||||
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
|
||||
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
|
||||
///
|
||||
/// <para>Our views are clip-space half-planes (≤8 per slice,
|
||||
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
|
||||
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
|
||||
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
|
||||
/// is one matrix fold: with row-vector convention (System.Numerics),
|
||||
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
|
||||
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
|
||||
/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).</para>
|
||||
///
|
||||
/// <para>A sphere is visible through a SLICE when it is not entirely outside
|
||||
/// any of the slice's planes (convex region); visible for a CELL when any of
|
||||
/// the cell's slices passes. A slice with zero planes is pass-all (the
|
||||
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
|
||||
/// retail an object whose cell is not in the draw list is simply never
|
||||
/// reached.</para>
|
||||
/// </summary>
|
||||
public sealed class ViewconeCuller
|
||||
{
|
||||
private const int MaxRetainedCellPlaneSets = 512;
|
||||
private const int MaxRetainedPlanesPerCell = 256;
|
||||
private const int MaxRetainedSlicesPerCell = 64;
|
||||
|
||||
private readonly Dictionary<uint, PlaneSet> _cellPlanes = new();
|
||||
private readonly Stack<PlaneSet> _planeSetPool = new();
|
||||
private PlaneSet _outsidePlanes = new();
|
||||
|
||||
private readonly record struct SliceRange(int Start, int Count);
|
||||
private readonly record struct LiftedPlane(Vector4 Equation, float NormalLength);
|
||||
|
||||
/// <summary>
|
||||
/// Contiguous per-cell plane storage. Reusing two Lists per visible cell
|
||||
/// avoids rebuilding a jagged array graph on every render frame while
|
||||
/// retaining the exact slice boundaries used by retail's any-view test.
|
||||
/// </summary>
|
||||
private sealed class PlaneSet
|
||||
{
|
||||
public List<LiftedPlane> Planes { get; } = new();
|
||||
public List<SliceRange> Slices { get; } = new();
|
||||
|
||||
public bool IsRetainable =>
|
||||
Planes.Capacity <= MaxRetainedPlanesPerCell
|
||||
&& Slices.Capacity <= MaxRetainedSlicesPerCell;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Planes.Clear();
|
||||
Slices.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True when the outside view is a full-screen pass-all (the
|
||||
/// synthetic outdoor root) — every outside-test passes.</summary>
|
||||
public bool OutsideIsFullScreen { get; private set; }
|
||||
|
||||
public static ViewconeCuller Build(
|
||||
ClipFrameAssembly assembly,
|
||||
in Matrix4x4 viewProjection,
|
||||
ViewconeCuller? reuse = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
var culler = reuse ?? new ViewconeCuller();
|
||||
culler.Reset();
|
||||
|
||||
foreach (var (cellId, slices) in assembly.CellIdToViewSlices)
|
||||
{
|
||||
PlaneSet lifted = culler.RentPlaneSet();
|
||||
for (int s = 0; s < slices.Length; s++)
|
||||
AppendLiftedSlice(lifted, slices[s].Planes, viewProjection);
|
||||
culler._cellPlanes[cellId] = lifted;
|
||||
}
|
||||
|
||||
var outside = assembly.OutsideViewSlices;
|
||||
bool fullScreen = false;
|
||||
for (int s = 0; s < outside.Length; s++)
|
||||
{
|
||||
AppendLiftedSlice(culler._outsidePlanes, outside[s].Planes, viewProjection);
|
||||
if (outside[s].Planes.Length == 0)
|
||||
fullScreen = true;
|
||||
}
|
||||
culler.OutsideIsFullScreen = fullScreen;
|
||||
return culler;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
foreach (PlaneSet set in _cellPlanes.Values)
|
||||
{
|
||||
set.Reset();
|
||||
if (set.IsRetainable && _planeSetPool.Count < MaxRetainedCellPlaneSets)
|
||||
_planeSetPool.Push(set);
|
||||
}
|
||||
_cellPlanes.Clear();
|
||||
if (_outsidePlanes.IsRetainable)
|
||||
_outsidePlanes.Reset();
|
||||
else
|
||||
_outsidePlanes = new PlaneSet();
|
||||
OutsideIsFullScreen = false;
|
||||
}
|
||||
|
||||
private PlaneSet RentPlaneSet()
|
||||
{
|
||||
PlaneSet result = _planeSetPool.Count != 0
|
||||
? _planeSetPool.Pop()
|
||||
: new PlaneSet();
|
||||
result.Reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AppendLiftedSlice(
|
||||
PlaneSet destination,
|
||||
Vector4[] clipPlanes,
|
||||
in Matrix4x4 m)
|
||||
{
|
||||
int start = destination.Planes.Count;
|
||||
for (int i = 0; i < clipPlanes.Length; i++)
|
||||
{
|
||||
var p = clipPlanes[i];
|
||||
var equation = new Vector4(
|
||||
m.M11 * p.X + m.M12 * p.Y + m.M13 * p.Z + m.M14 * p.W,
|
||||
m.M21 * p.X + m.M22 * p.Y + m.M23 * p.Z + m.M24 * p.W,
|
||||
m.M31 * p.X + m.M32 * p.Y + m.M33 * p.Z + m.M34 * p.W,
|
||||
m.M41 * p.X + m.M42 * p.Y + m.M43 * p.Z + m.M44 * p.W);
|
||||
float normalLength = MathF.Sqrt(
|
||||
equation.X * equation.X
|
||||
+ equation.Y * equation.Y
|
||||
+ equation.Z * equation.Z);
|
||||
destination.Planes.Add(new LiftedPlane(equation, normalLength));
|
||||
}
|
||||
destination.Slices.Add(new SliceRange(start, clipPlanes.Length));
|
||||
}
|
||||
|
||||
private static bool SphereInsidePlanes(
|
||||
PlaneSet set,
|
||||
SliceRange slice,
|
||||
in Vector3 center,
|
||||
float radius)
|
||||
{
|
||||
int end = slice.Start + slice.Count;
|
||||
for (int i = slice.Start; i < end; i++)
|
||||
{
|
||||
LiftedPlane plane = set.Planes[i];
|
||||
Vector4 l = plane.Equation;
|
||||
float nLen = plane.NormalLength;
|
||||
if (nLen < 1e-12f)
|
||||
continue; // degenerate plane — no constraint
|
||||
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
|
||||
if (dist < -radius * nLen)
|
||||
return false; // entirely outside this edge plane
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
|
||||
/// A cell with no views culls (not in the draw list ⇒ never reached in
|
||||
/// retail). A zero-plane slice is pass-all.</summary>
|
||||
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
|
||||
{
|
||||
if (!_cellPlanes.TryGetValue(cellId, out PlaneSet? set))
|
||||
return false;
|
||||
for (int s = 0; s < set.Slices.Count; s++)
|
||||
if (SphereInsidePlanes(set, set.Slices[s], center, radius))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Sphere-vs-the-outside-views (objects in outdoor space seen
|
||||
/// from an interior root through doorways; pass-all under the outdoor
|
||||
/// root's full-screen outside view).</summary>
|
||||
public bool SphereVisibleOutside(in Vector3 center, float radius)
|
||||
{
|
||||
if (OutsideIsFullScreen)
|
||||
return true;
|
||||
for (int s = 0; s < _outsidePlanes.Slices.Count; s++)
|
||||
if (SphereInsidePlanes(_outsidePlanes, _outsidePlanes.Slices[s], center, radius))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Sphere vs ONE outside slice (the landscape pass draws per
|
||||
/// slice; its statics pre-filter tests against exactly that slice).</summary>
|
||||
public bool SphereVisibleInOutsideSlice(int sliceIndex, in Vector3 center, float radius)
|
||||
{
|
||||
if ((uint)sliceIndex >= (uint)_outsidePlanes.Slices.Count)
|
||||
return false;
|
||||
return SphereInsidePlanes(
|
||||
_outsidePlanes,
|
||||
_outsidePlanes.Slices[sliceIndex],
|
||||
center,
|
||||
radius);
|
||||
}
|
||||
}
|
||||
|
|
@ -107,9 +107,8 @@ public sealed class RetailFrameWalk
|
|||
/// exit-view polygons THIS walk's own <c>ConstructView</c> installed
|
||||
/// during the most recent <see cref="DrawInside"/>. The production
|
||||
/// renderer materializes the frame's terrain/sky/punch clip slices from
|
||||
/// these views (retail has exactly one visibility structure; the old
|
||||
/// PortalVisibilityBuilder assembly no longer feeds the walk path's
|
||||
/// outside views). Valid after <see cref="WalkFrame"/> returns for an
|
||||
/// these views (retail has exactly one visibility structure). Valid after
|
||||
/// <see cref="WalkFrame"/> returns for an
|
||||
/// interior root, until the next interior root's view push resets it.</summary>
|
||||
internal WalkPortalView InteriorOutsideView => _interiorPView.OutsideView;
|
||||
|
||||
|
|
|
|||
|
|
@ -186,11 +186,6 @@ public sealed class WalkPView
|
|||
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)
|
||||
|
|
@ -198,11 +193,6 @@ public sealed class WalkPView
|
|||
cell.CachedNeighbors[j] = ctx.GetVisible(portal.OtherCellId);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -223,15 +213,8 @@ public sealed class WalkPView
|
|||
cell, portal.PortalSide,
|
||||
cell.PortalPolygons[portal.PolygonIndex],
|
||||
doClip: true, ctx, _clipScratch);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -257,123 +240,19 @@ public sealed class WalkPView
|
|||
SetView(top, i); // restore after the far-frame excursion
|
||||
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)
|
||||
appended = WalkCopyView.Append(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellDynamicCache = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellObjectsCache = new();
|
||||
private readonly Dictionary<string, string> _facilityShadowProbeSignatures = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorDynamicsMaterialized = new();
|
||||
|
|
@ -162,21 +161,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
int renderCenterLbX,
|
||||
int renderCenterLbY)
|
||||
{
|
||||
// Campaign OVERHAUL S2 chunk 5: report the PRIOR frame's count
|
||||
// before resetting it — the count is only final once that frame's
|
||||
// walk (a sequence of on-demand Get* calls this class has no other
|
||||
// "frame is done" hook for) has finished, so this is necessarily a
|
||||
// one-frame-delayed report, matching every other per-second/per-
|
||||
// change probe in this file family (print-only; never influences
|
||||
// admission).
|
||||
if (UnregisteredRenderMembershipCount > 0
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[walk-membership] unregistered={UnregisteredRenderMembershipCount} "
|
||||
+ $"tupleLandblock=0x{_tupleLandblockId:X8}");
|
||||
}
|
||||
|
||||
_scene = scene;
|
||||
_tupleLandblockId = tupleLandblockId;
|
||||
_cellCache.Clear();
|
||||
|
|
@ -217,41 +201,11 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Facility Hub / cathedral discriminator: does the given cell's
|
||||
/// borrowed retail render membership contain a record with this exact
|
||||
/// authored SourceId? Reimplemented directly over the registry's
|
||||
/// per-cell entries (chunk 5) rather than reading a swept bucket
|
||||
/// dictionary — <see cref="RetailPViewRenderer"/>'s
|
||||
/// <c>ProbeFacilityStairsEnabled</c> block is the only caller.</summary>
|
||||
internal bool StaticBucketContains(uint cellId, uint sourceId)
|
||||
{
|
||||
IReadOnlyList<RetailPartEntry> entries =
|
||||
_shadows.GetRetailPartEntriesInCell(cellId);
|
||||
uint previousEntityId = 0;
|
||||
bool havePrevious = false;
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
uint entityId = entries[i].EntityId;
|
||||
if (havePrevious && entityId == previousEntityId)
|
||||
continue;
|
||||
previousEntityId = entityId;
|
||||
havePrevious = true;
|
||||
|
||||
if (_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record)
|
||||
&& record.Source.SourceId == sourceId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetCellStatics(uint cellId)
|
||||
{
|
||||
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false);
|
||||
EmitFacilityShadowProbe(records.Records, cellId, "static");
|
||||
_cellCache[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
|
@ -261,7 +215,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
|
||||
EmitFacilityShadowProbe(records.Records, cellId, "combined");
|
||||
_cellObjectsCache[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
|
@ -271,7 +224,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true);
|
||||
EmitFacilityShadowProbe(records.Records, cellId, "dynamic");
|
||||
_cellDynamicCache[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
|
@ -391,68 +343,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
projectionClass is RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
|
||||
/// <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 GetBuildingShellStatics(WalkBuilding building)
|
||||
{
|
||||
uint anchor = BuildingShellBucketCellId(building);
|
||||
|
|
|
|||
|
|
@ -224,8 +224,7 @@ internal sealed class WalkStaticStreamPopulator
|
|||
liveDynamic,
|
||||
views,
|
||||
viewRouteIndex,
|
||||
cellId,
|
||||
diagnosticViewProjection: viewProjection);
|
||||
cellId);
|
||||
|
||||
for (int batchIndex = 0; batchIndex < _batchScratch.Count; batchIndex++)
|
||||
{
|
||||
|
|
@ -257,17 +256,10 @@ internal sealed class WalkStaticStreamPopulator
|
|||
WbDrawDispatcher.WalkClassifiedBatch batch = item.Batch;
|
||||
if (batch.IsOpaque)
|
||||
{
|
||||
int commandIndex = stream.Count;
|
||||
stream.Append(new OrderedDrawCommand(
|
||||
batch.Key, batch.Transform, item.Stage, cellId, batch.ClipSlot,
|
||||
batch.Lights, batch.IndoorFlag, batch.Alpha,
|
||||
batch.SelectionLighting, batch.DetailCategory));
|
||||
_dispatcher.ProbeFacilityStairCommandAppended(
|
||||
commandIndex,
|
||||
item.LocalEntityId,
|
||||
cellId,
|
||||
item.Stage,
|
||||
in batch);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -318,7 +310,6 @@ internal sealed class WalkStaticStreamPopulator
|
|||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
cellId,
|
||||
diagnosticViewProjection: viewProjection,
|
||||
buildingSelection: buildingSelection,
|
||||
buildingPartTransform: buildingPartTransform);
|
||||
|
||||
|
|
@ -327,17 +318,10 @@ internal sealed class WalkStaticStreamPopulator
|
|||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -220,12 +220,6 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
|
||||
}
|
||||
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||
{
|
||||
EmitSeamDrawProbe(_renderDrawCalls, allInstances, _seamProbeFilter);
|
||||
}
|
||||
|
||||
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
int globalLightUploadCount = lightCount > 0 ? lightCount : 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -203,21 +203,6 @@ public sealed partial class EnvCellRenderer :
|
|||
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
|
||||
private LastFrameStats _lastFrameStats;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
|
||||
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
|
||||
/// A divergence between expected and actual values would indicate a pool-
|
||||
/// management regression — exactly the bug class the 2026-05-28 audit caught.
|
||||
/// </summary>
|
||||
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
|
||||
{
|
||||
int poolTotal;
|
||||
lock (_listPool) poolTotal = _listPool.Count;
|
||||
int hwm;
|
||||
lock (_renderLock) hwm = _activeSnapshot.PostPreparePoolIndex;
|
||||
return (poolTotal, hwm);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// Campaign V slice V11: the raw-GL constructor + Initialize(Shader) two-step
|
||||
|
|
@ -1004,12 +989,6 @@ public sealed partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
// #176 seam-draw probe: stash this call's filter so the opaque-pass
|
||||
// emitter inside RenderModernMDIInternal can report flood membership
|
||||
// per target cell (null on the unfiltered/outdoor path).
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||
_seamProbeFilter = filter;
|
||||
|
||||
// WB EnvCellRenderManager.cs:470-483:
|
||||
if (allInstances.Count > 0)
|
||||
{
|
||||
|
|
@ -1421,104 +1400,6 @@ public sealed partial class EnvCellRenderer :
|
|||
ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount, materialState));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
|
||||
// The in-engine replacement for the RenderDoc pixel-history the pipeline
|
||||
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
|
||||
// startup gate throws). Per opaque pass: for each target cell — flood
|
||||
// membership, every shell instance (count + translation, F3 z shows the
|
||||
// +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
|
||||
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
|
||||
// intensity; raw indices shuffle when the pool rebuilds). Plus the
|
||||
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
|
||||
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
|
||||
// flipping with flood membership = the snapshot-scope mechanism; two
|
||||
// coincident instances = the z-fight. See RenderingDiagnostics.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private HashSet<uint>? _seamProbeFilter;
|
||||
private string? _seamSig;
|
||||
private long _seamLastEmitMs;
|
||||
|
||||
private void EmitSeamDrawProbe(
|
||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
||||
List<InstanceData> allInstances,
|
||||
HashSet<uint>? filter)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
var snap = _pointSnapshot;
|
||||
var sb = new System.Text.StringBuilder(640);
|
||||
|
||||
var sorted = new List<uint>(AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells);
|
||||
sorted.Sort();
|
||||
foreach (uint cell in sorted)
|
||||
{
|
||||
sb.Append("\n[seam-cell] cell=0x").Append(cell.ToString("X8"));
|
||||
sb.Append(" flood=").Append(filter is null ? '?' : (filter.Contains(cell) ? 'Y' : 'N'));
|
||||
|
||||
int totalInst = 0;
|
||||
foreach (var dc in drawCalls)
|
||||
{
|
||||
int n = 0;
|
||||
InstanceData first = default;
|
||||
for (int i = dc.offset; i < dc.offset + dc.count; i++)
|
||||
{
|
||||
if (allInstances[i].CellId != cell) continue;
|
||||
if (n == 0) first = allInstances[i];
|
||||
n++;
|
||||
}
|
||||
if (n == 0) continue;
|
||||
totalInst += n;
|
||||
var t = first.Transform.Translation;
|
||||
sb.AppendFormat(ci, " g=0x{0:X8}:n={1}@({2:F2},{3:F2},{4:F3})",
|
||||
dc.gfxObjId, n, t.X, t.Y, t.Z);
|
||||
}
|
||||
if (totalInst == 0) sb.Append(" inst=0");
|
||||
|
||||
// The 8-light set this cell's instances carry (fresh: the per-pass
|
||||
// cache was cleared at the top of RenderModernMDIInternal).
|
||||
int[] set = GetCellLightSet(cell);
|
||||
sb.Append(" L=[");
|
||||
bool any = false;
|
||||
for (int k = 0; k < set.Length; k++)
|
||||
{
|
||||
int idx = set[k];
|
||||
if (idx < 0) continue;
|
||||
if (any) sb.Append(',');
|
||||
if (snap is not null && idx < snap.Count)
|
||||
sb.AppendFormat(ci, "{0:X4}:I{1:F0}", snap[idx].CellId & 0xFFFFu, snap[idx].Intensity);
|
||||
else
|
||||
sb.Append('?').Append(idx);
|
||||
any = true;
|
||||
}
|
||||
sb.Append(']');
|
||||
}
|
||||
|
||||
sb.Append("\n[seam-snap] pool=").Append(snap?.Count ?? 0).Append(" hot=[");
|
||||
if (snap is not null)
|
||||
{
|
||||
bool anyHot = false;
|
||||
for (int i = 0; i < snap.Count; i++)
|
||||
{
|
||||
var ls = snap[i];
|
||||
if (ls.Intensity < 50f) continue;
|
||||
if (anyHot) sb.Append(',');
|
||||
sb.AppendFormat(ci, "0x{0:X8}:I{1:F0}rgb({2:F2},{3:F2},{4:F2})",
|
||||
ls.CellId, ls.Intensity, ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z);
|
||||
anyHot = true;
|
||||
}
|
||||
}
|
||||
sb.Append(']');
|
||||
|
||||
string sig = sb.ToString();
|
||||
long now = System.Environment.TickCount64;
|
||||
bool changed = sig != _seamSig;
|
||||
if (!changed && (now - _seamLastEmitMs) < 2000) return;
|
||||
_seamSig = sig;
|
||||
_seamLastEmitMs = now;
|
||||
System.Console.WriteLine($"[seam-blk] t={now} changed={(changed ? 1 : 0)}{sig}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List pool (GetPooledList)
|
||||
// Copied from WB ObjectRenderManagerBase (pattern).
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +350,6 @@ 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.
|
||||
|
|
@ -433,7 +432,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
checked((uint)(count * DrawCommandStride)));
|
||||
|
||||
_orderedPreparedCount = count;
|
||||
ProbeFacilityStairUploadCompleted(count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -587,11 +585,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
DrawIndirectRangeRhi(
|
||||
encoder, ref pushConstants, commandBuffer, commandBase,
|
||||
run.FirstCommand, run.CommandCount, _orderedDrawCullModes);
|
||||
ProbeFacilityStairRunSubmitted(
|
||||
firstCommand,
|
||||
commandCount,
|
||||
run.FirstCommand,
|
||||
run.CommandCount);
|
||||
}
|
||||
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
|
|
|
|||
|
|
@ -55,20 +55,6 @@ public sealed partial class WbDrawDispatcher
|
|||
{
|
||||
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);
|
||||
|
|
@ -93,7 +79,6 @@ public sealed partial class WbDrawDispatcher
|
|||
|
||||
_missRequested.Clear();
|
||||
_walkDrawnParts.Clear();
|
||||
BeginFacilityStairSubmissionProbeFrame();
|
||||
_walkPartFrameActive = true;
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +260,6 @@ public sealed partial class WbDrawDispatcher
|
|||
IWalkLookInViewSource? lookInViews = null,
|
||||
int lookInRouteIndex = -1,
|
||||
uint lookInCellId = 0,
|
||||
Matrix4x4 diagnosticViewProjection = default,
|
||||
WalkBuildingSelection? buildingSelection = null,
|
||||
Matrix4x4 buildingPartTransform = default)
|
||||
{
|
||||
|
|
@ -369,17 +353,6 @@ 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;
|
||||
|
|
@ -393,12 +366,6 @@ public sealed partial class WbDrawDispatcher
|
|||
: 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;
|
||||
}
|
||||
|
||||
|
|
@ -411,21 +378,11 @@ public sealed partial class WbDrawDispatcher
|
|||
lookInRouteIndex,
|
||||
partData,
|
||||
model,
|
||||
out Vector3 sphereCenter,
|
||||
out float sphereRadius,
|
||||
out bool hasSphere);
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
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(
|
||||
|
|
@ -448,12 +405,6 @@ public sealed partial class WbDrawDispatcher
|
|||
: 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;
|
||||
}
|
||||
|
||||
|
|
@ -463,21 +414,11 @@ public sealed partial class WbDrawDispatcher
|
|||
lookInRouteIndex,
|
||||
renderData,
|
||||
model,
|
||||
out Vector3 sphereCenter,
|
||||
out float sphereRadius,
|
||||
out bool hasSphere);
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
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(
|
||||
|
|
@ -585,213 +526,6 @@ public sealed partial class WbDrawDispatcher
|
|||
return lookInViews.SphereVisibleInLookInTurn(routeIndex, in sphereCenter, sphereRadius);
|
||||
}
|
||||
|
||||
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(
|
||||
IWalkLookInViewSource lookInViews,
|
||||
int routeIndex,
|
||||
|
|
|
|||
|
|
@ -750,31 +750,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private bool _disposing;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Per-cell-entity last-log frame number for rate-limiting the
|
||||
/// [indoor-walk] / [indoor-lookup] / [indoor-xform] / [indoor-cull]
|
||||
/// probes. Defaults to 30 frames at 30Hz = 1 sec.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ulong, int> _lastIndoorProbeFrame = new();
|
||||
private int _indoorProbeFrameCounter;
|
||||
private const int IndoorProbeRateLimitFrames = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||
/// frames per cellId. Caller must already have checked that an indoor
|
||||
/// probe flag is enabled.
|
||||
/// </summary>
|
||||
private bool ShouldEmitIndoorProbe(ulong cellId)
|
||||
{
|
||||
if (!_lastIndoorProbeFrame.TryGetValue(cellId, out int last)
|
||||
|| _indoorProbeFrameCounter - last >= IndoorProbeRateLimitFrames)
|
||||
{
|
||||
_lastIndoorProbeFrame[cellId] = _indoorProbeFrameCounter;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
|
||||
private int _entitiesSeen;
|
||||
private int _entitiesDrawn;
|
||||
|
|
@ -947,16 +922,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// list. <see cref="Draw"/> reuses a per-dispatcher scratch field across frames to
|
||||
/// avoid the 480+ KB / frame GC pressure that the test-friendly overload incurs.
|
||||
/// Returns walk count via <paramref name="result"/>'s <c>EntitiesWalked</c> field.
|
||||
///
|
||||
/// <para>
|
||||
/// When <paramref name="indoorProbeState"/> is non-null the method emits
|
||||
/// <c>[indoor-cull]</c> lines for cell entities rejected by the
|
||||
/// visibleCellIds or frustum filters, and <c>[indoor-walk]</c> lines for
|
||||
/// cell entities that pass all filters. Rate-limited by
|
||||
/// <see cref="IndoorProbeState"/>. Pass <see langword="null"/> (the default)
|
||||
/// to disable all probe emission — used by the test-friendly
|
||||
/// <see cref="WalkEntities"/> overload.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static void WalkEntitiesInto(
|
||||
IEnumerable<LandblockEntry> landblockEntries,
|
||||
|
|
@ -966,7 +931,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
HashSet<uint>? animatedEntityIds,
|
||||
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> scratch,
|
||||
ref WalkResult result,
|
||||
IndoorProbeState? indoorProbeState = null,
|
||||
EntitySet set = EntitySet.All)
|
||||
{
|
||||
scratch.Clear();
|
||||
|
|
@ -1015,13 +979,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
|
||||
// is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
|
||||
// result reused for all probe checks below.
|
||||
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
|
||||
bool isCellEntity = indoorProbeState is not null
|
||||
&& RenderingDiagnostics.IsEnvCellId(cellProbeId);
|
||||
|
||||
bool shellScoped = IsShellScopedSet(set)
|
||||
&& entity.IsBuildingShell
|
||||
&& visibleCellIds is not null;
|
||||
|
|
@ -1029,14 +986,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
if (!cellInVis)
|
||||
{
|
||||
if (shellScoped) result.BuildingShellAnchorReject++;
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||
$"reason=visibleCellIds-miss " +
|
||||
$"parentCell=0x{entity.ParentCellId!.Value:X8}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (shellScoped) result.BuildingShellAnchorPass++;
|
||||
|
|
@ -1054,31 +1003,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
|
||||
if (!aabbVisible)
|
||||
{
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||
$"reason=frustum " +
|
||||
$"aabbMin=({entity.AabbMin.X:F1},{entity.AabbMin.Y:F1},{entity.AabbMin.Z:F1}) " +
|
||||
$"aabbMax=({entity.AabbMax.X:F1},{entity.AabbMax.Y:F1},{entity.AabbMax.Z:F1})");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Passed all filters — emit walk probe.
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-walk] cellEnt=0x{entity.Id:X8} " +
|
||||
$"pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) " +
|
||||
$"parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||
$"meshRef0=0x{cellProbeId:X8} " +
|
||||
$"meshRefCount={entity.MeshRefs.Count} " +
|
||||
$"landblockVisible=true aabbVisible=true cellInVis=true");
|
||||
}
|
||||
|
||||
result.EntitiesWalked++;
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
scratch.Add((entity, i, entry.LandblockId));
|
||||
|
|
@ -1196,23 +1123,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// that populates _walkScratch (a per-dispatcher field reused across frames)
|
||||
// instead of allocating a fresh List<(WorldEntity, int)> per frame.
|
||||
//
|
||||
// Pass an IndoorProbeState when any indoor probe is active so the static
|
||||
// WalkEntitiesInto can emit rate-limited [indoor-cull] / [indoor-walk]
|
||||
// lines without needing access to instance fields. Null = probes off.
|
||||
IndoorProbeState? probeState = null;
|
||||
if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled)
|
||||
{
|
||||
// _currentFrame is snapped at construction time. Construct
|
||||
// once per Draw() call only — a second construction within
|
||||
// the same frame would stamp the dictionary with the
|
||||
// (already-advanced) counter value, suppressing the second
|
||||
// pass's emissions for IndoorProbeRateLimitFrames frames.
|
||||
// Today Draw() is called exactly once per frame; if a
|
||||
// future refactor adds a shadow / reflection / second pass,
|
||||
// this assumption needs revisiting.
|
||||
probeState = new IndoorProbeState(_lastIndoorProbeFrame, _indoorProbeFrameCounter);
|
||||
}
|
||||
|
||||
var walkResult = default(WalkResult);
|
||||
WalkEntitiesInto(
|
||||
ToEntries(landblockEntries),
|
||||
|
|
@ -1222,7 +1132,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
animatedEntityIds,
|
||||
_walkScratch,
|
||||
ref walkResult,
|
||||
probeState,
|
||||
set);
|
||||
_currentRenderSceneObserver?.ObserveDispatcherDraw(
|
||||
set,
|
||||
|
|
@ -1358,14 +1267,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
? new Vector2(lighting.Luminosity, lighting.Diffuse)
|
||||
: new Vector2(0f, 1f);
|
||||
|
||||
// #176 seam-draw probe: any entity parented to a target cell reports
|
||||
// its position + light set (a floor-coincident static/plate would be
|
||||
// the z-fight's second draw; the player entity is the positive
|
||||
// control). Before the culled-continue, like the dump above.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
|
||||
&& entity.ParentCell is { } seamPc
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(seamPc))
|
||||
MaybeEmitSeamEnt(entity);
|
||||
}
|
||||
prevTupleEntityId = entity.Id;
|
||||
|
||||
|
|
@ -1468,41 +1369,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
|
||||
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
||||
|
||||
// [indoor-lookup] probe — emit once per cell entity per sec.
|
||||
// Fires BEFORE the null-renderData early-continue so a miss still
|
||||
// emits hit=false, distinguishing H2 (empty batches) from H6
|
||||
// (dispatcher fails to traverse Setup).
|
||||
ulong lookupCellId = (ulong)gfxObjId;
|
||||
if (RenderingDiagnostics.IsEnvCellId(lookupCellId)
|
||||
&& RenderingDiagnostics.ProbeIndoorLookupEnabled
|
||||
// Rate-limit in a separate namespace from [indoor-walk]/[indoor-cull]
|
||||
// (which key on the same gfxObjId). Without this, IndoorAll=1 would
|
||||
// silence the lookup probe whenever the walk probe fired first.
|
||||
&& ShouldEmitIndoorProbe(lookupCellId | 0x8000_0000_0000_0000UL))
|
||||
{
|
||||
bool hit = renderData is not null;
|
||||
bool isSetup = hit && renderData!.IsSetup;
|
||||
int partCount = isSetup ? renderData!.SetupParts.Count : 0;
|
||||
|
||||
int partsHit = 0, partsMiss = 0;
|
||||
if (isSetup)
|
||||
{
|
||||
foreach (var (partId, _) in renderData!.SetupParts)
|
||||
{
|
||||
if (_meshAdapter.TryGetRenderData(partId) is not null) partsHit++;
|
||||
else partsMiss++;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasEnvCellGeom = isSetup
|
||||
&& renderData!.SetupParts.Exists(t => (t.GfxObjId & 0x1_0000_0000UL) != 0);
|
||||
|
||||
Console.WriteLine(
|
||||
$"[indoor-lookup] cellId=0x{lookupCellId:X8} " +
|
||||
$"hit={hit} isSetup={isSetup} partCount={partCount} " +
|
||||
$"hasEnvCellGeom={hasEnvCellGeom} partsHit={partsHit} partsMiss={partsMiss}");
|
||||
}
|
||||
|
||||
if (renderData is null)
|
||||
{
|
||||
// Tier 1 cache (#53): mesh data is still async-decoding via
|
||||
|
|
@ -1595,23 +1461,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
var model = ComposePartWorldMatrix(
|
||||
entityWorld, meshRef.PartTransform, partTransform);
|
||||
|
||||
// [indoor-xform] probe — only for the cell's synthetic
|
||||
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
|
||||
// cellGeomId convention). One line per part per sec.
|
||||
// Disambiguates hypothesis H5 (transform double-apply —
|
||||
// composedT lands at 2 × cellOrigin).
|
||||
if ((partGfxObjId & 0x1_0000_0000UL) != 0
|
||||
&& RenderingDiagnostics.ProbeIndoorXformEnabled
|
||||
&& ShouldEmitIndoorProbe(partGfxObjId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-xform] cellGeomId=0x{partGfxObjId:X16} " +
|
||||
$"entityWorldT=({entityWorld.Translation.X:F2},{entityWorld.Translation.Y:F2},{entityWorld.Translation.Z:F2}) " +
|
||||
$"meshRefT=({meshRef.PartTransform.Translation.X:F2},{meshRef.PartTransform.Translation.Y:F2},{meshRef.PartTransform.Translation.Z:F2}) " +
|
||||
$"partT=({partTransform.Translation.X:F2},{partTransform.Translation.Y:F2},{partTransform.Translation.Z:F2}) " +
|
||||
$"composedT=({model.Translation.X:F2},{model.Translation.Y:F2},{model.Translation.Z:F2})");
|
||||
}
|
||||
|
||||
var restPose = partTransform * meshRef.PartTransform;
|
||||
|
||||
// #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a
|
||||
|
|
@ -1718,11 +1567,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
populateEntityId, populateLandblockId, _cache,
|
||||
_populateScratch, _populateSelectionScratch);
|
||||
|
||||
// S3 review fix round 1 (F5): the §4 flap [clip-route-disp] probe
|
||||
// (2026-06-10, throwaway) is deleted with the routing state it
|
||||
// reported on — it could only ever print "cullEnt=0" now that
|
||||
// ResolveSlotForFrame never culls anything.
|
||||
|
||||
ExecuteClassifiedGroups(
|
||||
vp,
|
||||
camPos,
|
||||
|
|
@ -1777,7 +1621,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
out Vector3 cameraWorldPosition)
|
||||
{
|
||||
_selectionLighting?.TickLighting();
|
||||
_indoorProbeFrameCounter++;
|
||||
viewProjection = camera.View * camera.Projection;
|
||||
_missRequested.Clear();
|
||||
|
||||
|
|
@ -3085,44 +2928,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// (AP-43) and docs/research/2026-06-19-lighting-a7-fixD-round2-*.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One
|
||||
// [seam-ent] line per target-cell entity, re-emitted on state change: world
|
||||
// position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot,
|
||||
// and the SelectForObject light set resolved to identities (owner-cell
|
||||
// low16 + intensity). Sig dict is bounded by the handful of entities that
|
||||
// ever live in the target cells.
|
||||
private readonly Dictionary<ulong, string> _seamEntSigs = new();
|
||||
|
||||
private void MaybeEmitSeamEnt(in RenderInstanceCandidate entity)
|
||||
{
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
var snap = _pointSnapshot;
|
||||
var sb = new System.Text.StringBuilder(200);
|
||||
sb.AppendFormat(ci,
|
||||
"guid=0x{0:X8} cell=0x{1:X8} pos=({2:F2},{3:F2},{4:F3}) culled={5} slot={6} indoor={7} L=[",
|
||||
entity.ServerGuid, entity.ParentCellId,
|
||||
entity.Position.X, entity.Position.Y, entity.Position.Z,
|
||||
_currentEntityCulled ? 1 : 0, _currentEntitySlot, _currentEntityIndoor ? 1 : 0);
|
||||
bool any = false;
|
||||
for (int k = 0; k < LightManager.MaxLightsPerObject; k++)
|
||||
{
|
||||
int idx = _currentEntityLightSet[k];
|
||||
if (idx < 0) continue;
|
||||
if (any) sb.Append(',');
|
||||
if (snap is not null && idx < snap.Count)
|
||||
sb.AppendFormat(ci, "{0:X4}:I{1:F0}", snap[idx].CellId & 0xFFFFu, snap[idx].Intensity);
|
||||
else
|
||||
sb.Append('?').Append(idx);
|
||||
any = true;
|
||||
}
|
||||
sb.Append(']');
|
||||
|
||||
string sig = sb.ToString();
|
||||
if (_seamEntSigs.TryGetValue(entity.Id, out var prev) && prev == sig) return;
|
||||
_seamEntSigs[entity.Id] = sig;
|
||||
Console.WriteLine($"[seam-ent] t={Environment.TickCount64} {sig}");
|
||||
}
|
||||
|
||||
private void ComputeEntityLightSet(
|
||||
in RenderInstanceCandidate entity)
|
||||
{
|
||||
|
|
@ -3632,41 +3437,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around an instance's rate-limit dictionary + frame
|
||||
/// counter, passed into the static <see cref="WalkEntitiesInto"/>
|
||||
/// overload so it can emit rate-limited probe lines without access
|
||||
/// to instance fields. Null = probes disabled (test-friendly overload).
|
||||
/// </summary>
|
||||
internal sealed class IndoorProbeState
|
||||
{
|
||||
private readonly Dictionary<ulong, int> _lastFrame;
|
||||
private readonly int _currentFrame;
|
||||
private const int RateLimit = IndoorProbeRateLimitFrames;
|
||||
|
||||
internal IndoorProbeState(Dictionary<ulong, int> lastFrame, int currentFrame)
|
||||
{
|
||||
_lastFrame = lastFrame;
|
||||
_currentFrame = currentFrame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||
/// frames per <paramref name="cellId"/>. Side-effect: stamps the frame
|
||||
/// number into the dictionary on success.
|
||||
/// </summary>
|
||||
internal bool ShouldEmit(ulong cellId)
|
||||
{
|
||||
if (!_lastFrame.TryGetValue(cellId, out int last)
|
||||
|| _currentFrame - last >= RateLimit)
|
||||
{
|
||||
_lastFrame[cellId] = _currentFrame;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InstanceGroup
|
||||
{
|
||||
// Nonzero only while this exact object is registered in _groups.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ internal interface IRenderFrameGlState
|
|||
{
|
||||
void RestoreFrameDefaults();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6j: the small graphics surface the two world pass executors
|
||||
/// touch directly, expressed once so their ordering logic — which is retail's,
|
||||
|
|
@ -186,25 +185,3 @@ internal sealed class NullRenderFrameGlState : IRenderFrameGlState
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6j: the GL state reader on a backend with no GL state.
|
||||
///
|
||||
/// <para><c>WorldRenderDiagnostics</c> reads live GL state for explicitly enabled
|
||||
/// probes only. Every snapshot below is the truthful answer for a Vulkan frame —
|
||||
/// there is no ambient capability state to sample — which keeps every other
|
||||
/// diagnostic the class emits (render signature, PView input, out-stage routing,
|
||||
/// phantom objects) working unchanged on both backends.</para>
|
||||
/// </summary>
|
||||
internal sealed class NullRenderGlStateReader : IRenderGlStateReader
|
||||
{
|
||||
public static NullRenderGlStateReader Instance { get; } = new();
|
||||
|
||||
private NullRenderGlStateReader()
|
||||
{
|
||||
}
|
||||
|
||||
public RenderGlStateSnapshot CaptureState() => default;
|
||||
|
||||
public RenderGlScissorSnapshot CaptureScissor() => default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,22 @@
|
|||
using System.Numerics;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct IntRenderRectangle(int X, int Y, int Width, int Height);
|
||||
|
||||
internal readonly record struct RenderGlStateSnapshot(
|
||||
bool DepthTest,
|
||||
bool DepthWrite,
|
||||
int DepthFunction,
|
||||
bool Blend,
|
||||
int BlendSource,
|
||||
int BlendDestination,
|
||||
bool CullFace,
|
||||
int CullMode,
|
||||
int FrontFace,
|
||||
bool Scissor,
|
||||
IntRenderRectangle ScissorBox,
|
||||
IntRenderRectangle Viewport,
|
||||
int DrawFramebuffer,
|
||||
bool AlphaToCoverage,
|
||||
bool Stencil,
|
||||
int ClipBits,
|
||||
int Error);
|
||||
|
||||
internal readonly record struct RenderGlScissorSnapshot(
|
||||
bool Enabled,
|
||||
IntRenderRectangle Box);
|
||||
|
||||
internal readonly record struct TerrainRenderDiagnosticFacts(
|
||||
int VisibleSlots,
|
||||
int Draws,
|
||||
int LoadedSlots,
|
||||
int CapacitySlots);
|
||||
|
||||
internal interface IRenderGlStateReader
|
||||
{
|
||||
RenderGlStateSnapshot CaptureState();
|
||||
|
||||
RenderGlScissorSnapshot CaptureScissor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns print-on-change world-render probes and their reusable scratch. Inputs
|
||||
/// are borrowed for one call; the owner retains only copied signatures and IDs.
|
||||
/// </summary>
|
||||
/// <summary>Owns permanent terrain timing and diagnostic publication.</summary>
|
||||
internal sealed class WorldRenderDiagnostics
|
||||
{
|
||||
private readonly IRenderGlStateReader _gl;
|
||||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private readonly Stopwatch _terrainStopwatch = new();
|
||||
private readonly RollingTimingSampleWindow _terrainSamples = new(256);
|
||||
private string? _lastRenderSignature;
|
||||
private int _renderSignatureFrame;
|
||||
private int _renderSignatureStableFrames;
|
||||
private string? _lastGlStateSignature;
|
||||
private long _glStateFrame;
|
||||
private long _glStateStableFrames;
|
||||
private string? _lastPostWorldGlStateSignature;
|
||||
private long _postWorldGlStateFrame;
|
||||
private long _postWorldGlStateStableFrames;
|
||||
private string? _lastScissorSignature;
|
||||
private long _scissorSequence;
|
||||
private string? _lastClipRouteSignature;
|
||||
private long _clipRouteSequence;
|
||||
private readonly List<uint> _clipRouteCellKeys = [];
|
||||
|
||||
public WorldRenderDiagnostics(
|
||||
IRenderGlStateReader gl,
|
||||
IRenderFrameDiagnosticLog log)
|
||||
public WorldRenderDiagnostics(IRenderFrameDiagnosticLog log)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
|
|
@ -86,12 +29,6 @@ internal sealed class WorldRenderDiagnostics
|
|||
(long)(_terrainStopwatch.Elapsed.TotalMicroseconds * 100.0));
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): the walk path's per-frame
|
||||
/// analogue of <see cref="EndTerrainDraw"/> — pushes ONE precomputed
|
||||
/// elapsed-time sample (the sum of every land-cell batch's own
|
||||
/// Stopwatch.GetTimestamp() delta this frame, converted by the caller)
|
||||
/// instead of stopping this owner's own stopwatch, since the walk path
|
||||
/// times many small batches per frame rather than one bracketed call.</summary>
|
||||
public void PushTerrainSample(long elapsedHundredthsMicroseconds) =>
|
||||
_terrainSamples.PushHundredthsMicroseconds(elapsedHundredthsMicroseconds);
|
||||
|
||||
|
|
@ -107,442 +44,4 @@ internal sealed class WorldRenderDiagnostics
|
|||
+ $"visible={facts.VisibleSlots} loaded={facts.LoadedSlots} "
|
||||
+ $"capacity={facts.CapacitySlots}");
|
||||
}
|
||||
|
||||
public void EmitGlStateTripwireIfChanged(bool enabled)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
_glStateFrame++;
|
||||
string signature = FormatGlState(_gl.CaptureState());
|
||||
if (signature == _lastGlStateSignature)
|
||||
{
|
||||
_glStateStableFrames++;
|
||||
return;
|
||||
}
|
||||
|
||||
_log.WriteLine(
|
||||
$"[gl-state] frame={_glStateFrame} stable={_glStateStableFrames} {signature}");
|
||||
_lastGlStateSignature = signature;
|
||||
_glStateStableFrames = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second sample of the same snapshot, taken at the END of the normal-world
|
||||
/// phase instead of at the frame clear.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="EmitGlStateTripwireIfChanged"/> samples immediately after the
|
||||
/// clear phase has run <c>RestoreFrameDefaults</c>, so it can only observe
|
||||
/// state that survives from one frame into the next. State that a world pass
|
||||
/// establishes and something in private presentation puts back before the
|
||||
/// next clear is invisible to it — including the draw framebuffer, which no
|
||||
/// frame-global restore touches. Sampling here as well brackets the world
|
||||
/// phase, so a binding that the world's geometry drew into but the retained
|
||||
/// UI did not shows up as a difference between the two lines rather than as
|
||||
/// no line at all.
|
||||
/// </remarks>
|
||||
public void EmitPostWorldGlStateIfChanged(bool enabled)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
_postWorldGlStateFrame++;
|
||||
string signature = FormatGlState(_gl.CaptureState());
|
||||
if (signature == _lastPostWorldGlStateSignature)
|
||||
{
|
||||
_postWorldGlStateStableFrames++;
|
||||
return;
|
||||
}
|
||||
|
||||
_log.WriteLine(
|
||||
$"[gl-state-postworld] frame={_postWorldGlStateFrame} "
|
||||
+ $"stable={_postWorldGlStateStableFrames} {signature}");
|
||||
_lastPostWorldGlStateSignature = signature;
|
||||
_postWorldGlStateStableFrames = 0;
|
||||
}
|
||||
|
||||
public void EmitClipRouteScissorProbe(
|
||||
bool enabled,
|
||||
bool applied,
|
||||
Vector4 ndcAabb)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
RenderGlScissorSnapshot snapshot = _gl.CaptureScissor();
|
||||
string signature = FormattableString.Invariant(
|
||||
$"applied={(applied ? 1 : 0)} scis={(snapshot.Enabled ? 1 : 0)} box=({snapshot.Box.X},{snapshot.Box.Y},{snapshot.Box.Width},{snapshot.Box.Height}) ndc=({ndcAabb.X:F3},{ndcAabb.Y:F3},{ndcAabb.Z:F3},{ndcAabb.W:F3})");
|
||||
_scissorSequence++;
|
||||
if (signature == _lastScissorSignature)
|
||||
return;
|
||||
|
||||
_lastScissorSignature = signature;
|
||||
_log.WriteLine($"[clip-route-scis] n={_scissorSequence} {signature}");
|
||||
}
|
||||
|
||||
public void EmitClipRouteProbe(
|
||||
bool enabled,
|
||||
ClipFrame clipFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
ClipViewSlice slice,
|
||||
int sliceIndex)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
var text = new StringBuilder(256);
|
||||
text.Append(FormattableString.Invariant(
|
||||
$"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}"));
|
||||
text.Append(FormattableString.Invariant(
|
||||
$" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})"));
|
||||
text.Append(FormattableString.Invariant($" planes={slice.Planes.Length}["));
|
||||
for (int i = 0; i < slice.Planes.Length; i++)
|
||||
{
|
||||
Vector4 plane = slice.Planes[i];
|
||||
if (i > 0)
|
||||
text.Append(' ');
|
||||
text.Append(FormattableString.Invariant(
|
||||
$"({plane.X:F3},{plane.Y:F3},{plane.Z:F3},{plane.W:F3})"));
|
||||
}
|
||||
|
||||
text.Append("] cells={");
|
||||
_clipRouteCellKeys.Clear();
|
||||
foreach (uint key in clipAssembly.CellIdToSlot.Keys)
|
||||
_clipRouteCellKeys.Add(key);
|
||||
_clipRouteCellKeys.Sort();
|
||||
for (int i = 0; i < _clipRouteCellKeys.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
text.Append(',');
|
||||
text.Append(FormattableString.Invariant(
|
||||
$"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}"));
|
||||
}
|
||||
text.Append('}');
|
||||
|
||||
ReadOnlySpan<byte> regionBytes = clipFrame.RegionBytesForTest;
|
||||
int offset = slice.Slot * ClipFrame.CellClipStrideBytes;
|
||||
if (offset >= 0 && offset + ClipFrame.CellClipStrideBytes <= regionBytes.Length)
|
||||
{
|
||||
uint count = BitConverter.ToUInt32(regionBytes.Slice(offset, 4));
|
||||
text.Append(FormattableString.Invariant($" ssbo[{slice.Slot}]: n={count}"));
|
||||
int planeCount = (int)Math.Min(count, (uint)ClipFrame.MaxPlanes);
|
||||
for (int i = 0; i < planeCount; i++)
|
||||
{
|
||||
int planeOffset = offset + ClipFrame.CellClipPlanesOffset + i * 16;
|
||||
float x = BitConverter.ToSingle(regionBytes.Slice(planeOffset, 4));
|
||||
float y = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 4, 4));
|
||||
float z = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 8, 4));
|
||||
float w = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 12, 4));
|
||||
text.Append(FormattableString.Invariant($" ({x:F3},{y:F3},{z:F3},{w:F3})"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(FormattableString.Invariant(
|
||||
$" ssbo[{slice.Slot}]: OUT-OF-RANGE len={regionBytes.Length}"));
|
||||
}
|
||||
|
||||
// S3 chunk 4 fix round 2 (L3): the "ubo: n=... p0=..." segment this
|
||||
// used to append read ClipFrame's own screen-space clip gate bytes,
|
||||
// deleted along with that gate — no shader declares the block any
|
||||
// more, so there is nothing left here to read.
|
||||
|
||||
string signature = text.ToString();
|
||||
_clipRouteSequence++;
|
||||
if (signature == _lastClipRouteSignature)
|
||||
return;
|
||||
_lastClipRouteSignature = signature;
|
||||
_log.WriteLine($"[clip-route] n={_clipRouteSequence} {signature}");
|
||||
}
|
||||
|
||||
public void EmitSeamMask(
|
||||
bool enabled,
|
||||
IReadOnlySet<uint> targetCells,
|
||||
uint cellId,
|
||||
int portalIndex,
|
||||
bool forceFarZ,
|
||||
ReadOnlySpan<Vector3> vertices)
|
||||
{
|
||||
if (!enabled || !targetCells.Contains(cellId))
|
||||
return;
|
||||
|
||||
float minimumZ = float.MaxValue;
|
||||
float maximumZ = float.MinValue;
|
||||
foreach (Vector3 vertex in vertices)
|
||||
{
|
||||
minimumZ = Math.Min(minimumZ, vertex.Z);
|
||||
maximumZ = Math.Max(maximumZ, vertex.Z);
|
||||
}
|
||||
|
||||
_log.WriteLine(FormattableString.Invariant(
|
||||
$"[seam-mask] t={Environment.TickCount64} cell=0x{cellId:X8} portal={portalIndex} far={forceFarZ} n={vertices.Length} z=[{minimumZ:F3},{maximumZ:F3}]"));
|
||||
}
|
||||
|
||||
public void EmitPViewInput(
|
||||
bool enabled,
|
||||
IReadOnlySet<uint> visibleCells,
|
||||
int outsideViewCount,
|
||||
Matrix4x4 viewProjection,
|
||||
bool outdoorRoot,
|
||||
Vector3 eye,
|
||||
Vector3 player,
|
||||
Vector3 rawPlayer,
|
||||
float yaw,
|
||||
float? terrainHeight)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
string terrain = terrainHeight is { } height
|
||||
? FormattableString.Invariant(
|
||||
$"terrZ={height:F3} eyeAbove={eye.Z - height:F3}")
|
||||
: "terrZ=n/a eyeAbove=n/a";
|
||||
char root = outdoorRoot ? 'Y' : 'n';
|
||||
Matrix4x4 vp = viewProjection;
|
||||
_log.WriteLine(FormattableString.Invariant(
|
||||
$"[pv-input] outRoot={root} visible={visibleCells.Count} outsideViews={outsideViewCount} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
|
||||
}
|
||||
|
||||
public void EmitRetailPViewDiagnostics(
|
||||
bool visibilityEnabled,
|
||||
bool flapEnabled,
|
||||
RetailPViewFrameResult result,
|
||||
LoadedCell clipRoot,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition,
|
||||
CameraCellResolution cameraCellResolution)
|
||||
{
|
||||
if (visibilityEnabled)
|
||||
{
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
|
||||
clipRoot.CellId,
|
||||
result.VisibleCells.OrderBy(static id => id).ToArray(),
|
||||
result.ClipAssembly.OutsideViewSlices.Length,
|
||||
result.ClipAssembly.OutsidePlaneCount,
|
||||
result.ClipAssembly.PerCellPlaneCounts,
|
||||
result.ClipAssembly.ScissorFallbacks);
|
||||
}
|
||||
|
||||
if (flapEnabled)
|
||||
{
|
||||
bool eyeInRoot = CellVisibility.PointInCell(cameraPosition, clipRoot);
|
||||
bool playerInRoot = CellVisibility.PointInCell(playerPosition, clipRoot);
|
||||
_log.WriteLine(
|
||||
$"[flap-cam] root=0x{clipRoot.CellId:X8} "
|
||||
+ $"viewerCell=0x{viewerCellId:X8} playerCell=0x{playerCellId:X8} "
|
||||
+ $"res={cameraCellResolution} "
|
||||
+ $"eyeInRoot={(eyeInRoot ? "Y" : "n")} "
|
||||
+ $"playerInRoot={(playerInRoot ? "Y" : "n")} "
|
||||
+ $"eye=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}) "
|
||||
+ $"player=({playerPosition.X:F2},{playerPosition.Y:F2},{playerPosition.Z:F2}) "
|
||||
// S3 chunk 4 fix round 2 (L2): the "terrain=" field read
|
||||
// ClipFrameAssembly.TerrainMode, deleted along with the
|
||||
// walk's own terrain-clip-mode tracking (the walk draws
|
||||
// terrain unclipped, one call, matching retail — there is no
|
||||
// longer a mode to report here).
|
||||
+ $"outVisible={result.ClipAssembly.OutdoorVisible}");
|
||||
}
|
||||
}
|
||||
|
||||
public void EmitRenderSignatureIfChanged(
|
||||
bool enabled,
|
||||
string branch,
|
||||
LoadedCell? clipRoot,
|
||||
LoadedCell? viewerRoot,
|
||||
LoadedCell? playerRoot,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
bool playerIndoorGate,
|
||||
bool cameraInsideCell,
|
||||
bool renderSkyGate,
|
||||
bool drawSkyThisFrame,
|
||||
bool terrainDrawn,
|
||||
TerrainClipMode terrainClipMode,
|
||||
bool skyDrawn,
|
||||
bool depthClear,
|
||||
bool outdoorSceneryDrawn,
|
||||
int liveDynamicDrawnCount,
|
||||
string sceneParticles,
|
||||
IReadOnlySet<uint>? visibleCells,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IReadOnlySet<uint>? drawableCells,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
_renderSignatureFrame++;
|
||||
bool eyeInRoot = clipRoot is not null
|
||||
&& CellVisibility.PointInCell(cameraPosition, clipRoot);
|
||||
bool playerInRoot = clipRoot is not null
|
||||
&& CellVisibility.PointInCell(playerPosition, clipRoot);
|
||||
|
||||
var text = new StringBuilder(512);
|
||||
text.Append("branch=").Append(branch);
|
||||
text.Append(" root=0x").Append((clipRoot?.CellId ?? 0u).ToString("X8"));
|
||||
text.Append(" viewerRoot=0x").Append((viewerRoot?.CellId ?? 0u).ToString("X8"));
|
||||
text.Append(" playerRoot=0x").Append((playerRoot?.CellId ?? 0u).ToString("X8"));
|
||||
text.Append(" viewerCell=0x").Append(viewerCellId.ToString("X8"));
|
||||
text.Append(" playerCell=0x").Append(playerCellId.ToString("X8"));
|
||||
text.Append(" gate=").Append(playerIndoorGate ? "in" : "out");
|
||||
text.Append(" camIn=").Append(cameraInsideCell ? 'Y' : 'n');
|
||||
text.Append(" eyeInRoot=").Append(eyeInRoot ? 'Y' : 'n');
|
||||
text.Append(" playerInRoot=").Append(playerInRoot ? 'Y' : 'n');
|
||||
text.Append(" eye=").Append(FormatVector(cameraPosition));
|
||||
text.Append(" player=").Append(FormatVector(playerPosition));
|
||||
text.Append(" terrain=").Append(terrainClipMode);
|
||||
text.Append('/').Append(terrainDrawn ? "draw" : "skip");
|
||||
text.Append(" skyGate=").Append(renderSkyGate ? 'Y' : 'n');
|
||||
text.Append(" sky=").Append(skyDrawn ? 'Y' : 'n');
|
||||
text.Append(" skyFrame=").Append(drawSkyThisFrame ? 'Y' : 'n');
|
||||
text.Append(" zclear=").Append(depthClear ? 'Y' : 'n');
|
||||
text.Append(" sceneParticles=").Append(sceneParticles);
|
||||
// S3 chunk 4 fix round 2 (L2): "outMode=" read
|
||||
// ClipFrameAssembly.TerrainMode, deleted with the walk's own
|
||||
// terrain-clip-mode tracking.
|
||||
if (clipAssembly is not null)
|
||||
{
|
||||
text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length);
|
||||
text.Append(" outPolys=").Append(clipAssembly.OutsideViewSlices.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(" outSlices=0 outPolys=0");
|
||||
}
|
||||
|
||||
text.Append(" ids=").Append(FormatIds(visibleCells, false));
|
||||
text.Append(" draw=").Append(FormatIds(drawableCells, false));
|
||||
text.Append(" miss=").Append(FormatMissingDrawableCells(visibleCells, drawableCells));
|
||||
text.Append(" obj=").Append(FormatPartitionCounts(partition));
|
||||
text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n');
|
||||
text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount);
|
||||
text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n');
|
||||
if (partition is not null)
|
||||
{
|
||||
int totalShells = 0;
|
||||
int shellsWithMeshes = 0;
|
||||
foreach (var entity in partition.OutdoorStatic)
|
||||
{
|
||||
if (!entity.IsBuildingShell)
|
||||
continue;
|
||||
totalShells++;
|
||||
if (entity.MeshRefs.Count > 0)
|
||||
shellsWithMeshes++;
|
||||
}
|
||||
text.Append(" bshell=").Append(totalShells).Append('/').Append(shellsWithMeshes);
|
||||
}
|
||||
|
||||
string signature = text.ToString();
|
||||
if (signature == _lastRenderSignature)
|
||||
{
|
||||
_renderSignatureStableFrames++;
|
||||
return;
|
||||
}
|
||||
|
||||
_log.WriteLine(
|
||||
$"[render-sig] frame={_renderSignatureFrame} "
|
||||
+ $"stable={_renderSignatureStableFrames} {signature}");
|
||||
_lastRenderSignature = signature;
|
||||
_renderSignatureStableFrames = 0;
|
||||
}
|
||||
|
||||
internal static string FormatGlState(RenderGlStateSnapshot state) =>
|
||||
$"depth={(state.DepthTest ? 1 : 0)} "
|
||||
+ $"dmask={(state.DepthWrite ? 1 : 0)} "
|
||||
+ $"dfunc=0x{state.DepthFunction:X} "
|
||||
+ $"blend={(state.Blend ? 1 : 0)} "
|
||||
+ $"bsrc=0x{state.BlendSource:X} bdst=0x{state.BlendDestination:X} "
|
||||
+ $"cull={(state.CullFace ? 1 : 0)} cmode=0x{state.CullMode:X} "
|
||||
+ $"fface=0x{state.FrontFace:X} "
|
||||
+ $"scis={(state.Scissor ? 1 : 0)} "
|
||||
+ $"sbox=({state.ScissorBox.X},{state.ScissorBox.Y},"
|
||||
+ $"{state.ScissorBox.Width},{state.ScissorBox.Height}) "
|
||||
+ $"vp=({state.Viewport.X},{state.Viewport.Y},"
|
||||
+ $"{state.Viewport.Width},{state.Viewport.Height}) "
|
||||
+ $"fbo={state.DrawFramebuffer} "
|
||||
+ $"a2c={(state.AlphaToCoverage ? 1 : 0)} "
|
||||
+ $"stencil={(state.Stencil ? 1 : 0)} "
|
||||
+ $"clip=0x{state.ClipBits:X2} err=0x{state.Error:X}";
|
||||
|
||||
private static string FormatVector(Vector3 value)
|
||||
{
|
||||
static float Quantize(float component) => MathF.Round(component * 20f) / 20f;
|
||||
return $"({Quantize(value.X):F2},{Quantize(value.Y):F2},{Quantize(value.Z):F2})";
|
||||
}
|
||||
|
||||
private static string FormatIds(IEnumerable<uint>? ids, bool preserveOrder)
|
||||
{
|
||||
if (ids is null)
|
||||
return "[]";
|
||||
|
||||
var values = new List<uint>(ids);
|
||||
if (!preserveOrder)
|
||||
values.Sort();
|
||||
var text = new StringBuilder(96).Append('[');
|
||||
const int MaximumIds = 12;
|
||||
for (int index = 0; index < values.Count && index < MaximumIds; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(values[index].ToString("X8"));
|
||||
}
|
||||
if (values.Count > MaximumIds)
|
||||
text.Append(",...");
|
||||
return text.Append(']').ToString();
|
||||
}
|
||||
|
||||
private static string FormatMissingDrawableCells(
|
||||
IReadOnlySet<uint>? visibleCells,
|
||||
IReadOnlySet<uint>? drawableCells)
|
||||
{
|
||||
if (visibleCells is null || drawableCells is null)
|
||||
return "[]";
|
||||
|
||||
var text = new StringBuilder(96).Append('[');
|
||||
int written = 0;
|
||||
const int MaximumCells = 8;
|
||||
foreach (uint id in visibleCells.OrderBy(static id => id))
|
||||
{
|
||||
if (drawableCells.Contains(id))
|
||||
continue;
|
||||
if (written > 0)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(id.ToString("X8"));
|
||||
if (++written >= MaximumCells)
|
||||
{
|
||||
text.Append(",...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return text.Append(']').ToString();
|
||||
}
|
||||
|
||||
private static string FormatPartitionCounts(InteriorEntityPartition.Result? partition)
|
||||
{
|
||||
if (partition is null)
|
||||
return "cell=[] out=0 live=0";
|
||||
|
||||
var keys = new List<uint>(partition.ByCell.Keys);
|
||||
keys.Sort();
|
||||
var text = new StringBuilder(128).Append("cell=[");
|
||||
const int MaximumCells = 10;
|
||||
for (int index = 0; index < keys.Count && index < MaximumCells; index++)
|
||||
{
|
||||
uint id = keys[index];
|
||||
if (index > 0)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(id.ToString("X8"))
|
||||
.Append(':').Append(partition.ByCell[id].Count);
|
||||
}
|
||||
if (keys.Count > MaximumCells)
|
||||
text.Append(",...");
|
||||
return text.Append("] out=").Append(partition.OutdoorStatic.Count)
|
||||
.Append(" live=").Append(partition.Dynamics.Count)
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -718,11 +718,6 @@ internal sealed class RuntimeWorldFrameBuildingSource : IWorldFrameBuildingSourc
|
|||
|
||||
if (viewerRoot is null)
|
||||
outdoorNode = OutdoorCellNode.Build(viewerCellId);
|
||||
if (RenderingDiagnostics.ProbeFlapEnabled)
|
||||
{
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_scratch.Count} (T2 frustum-gated per-building floods)"));
|
||||
}
|
||||
|
||||
return new WorldBuildingFrame(outdoorNode, _scratch);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,36 +13,6 @@ internal interface IWorldSceneDiagnostics
|
|||
{
|
||||
CameraCellResolution CameraCellResolution { get; }
|
||||
|
||||
void EmitPViewInput(
|
||||
IReadOnlySet<uint> visibleCells,
|
||||
int outsideViewCount,
|
||||
Matrix4x4 viewProjection,
|
||||
LoadedCell clipRoot,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition);
|
||||
|
||||
void EmitRenderSignature(
|
||||
string branch,
|
||||
LoadedCell? clipRoot,
|
||||
LoadedCell? viewerRoot,
|
||||
LoadedCell? playerRoot,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
bool playerIndoorGate,
|
||||
bool cameraInsideCell,
|
||||
bool renderSky,
|
||||
bool drawSkyThisFrame,
|
||||
bool terrainDrawn,
|
||||
TerrainClipMode terrainClipMode,
|
||||
bool skyDrawn,
|
||||
bool depthClear,
|
||||
bool outdoorSceneryDrawn,
|
||||
int liveDynamicDrawnCount,
|
||||
string sceneParticles,
|
||||
RetailPViewFrameResult? pviewResult,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition);
|
||||
|
||||
WorldSceneDiagnosticOutcome DrawAndPublish(
|
||||
in WorldCameraFrame camera,
|
||||
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
|
||||
|
|
@ -55,7 +25,6 @@ internal interface IWorldSceneDiagnostics
|
|||
/// </summary>
|
||||
internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
||||
{
|
||||
private readonly WorldRenderDiagnostics _diagnostics;
|
||||
private readonly IWorldScenePViewDiagnosticSource _pview;
|
||||
private readonly IWorldSceneDebugStateSource _state;
|
||||
private readonly DebugLineRenderer? _lines;
|
||||
|
|
@ -67,7 +36,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
|||
private int _debugDrawLogCount;
|
||||
|
||||
public WorldSceneDiagnosticsController(
|
||||
WorldRenderDiagnostics diagnostics,
|
||||
IWorldScenePViewDiagnosticSource pview,
|
||||
IWorldSceneDebugStateSource state,
|
||||
DebugLineRenderer? lines,
|
||||
|
|
@ -77,7 +45,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
|||
DebugVmRenderFactsPublisher debugVm,
|
||||
bool debugVmConsumerActive)
|
||||
{
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
|
||||
_state = state ?? throw new ArgumentNullException(nameof(state));
|
||||
_lines = lines;
|
||||
|
|
@ -90,89 +57,11 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
|||
|
||||
public CameraCellResolution CameraCellResolution => _pview.CameraCellResolution;
|
||||
|
||||
public void EmitPViewInput(
|
||||
IReadOnlySet<uint> visibleCells,
|
||||
int outsideViewCount,
|
||||
Matrix4x4 viewProjection,
|
||||
LoadedCell clipRoot,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition)
|
||||
{
|
||||
if (!RenderingDiagnostics.ProbePvInputEnabled)
|
||||
return;
|
||||
|
||||
_diagnostics.EmitPViewInput(
|
||||
enabled: true,
|
||||
visibleCells,
|
||||
outsideViewCount,
|
||||
viewProjection,
|
||||
clipRoot.IsOutdoorNode,
|
||||
cameraPosition,
|
||||
playerPosition,
|
||||
_pview.RawPlayerPositionOr(playerPosition),
|
||||
_pview.PlayerYaw,
|
||||
_pview.SampleTerrainZ(cameraPosition.X, cameraPosition.Y));
|
||||
}
|
||||
|
||||
public void EmitRenderSignature(
|
||||
string branch,
|
||||
LoadedCell? clipRoot,
|
||||
LoadedCell? viewerRoot,
|
||||
LoadedCell? playerRoot,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
bool playerIndoorGate,
|
||||
bool cameraInsideCell,
|
||||
bool renderSky,
|
||||
bool drawSkyThisFrame,
|
||||
bool terrainDrawn,
|
||||
TerrainClipMode terrainClipMode,
|
||||
bool skyDrawn,
|
||||
bool depthClear,
|
||||
bool outdoorSceneryDrawn,
|
||||
int liveDynamicDrawnCount,
|
||||
string sceneParticles,
|
||||
RetailPViewFrameResult? pviewResult,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition)
|
||||
{
|
||||
_diagnostics.EmitRenderSignatureIfChanged(
|
||||
RenderingDiagnostics.ProbeFlapEnabled,
|
||||
branch,
|
||||
clipRoot,
|
||||
viewerRoot,
|
||||
playerRoot,
|
||||
viewerCellId,
|
||||
playerCellId,
|
||||
playerIndoorGate,
|
||||
cameraInsideCell,
|
||||
renderSky,
|
||||
drawSkyThisFrame,
|
||||
terrainDrawn,
|
||||
terrainClipMode,
|
||||
skyDrawn,
|
||||
depthClear,
|
||||
outdoorSceneryDrawn,
|
||||
liveDynamicDrawnCount,
|
||||
sceneParticles,
|
||||
pviewResult?.VisibleCells,
|
||||
pviewResult?.ClipAssembly,
|
||||
pviewResult?.DrawableCells,
|
||||
pviewResult?.DiagnosticPartition,
|
||||
cameraPosition,
|
||||
playerPosition);
|
||||
}
|
||||
|
||||
public WorldSceneDiagnosticOutcome DrawAndPublish(
|
||||
in WorldCameraFrame camera,
|
||||
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bounds);
|
||||
// Brackets the normal-world phase against the clear-phase tripwire: any
|
||||
// GL state the world's geometry drew under but the next frame's clear
|
||||
// no longer sees shows up as a difference between the two lines.
|
||||
_diagnostics.EmitPostWorldGlStateIfChanged(
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.ProbeGlStateEnabled);
|
||||
DrawCollisionWireframes(in camera);
|
||||
|
||||
int visible = 0;
|
||||
|
|
|
|||
|
|
@ -29,11 +29,10 @@ internal interface IWorldScenePassExecutor
|
|||
uint? playerLandblockId,
|
||||
HashSet<uint> animatedEntityIds);
|
||||
|
||||
string DrawPostWorldParticles(
|
||||
void DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
string currentSignature);
|
||||
in WorldCameraFrame camera);
|
||||
|
||||
void DrawFlatWeather(
|
||||
in WorldCameraFrame camera,
|
||||
|
|
@ -182,14 +181,13 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
visibleCellIds: null,
|
||||
animatedEntityIds: animatedEntityIds);
|
||||
|
||||
public string DrawPostWorldParticles(
|
||||
public void DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
string currentSignature)
|
||||
in WorldCameraFrame camera)
|
||||
{
|
||||
if (_particles is null || _particleRenderer is null)
|
||||
return currentSignature;
|
||||
return;
|
||||
|
||||
if (clipRoot is null)
|
||||
{
|
||||
|
|
@ -202,21 +200,21 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
_visibleParticleOwners,
|
||||
includeUnattached: true,
|
||||
excludedAttachedOwnerIds: _noExcludedParticleOwners);
|
||||
return AppendSignature(currentSignature, "filtered");
|
||||
return;
|
||||
}
|
||||
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.Scene);
|
||||
return AppendSignature(currentSignature, "global");
|
||||
return;
|
||||
}
|
||||
|
||||
// Every PView root, including the outdoor sentinel, now submits scene
|
||||
// particles inside LScape::draw. Replaying them here is both a duplicate
|
||||
// and too late: it occurs after nested building cells, allowing exterior
|
||||
// waterfall/foliage alpha to repaint an indoor/outdoor transition.
|
||||
return currentSignature;
|
||||
return;
|
||||
}
|
||||
|
||||
public void DrawFlatWeather(
|
||||
|
|
@ -291,6 +289,4 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
}
|
||||
}
|
||||
|
||||
private static string AppendSignature(string current, string value) =>
|
||||
current == "none" ? value : current + "+" + value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,23 +176,15 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
|
||||
WorldCameraFrame camera = world.Camera;
|
||||
WorldRootFrame roots = world.Roots;
|
||||
LoadedCell? clipRoot = world.ClipRoot;
|
||||
bool renderSky = roots.RenderSky;
|
||||
bool drawSkyThisFrame = false;
|
||||
bool terrainDrawn = false;
|
||||
bool skyDrawn = false;
|
||||
bool depthClear = false;
|
||||
bool outdoorSceneryDrawn = false;
|
||||
int liveDynamicDrawnCount = 0;
|
||||
string sceneParticles = "none";
|
||||
TerrainClipMode terrainClipMode = TerrainClipMode.Planes;
|
||||
RetailPViewFrameResult? pviewResult = null;
|
||||
LoadedCell? clipRoot = world.ClipRoot;
|
||||
bool renderSky = roots.RenderSky;
|
||||
bool drawSkyThisFrame = false;
|
||||
RetailPViewFrameResult? pviewResult = null;
|
||||
|
||||
if (clipRoot is null)
|
||||
{
|
||||
_passes.PrepareFlatWorldClip();
|
||||
drawSkyThisFrame = renderSky;
|
||||
skyDrawn = drawSkyThisFrame;
|
||||
_passes.PrepareFlatWorldClip();
|
||||
drawSkyThisFrame = renderSky;
|
||||
if (drawSkyThisFrame)
|
||||
{
|
||||
_passes.DrawFlatSky(
|
||||
|
|
@ -210,12 +202,7 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
terrainClipMode = TerrainClipMode.Skip;
|
||||
}
|
||||
}
|
||||
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
|
|
@ -253,30 +240,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
// VisibleCells remains a diagnostics-only union.
|
||||
_particleVisibility.MarkVisibleLandscapeCells(
|
||||
pviewResult.VisibleLandscapeCells);
|
||||
_diagnostics.EmitPViewInput(
|
||||
pviewResult.VisibleCells,
|
||||
pviewResult.ClipAssembly.OutsideViewSlices.Length,
|
||||
camera.ViewProjection,
|
||||
clipRoot,
|
||||
camera.Position,
|
||||
roots.PlayerViewPosition);
|
||||
|
||||
bool hasOutsideSlice = pviewResult.ClipAssembly.OutsideViewSlices.Length > 0;
|
||||
terrainDrawn = hasOutsideSlice;
|
||||
skyDrawn = renderSky && hasOutsideSlice;
|
||||
// This mirrors the established diagnostic meaning, including
|
||||
// outdoor roots whose PView executor does not issue an interior
|
||||
// depth clear.
|
||||
depthClear = hasOutsideSlice;
|
||||
RenderProjectionCounts sourceCounts =
|
||||
pviewResult.SourceCounts;
|
||||
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
|
||||
sceneParticles = "pviewScoped";
|
||||
outdoorSceneryDrawn =
|
||||
sourceCounts.OutdoorStatic > 0
|
||||
&& hasOutsideSlice;
|
||||
liveDynamicDrawnCount =
|
||||
sourceCounts.LiveDynamicRoot;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -288,43 +251,20 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
|
|||
}
|
||||
|
||||
_passes.DisableClipDistances();
|
||||
sceneParticles = _passes.DrawPostWorldParticles(
|
||||
clipRoot,
|
||||
pviewResult?.ClipAssembly,
|
||||
in camera,
|
||||
sceneParticles);
|
||||
_passes.DrawPostWorldParticles(
|
||||
clipRoot,
|
||||
pviewResult?.ClipAssembly,
|
||||
in camera);
|
||||
|
||||
if (clipRoot is null && drawSkyThisFrame)
|
||||
{
|
||||
skyDrawn = true;
|
||||
_passes.DrawFlatWeather(
|
||||
_passes.DrawFlatWeather(
|
||||
in camera,
|
||||
in foundation,
|
||||
_sky.ActiveDayGroup,
|
||||
_sky.DayFraction);
|
||||
}
|
||||
|
||||
_diagnostics.EmitRenderSignature(
|
||||
clipRoot is null ? "OutdoorRoot" : "RetailPViewInside",
|
||||
clipRoot,
|
||||
roots.ViewerRoot,
|
||||
roots.PlayerRoot,
|
||||
roots.ViewerCellId,
|
||||
roots.PlayerCellId,
|
||||
roots.PlayerIndoorGate,
|
||||
roots.CameraInsideCell,
|
||||
renderSky,
|
||||
drawSkyThisFrame,
|
||||
terrainDrawn,
|
||||
terrainClipMode,
|
||||
skyDrawn,
|
||||
depthClear,
|
||||
outdoorSceneryDrawn,
|
||||
liveDynamicDrawnCount,
|
||||
sceneParticles,
|
||||
pviewResult,
|
||||
camera.Position,
|
||||
roots.PlayerViewPosition);
|
||||
|
||||
WorldSceneDiagnosticOutcome diagnostic = _diagnostics.DrawAndPublish(
|
||||
in camera,
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ public sealed class LightManager
|
|||
/// registered lights (chase-boom swing churned the eviction boundary), then
|
||||
/// (2) frame-FLOOD scoping `c500912b` (gaze-dependent: the under-room portal
|
||||
/// purples entered/left the pool as the camera turned — the seam-floor
|
||||
/// blink; probe: [seam-blk]/[seam-snap]). Current model: all registered
|
||||
/// blink). Current model: all registered
|
||||
/// (=resident) lit lights, then dynamics-first nearest-player, capped here.
|
||||
/// A later last-frame drawable-cell filter was removed after the Facility Hub
|
||||
/// zoom trace proved it recreated the same camera-root coupling: the pool
|
||||
|
|
@ -341,10 +341,6 @@ public sealed class LightManager
|
|||
}
|
||||
}
|
||||
|
||||
// A7.L1 SET-COMPOSITION probe. Inert unless ACDREAM_PROBE_INDOOR_LIGHT=1;
|
||||
// the flag check keeps it zero-cost off.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeIndoorLightEnabled)
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitIndoorLight(_all, _pointSnapshot);
|
||||
}
|
||||
|
||||
private static int CompareRankedLights(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public sealed class LightSource
|
|||
// cross-cell block-offset distance math. #176 correction (2026-07-06): it is
|
||||
// NOT a pool filter — retail collects from ALL resident cells
|
||||
// (CEnvCell::visible_cell_table = the loaded-cell registry, not the flood);
|
||||
// acdream keeps the tag for probes ([indoor-light]/[seam-*]) + future parity.
|
||||
// acdream keeps the tag for future parity work.
|
||||
public bool IsLit = true; // SetLightHook latch
|
||||
public bool IsDynamic; // #143: true = D3D hardware path (1/d att, range×1.5);
|
||||
// false = static dat-baked bake (1/d³, range×1.3)
|
||||
|
|
|
|||
|
|
@ -1,612 +1,44 @@
|
|||
using System.Globalization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AcDream.Core.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell
|
||||
/// rendering pipeline. Initialized from env vars at process start;
|
||||
/// flippable at runtime by direct assignment. Log call sites read these
|
||||
/// statics so a change takes effect on the next frame without relaunching.
|
||||
/// (#434: these used to have a DebugPanel checkbox mirror. That panel has
|
||||
/// been unreachable since Campaign V slice V11 removed its ImGui host, so
|
||||
/// every flag here is startup-or-assignment only.)
|
||||
///
|
||||
/// <para>
|
||||
/// Mirrors the L.2a <see cref="AcDream.Core.Physics.PhysicsDiagnostics"/>
|
||||
/// pattern. The master <see cref="IndoorAll"/> toggle is the user's
|
||||
/// common case — flipping it cascades to all five probe flags.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Spec: <c>docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <summary>Permanent rendering diagnostics and retail render-route helpers.</summary>
|
||||
public static class RenderingDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||
/// <c>[indoor-walk]</c> line per visible cell entity per second:
|
||||
/// entity id, world position, parent cell id, landblock visible flag,
|
||||
/// AABB-visible flag, "in visible cells" flag, drew flag.
|
||||
/// Initial state from <c>ACDREAM_PROBE_INDOOR_WALK=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorWalkEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_WALK") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-lookup]</c>
|
||||
/// line per visible cell entity per second: render-data hit/miss,
|
||||
/// IsSetup flag, SetupParts count, parts-hit / parts-miss tallies.
|
||||
/// Initial state from <c>ACDREAM_PROBE_INDOOR_LOOKUP=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorLookupEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LOOKUP") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// When true, <c>WbMeshAdapter</c> emits two lines per EnvCell id:
|
||||
/// <c>[indoor-upload] requested</c> on first IncrementRefCount and
|
||||
/// <c>[indoor-upload] completed</c> when WB's staged drain produces
|
||||
/// its <c>ObjectMeshData</c>. Missing "completed" lines indicate WB
|
||||
/// silently returned null (hypothesis H1).
|
||||
/// Initial state from <c>ACDREAM_PROBE_INDOOR_UPLOAD=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorUploadEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_UPLOAD") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-xform]</c>
|
||||
/// line per visible cell entity per second: cell-geometry SetupPart's
|
||||
/// composed world matrix translation. Disambiguates transform
|
||||
/// double-apply (hypothesis H5).
|
||||
/// Initial state from <c>ACDREAM_PROBE_INDOOR_XFORM=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorXformEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_XFORM") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||
/// <c>[indoor-cull]</c> line per cell entity that gets culled, with
|
||||
/// the reason (visibleCellIds-miss, frustum, landblock). Disambiguates
|
||||
/// cull bugs (hypothesis H3).
|
||||
/// Initial state from <c>ACDREAM_PROBE_INDOOR_CULL=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorCullEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_CULL") == "1"
|
||||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// When true, the unified portal-visibility pass emits one <c>[vis]</c>
|
||||
/// line whenever the camera's root cell CHANGES (see <see cref="EmitVis"/>):
|
||||
/// root cell id, visible-cell count + ids, the single OutsideView's polygon
|
||||
/// + plane counts, a per-cell plane-count summary, and the scissor-fallback
|
||||
/// count for the frame. This is the runtime apparatus #103 lacked — it lets
|
||||
/// us confirm "OutsideView non-empty and narrowing at the cellar window" off
|
||||
/// a live launch.log before any GL/visual work.
|
||||
/// Initial state from <c>ACDREAM_PROBE_VIS=1</c>.
|
||||
/// <para>
|
||||
/// Phase U.2d (2026-05-30) repurposed this flag from the abandoned A8
|
||||
/// two-pipe stencil pass to the Phase U unified pipeline. The env var name
|
||||
/// is unchanged (its DebugPanel mirror is gone — #434).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static bool ProbeVisibilityEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Temporary Facility Hub staircase discriminator. The frame walk emits
|
||||
/// camera/root/flood facts and the leaf classifier emits change-only
|
||||
/// decisions for the authored stair GfxObj (0x010000DE) and local-player
|
||||
/// setup parts in cells 0x8A02015E/015F/01C1. Output-only; it must never
|
||||
/// influence admission. Initial state from
|
||||
/// <c>ACDREAM_PROBE_FACILITY_STAIRS=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeFacilityStairsEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FACILITY_STAIRS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal
|
||||
/// visibility pass emits, EVERY frame the camera root is an indoor cell, a
|
||||
/// <c>[flap]</c> line (root cell's per-portal side-test D + traverse/cull +
|
||||
/// projection, plus the frame's OutsideView/visible counts) and the call site
|
||||
/// emits a paired <c>[flap-cam]</c> line (FindCameraCell resolution reason,
|
||||
/// camera EYE worldpos, player worldpos, eye-in-root-AABB flag). Unlike the
|
||||
/// cell-change-throttled <see cref="ProbeVisibilityEnabled"/> probe, this fires
|
||||
/// per-frame so it captures the flicker (the exit cell dropping in/out at a
|
||||
/// STABLE root). Pinpoints WHY the exit cell drops: side-test cull (eye past an
|
||||
/// interior portal plane), empty projection, or a stale root (eye outside the
|
||||
/// cell while FindCameraCell still reports it via cache/grace). Throwaway
|
||||
/// apparatus — strip once the flap mechanism is confirmed.
|
||||
/// Initial state from <c>ACDREAM_PROBE_FLAP=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeFlapEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
|
||||
/// <c>[pv-input]</c> line per frame with the EXACT PortalVisibilityBuilder.Build inputs at HIGH
|
||||
/// precision (camera eye + player position to 6 dp, plus orientation-sensitive view-projection
|
||||
/// elements) alongside the resulting flood cell count. The live flap shows the flood set flipping
|
||||
/// 2↔6 at an eye/player that is identical to cm; this probe answers whether the Build INPUTS differ
|
||||
/// below cm precision (sub-cm view jitter → robustness fix) or are byte-identical while the output
|
||||
/// still flips (nondeterminism → surgical bug). Runs WITHOUT the heavy <c>[flap]</c>/<c>[render-sig]</c>
|
||||
/// spam so the log stays diffable. Throwaway apparatus — strip once the jitter source is pinned.
|
||||
/// Initial state from <c>ACDREAM_PROBE_PVINPUT=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbePvInputEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PVINPUT") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// §4 outdoor full-world flap apparatus (2026-06-09). When true, GameWindow snapshots the
|
||||
/// GL fixed-function state entering the world passes each frame (depth test/mask/func, blend
|
||||
/// + factors, cull, front-face, scissor + box, viewport, draw-FBO, color mask, glGetError)
|
||||
/// and emits one <c>[gl-state]</c> line whenever the snapshot CHANGES. Pins or refutes the
|
||||
/// "leaked GL state" family for the flap (every CPU-side input — matrix, flood, clip planes,
|
||||
/// scissor box, membership, eye-vs-terrain — is already probe-exonerated). Throwaway
|
||||
/// apparatus — strip once §4 ships. Initial state from <c>ACDREAM_PROBE_GLSTATE=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeGlStateEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_GLSTATE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// §4 outdoor full-world flap apparatus (2026-06-10) — the decisive probe between the two
|
||||
/// surviving suspects (handoff 2026-06-09 §1): (a) per-instance clip-slot routing under
|
||||
/// outdoor roots, (b) terrain/sky UBO content at draw time — plus the landscape-pass scissor
|
||||
/// box as a third ground truth. When true: RetailPViewRenderer.DrawLandscapeThroughOutsideView
|
||||
/// emits one <c>[clip-route]</c> line (print-on-change) with the outside slice's slot + NDC
|
||||
/// AABB + planes, the CellIdToSlot routing table, the region-SSBO bytes decoded at the routed
|
||||
/// slot, and the terrain-UBO head as uploaded. The <c>[clip-route-disp]</c> producer in
|
||||
/// WbDrawDispatcher.Draw was deleted with the per-instance clip routing at S3 review fix
|
||||
/// round 1 (the routing had no caller), and the <c>[clip-route-scis]</c> scissor line's
|
||||
/// landscape-slice producer went with the doorway scissor at S3 chunk 4; only the
|
||||
/// WorldRenderDiagnostics lines remain. Throwaway apparatus — the S5 cleanup inventory owns
|
||||
/// its deletion. Initial state from <c>ACDREAM_PROBE_CLIPROUTE=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeClipRouteEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
|
||||
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
|
||||
/// max pop count, and — per re-enqueue — the reciprocal-clip pre→post region count + grew flag. Pins
|
||||
/// whether the flap's churn is redundant reciprocal back-contributions producing non-empty drifted
|
||||
/// slivers (the hypothesis) vs another source. Throwaway apparatus — strip once the bound ships.
|
||||
/// Initial state from ACDREAM_PROBE_PORTAL_CHURN=1.
|
||||
/// </summary>
|
||||
public static bool ProbePortalChurnEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// A7.L1 (2026-07-06) light-pool SET-COMPOSITION probe — the apparatus the
|
||||
/// <c>[light]</c> counts could not provide (the #176/#177 discriminator: the bug
|
||||
/// lived in set MEMBERSHIP, not counts). When true,
|
||||
/// <c>LightManager.BuildPointLightSnapshot</c> emits ONE rate-limited
|
||||
/// <c>[indoor-light]</c> line describing the point-light pool
|
||||
/// (see <see cref="EmitIndoorLight"/>):
|
||||
/// <code>
|
||||
/// [indoor-light] pool=<M> cellLess=<K> registered=<R> capped=<R-M>
|
||||
/// byCell=[0x<id>:<count>,...]
|
||||
/// </code>
|
||||
/// #176 correction (2026-07-06): the pool became retail's RESIDENT-cell
|
||||
/// collection capped nearest-the-PLAYER — the earlier gaze-coupled scoping
|
||||
/// (rebuilding the pool from a freshly re-flooded CAMERA-seeded set,
|
||||
/// <c>c500912b</c>) was the #176 flicker mechanism and was deleted.
|
||||
/// A7.L1 later added last-frame drawable-cell scoping, but the Facility Hub
|
||||
/// zoom trace proved that it also couples pool membership to the camera root
|
||||
/// (five lights became one while the player stood still), so it was removed.
|
||||
/// <c>byCell</c> now describes the resident pool; <c>cellLess==pool</c> in a
|
||||
/// fixture-rich room still
|
||||
/// means cell tagging FAILED (ParentCellId not flowing).
|
||||
/// Output-only, inert when off. Initial state from <c>ACDREAM_PROBE_INDOOR_LIGHT=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeIndoorLightEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LIGHT") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #176 seam-floor flicker decisive probe (2026-07-06). RenderDoc pixel-history
|
||||
/// is infeasible on this pipeline (RenderDoc does not support
|
||||
/// GL_ARB_bindless_texture and hides it from the app → our mandatory-modern
|
||||
/// startup gate throws), so this is the in-engine equivalent at draw
|
||||
/// granularity: every draw route that can put geometry at the corridor seam
|
||||
/// floor reports itself, plus the per-cell light sets ACTUALLY applied.
|
||||
/// When set, four line families emit (all change-deduped, Console):
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>[seam-cell]</c> — <c>EnvCellRenderer.Render</c>
|
||||
/// (opaque pass): per target cell — in-filter flag, per-gfx instance count +
|
||||
/// transform translation (shows the +0.02 shell lift), per-batch
|
||||
/// cull/translucency, and the cell's 8-light set RESOLVED to identities
|
||||
/// (owner cell + intensity — raw snapshot indices shuffle when the pool
|
||||
/// rebuilds, so identities are the stable signature). Two instances of one
|
||||
/// (cell,gfx) = the runtime double-draw; light identities flipping with the
|
||||
/// flood = the snapshot-scope mechanism.</description></item>
|
||||
/// <item><description><c>[seam-snap]</c> — the point-light snapshot's HOT
|
||||
/// subset (intensity ≥ 50: the portal purples; fixtures are ~1–2, the viewer
|
||||
/// fill 2.25) with owner cells, emitted with the block.</description></item>
|
||||
/// <item><description><c>[seam-ent]</c> — <c>WbDrawDispatcher</c>: any entity
|
||||
/// parented to a target cell — position, culled/slot, resolved light set. A
|
||||
/// floor-coincident entity (plate/static) would be the z-fight's second draw;
|
||||
/// the player entity doubles as the probe's positive control.</description></item>
|
||||
/// <item><description><c>[seam-mask]</c> — <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>:
|
||||
/// every portal depth fan drawn in a target cell. A sealed dungeon must show
|
||||
/// ZERO (seals fire only for OtherCellId==0xFFFF) — any line is a finding.</description></item>
|
||||
/// </list>
|
||||
/// Value <c>1</c> = the default Facility Hub target set (corridor 0x8A020164 +
|
||||
/// seam neighbors 0165/016E/017A + under-hall 011E + portal-light cells
|
||||
/// 0118/0119); a comma-separated hex cell-id list overrides it. Throwaway
|
||||
/// apparatus — strip when #176 closes. Initial state from
|
||||
/// <c>ACDREAM_PROBE_SEAMDRAW</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeSeamDrawEnabled { get; set; } =
|
||||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ACDREAM_PROBE_SEAMDRAW"));
|
||||
|
||||
/// <summary>Target cell ids for the #176 seam-draw probe (see
|
||||
/// <see cref="ProbeSeamDrawEnabled"/>). Full 32-bit cell ids.</summary>
|
||||
public static IReadOnlySet<uint> SeamDrawTargetCells { get; } =
|
||||
ParseSeamDrawTargets(Environment.GetEnvironmentVariable("ACDREAM_PROBE_SEAMDRAW"));
|
||||
|
||||
/// <summary>
|
||||
/// #176 stripe-hunt shader isolation mode (<c>ACDREAM_LIGHT_DEBUG</c>) —
|
||||
/// throwaway diagnostic, uploaded as <c>uLightDebug</c> by EnvCellRenderer +
|
||||
/// WbDrawDispatcher each pass. 0 = off; 1 = ambient-only vertex lighting
|
||||
/// (all point/sun contributions killed); 2 = DYNAMIC point lights killed
|
||||
/// (the intensity-100 portal purples + the viewer fill off; statics stay);
|
||||
/// 3 = raw vLit visualization in the fragment shader (texture ignored).
|
||||
/// Discriminates lighting-driven stripes (gone at 1/2, visible in the field
|
||||
/// at 3) from texture/per-pixel machinery (survive 1). Settable at
|
||||
/// runtime by direct assignment.
|
||||
/// #176 stripe-hunt shader isolation mode. Zero disables the override;
|
||||
/// values 1-3 retain the established lighting diagnostic modes.
|
||||
/// </summary>
|
||||
public static int LightDebugMode { get; set; } =
|
||||
int.TryParse(
|
||||
Environment.GetEnvironmentVariable("ACDREAM_LIGHT_DEBUG"),
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out int ldm)
|
||||
? ldm
|
||||
out int mode)
|
||||
? mode
|
||||
: 0;
|
||||
|
||||
// S3 review fix round 1 (F5): ClipDebugNoShellTrim (ACDREAM_CLIP_DEBUG)
|
||||
// is deleted — the #176 stripe-hunt isolation it toggled between "clip
|
||||
// shells to their cell's portal-slice region" and "map every instance to
|
||||
// slot 0" is moot now that EnvCellRenderer's per-cell clip routing is
|
||||
// deleted outright (its arming method never had a live caller): shells
|
||||
// always draw WHOLE, unconditionally, the same behavior this flag used
|
||||
// to force.
|
||||
|
||||
/// <summary>Parse ACDREAM_PROBE_SEAMDRAW: "1"/"true"/empty → the default #176
|
||||
/// Facility Hub set; otherwise a comma-separated hex cell-id list (same forgiving
|
||||
/// grammar as <see cref="ParseDumpEntityIds"/>). Internal for unit tests.</summary>
|
||||
internal static IReadOnlySet<uint> ParseSeamDrawTargets(string? raw)
|
||||
{
|
||||
// Default: the corridor + the coplanar-sweep seed neighbors + the under-hall
|
||||
// + the two portal-weenie cells whose intensity-100 purple lights are the
|
||||
// wedge's source (handoff 2026-07-06-176-seam-floor-zfight-handoff.md).
|
||||
var defaults = new HashSet<uint>
|
||||
{
|
||||
0x8A020164u, 0x8A020165u, 0x8A02016Eu, 0x8A02017Au,
|
||||
0x8A02011Eu, 0x8A020118u, 0x8A020119u,
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(raw)) return defaults;
|
||||
var trimmed = raw.Trim();
|
||||
if (trimmed == "1" || trimmed.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
return defaults;
|
||||
var parsed = ParseDumpEntityIds(raw);
|
||||
return parsed.Count > 0 ? parsed : defaults;
|
||||
}
|
||||
|
||||
// Cell-change gate for EmitVis. The probe fires once per distinct root cell
|
||||
// so launch.log stays readable under motion (the per-frame call is a no-op
|
||||
// when the root is unchanged). Sentinel 0 = "no root yet" — the first real
|
||||
// root id always differs and fires. Reset between tests via
|
||||
// ResetVisibilityProbeForTests so the gate doesn't leak across cases.
|
||||
private static uint _lastVisRootCellId;
|
||||
|
||||
/// <summary>
|
||||
/// Emit ONE concise, information-dense <c>[vis]</c> line for the portal-
|
||||
/// visibility frame, but only when <see cref="ProbeVisibilityEnabled"/> is
|
||||
/// true AND <paramref name="rootCellId"/> differs from the last root the
|
||||
/// probe reported (cell-change gating). Cheap no-op otherwise.
|
||||
/// <para>
|
||||
/// Decoupled by design: the OutsideView is passed as pre-computed
|
||||
/// <paramref name="outsidePolyCount"/> + <paramref name="outsidePlaneCount"/>
|
||||
/// primitives rather than the App-layer <c>CellView</c>/<c>ClipPlaneSet</c>
|
||||
/// types, because this owner lives in <c>AcDream.Core</c> and Core must not
|
||||
/// reference the App project (Code Structure Rule 2). The U.4a call site
|
||||
/// supplies <c>OutsideView.Polygons.Count</c> and the OutsideView's
|
||||
/// <c>ClipPlaneSet.Count</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="rootCellId">The camera's root cell id (the BFS seed).</param>
|
||||
/// <param name="visibleCells">Ordered visible cell ids for this frame.</param>
|
||||
/// <param name="outsidePolyCount">Polygon count of the single OutsideView region.</param>
|
||||
/// <param name="outsidePlaneCount">Clip-plane count the OutsideView reduced to (0 ⇒ scissor/empty).</param>
|
||||
/// <param name="perCellPlaneCounts">Per-cell clip-plane count (cell id → plane count).</param>
|
||||
/// <param name="scissorFallbacks">Number of regions that fell back to a scissor AABB this frame.</param>
|
||||
public static void EmitVis(uint rootCellId,
|
||||
IReadOnlyList<uint> visibleCells,
|
||||
int outsidePolyCount,
|
||||
int outsidePlaneCount,
|
||||
IReadOnlyDictionary<uint, int> perCellPlaneCounts,
|
||||
int scissorFallbacks)
|
||||
{
|
||||
if (!ProbeVisibilityEnabled) return;
|
||||
if (rootCellId == _lastVisRootCellId) return; // unchanged root ⇒ suppress
|
||||
_lastVisRootCellId = rootCellId;
|
||||
|
||||
int cellN = visibleCells?.Count ?? 0;
|
||||
|
||||
var sb = new StringBuilder(160);
|
||||
sb.Append("[vis] root=0x").Append(rootCellId.ToString("X8"));
|
||||
sb.Append(" cells=").Append(cellN);
|
||||
|
||||
// Visible cell id list, capped so a wide BFS doesn't blow up the line.
|
||||
sb.Append(" ids=[");
|
||||
if (visibleCells is not null)
|
||||
{
|
||||
const int MaxIds = 12;
|
||||
int shown = 0;
|
||||
foreach (uint id in visibleCells)
|
||||
{
|
||||
if (shown >= MaxIds) { sb.Append(",..."); break; }
|
||||
if (shown > 0) sb.Append(',');
|
||||
sb.Append("0x").Append(id.ToString("X8"));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
sb.Append(']');
|
||||
|
||||
sb.Append(" outside(polys=").Append(outsidePolyCount)
|
||||
.Append(",planes=").Append(outsidePlaneCount).Append(')');
|
||||
|
||||
// Per-cell plane-count summary, capped like the id list.
|
||||
sb.Append(" percell=[");
|
||||
if (perCellPlaneCounts is not null)
|
||||
{
|
||||
const int MaxPerCell = 12;
|
||||
int shown = 0;
|
||||
foreach (var kv in perCellPlaneCounts)
|
||||
{
|
||||
if (shown >= MaxPerCell) { sb.Append(",..."); break; }
|
||||
if (shown > 0) sb.Append(',');
|
||||
sb.Append("0x").Append(kv.Key.ToString("X8")).Append(':').Append(kv.Value);
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
sb.Append(']');
|
||||
|
||||
sb.Append(" fallbacks=").Append(scissorFallbacks);
|
||||
|
||||
Console.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the <see cref="EmitVis"/> cell-change gate. Test-only — this is a
|
||||
/// process-wide static and the gate would otherwise leak across test cases
|
||||
/// (this codebase has documented static-leak flakiness; keep tests
|
||||
/// self-contained). Not part of the public runtime surface.
|
||||
/// </summary>
|
||||
internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0;
|
||||
|
||||
private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks
|
||||
|
||||
// Wall-clock rate-limit gate for EmitIndoorLight (shares the 1 s interval).
|
||||
private static long _lastIndoorLightEmitTicks;
|
||||
|
||||
/// <summary>
|
||||
/// A7.L1 — emit ONE rate-limited <c>[indoor-light]</c> line describing the
|
||||
/// point-light pool: the SET COMPOSITION the <c>[light]</c> counts can't show.
|
||||
/// Cheap no-op when <see cref="ProbeIndoorLightEnabled"/> is false; otherwise
|
||||
/// fires at most once per second. Called from
|
||||
/// <c>LightManager.BuildPointLightSnapshot</c> after resident collection and
|
||||
/// the bounded player-nearest selection.
|
||||
/// </summary>
|
||||
/// <param name="allRegistered">Every registered light (<c>LightManager._all</c>).</param>
|
||||
/// <param name="pointSnapshot">The point-light pool just built.</param>
|
||||
public static void EmitIndoorLight(
|
||||
IReadOnlyList<AcDream.Core.Lighting.LightSource> allRegistered,
|
||||
IReadOnlyList<AcDream.Core.Lighting.LightSource> pointSnapshot)
|
||||
{
|
||||
if (!ProbeIndoorLightEnabled) return;
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
if (_lastIndoorLightEmitTicks != 0 && (now - _lastIndoorLightEmitTicks) < LightEmitIntervalTicks)
|
||||
return;
|
||||
_lastIndoorLightEmitTicks = now;
|
||||
|
||||
int registeredLitPoints = 0;
|
||||
foreach (var l in allRegistered)
|
||||
if (l.IsLit && l.Kind != AcDream.Core.Lighting.LightKind.Directional) registeredLitPoints++;
|
||||
|
||||
int pool = pointSnapshot.Count;
|
||||
int cellLess = 0;
|
||||
var hist = new Dictionary<uint, int>();
|
||||
foreach (var l in pointSnapshot)
|
||||
{
|
||||
if (l.CellId == 0) cellLess++;
|
||||
hist.TryGetValue(l.CellId, out var c);
|
||||
hist[l.CellId] = c + 1;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(220);
|
||||
sb.Append("[indoor-light] pool=").Append(pool);
|
||||
sb.Append(" cellLess=").Append(cellLess);
|
||||
sb.Append(" registered=").Append(registeredLitPoints);
|
||||
// Lights dropped by the MaxGlobalLights nearest-player cap (0 in Hub-scale
|
||||
// rooms for dynamics — statics beyond the 128th-nearest are out of range).
|
||||
sb.Append(" capped=").Append(registeredLitPoints - pool);
|
||||
sb.Append(" byCell=[");
|
||||
const int MaxCells = 12;
|
||||
int shown = 0;
|
||||
foreach (var kv in hist)
|
||||
{
|
||||
if (shown >= MaxCells) { sb.Append(",..."); break; }
|
||||
if (shown > 0) sb.Append(',');
|
||||
sb.Append("0x").Append(kv.Key.ToString("X8")).Append(':').Append(kv.Value);
|
||||
shown++;
|
||||
}
|
||||
sb.Append(']');
|
||||
Console.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
private static bool _probeEnvCellEnabled =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENVCELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 Task 9 (2026-05-28): when true, the indoor EnvCell draw path's
|
||||
/// <c>[envcells]</c> probe emits one line per indoor frame —
|
||||
/// CellsRendered / TrianglesDrawn from <c>EnvCellRenderer.Stats</c> +
|
||||
/// ourBldgs/otherBldgs/filterCnt.
|
||||
/// Also enabled implicitly when <see cref="ProbeVisibilityEnabled"/> is true.
|
||||
/// Initial state from <c>ACDREAM_PROBE_ENVCELL=1</c>.
|
||||
/// (The two-pipe <c>RenderInsideOutAcdream</c> pass that originally owned
|
||||
/// this probe was removed in Phase U.1; the env var + the
|
||||
/// <c>EnvCellRenderer.Stats</c> source remain.)
|
||||
/// </summary>
|
||||
public static bool ProbeEnvCellEnabled
|
||||
{
|
||||
get => _probeEnvCellEnabled || ProbeVisibilityEnabled;
|
||||
set => _probeEnvCellEnabled = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Master toggle. Reading reflects the AND of all five flags
|
||||
/// (true only when every probe is on). Writing cascades — setting
|
||||
/// to <see langword="true"/> turns ALL five flags on; setting to
|
||||
/// <see langword="false"/> turns ALL five off.
|
||||
/// </summary>
|
||||
public static bool IndoorAll
|
||||
{
|
||||
get => ProbeIndoorWalkEnabled
|
||||
&& ProbeIndoorLookupEnabled
|
||||
&& ProbeIndoorUploadEnabled
|
||||
&& ProbeIndoorXformEnabled
|
||||
&& ProbeIndoorCullEnabled;
|
||||
set
|
||||
{
|
||||
ProbeIndoorWalkEnabled = value;
|
||||
ProbeIndoorLookupEnabled = value;
|
||||
ProbeIndoorUploadEnabled = value;
|
||||
ProbeIndoorXformEnabled = value;
|
||||
ProbeIndoorCullEnabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for probe call sites. Returns <see langword="true"/> when
|
||||
/// the low 16 bits of <paramref name="id"/> are ≥ 0x0100 — the AC
|
||||
/// convention for EnvCell (indoor) cells, as opposed to outdoor cells
|
||||
/// in the 8×8 landblock grid (0x0001–0x0040).
|
||||
/// </summary>
|
||||
/// <summary>Returns true for AC indoor EnvCell ids.</summary>
|
||||
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
|
||||
|
||||
/// <summary>
|
||||
/// Parse the <c>ACDREAM_DUMP_ENTITY</c> value: comma-separated hex ids,
|
||||
/// optional 0x prefix, whitespace tolerated, malformed segments ignored
|
||||
/// (probes are forgiving — a typo'd segment must not take the launch down).
|
||||
/// Internal for unit tests.
|
||||
/// </summary>
|
||||
internal static IReadOnlySet<uint> ParseDumpEntityIds(string? raw)
|
||||
{
|
||||
var set = new HashSet<uint>();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return set;
|
||||
foreach (var seg in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var s = seg.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? seg[2..] : seg;
|
||||
if (uint.TryParse(s, System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var id))
|
||||
set.Add(id);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The top-level render branch: should this frame run the indoor (DrawInside) path?
|
||||
///
|
||||
/// <para>Retail <c>SmartBox::RenderNormalMode</c> (0x453aa0, pc:92665) branches
|
||||
/// DrawInside vs the outdoor <c>LScape::draw</c> on <c>is_player_outside</c> — the
|
||||
/// <b>PLAYER's</b> cell (<c>(player->m_position.objcell_id & 0xFFFF) < 0x100</c>,
|
||||
/// <c>SmartBox::is_player_outside</c> 0x451e80) — NOT the camera/viewer cell. When the
|
||||
/// player is inside, acdream roots the portal flood at the player's transition-owned
|
||||
/// physics cell and projects from the camera eye, so the shell around the player remains
|
||||
/// sealed during chase-camera cell transitions.</para>
|
||||
///
|
||||
/// <para>acdream historically branched on the camera cell (a non-null
|
||||
/// <c>visibility.CameraCell</c>). A 3rd-person chase camera lags the player, so when the
|
||||
/// player had already stepped outside but the camera still sat in the doorway, the camera
|
||||
/// branch wrongly chose DrawInside rooted at the doorway cell, where the exit-portal flood
|
||||
/// degenerates → the whole static world (terrain + shells) gated off → grey screen with
|
||||
/// only entities (which bypass the gate) showing through. Branching on the player removes it.</para>
|
||||
///
|
||||
/// <param name="playerCellId">The player's current cell id (0 if unresolved → outside).</param>
|
||||
/// <param name="renderRootResolved">Whether the player's indoor render root is loaded and
|
||||
/// available to DrawInside.</param>
|
||||
/// Retail chooses the indoor path from the player's cell, provided the
|
||||
/// transition-owned render root is available.
|
||||
/// </summary>
|
||||
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
|
||||
=> renderRootResolved && IsEnvCellId(playerCellId);
|
||||
|
||||
/// <summary>
|
||||
/// MP0 (2026-07-05) — master toggle for the permanent frame profiler
|
||||
/// (<c>AcDream.App.Diagnostics.FrameProfiler</c>): CPU frame time
|
||||
/// (swap-to-swap), whole-frame GPU time, per-stage CPU attribution,
|
||||
/// per-frame allocation counters, reported as one <c>[frame-prof]</c>
|
||||
/// line every ~5 s. Permanent apparatus (every MP-track gate reads it) —
|
||||
/// do NOT strip with session probes. This paragraph previously claimed
|
||||
/// the whole-frame GPU query self-disables under <c>ACDREAM_WB_DIAG=1</c>;
|
||||
/// Campaign V slice V11 deleted that self-disable along with the GL query
|
||||
/// ring it protected, and the two flags are now independent — see
|
||||
/// <c>FrameProfiler</c>'s own class doc. Every backend reports GPU time
|
||||
/// through <c>FrameProfiler.RecordGpuSample</c>.
|
||||
/// Initial state from <c>ACDREAM_FRAME_PROF=1</c>; runtime-toggleable
|
||||
/// by direct assignment (its DebugPanel mirror is gone — #434).
|
||||
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
||||
/// </summary>
|
||||
/// <summary>Permanent frame-profiler toggle.</summary>
|
||||
public static bool FrameProfEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_FRAME_PROF") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// 2026-07-24 measurement-tooling review — opt-in per-frame history
|
||||
/// export path for <c>AcDream.App.Diagnostics.FrameProfiler</c>. When
|
||||
/// set, the profiler accumulates one record per frame (frame index,
|
||||
/// wall timestamp, per-stage CPU microseconds, GPU microseconds,
|
||||
/// frame-thread allocation delta) in memory and writes them as CSV to
|
||||
/// this path on <c>Dispose</c> — the aggregated 5-second
|
||||
/// <c>[frame-prof]</c> report is unaffected. History recording only
|
||||
/// takes effect while <see cref="FrameProfEnabled"/> is also true: it
|
||||
/// reuses the same per-frame instrumentation rather than duplicating
|
||||
/// it. Startup-only (not runtime-toggleable) — read once, like every
|
||||
/// other <c>ACDREAM_*</c> launch flag on this owner.
|
||||
/// </summary>
|
||||
/// <summary>Optional per-frame CSV history path for the frame profiler.</summary>
|
||||
public static string? FrameHistoryPath { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_FRAME_HISTORY");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): print-only walk-transcript
|
||||
/// emitter. When true, the production frame walk
|
||||
/// (<c>RetailFrameWalk</c> + <c>WalkFrameDriver</c>,
|
||||
/// <c>src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs</c>) prints
|
||||
/// the OH oracle-trace line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC — to
|
||||
/// Console at the exact points retail's cdb breakpoints sit
|
||||
/// (<c>tools/walk-oracle/oh/oh-capture-walk.cdb.template</c>). Print-
|
||||
/// only: it never gates admission, depth state, or draw order — every
|
||||
/// print call sits AFTER the walk has already decided to emit the
|
||||
/// corresponding turn, and every print call bails out before any string
|
||||
/// work when this is false.
|
||||
/// <para>
|
||||
/// This is the "owned by RenderingDiagnostics (rule 5)" half of §11.2
|
||||
/// B1's design; the sole authoritative read of
|
||||
/// <c>ACDREAM_DUMP_WALK_TRANSCRIPT</c> lives in
|
||||
/// <c>RuntimeOptions.DumpWalkTranscript</c> (rule 4) — this property
|
||||
/// defaults false and is set exactly once, at <c>GameWindow</c>
|
||||
/// construction, from that typed option (never reads the environment
|
||||
/// directly itself, unlike this file's other flags), so the deep walk
|
||||
/// call sites that have no reachable <c>RuntimeOptions</c> reference
|
||||
/// still get one static, flippable-at-runtime gate to check.
|
||||
/// </para>
|
||||
/// Print-only production walk transcript gate. RuntimeOptions owns the
|
||||
/// sole environment read and assigns this property during construction.
|
||||
/// </summary>
|
||||
public static bool DumpWalkTranscriptEnabled { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1969,7 +1969,7 @@ public sealed class PlayerMovementController
|
|||
// PhysicsEngine.ResolveWithTransition. That ran for EVERY entity, so a Holtburg NPC
|
||||
// jump-looping near the cottage doorway clobbered the render root every tick → the render
|
||||
// rooted at the NPC's tiny connector cell → only its ~8-tri shell drew, rest = GL clear
|
||||
// color = the cottage doorway "blue-hole" flap (diagnosed 2026-06-03 via [flap-cam]/[shell]).
|
||||
// color = the cottage doorway "blue-hole" flap (diagnosed 2026-06-03 from a camera capture).
|
||||
if (publishRenderRoot)
|
||||
_physics.UpdatePlayerCurrCell(newCellId);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue