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:
Erik 2026-09-05 04:46:31 +02:00
parent b77989c323
commit bf53e2ad6e
82 changed files with 451 additions and 10108 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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) =>

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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
{

View file

@ -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;

View file

@ -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
// ~12). 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).

View file

@ -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}");
}
}

View file

@ -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);

View file

@ -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,

View file

@ -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.

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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,

View file

@ -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(

View file

@ -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)

View file

@ -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=&lt;M&gt; cellLess=&lt;K&gt; registered=&lt;R&gt; capped=&lt;R-M&gt;
/// byCell=[0x&lt;id&gt;:&lt;count&gt;,...]
/// </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 ~12, 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 (0x00010x0040).
/// </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-&gt;m_position.objcell_id &amp; 0xFFFF) &lt; 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; }
}

View file

@ -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);
}