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"; /// /// #131 (2026-06-12) outside-stage dynamics probe. When true, the renderer /// emits one [outstage] line per CHANGE of the outside-stage /// routing + per-slice cone verdict set under an interior root (which /// outdoor dynamics were routed to the landscape slice, which survived the /// slice viewcone), and GameWindow emits one [outstage-pt] line per /// change of the slice Scene-particle id set + matched-emitter count. /// Built for the portal-swirl-missing-through-doorway capture. Light: /// silent while the set is stable. Initial state from /// ACDREAM_PROBE_OUTSTAGE=1. /// public static bool ProbeOutStageEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUTSTAGE") == "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"; /// /// #133 A7 (2026-06-13) dungeon-lighting objective probe. When true, /// the per-frame scene-lighting build emits ONE [light] line /// roughly every second (wall-clock rate-limited like WB-DIAG) via /// : /// /// [light] insideCell=<bool> ambient=(r,g,b) sun=<intensity> /// registeredLights=<N> activeLights=<uCellAmbient.w> playerCell=0x<id> /// /// This is the self-verification signal for the dungeon-dim question: /// /// insideCell=true ambient=(0.20,0.20,0.20) sun=0 /// confirms the indoor branch fired (retail flat ambient, sun killed). /// registeredLights is the count of dat-baked /// point/spot lights (Setup.Lights) registered with the /// LightManager — if this is 0 in a dungeon, the cell's static /// objects carry no baked torches (so the only illumination IS the /// 0.2 ambient → dim). /// activeLights is uCellAmbient.w — the /// shader's active-slot count, which INCLUDES the (zeroed) sun slot /// indoors. So activeLights=1 registeredLights=0 = "only the dead /// sun slot, no torches in range". /// /// Output-only, inert when off. Initial state from ACDREAM_PROBE_LIGHT=1. /// public static bool ProbeLightEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIGHT") == "1"; /// /// A7.L1 (2026-07-06) per-cell light SET-COMPOSITION probe — the apparatus the /// [light] counts could not provide (the #176/#177 discriminator: the bug /// lived in set MEMBERSHIP, not counts). When true, the scoped /// LightManager.BuildPointLightSnapshot emits ONE rate-limited /// [indoor-light] line describing the visible-cell-scoped point-light pool /// (see ): /// /// [indoor-light] visibleCells=<N> pool=<M> cellLess=<K> registered=<R> /// droppedNonVisible=<R-M> byCell=[0x<id>:<count>,...] /// /// This validates the A7 fix's load-bearing assumption end-to-end: /// /// cellLess==pool (every pool light is CellId 0) ⇒ /// cell tagging FAILED (ParentCellId not flowing) — scoping is a silent no-op. /// pool==cellLess while registered is large in a /// LIT room ⇒ tagged CellIds never match the visible set (wrong id form) — the /// room would go dark. /// droppedNonVisible>0 with byCell tracking the /// visible rooms ⇒ scoping WORKING (the under-room/through-floor lights are the /// dropped ones). /// /// Output-only, inert when off. Initial state from ACDREAM_PROBE_INDOOR_LIGHT=1. /// public static bool ProbeIndoorLightEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LIGHT") == "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; // Wall-clock rate-limit gate for EmitLight. Ticks (100 ns) is plenty — // we only need ~1 Hz and avoid a Stopwatch allocation/field. Sentinel 0 // = "never emitted" so the first call always fires. private static long _lastLightEmitTicks; private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks /// /// #133 A7 — emit ONE rate-limited [light] line describing the /// current scene-lighting state, followed (when /// is supplied) by up to three [light-detail] lines for the nearest /// ACTIVE point/spot lights. Cheap no-op when /// is false; otherwise fires at most /// once per second. Pull the values from the spot where /// GameWindow.UpdateSunFromSky set Lighting.CurrentAmbient /// / Lighting.Sun and where SceneLightingUbo.Build computed /// the active-slot count. /// /// The [light-detail] lines are the answer to the "candle-spotlight" /// question — they expose each torch's REAL dat-derived runtime values /// (range= Falloff metres, intensity=, cone= radians, /// color=, distToViewer=) so it is visible in launch.log /// whether dungeon torches are tiny-range points or wide cones and at what /// intensity — without a screenshot: /// /// [light-detail] kind=Point range=<Falloff m> intensity=<I> cone=<rad> color=(r,g,b) distToViewer=<m> /// /// /// /// The playerInsideCell value driving the indoor branch. /// Cell ambient red (xyz of uCellAmbient). /// Cell ambient green. /// Cell ambient blue. /// The sun LightSource.Intensity (0 indoors). /// Total point/spot lights registered with the LightManager. /// uCellAmbient.w — shader active-slot count (includes the zeroed sun slot indoors). /// The player's current cell id (0 if unresolved → outside). /// The ticked LightManager (its Active list, sorted nearest-first by the /// just-completed Tick). When non-null, drives the [light-detail] lines. Optional so existing call /// sites / tests that only want the aggregate line keep compiling. public static void EmitLight(bool insideCell, float ambientR, float ambientG, float ambientB, float sunIntensity, int registeredLights, int activeLights, uint playerCellId, AcDream.Core.Lighting.LightManager? lights = null) { if (!ProbeLightEnabled) return; long now = DateTime.UtcNow.Ticks; if (_lastLightEmitTicks != 0 && (now - _lastLightEmitTicks) < LightEmitIntervalTicks) return; _lastLightEmitTicks = now; var ci = System.Globalization.CultureInfo.InvariantCulture; Console.WriteLine(string.Format(ci, "[light] insideCell={0} ambient=({1:0.###},{2:0.###},{3:0.###}) sun={4:0.###} registeredLights={5} activeLights={6} playerCell=0x{7:X8}", insideCell, ambientR, ambientG, ambientB, sunIntensity, registeredLights, activeLights, playerCellId)); // #133 A7 (2026-06-13) — per-light detail for the "spotlight bubble" // question. Dump the actual runtime dat-derived values of the nearest // ~3 ACTIVE point/spot lights so the real Falloff/Intensity/ConeAngle // are visible in launch.log (are torch ranges 1m or 10m? points or // spots? what intensity?). The sun (Directional, slot 0) is skipped — // it carries no Range/cone meaning. DistSq is already cached by // LightManager.Tick this frame, so the active list is sorted nearest- // first; we just take the first few non-directional entries. if (lights is null) return; var active = lights.Active; int shown = 0; const int MaxDetail = 3; for (int i = 0; i < active.Length && shown < MaxDetail; i++) { var ls = active[i]; if (ls is null) continue; if (ls.Kind == AcDream.Core.Lighting.LightKind.Directional) continue; float dist = ls.DistSq >= 0f ? MathF.Sqrt(ls.DistSq) : 0f; Console.WriteLine(string.Format(ci, "[light-detail] kind={0} range={1:0.###} intensity={2:0.###} cone={3:0.####} color=({4:0.###},{5:0.###},{6:0.###}) distToViewer={7:0.###}", ls.Kind, ls.Range, ls.Intensity, ls.ConeAngle, ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z, dist)); shown++; } } // Wall-clock rate-limit gate for EmitIndoorLight (shares the 1 s interval). private static long _lastIndoorLightEmitTicks; /// /// A7.L1 — emit ONE rate-limited [indoor-light] line describing the /// visible-cell-scoped point-light pool: the SET COMPOSITION the [light] /// counts can't show. Cheap no-op when is /// false; otherwise fires at most once per second. Called from the scoped /// LightManager.BuildPointLightSnapshot (visibleCells != null path). /// /// Size of the portal-flood visible-cell set this frame. /// Every registered light (LightManager._all). /// The visible-cell-scoped point-light pool just built. public static void EmitIndoorLight(int visibleCellCount, IReadOnlyList allRegistered, IReadOnlyList scopedSnapshot) { 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 = scopedSnapshot.Count; int cellLess = 0; var hist = new Dictionary(); foreach (var l in scopedSnapshot) { 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] visibleCells=").Append(visibleCellCount); sb.Append(" pool=").Append(pool); sb.Append(" cellLess=").Append(cellLess); sb.Append(" registered=").Append(registeredLitPoints); // Lights excluded by visibility scoping (retail: cells not in visible_cell_table // contribute nothing) — the through-floor/under-room lights kept out of the pool. sb.Append(" droppedNonVisible=").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"; /// /// 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); }