acdream/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
Erik 3cf6bcc219 #119 decisive probe: ACDREAM_DUMP_ENTITY one-shot entity dump (H-A/H-B/H-C discriminator)
The broken-state log (user-session-capture2.log) shows meshMissing=0 /
entSeen==entDrawn WHILE broken stairs are on screen - the staircase is
DRAWN WRONG, not missing. This probe discriminates the three live
hypotheses in ONE launch (handoff 2026-06-11 s4):

- HYDRATE dump (GameWindow.BuildInteriorEntitiesForStreaming): per-part
  placement-frame translations + dropped-part accounting at the MOMENT
  MeshRefs are constructed. H-A (SetupMesh.Flatten identity fallback /
  silent gfx-null part drops under degraded dat reads) shows here as
  zero translations or built<43.
- DRAW dump (WbDrawDispatcher, first tuple per entity): live MeshRefs
  translation summary + per-part loaded flags + Tier-1 classification
  cache state (batch count + RestPose translation summary), re-emitted
  compactly on signature change. H-B (partial/stale cached batch set)
  shows as correct translations + odd batch count.
- WALK-REJECT lines (rate-limited): attributes 'entity never reaches
  the draw loop' to the specific gate (visibleCellIds/frustum).
- Correct everything -> H-C (draw-side compose), instrument next.

Targets: ACDREAM_DUMP_ENTITY=0x020003F2,0x020005D8 (the 43-part spiral
staircase Setup + the wall barrels; H-A predicts the user's 'barrel' IS
the collapsed staircase). Probe is inert when the env var is unset.
Parser in RenderingDiagnostics (diagnostic-owner pattern) + 5 unit tests.

Suites: App 242+1skip / Core 1427+2skip / UI 420 / Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:01:08 +02:00

441 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 via the DebugPanel mirror. Log call sites read
/// these statics so a checkbox toggle takes effect on the next frame
/// without relaunching.
///
/// <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>
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 +
/// the DebugPanel mirror (<c>DebugVM.ProbeVisibility</c>) are unchanged.
/// </para>
/// </summary>
public static bool ProbeVisibilityEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
/// <summary>
/// #119-residual viewer/flood capture (2026-06-11): one <c>[viewer]</c>
/// 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
/// <c>ACDREAM_PROBE_VIEWER=1</c>.
/// </summary>
public static bool ProbeViewerEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIEWER") == "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>
/// Issue #78 (2026-05-31) cell-shell render probe. When true,
/// <c>EnvCellRenderer.Render</c> emits one <c>[shell]</c> 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 <c>ACDREAM_PROBE_SHELL=1</c>.
/// </summary>
public static bool ProbeShellEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "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; WbDrawDispatcher.Draw emits one
/// <c>[clip-route-disp]</c> 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 <c>[clip-route-scis]</c> 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 <c>ACDREAM_PROBE_CLIPROUTE=1</c>.
/// </summary>
public static bool ProbeClipRouteEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
/// <summary>
/// #105 white-indoor-textures apparatus (2026-06-10). When true, <c>WbMeshAdapter.Tick</c>
/// emits one <c>[tex-flush]</c> line whenever the staged-texture-update picture changes:
/// pending layer updates across all shared atlases BEFORE and AFTER the per-frame
/// <c>ObjectMeshManager.GenerateMipmaps()</c> flush, plus arrays-with-pending / total-array
/// counts. The broken contract this pins: <c>TextureAtlasManager.AddTexture</c> 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 <c>after=0</c> on every
/// line; a stuck <c>before==after&gt;0</c> at standstill is the #105 mechanism live.
/// Initial state from <c>ACDREAM_PROBE_TEXFLUSH=1</c>.
/// </summary>
public static bool ProbeTexFlushEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_TEXFLUSH") == "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>
/// BR-2 phantom-site probe (2026-06-11; plan
/// <c>docs/plans/2026-06-11-building-render-port-plan.md</c> §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, <c>RetailPViewRenderer</c> emits, print-on-change
/// per cell: <c>[phantom-shell]</c> — 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 <c>[phantom-objs]</c> — 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 <c>ACDREAM_PROBE_PHANTOM=1</c>.
/// </summary>
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;
/// <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 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>
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
/// <summary>
/// #119 tower-staircase decisive probe (2026-06-11). Comma-separated
/// Setup / GfxObj source ids (hex, optional 0x prefix) from
/// <c>ACDREAM_DUMP_ENTITY</c>. Any <c>WorldEntity</c> whose
/// <c>SourceGfxObjOrSetupId</c> is in this set emits:
/// (a) a <c>[dump-entity] HYDRATE</c> dump at MeshRef construction time
/// (<c>GameWindow.BuildInteriorEntitiesForStreaming</c>) — 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 <c>[dump-entity] DRAW</c> dump in <c>WbDrawDispatcher</c> 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 <c>[dump-entity] WALK-REJECT</c> lines when the
/// dispatcher's walk filters the entity out (absence-of-draw attribution).
/// Empty set = probe off; every call site early-outs on <c>Count == 0</c>.
/// </summary>
public static IReadOnlySet<uint> DumpEntitySourceIds { get; } =
ParseDumpEntityIds(Environment.GetEnvironmentVariable("ACDREAM_DUMP_ENTITY"));
/// <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>
/// </summary>
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
=> renderRootResolved && IsEnvCellId(playerCellId);
}