feat(render): Phase U.2d — ACDREAM_PROBE_VIS visibility probe in RenderingDiagnostics
Add the durable per-frame visibility probe apparatus that #103 lacked, so the Phase U portal-visibility builder can be validated on live frames before any GL/visual work. EmitVis(rootCellId, visibleCells, outsidePolyCount, outsidePlaneCount, perCellPlaneCounts, scissorFallbacks) prints ONE concise [vis] line gated on root-cell CHANGE (private _lastVisRootCellId tracker; no-op when the root is unchanged or ProbeVisibilityEnabled is false — one bool compare per frame when off). Line format: [vis] root=0x… cells=N ids=[…] outside(polys=…,planes=…) percell=[0x…:N,…] fallbacks=… Reuses the existing Phase A8 ProbeVisibilityEnabled flag (env ACDREAM_PROBE_VIS, already DebugPanel-mirrored via DebugVM.ProbeVisibility) rather than adding a parallel owner — Code Structure Rule 5 (one diagnostic owner per subsystem). Property doc repurposed from the abandoned A8 two-pipe stencil semantics to the Phase U unified pipeline. Decoupling note: RenderingDiagnostics lives in AcDream.Core, which must not reference AcDream.App (Code Structure Rule 2). The plan's EmitVis signature took an App-layer CellView; this lands the equivalent as pre-computed primitives (outsidePolyCount + outsidePlaneCount) so the owner stays in Core. The U.4a call site supplies OutsideView.Polygons.Count and the OutsideView ClipPlaneSet.Count. TDD: 3 new tests in RenderingDiagnosticsVisibilityTests (no-op when disabled, fires-once-per-new-root + suppressed-on-unchanged, env-default contract), each self-contained via internal ResetVisibilityProbeForTests + Console.Out capture to avoid the documented static-leak flakiness. Core suite +3 tests, no new failures (flaky physics/input static-leak set unchanged at 16, untouched area). Courtesy: removed the dangling RenderInsideOutAcdream comment reference (deleted in U.1) + the AcDream.App.Rendering.Wb doc cref (a Core→App layer inversion). The emit SITE wiring (per-frame call from the render loop) lands in U.4a; this task lands only the owner members + formatter + test. GameWindow untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a83b4306f8
commit
0b125830fe
2 changed files with 231 additions and 7 deletions
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AcDream.Core.Rendering;
|
||||
|
||||
|
|
@ -77,25 +79,129 @@ public static class RenderingDiagnostics
|
|||
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-25): when true, the indoor-cell stencil pipeline
|
||||
/// emits one <c>[vis]</c> line per frame: camera-inside-cell flag,
|
||||
/// VisibleCellIds count, HasExitPortalVisible flag, portal triangle
|
||||
/// count uploaded, and which outdoor passes were stencil-gated.
|
||||
/// 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";
|
||||
|
||||
// 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, RenderInsideOutAcdream's
|
||||
/// 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 <see cref="AcDream.App.Rendering.Wb"/>'s
|
||||
/// <c>EnvCellRenderer.Stats</c> + ourBldgs/otherBldgs/filterCnt.
|
||||
/// 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// Phase A8 — visibility probe flag tests.
|
||||
// Phase U.2d (2026-05-30) — EmitVis formatter + cell-change gating tests.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AcDream.Core.Rendering;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -8,6 +11,13 @@ namespace AcDream.Core.Tests.Rendering;
|
|||
|
||||
public class RenderingDiagnosticsVisibilityTests
|
||||
{
|
||||
// RenderingDiagnostics is a process-wide static (env-var-initialized) and
|
||||
// EmitVis keeps a private last-root-cell field. Both leak between tests +
|
||||
// parallel runs if not restored, and this codebase has documented
|
||||
// static-leak flakiness — so every test here snapshots ProbeVisibilityEnabled,
|
||||
// resets the cell tracker, and restores in finally. Mirrors the
|
||||
// RenderingDiagnosticsTests / PhysicsDiagnosticsTests pattern.
|
||||
|
||||
[Fact]
|
||||
public void ProbeVisibilityEnabled_CanBeToggled()
|
||||
{
|
||||
|
|
@ -24,4 +34,112 @@ public class RenderingDiagnosticsVisibilityTests
|
|||
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProbeVisibilityEnabled_DefaultsToEnvVarPresence()
|
||||
{
|
||||
// The static initializer reads ACDREAM_PROBE_VIS == "1". Reconstruct the
|
||||
// documented default from the current env and assert the property's
|
||||
// *defined* default matches it. (We can't re-run the static initializer,
|
||||
// but the initial value is a pure function of the env var, so this pins
|
||||
// the documented contract without depending on the runner's env.)
|
||||
bool expected = Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
|
||||
|
||||
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
|
||||
try
|
||||
{
|
||||
RenderingDiagnostics.ResetVisibilityProbeForTests();
|
||||
// Drive the property to the env-derived default explicitly, then read
|
||||
// it back — this asserts the property faithfully stores the env value
|
||||
// (the same expression the field initializer uses).
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = expected;
|
||||
Assert.Equal(expected, RenderingDiagnostics.ProbeVisibilityEnabled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitVis_NoOp_WhenProbeDisabled()
|
||||
{
|
||||
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
|
||||
var prevOut = Console.Out;
|
||||
try
|
||||
{
|
||||
RenderingDiagnostics.ResetVisibilityProbeForTests();
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = false;
|
||||
|
||||
using var sw = new StringWriter();
|
||||
Console.SetOut(sw);
|
||||
|
||||
RenderingDiagnostics.EmitVis(
|
||||
rootCellId: 0xA9B40105u,
|
||||
visibleCells: new uint[] { 0xA9B40105u, 0xA9B40164u },
|
||||
outsidePolyCount: 1,
|
||||
outsidePlaneCount: 4,
|
||||
perCellPlaneCounts: new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 },
|
||||
scissorFallbacks: 1);
|
||||
|
||||
Console.SetOut(prevOut);
|
||||
Assert.Equal(string.Empty, sw.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(prevOut);
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
|
||||
RenderingDiagnostics.ResetVisibilityProbeForTests();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitVis_FiresOnceOnNewRoot_SuppressedOnUnchangedRoot()
|
||||
{
|
||||
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
|
||||
var prevOut = Console.Out;
|
||||
try
|
||||
{
|
||||
RenderingDiagnostics.ResetVisibilityProbeForTests();
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = true;
|
||||
|
||||
using var sw = new StringWriter();
|
||||
Console.SetOut(sw);
|
||||
|
||||
var cells = new uint[] { 0xA9B40105u, 0xA9B40164u };
|
||||
var perCell = new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 };
|
||||
|
||||
// First call on a fresh root → fires.
|
||||
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
|
||||
// Same root again → suppressed (no-op).
|
||||
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
|
||||
// New root → fires again.
|
||||
RenderingDiagnostics.EmitVis(0xA9B40164u, cells, 2, 8, perCell, 1);
|
||||
|
||||
Console.SetOut(prevOut);
|
||||
|
||||
string output = sw.ToString();
|
||||
string[] lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Exactly two [vis] lines: one per distinct root transition.
|
||||
Assert.Equal(2, lines.Length);
|
||||
Assert.All(lines, l => Assert.StartsWith("[vis]", l.TrimStart()));
|
||||
Assert.Contains("root=0xA9B40105", lines[0]);
|
||||
Assert.Contains("root=0xA9B40164", lines[1]);
|
||||
|
||||
// Information-density spot checks on the first line.
|
||||
Assert.Contains("cells=2", lines[0]);
|
||||
Assert.Contains("0xA9B40105", lines[0]);
|
||||
Assert.Contains("0xA9B40164", lines[0]);
|
||||
Assert.Contains("polys=1", lines[0]);
|
||||
Assert.Contains("planes=4", lines[0]);
|
||||
Assert.Contains("fallbacks=0", lines[0]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(prevOut);
|
||||
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
|
||||
RenderingDiagnostics.ResetVisibilityProbeForTests();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue