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:
parent
b77989c323
commit
bf53e2ad6e
82 changed files with 451 additions and 10108 deletions
|
|
@ -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=<M> cellLess=<K> registered=<R> capped=<R-M>
|
||||
/// byCell=[0x<id>:<count>,...]
|
||||
/// </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 ~1–2, 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 (0x0001–0x0040).
|
||||
/// </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->m_position.objcell_id & 0xFFFF) < 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; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue