using System;
using System.Collections.Generic;
using System.Text;
namespace AcDream.Core.Rendering;
///
/// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell
/// rendering pipeline. Initialized from env vars at process start;
/// flippable at runtime via the DebugPanel mirror. Log call sites read
/// these statics so a checkbox toggle takes effect on the next frame
/// without relaunching.
///
///
/// Mirrors the L.2a
/// pattern. The master toggle is the user's
/// common case — flipping it cascades to all five probe flags.
///
///
///
/// Spec: docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md.
///
///
public static class RenderingDiagnostics
{
///
/// When true, WbDrawDispatcher.WalkVisibleEntities emits one
/// [indoor-walk] 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 ACDREAM_PROBE_INDOOR_WALK=1.
///
public static bool ProbeIndoorWalkEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_WALK") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
///
/// When true, WbDrawDispatcher emits one [indoor-lookup]
/// line per visible cell entity per second: render-data hit/miss,
/// IsSetup flag, SetupParts count, parts-hit / parts-miss tallies.
/// Initial state from ACDREAM_PROBE_INDOOR_LOOKUP=1.
///
public static bool ProbeIndoorLookupEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LOOKUP") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
///
/// When true, WbMeshAdapter emits two lines per EnvCell id:
/// [indoor-upload] requested on first IncrementRefCount and
/// [indoor-upload] completed when WB's staged drain produces
/// its ObjectMeshData. Missing "completed" lines indicate WB
/// silently returned null (hypothesis H1).
/// Initial state from ACDREAM_PROBE_INDOOR_UPLOAD=1.
///
public static bool ProbeIndoorUploadEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_UPLOAD") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
///
/// When true, WbDrawDispatcher emits one [indoor-xform]
/// line per visible cell entity per second: cell-geometry SetupPart's
/// composed world matrix translation. Disambiguates transform
/// double-apply (hypothesis H5).
/// Initial state from ACDREAM_PROBE_INDOOR_XFORM=1.
///
public static bool ProbeIndoorXformEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_XFORM") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
///
/// When true, WbDrawDispatcher.WalkVisibleEntities emits one
/// [indoor-cull] line per cell entity that gets culled, with
/// the reason (visibleCellIds-miss, frustum, landblock). Disambiguates
/// cull bugs (hypothesis H3).
/// Initial state from ACDREAM_PROBE_INDOOR_CULL=1.
///
public static bool ProbeIndoorCullEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_CULL") == "1"
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
///
/// When true, the unified portal-visibility pass emits one [vis]
/// line whenever the camera's root cell CHANGES (see ):
/// 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 ACDREAM_PROBE_VIS=1.
///
/// 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 +
/// the DebugPanel mirror (DebugVM.ProbeVisibility) are unchanged.
///
///
public static bool ProbeVisibilityEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
///
/// #119-residual viewer/flood capture (2026-06-11): one [viewer]
/// line per CHANGE of (root cell, flood size, OutsideView poly count,
/// player cell), with the projection EYE at mm precision on every line —
/// the capture half of the tower-ascent capture→replay loop
/// (TowerAscentReplayTests replays the captured pairs deterministically).
/// Light: silent while the visibility state is stable; a tower climb
/// emits a few dozen lines. Initial state from
/// ACDREAM_PROBE_VIEWER=1.
///
public static bool ProbeViewerEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIEWER") == "1";
///
/// 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
/// [flap] 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 [flap-cam] line (FindCameraCell resolution reason,
/// camera EYE worldpos, player worldpos, eye-in-root-AABB flag). Unlike the
/// cell-change-throttled 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 ACDREAM_PROBE_FLAP=1.
///
public static bool ProbeFlapEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
///
/// Issue #78 (2026-05-31) cell-shell render probe. When true,
/// EnvCellRenderer.Render emits one [shell] line per opaque-pass
/// call: per visible (filtered) cell — is it present in the prepared snapshot,
/// how many gfxObjs + instances, and per-gfxObj batch count / index count /
/// translucent / zero-bindless-handle (missing texture) — plus the pass totals.
/// This directly answers WHY interior walls/ceilings don't appear: no geometry
/// prepared for the cell (cell absent / 0 instances), drawn-but-invisible
/// (zeroHandle / translucent against the clear color), or prepared+drawn (so the
/// fault is elsewhere — depth/occlusion). Throwaway apparatus — strip once the
/// indoor-enclosure render is fixed. Initial state from ACDREAM_PROBE_SHELL=1.
///
public static bool ProbeShellEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "1";
///
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
/// [pv-input] 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 [flap]/[render-sig]
/// spam so the log stays diffable. Throwaway apparatus — strip once the jitter source is pinned.
/// Initial state from ACDREAM_PROBE_PVINPUT=1.
///
public static bool ProbePvInputEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PVINPUT") == "1";
///
/// §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 [gl-state] 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 ACDREAM_PROBE_GLSTATE=1.
///
public static bool ProbeGlStateEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_GLSTATE") == "1";
///
/// §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 [clip-route] 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; WbDrawDispatcher.Draw emits one
/// [clip-route-disp] line (print-on-change, routed draws only) with the per-slot
/// instance histogram exactly as uploaded to binding=3 plus the culled-entity count; and
/// GameWindow.DrawRetailPViewLandscapeSlice emits one [clip-route-scis] line
/// (print-on-change) with the ACTUAL GL scissor enable + box the landscape pass draws under.
/// Throwaway apparatus — strip once §4 ships. Initial state from ACDREAM_PROBE_CLIPROUTE=1.
///
public static bool ProbeClipRouteEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
///
/// #105 white-indoor-textures apparatus (2026-06-10). When true, WbMeshAdapter.Tick
/// emits one [tex-flush] line whenever the staged-texture-update picture changes:
/// pending layer updates across all shared atlases BEFORE and AFTER the per-frame
/// ObjectMeshManager.GenerateMipmaps() flush, plus arrays-with-pending / total-array
/// counts. The broken contract this pins: TextureAtlasManager.AddTexture only STAGES
/// pixel data (PBO + pending list); without the per-frame flush (WB GameScene.cs:975) the
/// data never reaches the GL texture and the batch samples undefined content behind a valid
/// bindless handle — the classic white walls. A healthy run shows after=0 on every
/// line; a stuck before==after>0 at standstill is the #105 mechanism live.
/// Initial state from ACDREAM_PROBE_TEXFLUSH=1.
///
public static bool ProbeTexFlushEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_TEXFLUSH") == "1";
///
/// 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.
///
public static bool ProbePortalChurnEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
///
/// BR-2 phantom-site probe (2026-06-11; plan
/// docs/plans/2026-06-11-building-render-port-plan.md §BR-2 first
/// task). The BR-1 pre-check proved the #113 phantom residual cannot be
/// GfxObj portal fills (never extracted); the surviving suspects are
/// cell-side. When true, RetailPViewRenderer emits, print-on-change
/// per cell: [phantom-shell] — per shell-pass cell, the clip-enable
/// state and each drawn slice's slot + plane count, flagging the pass-all
/// cases (NoClipSlice fallback for slot-less cells; assembler slot-0
/// scissor fallback) — and [phantom-objs] — per object-list cell,
/// the entity-bucket size drawn unclipped/un-viewcone'd. Reproducing the
/// phantom with this on pins which mechanism draws it (shells → BR-2/BR-3;
/// statics → BR-5). Throwaway apparatus — strip when the phantom closes.
/// Initial state from ACDREAM_PROBE_PHANTOM=1.
///
public static bool ProbePhantomEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PHANTOM") == "1";
// 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;
///
/// Emit ONE concise, information-dense [vis] line for the portal-
/// visibility frame, but only when is
/// true AND differs from the last root the
/// probe reported (cell-change gating). Cheap no-op otherwise.
///
/// Decoupled by design: the OutsideView is passed as pre-computed
/// +
/// primitives rather than the App-layer CellView/ClipPlaneSet
/// types, because this owner lives in AcDream.Core and Core must not
/// reference the App project (Code Structure Rule 2). The U.4a call site
/// supplies OutsideView.Polygons.Count and the OutsideView's
/// ClipPlaneSet.Count.
///
///
/// The camera's root cell id (the BFS seed).
/// Ordered visible cell ids for this frame.
/// Polygon count of the single OutsideView region.
/// Clip-plane count the OutsideView reduced to (0 ⇒ scissor/empty).
/// Per-cell clip-plane count (cell id → plane count).
/// Number of regions that fell back to a scissor AABB this frame.
public static void EmitVis(uint rootCellId,
IReadOnlyList visibleCells,
int outsidePolyCount,
int outsidePlaneCount,
IReadOnlyDictionary 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());
}
///
/// Reset the 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.
///
internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0;
private static bool _probeEnvCellEnabled =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENVCELL") == "1";
///
/// Phase A8 Task 9 (2026-05-28): when true, the indoor EnvCell draw path's
/// [envcells] probe emits one line per indoor frame —
/// CellsRendered / TrianglesDrawn from EnvCellRenderer.Stats +
/// ourBldgs/otherBldgs/filterCnt.
/// Also enabled implicitly when is true.
/// Initial state from ACDREAM_PROBE_ENVCELL=1.
/// (The two-pipe RenderInsideOutAcdream pass that originally owned
/// this probe was removed in Phase U.1; the env var + the
/// EnvCellRenderer.Stats source remain.)
///
public static bool ProbeEnvCellEnabled
{
get => _probeEnvCellEnabled || ProbeVisibilityEnabled;
set => _probeEnvCellEnabled = value;
}
///
/// Master toggle. Reading reflects the AND of all five flags
/// (true only when every probe is on). Writing cascades — setting
/// to turns ALL five flags on; setting to
/// turns ALL five off.
///
public static bool IndoorAll
{
get => ProbeIndoorWalkEnabled
&& ProbeIndoorLookupEnabled
&& ProbeIndoorUploadEnabled
&& ProbeIndoorXformEnabled
&& ProbeIndoorCullEnabled;
set
{
ProbeIndoorWalkEnabled = value;
ProbeIndoorLookupEnabled = value;
ProbeIndoorUploadEnabled = value;
ProbeIndoorXformEnabled = value;
ProbeIndoorCullEnabled = value;
}
}
///
/// Helper for probe call sites. Returns when
/// the low 16 bits of are ≥ 0x0100 — the AC
/// convention for EnvCell (indoor) cells, as opposed to outdoor cells
/// in the 8×8 landblock grid (0x0001–0x0040).
///
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
///
/// #119 tower-staircase decisive probe (2026-06-11). Comma-separated
/// Setup / GfxObj source ids (hex, optional 0x prefix) from
/// ACDREAM_DUMP_ENTITY. Any WorldEntity whose
/// SourceGfxObjOrSetupId is in this set emits:
/// (a) a [dump-entity] HYDRATE dump at MeshRef construction time
/// (GameWindow.BuildInteriorEntitiesForStreaming) — per-part
/// placement-frame translations + dropped-part accounting — discriminating
/// hydration-time corruption (H-A: SetupMesh.Flatten identity fallback /
/// silent gfx-null part drops under degraded dat reads);
/// (b) a [dump-entity] DRAW dump in WbDrawDispatcher at first
/// draw — live MeshRefs translations + Tier-1 classification cache state —
/// re-emitted compactly whenever that state changes (H-B: stale/partial
/// cached batch set); and
/// (c) rate-limited [dump-entity] WALK-REJECT lines when the
/// dispatcher's walk filters the entity out (absence-of-draw attribution).
/// Empty set = probe off; every call site early-outs on Count == 0.
///
public static IReadOnlySet DumpEntitySourceIds { get; } =
ParseDumpEntityIds(Environment.GetEnvironmentVariable("ACDREAM_DUMP_ENTITY"));
///
/// Parse the ACDREAM_DUMP_ENTITY 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.
///
internal static IReadOnlySet ParseDumpEntityIds(string? raw)
{
var set = new HashSet();
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;
}
///
/// The top-level render branch: should this frame run the indoor (DrawInside) path?
///
/// Retail SmartBox::RenderNormalMode (0x453aa0, pc:92665) branches
/// DrawInside vs the outdoor LScape::draw on is_player_outside — the
/// PLAYER's cell ((player->m_position.objcell_id & 0xFFFF) < 0x100,
/// SmartBox::is_player_outside 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.
///
/// acdream historically branched on the camera cell (a non-null
/// visibility.CameraCell). 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.
///
/// The player's current cell id (0 if unresolved → outside).
/// Whether the player's indoor render root is loaded and
/// available to DrawInside.
///
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
=> renderRootResolved && IsEnvCellId(playerCellId);
}