chore(diag): FPS_PROF=2 clean split + [CPU-PHASE] DrawInside timers [throwaway]
Decouple the whole-frame TimeElapsed query from the per-pass glFinish so the CPU-vs-GPU split is honest: ACDREAM_FPS_PROF=2 runs the frame query with NO per-pass glFinish (PassGpuEnabled stays "1"-gated). Plus [CPU-PHASE] timers around each DrawInside phase (flood/assemble/prepare/partition/landscape/ portalmask/shells/cellobjects/dynamics) — the CPU analog of [PASS-GPU]. This is what proved the dense town is ~96% CPU-bound (GPU=0.5ms) and that the cost is per-cell draw submission (cellobjects ~3.5ms), not the portal floods / punch-seal / clip allocs the static analysis had guessed. Throwaway; strip with the rest of the FPS apparatus (plan Task 5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1473e4dbf9
commit
fe1f81371a
3 changed files with 57 additions and 2 deletions
|
|
@ -82,6 +82,22 @@ internal sealed class FrameProfiler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<string, (double sum, int n)> _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Call at the very top of OnUpdate. Stamps the start of the update phase.</summary>
|
/// <summary>Call at the very top of OnUpdate. Stamps the start of the update phase.</summary>
|
||||||
public void MarkUpdateStart() => _updateStartMs = _clock.Elapsed.TotalMilliseconds;
|
public void MarkUpdateStart() => _updateStartMs = _clock.Elapsed.TotalMilliseconds;
|
||||||
|
|
||||||
|
|
@ -177,6 +193,18 @@ internal sealed class FrameProfiler
|
||||||
_rendererGpu.Clear();
|
_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)
|
private static (double p50, double p95, double min, double max) Stats(double[] src, int n)
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,17 @@ public sealed class GameWindow : IDisposable
|
||||||
private double _lastFps = 60.0;
|
private double _lastFps = 60.0;
|
||||||
private double _lastFrameMs = 16.7;
|
private double _lastFrameMs = 16.7;
|
||||||
|
|
||||||
// THROWAWAY FPS-investigation apparatus (2026-06-23, ACDREAM_FPS_PROF=1):
|
// THROWAWAY FPS-investigation apparatus (2026-06-23):
|
||||||
// frame-level CPU/GPU/wall profiler to find the dense-town bottleneck. See
|
// frame-level CPU/GPU/wall profiler to find the dense-town bottleneck. See
|
||||||
// FrameProfiler. Lazy-constructed on first OnRender once _gl is live.
|
// 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 =
|
private readonly bool _fpsProf =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_FPS_PROF"), "1", StringComparison.Ordinal);
|
Environment.GetEnvironmentVariable("ACDREAM_FPS_PROF") is "1" or "2";
|
||||||
private FrameProfiler? _frameProfiler;
|
private FrameProfiler? _frameProfiler;
|
||||||
private int _msaaSamples;
|
private int _msaaSamples;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,18 @@ public sealed class RetailPViewRenderer
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(ctx);
|
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();
|
||||||
|
void Phase(string n)
|
||||||
|
{
|
||||||
|
if (FrameProfiler.CpuPhaseEnabled)
|
||||||
|
FrameProfiler.AddCpuPhase(n, System.Diagnostics.Stopwatch.GetElapsedTime(_ts).TotalMilliseconds);
|
||||||
|
_ts = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
var pvFrame = PortalVisibilityBuilder.Build(
|
var pvFrame = PortalVisibilityBuilder.Build(
|
||||||
ctx.RootCell,
|
ctx.RootCell,
|
||||||
ctx.ViewerEyePos,
|
ctx.ViewerEyePos,
|
||||||
|
|
@ -101,9 +113,11 @@ public sealed class RetailPViewRenderer
|
||||||
&& ctx.NearbyBuildingCells is not null
|
&& ctx.NearbyBuildingCells is not null
|
||||||
&& pvFrame.OutsideView.Polygons.Count > 0)
|
&& pvFrame.OutsideView.Polygons.Count > 0)
|
||||||
BuildInteriorRootLookIns(ctx, pvFrame);
|
BuildInteriorRootLookIns(ctx, pvFrame);
|
||||||
|
Phase("flood");
|
||||||
|
|
||||||
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
||||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||||
|
Phase("assemble");
|
||||||
|
|
||||||
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
// 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,
|
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
||||||
|
|
@ -136,6 +150,7 @@ public sealed class RetailPViewRenderer
|
||||||
centerLbX: ctx.RenderCenterLbX,
|
centerLbX: ctx.RenderCenterLbX,
|
||||||
centerLbY: ctx.RenderCenterLbY,
|
centerLbY: ctx.RenderCenterLbY,
|
||||||
renderRadius: ctx.RenderRadius);
|
renderRadius: ctx.RenderRadius);
|
||||||
|
Phase("prepare");
|
||||||
|
|
||||||
var partition = InteriorEntityPartition.Partition(prepareCells, ctx.LandblockEntries);
|
var partition = InteriorEntityPartition.Partition(prepareCells, ctx.LandblockEntries);
|
||||||
var result = new RetailPViewFrameResult
|
var result = new RetailPViewFrameResult
|
||||||
|
|
@ -189,12 +204,18 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Phase("partition");
|
||||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
|
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
|
||||||
|
Phase("landscape");
|
||||||
UseIndoorMembershipOnlyRouting();
|
UseIndoorMembershipOnlyRouting();
|
||||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||||
|
Phase("portalmask");
|
||||||
DrawEnvCellShells(pvFrame);
|
DrawEnvCellShells(pvFrame);
|
||||||
|
Phase("shells");
|
||||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||||
|
Phase("cellobjects");
|
||||||
DrawDynamicsLast(ctx, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
DrawDynamicsLast(ctx, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||||
|
Phase("dynamics");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue