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

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