diff --git a/src/AcDream.App/Rendering/FrameProfiler.cs b/src/AcDream.App/Rendering/FrameProfiler.cs
deleted file mode 100644
index ce246725..00000000
--- a/src/AcDream.App/Rendering/FrameProfiler.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-///
-/// THROWAWAY perf apparatus (2026-06-23) for the dense-town FPS investigation.
-/// Gated on ACDREAM_FPS_PROF=1. Answers the first fork — is the frame
-/// CPU-render-bound, GPU-bound, or wait/vsync-bound — by measuring three numbers
-/// per frame and printing their distribution every ~1 s:
-///
-///
-/// - wall: the real frame period the user sees (Silk's inter-frame
-/// delta). 1000/wall = the title-bar FPS.
-/// - cpuRender: wall-clock CPU time spent inside OnRender
-/// (the render-thread submission cost — walk, group-build, draw issue).
-/// - gpu: GPU time for the whole frame's GL work, via a
-/// TimeElapsed query bracketing the frame (ring of 3 for async readback,
-/// mirroring WbDrawDispatcher's #125-safe pattern).
-///
-///
-/// Interpretation: gpu≈wall ⇒ GPU-bound (then break down by pass); cpuRender≈wall
-/// & gpu≪ ⇒ CPU-render-bound (draw-call submission / batching / the walk);
-/// wall≫both ⇒ wait/vsync-bound (the 60→30 quantization cliff — confirm via the
-/// printed vsync state). Also prints live vsync + MSAA sample count.
-///
-/// Do NOT run alongside ACDREAM_WB_DIAG=1: WB-DIAG opens its own per-pass
-/// TimeElapsed queries and GL forbids nesting them inside this frame query.
-///
-internal sealed class FrameProfiler
-{
- private const int RingDepth = 3;
- private const int SampleCap = 512;
-
- private readonly GL _gl;
- private readonly uint[] _gpuQuery = new uint[RingDepth];
- private readonly bool[] _begun = new bool[RingDepth];
- private bool _queriesInit;
- private int _frame;
-
- private readonly Stopwatch _cpuSw = new();
- // Free-running clock to measure the OnUpdate→OnRender gap robustly (no
- // EndUpdate hook needed → survives any early-return in OnUpdate).
- private readonly Stopwatch _clock = Stopwatch.StartNew();
- private double _updateStartMs;
- private double _lastUpdateMs;
-
- private readonly double[] _wallMs = new double[SampleCap];
- private readonly double[] _cpuMs = new double[SampleCap];
- private readonly double[] _gpuMs = new double[SampleCap];
- private readonly double[] _updMs = new double[SampleCap];
- private readonly double[] _presMs = new double[SampleCap];
- private int _count;
- private double _lastGpuMs; // carried forward when a slot isn't ready yet
-
- private double _accumSeconds;
- private int _flushFrames = 1;
- private const double FlushSeconds = 1.0;
-
- private bool _vsync;
- private int _msaa;
-
- public FrameProfiler(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
-
- // THROWAWAY per-renderer GPU attribution (glFinish-bracketed, set true by
- // ACDREAM_FPS_PROF=1). Renderers drain+time their own draw and report here;
- // glFinish serializes CPU/GPU so the FRAME time inflates, but each renderer's
- // GPU cost is exact while the view is held still. Read+cleared each Flush.
- internal static readonly bool PassGpuEnabled =
- string.Equals(Environment.GetEnvironmentVariable("ACDREAM_FPS_PROF"), "1", StringComparison.Ordinal);
- private static readonly Dictionary _rendererGpu = new();
- private static readonly object _rgLock = new();
- internal static void AddRendererGpu(string name, double ms)
- {
- lock (_rgLock)
- {
- var cur = _rendererGpu.TryGetValue(name, out var v) ? v : (sum: 0.0, n: 0);
- _rendererGpu[name] = (cur.sum + ms, cur.n + 1);
- }
- }
-
- // THROWAWAY per-frame CPU sub-phase timing for DrawInside. Gated on
- // ACDREAM_FPS_PROF in {1,2}; run under =2 (no glFinish) so the DRAW phases
- // measure pure CPU submission cost, not GPU. Read+cleared each Flush.
- internal static readonly bool CpuPhaseEnabled =
- Environment.GetEnvironmentVariable("ACDREAM_FPS_PROF") is "1" or "2";
- private static readonly Dictionary _cpuPhase = new();
- private static readonly object _cpLock = new();
- internal static void AddCpuPhase(string name, double ms)
- {
- lock (_cpLock)
- {
- var cur = _cpuPhase.TryGetValue(name, out var v) ? v : (sum: 0.0, n: 0);
- _cpuPhase[name] = (cur.sum + ms, cur.n + 1);
- }
- }
-
- /// Call at the very top of OnUpdate. Stamps the start of the update phase.
- public void MarkUpdateStart() => _updateStartMs = _clock.Elapsed.TotalMilliseconds;
-
- /// Call at the very top of OnRender, before any GL work.
- public void BeginFrame()
- {
- if (!_queriesInit)
- {
- for (int i = 0; i < RingDepth; i++) _gpuQuery[i] = _gl.GenQuery();
- _queriesInit = true;
- }
- // OnUpdate ran immediately before this; its duration ≈ render-start − update-start.
- double nowMs = _clock.Elapsed.TotalMilliseconds;
- _lastUpdateMs = _updateStartMs > 0 ? nowMs - _updateStartMs : 0;
- _cpuSw.Restart();
- int slot = _frame % RingDepth;
- _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQuery[slot]);
- _begun[slot] = true;
- }
-
- /// Call at the very end of OnRender, after all GL work (incl. ImGui).
- public void EndFrame(double wallMs, bool vsync, int msaa, double deltaSeconds)
- {
- _gl.EndQuery(QueryTarget.TimeElapsed);
- _cpuSw.Stop();
- double cpuMs = _cpuSw.Elapsed.TotalMilliseconds;
- _vsync = vsync;
- _msaa = msaa;
-
- // Read the oldest ring slot (written RingDepth-1 frames ago, now ready).
- int readSlot = (_frame + 1) % RingDepth;
- if (_begun[readSlot])
- {
- _gl.GetQueryObject(_gpuQuery[readSlot], QueryObjectParameterName.ResultAvailable, out int avail);
- if (avail != 0)
- {
- _gl.GetQueryObject(_gpuQuery[readSlot], QueryObjectParameterName.Result, out ulong ns);
- _lastGpuMs = ns / 1_000_000.0;
- }
- }
-
- // Present/swap/GPU-tail wait = whatever the frame period isn't update or render CPU.
- double presentMs = Math.Max(0.0, wallMs - _lastUpdateMs - cpuMs);
-
- if (_count < SampleCap)
- {
- _wallMs[_count] = wallMs;
- _cpuMs[_count] = cpuMs;
- _gpuMs[_count] = _lastGpuMs;
- _updMs[_count] = _lastUpdateMs;
- _presMs[_count] = presentMs;
- _count++;
- }
-
- _frame++;
- _accumSeconds += deltaSeconds;
- if (_accumSeconds >= FlushSeconds)
- {
- Flush();
- _accumSeconds = 0;
- _count = 0;
- }
- }
-
- private void Flush()
- {
- if (_count == 0) return;
- _flushFrames = _count;
- var (wP50, wP95, wMin, wMax) = Stats(_wallMs, _count);
- var (cP50, cP95, _, cMax) = Stats(_cpuMs, _count);
- var (gP50, gP95, _, gMax) = Stats(_gpuMs, _count);
- var (uP50, uP95, _, uMax) = Stats(_updMs, _count);
- var (pP50, pP95, _, pMax) = Stats(_presMs, _count);
- double fpsP50 = wP50 > 0 ? 1000.0 / wP50 : 0;
- double fpsLow = wMax > 0 ? 1000.0 / wMax : 0; // worst frame → lowest fps
-
- Console.WriteLine(
- $"[FPS-PROF] n={_count} vsync={(_vsync ? "ON" : "off")} msaa={_msaa}x | "
- + $"wall={wP50:F1}/{wP95:F1}/{wMin:F1}/{wMax:F1} ms (fps {fpsP50:F0} p50, {fpsLow:F0} low)\n"
- + $" update={uP50:F1}/{uP95:F1} ms (max {uMax:F1}) | "
- + $"cpuRender={cP50:F1}/{cP95:F1} ms (max {cMax:F1}) | "
- + $"present(wait)={pP50:F1}/{pP95:F1} ms (max {pMax:F1}) | "
- + $"gpu={gP50:F1}/{gP95:F1} ms (max {gMax:F1})");
-
- lock (_rgLock)
- {
- if (_rendererGpu.Count > 0)
- {
- var parts = _rendererGpu
- .OrderByDescending(kv => kv.Value.sum)
- .Select(kv => $"{kv.Key}={kv.Value.sum / Math.Max(1, _flushFrames):F2}ms/frame (calls/frame={kv.Value.n / (double)Math.Max(1, _flushFrames):F1})");
- Console.WriteLine("[PASS-GPU] (glFinish, frame time inflated) " + string.Join(" ", parts));
- _rendererGpu.Clear();
- }
- }
-
- lock (_cpLock)
- {
- if (_cpuPhase.Count > 0)
- {
- var parts = _cpuPhase
- .OrderByDescending(kv => kv.Value.sum)
- .Select(kv => $"{kv.Key}={kv.Value.sum / Math.Max(1, _flushFrames):F2}ms/frame (calls/frame={kv.Value.n / (double)Math.Max(1, _flushFrames):F1})");
- Console.WriteLine("[CPU-PHASE] (DrawInside CPU ms/frame; =2 no-glFinish) " + string.Join(" ", parts));
- _cpuPhase.Clear();
- }
- }
- }
-
- private static (double p50, double p95, double min, double max) Stats(double[] src, int n)
- {
- var buf = new double[n];
- Array.Copy(src, buf, n);
- Array.Sort(buf);
- double p50 = buf[(int)(n * 0.50)];
- double p95 = buf[Math.Min(n - 1, (int)(n * 0.95))];
- return (p50, p95, buf[0], buf[n - 1]);
- }
-}
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 6d599b95..8910e919 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -62,20 +62,6 @@ public sealed class GameWindow : IDisposable
private double _lastFps = 60.0;
private double _lastFrameMs = 16.7;
- // THROWAWAY FPS-investigation apparatus (2026-06-23):
- // frame-level CPU/GPU/wall profiler to find the dense-town bottleneck. See
- // FrameProfiler. Lazy-constructed on first OnRender once _gl is live.
- // ACDREAM_FPS_PROF=1 — whole-frame query + per-pass glFinish brackets
- // ([PASS-GPU]); the glFinish SERIALIZES the pipe so cpuRender/gpu/wall
- // collapse together — relative per-pass attribution only.
- // ACDREAM_FPS_PROF=2 — whole-frame TimeElapsed query ONLY, NO per-pass
- // glFinish (PassGpuEnabled stays "1"-gated). This is the honest CPU-vs-GPU
- // split: gpu≈wall+present>0 ⇒ GPU-bound; cpuRender≈wall+present≈0 ⇒ CPU-bound.
- private readonly bool _fpsProf =
- Environment.GetEnvironmentVariable("ACDREAM_FPS_PROF") is "1" or "2";
- private FrameProfiler? _frameProfiler;
- private int _msaaSamples;
-
// Phase I.2: per-frame counters surfaced through the ImGui DebugPanel
// VM closures. Computed once per render pass alongside the frustum
// walk + nearest-object scan; the VM closures just read the cached
@@ -990,8 +976,6 @@ public sealed class GameWindow : IDisposable
var startupDisplay = startupStore.LoadDisplay();
var startupBase = AcDream.UI.Abstractions.Settings.QualitySettings.From(startupDisplay.Quality);
var startupQuality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(startupBase);
- _msaaSamples = startupQuality.MsaaSamples; // ACDREAM_FPS_PROF apparatus
-
var options = WindowOptions.Default with
{
Size = new Vector2D(1280, 720),
@@ -7628,11 +7612,6 @@ public sealed class GameWindow : IDisposable
private void OnUpdate(double dt)
{
- // THROWAWAY FPS apparatus (ACDREAM_FPS_PROF=1): stamp the start of the
- // update phase so the profiler can split frame time into update vs render
- // vs present. Robust to early-returns below (no matching EndUpdate needed).
- if (_fpsProf) _frameProfiler?.MarkUpdateStart();
-
// [FRAME-DIAG]: applies run later this frame inside _streamingController.Tick;
// flush the PREVIOUS OnUpdate's accumulated apply cost here (a clean per-frame
// boundary, independent of where in OnUpdate the applies landed) and reset.
@@ -8367,14 +8346,6 @@ public sealed class GameWindow : IDisposable
private void OnRender(double deltaSeconds)
{
- // THROWAWAY FPS apparatus (ACDREAM_FPS_PROF=1): bracket the whole frame's
- // CPU (this method) + GPU (TimeElapsed query) work. Begin before any GL.
- if (_fpsProf && _gl is not null)
- {
- _frameProfiler ??= new FrameProfiler(_gl);
- _frameProfiler.BeginFrame();
- }
-
// Phase G.1: set the clear color from the current sky's fog
// tint so the horizon band continues naturally past the
// rendered geometry. Fog blends to this color at max distance
@@ -9427,11 +9398,6 @@ public sealed class GameWindow : IDisposable
_imguiBootstrap.Render();
}
- // THROWAWAY FPS apparatus (ACDREAM_FPS_PROF=1): close the frame's CPU+GPU
- // brackets after ALL GL work (incl. ImGui). wall = the real frame period.
- if (_fpsProf && _frameProfiler is not null)
- _frameProfiler.EndFrame(deltaSeconds * 1000.0, _window!.VSync, _msaaSamples, deltaSeconds);
-
// Update the window title with performance stats every ~0.5s.
_perfAccum += deltaSeconds;
_perfFrameCount++;
@@ -10798,15 +10764,9 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene);
EnableClipDistances();
- // THROWAWAY FPS apparatus: glFinish-bracket the PER-SLICE terrain draw so
- // [PASS-GPU] reports terrain ms/frame + calls/frame (= slice count). This
- // is the suspected multiplier: terrain is redrawn fully once per slice.
- System.Diagnostics.Stopwatch? _terrGpuSw = null;
- if (FrameProfiler.PassGpuEnabled) { _gl!.Finish(); _terrGpuSw = System.Diagnostics.Stopwatch.StartNew(); }
_terrainCpuStopwatch.Restart();
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
_terrainCpuStopwatch.Stop();
- if (_terrGpuSw is not null) { _gl!.Finish(); FrameProfiler.AddRendererGpu("terrain", _terrGpuSw.Elapsed.TotalMilliseconds); }
_terrainCpuSamples[_terrainCpuSampleCursor] =
(long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds * 100.0);
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs
index c5f5f453..e47fc338 100644
--- a/src/AcDream.App/Rendering/ParticleRenderer.cs
+++ b/src/AcDream.App/Rendering/ParticleRenderer.cs
@@ -123,11 +123,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
ParticleRenderPass renderPass = ParticleRenderPass.Scene,
Func? emitterFilter = null)
{
- // THROWAWAY FPS apparatus: glFinish-bracket particle GPU (ACDREAM_FPS_PROF).
- System.Diagnostics.Stopwatch? _pSw = null;
- if (AcDream.App.Rendering.FrameProfiler.PassGpuEnabled) { _gl.Finish(); _pSw = System.Diagnostics.Stopwatch.StartNew(); }
- try
- {
if (particles is null || camera is null)
return;
@@ -173,15 +168,6 @@ public sealed unsafe class ParticleRenderer : IDisposable
_gl.BindVertexArray(0);
_gl.DepthMask(true);
_gl.Disable(EnableCap.Blend);
- }
- finally
- {
- if (_pSw is not null)
- {
- _gl.Finish();
- AcDream.App.Rendering.FrameProfiler.AddRendererGpu("particles", _pSw.Elapsed.TotalMilliseconds);
- }
- }
}
private List BuildDrawList(
diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
index 79a48d2c..4f0a6436 100644
--- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
+++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
@@ -208,12 +208,6 @@ void main() { } // depth-only: color writes are masked off by the caller state
ReadOnlySpan planes,
bool forceFarZ)
{
- // THROWAWAY FPS apparatus: glFinish-bracket each punch/seal fan (accumulated
- // into "punchseal") under ACDREAM_FPS_PROF.
- System.Diagnostics.Stopwatch? _fSw = null;
- if (AcDream.App.Rendering.FrameProfiler.PassGpuEnabled) { _gl.Finish(); _fSw = System.Diagnostics.Stopwatch.StartNew(); }
- try
- {
if (worldVerts.Length < 3)
return;
int n = Math.Min(worldVerts.Length, MaxFanVerts);
@@ -311,15 +305,6 @@ void main() { } // depth-only: color writes are masked off by the caller state
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.UseProgram(0);
- }
- finally
- {
- if (_fSw is not null)
- {
- _gl.Finish();
- AcDream.App.Rendering.FrameProfiler.AddRendererGpu("punchseal", _fSw.Elapsed.TotalMilliseconds);
- }
- }
}
public void Dispose()
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 0038c896..2819c3c5 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -65,20 +65,6 @@ public sealed class RetailPViewRenderer
{
ArgumentNullException.ThrowIfNull(ctx);
- // THROWAWAY CPU sub-phase timing (ACDREAM_FPS_PROF=2 clean-split): attribute
- // which CPU phase of DrawInside eats the dense-town frame. Strip with the FPS
- // apparatus (plan Task 5). Zero-alloc: GetTimestamp/GetElapsedTime + a direct-
- // call local fn (struct closure over _ts, never a delegate).
- long _ts = System.Diagnostics.Stopwatch.GetTimestamp();
- FrameProfiler.MarkGpu("_start"); // GPU timestamp baseline (=2; CPU-only phases diff ~0)
- void Phase(string n)
- {
- if (FrameProfiler.CpuPhaseEnabled)
- FrameProfiler.AddCpuPhase(n, System.Diagnostics.Stopwatch.GetElapsedTime(_ts).TotalMilliseconds);
- FrameProfiler.MarkGpu(n);
- _ts = System.Diagnostics.Stopwatch.GetTimestamp();
- }
-
var pvFrame = PortalVisibilityBuilder.Build(
ctx.RootCell,
ctx.ViewerEyePos,
@@ -115,11 +101,9 @@ public sealed class RetailPViewRenderer
&& ctx.NearbyBuildingCells is not null
&& pvFrame.OutsideView.Polygons.Count > 0)
BuildInteriorRootLookIns(ctx, pvFrame);
- Phase("flood");
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
UploadClipFrame(ctx.SetTerrainClipUbo);
- Phase("assemble");
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
@@ -152,7 +136,6 @@ public sealed class RetailPViewRenderer
centerLbX: ctx.RenderCenterLbX,
centerLbY: ctx.RenderCenterLbY,
renderRadius: ctx.RenderRadius);
- Phase("prepare");
var partition = InteriorEntityPartition.Partition(prepareCells, ctx.LandblockEntries);
var result = new RetailPViewFrameResult
@@ -206,18 +189,12 @@ public sealed class RetailPViewRenderer
}
}
- Phase("partition");
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
- Phase("landscape");
UseIndoorMembershipOnlyRouting();
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
- Phase("portalmask");
DrawEnvCellShells(pvFrame);
- Phase("shells");
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
- Phase("cellobjects");
DrawDynamicsLast(ctx, partition, viewcone, ctx.RootCell.IsOutdoorNode);
- Phase("dynamics");
return result;
}
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
index f7b13aa4..0a65655b 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
@@ -836,18 +836,6 @@ public sealed unsafe class EnvCellRenderer : IDisposable
///
public void Render(WbRenderPass renderPass, HashSet? filter)
{
- // THROWAWAY FPS apparatus (ACDREAM_FPS_PROF=1): glFinish-bracket this cell
- // draw so the profiler can attribute its exact GPU cost. Serializes the
- // pipeline (inflates frame time) but isolates cell-draw GPU at a held view.
- System.Diagnostics.Stopwatch? _passSw = null;
- if (AcDream.App.Rendering.FrameProfiler.PassGpuEnabled)
- {
- _gl.Finish();
- _passSw = System.Diagnostics.Stopwatch.StartNew();
- }
- try
- {
-
// WB EnvCellRenderManager.cs:400:
if (!_initialized || _shader is null || _shader.Program == 0) return;
@@ -1042,15 +1030,6 @@ public sealed unsafe class EnvCellRenderer : IDisposable
System.Console.WriteLine(sb.ToString());
}
}
- }
- finally
- {
- if (_passSw is not null)
- {
- _gl.Finish();
- AcDream.App.Rendering.FrameProfiler.AddRendererGpu("cells", _passSw.Elapsed.TotalMilliseconds);
- }
- }
}
///
diff --git a/tests/AcDream.Core.Tests/Conformance/DegradeCoverageProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/DegradeCoverageProbeTests.cs
deleted file mode 100644
index 105de5da..00000000
--- a/tests/AcDream.Core.Tests/Conformance/DegradeCoverageProbeTests.cs
+++ /dev/null
@@ -1,237 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using AcDream.Core.World;
-using DatReaderWriter;
-using DatReaderWriter.Options;
-using DatReaderWriter.Enums;
-using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
-using DatGfxObjDegradeInfo = DatReaderWriter.DBObjs.GfxObjDegradeInfo;
-using DatSetup = DatReaderWriter.DBObjs.Setup;
-using DatRegion = DatReaderWriter.DBObjs.Region;
-using DatLandBlock = DatReaderWriter.DBObjs.LandBlock;
-using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace AcDream.Core.Tests.Conformance;
-
-///
-/// DISTANCE-DEGRADE design probe (2026-06-23, throwaway). Answers the single
-/// load-bearing question for the FPS distance-degrade port: do the GfxObjs that
-/// a dense town actually DRAWS (procedural scenery + placed stabs) even carry a
-/// DIDDegrade table, and if so, at what distance does retail HIDE them
-/// (last non-zero slot's MaxDist, since past that the table degrades to
-/// gfxobj id 0)? This decides whether the recommended "hide-only" phase 1 is the
-/// real FPS win for scenery or a no-op. Output-only — no assertions. Delete after
-/// the design lands. Run with the real client dats present (ACDREAM_DAT_DIR).
-///
-public sealed class DegradeCoverageProbeTests
-{
- private readonly ITestOutputHelper _out;
- public DegradeCoverageProbeTests(ITestOutputHelper output) => _out = output;
-
- private const uint RegionDatId = 0x13000000u;
- private const float Inf = float.PositiveInfinity;
-
- // Per-GfxObj degrade summary.
- private readonly record struct GfxDeg(bool HasTable, bool DegradesToHidden, float HideDist, int NumSlots, float Slot0Max);
-
- [Fact]
- public void Probe_DenseTown_Holtburg_DegradeCoverage()
- {
- var datDir = ConformanceDats.ResolveDatDir();
- if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
- using var dats = new DatCollection(datDir, DatAccessType.Read);
-
- var region = dats.Get(RegionDatId);
- if (region is null) { _out.WriteLine("SKIP: Region 0x13000000 missing"); return; }
-
- // 3x3 landblock neighbourhood centred on Holtburg 0xA9B4 — approximates the
- // scenery volume that gets drawn when facing the town.
- byte[] xs = { 0xA8, 0xA9, 0xAA };
- byte[] ys = { 0xB3, 0xB4, 0xB5 };
-
- // GfxObj-level degrade cache.
- var gfxCache = new Dictionary();
- GfxDeg DegOf(uint gfxId)
- {
- if (gfxCache.TryGetValue(gfxId, out var c)) return c;
- var g = dats.Get(gfxId);
- GfxDeg r;
- if (g is null || !g.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || g.DIDDegrade == 0)
- r = new GfxDeg(false, false, Inf, 0, Inf);
- else
- {
- var info = dats.Get(g.DIDDegrade);
- if (info is null || info.Degrades.Count == 0)
- r = new GfxDeg(false, false, Inf, 0, Inf);
- else
- {
- int n = info.Degrades.Count;
- int hideSlot = -1;
- for (int i = 0; i < n; i++)
- if ((uint)info.Degrades[i].Id == 0u) { hideSlot = i; break; }
- float slot0Max = info.Degrades[0].MaxDist;
- if (hideSlot < 0)
- r = new GfxDeg(true, false, Inf, n, slot0Max); // LOD-only, never hides
- else if (hideSlot == 0)
- r = new GfxDeg(true, true, 0f, n, slot0Max); // editor-only marker
- else
- r = new GfxDeg(true, true, info.Degrades[hideSlot - 1].MaxDist, n, slot0Max);
- }
- }
- gfxCache[gfxId] = r;
- return r;
- }
-
- // Resolve an object id (Setup 0x02 or GfxObj 0x01) to its part GfxObj ids.
- var partsCache = new Dictionary>();
- List PartsOf(uint objId)
- {
- if (partsCache.TryGetValue(objId, out var c)) return c;
- var list = new List();
- if ((objId >> 24) == 0x02u)
- {
- var s = dats.Get(objId);
- if (s is not null) foreach (var p in s.Parts) list.Add((uint)p);
- }
- else if ((objId >> 24) == 0x01u)
- list.Add(objId);
- partsCache[objId] = list;
- return list;
- }
-
- // Per-entity (object) hide distance = MAX over parts' hide dist (entity is
- // fully hidden only when EVERY part has degraded to nothing). A part with no
- // table / no hidden slot contributes +inf → entity never fully hides.
- var entityHideCache = new Dictionary();
- (float hideDist, bool anyTable) EntityHide(uint objId)
- {
- if (entityHideCache.TryGetValue(objId, out var c)) return c;
- var parts = PartsOf(objId);
- float maxHide = 0f; bool anyTable = false; bool sawNonHiding = false;
- if (parts.Count == 0) { var e0 = (Inf, false); entityHideCache[objId] = e0; return e0; }
- foreach (var pid in parts)
- {
- var d = DegOf(pid);
- if (d.HasTable) anyTable = true;
- if (d.DegradesToHidden) maxHide = MathF.Max(maxHide, d.HideDist);
- else sawNonHiding = true;
- }
- float hide = sawNonHiding ? Inf : maxHide;
- var e = (hide, anyTable);
- entityHideCache[objId] = e;
- return e;
- }
-
- // Tally scenery placements (weighted by count) + stabs.
- var sceneryCount = new Dictionary();
- var stabCount = new Dictionary();
- int lbWithTerrain = 0;
-
- foreach (var x in xs)
- foreach (var y in ys)
- {
- uint lbId = ((uint)x << 24) | ((uint)y << 16);
- var block = dats.Get(lbId | 0xFFFFu);
- if (block is null || block.Terrain is null || block.Terrain.Length == 0) continue;
- lbWithTerrain++;
-
- var spawns = SceneryGenerator.Generate(dats, region, block, lbId);
- foreach (var sp in spawns)
- sceneryCount[sp.ObjectId] = sceneryCount.GetValueOrDefault(sp.ObjectId) + 1;
-
- var info = dats.Get(lbId | 0xFFFEu);
- if (info?.Objects is not null)
- foreach (var stab in info.Objects)
- stabCount[stab.Id] = stabCount.GetValueOrDefault(stab.Id) + 1;
- }
-
- _out.WriteLine($"=== Dense-town degrade coverage (3x3 around Holtburg 0xA9B4, {lbWithTerrain}/9 lb w/ terrain) ===");
-
- Summarize("PROCEDURAL SCENERY (the bulk)", sceneryCount, EntityHide);
- Summarize("PLACED STABS (LandBlockInfo.Objects)", stabCount, EntityHide);
-
- // GfxObj-level coverage across every unique part seen.
- var allObjIds = sceneryCount.Keys.Concat(stabCount.Keys).Distinct();
- var allGfx = allObjIds.SelectMany(PartsOf).Distinct().ToList();
- int gfxWithTable = allGfx.Count(g => DegOf(g).HasTable);
- int gfxHides = allGfx.Count(g => DegOf(g).DegradesToHidden);
- _out.WriteLine($"\n[GfxObj-level] unique part GfxObjs={allGfx.Count} withDegradeTable={gfxWithTable} ({Pct(gfxWithTable, allGfx.Count)}) degradesToHidden={gfxHides} ({Pct(gfxHides, allGfx.Count)})");
-
- // Sample the top scenery objects with their tables.
- _out.WriteLine("\n[Top scenery objects by placement count]");
- foreach (var kv in sceneryCount.OrderByDescending(k => k.Value).Take(20))
- {
- var (hide, anyTable) = EntityHide(kv.Key);
- string hideStr = float.IsPositiveInfinity(hide) ? "NEVER" : $"{hide:F0}m";
- var parts = PartsOf(kv.Key);
- string slots = parts.Count > 0
- ? string.Join(",", parts.Take(3).Select(p => { var d = DegOf(p); return d.HasTable ? $"{d.NumSlots}sl/hide={(float.IsPositiveInfinity(d.HideDist) ? "inf" : d.HideDist.ToString("F0"))}" : "noTable"; }))
- : "(unresolved)";
- _out.WriteLine($" 0x{kv.Key:X8} x{kv.Value,-5} parts={parts.Count} hideDist={hideStr,-7} [{slots}]");
- }
-
- // LOD ladder with poly counts for the top 6 scenery objects: quantifies the
- // per-slot triangle savings (the LOD-degrade win) and at what distances the
- // swaps land.
- int PolyCount(uint gfxId)
- {
- var g = dats.Get(gfxId);
- return g?.Polygons?.Count ?? -1;
- }
- _out.WriteLine("\n[LOD ladder — top scenery objects, per part: slot=(Id polys min/ideal/max mode)]");
- foreach (var kv in sceneryCount.OrderByDescending(k => k.Value).Take(6))
- {
- _out.WriteLine($" OBJ 0x{kv.Key:X8} (x{kv.Value}):");
- foreach (var pid in PartsOf(kv.Key).Distinct())
- {
- var g = dats.Get(pid);
- int basePolys = g?.Polygons?.Count ?? -1;
- if (g is null || !g.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || g.DIDDegrade == 0)
- { _out.WriteLine($" part 0x{pid:X8} polys={basePolys} : NO TABLE (always full detail)"); continue; }
- var info = dats.Get(g.DIDDegrade);
- if (info is null || info.Degrades.Count == 0)
- { _out.WriteLine($" part 0x{pid:X8} polys={basePolys} : empty table"); continue; }
- var ladder = string.Join(" ", info.Degrades.Select((d, i) =>
- {
- uint sid = (uint)d.Id;
- int p = sid == 0 ? 0 : PolyCount(sid);
- return $"[{i}]={(sid == 0 ? "HIDE" : $"0x{sid:X8}/{p}p")} {d.MinDist:F0}/{d.IdealDist:F0}/{d.MaxDist:F0} m{d.DegradeMode}";
- }));
- _out.WriteLine($" part 0x{pid:X8} base{basePolys}p : {ladder}");
- }
- }
- }
-
- private void Summarize(string label, Dictionary counts, Func entityHide)
- {
- int total = counts.Values.Sum();
- if (total == 0) { _out.WriteLine($"\n[{label}] no placements"); return; }
-
- int placementsWithTable = 0, placementsHide = 0;
- var hideBuckets = new int[] { 0, 0, 0, 0, 0, 0 }; // <25, <50, <75, <100, <150, >=150
- int hideTotalPlacements = 0;
- foreach (var (id, cnt) in counts)
- {
- var (hide, anyTable) = entityHide(id);
- if (anyTable) placementsWithTable += cnt;
- if (!float.IsPositiveInfinity(hide))
- {
- placementsHide += cnt;
- hideTotalPlacements += cnt;
- int b = hide < 25 ? 0 : hide < 50 ? 1 : hide < 75 ? 2 : hide < 100 ? 3 : hide < 150 ? 4 : 5;
- hideBuckets[b] += cnt;
- }
- }
-
- _out.WriteLine($"\n[{label}] uniqueObjs={counts.Count} totalPlacements={total}");
- _out.WriteLine($" placements whose obj has a degrade table : {placementsWithTable} ({Pct(placementsWithTable, total)})");
- _out.WriteLine($" placements that HIDE at finite distance : {placementsHide} ({Pct(placementsHide, total)})");
- if (hideTotalPlacements > 0)
- _out.WriteLine($" hide-distance histogram (placements): <25m={hideBuckets[0]} <50m={hideBuckets[1]} <75m={hideBuckets[2]} <100m={hideBuckets[3]} <150m={hideBuckets[4]} >=150m={hideBuckets[5]}");
- }
-
- private static string Pct(int n, int d) => d == 0 ? "n/a" : $"{100.0 * n / d:F1}%";
-}