refactor(render): extract frame presentation diagnostics
This commit is contained in:
parent
7e4cfb37c3
commit
733126a272
20 changed files with 3767 additions and 1021 deletions
269
src/AcDream.App/Rendering/RenderFrameDiagnosticsController.cs
Normal file
269
src/AcDream.App/Rendering/RenderFrameDiagnosticsController.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>Current values published by the successful render-frame cadence.</summary>
|
||||
internal readonly record struct RenderFrameDiagnosticsSnapshot(
|
||||
double Fps,
|
||||
double FrameMilliseconds,
|
||||
int VisibleLandblocks,
|
||||
int TotalLandblocks,
|
||||
int EntityCount,
|
||||
int AnimatedEntityCount)
|
||||
{
|
||||
public static RenderFrameDiagnosticsSnapshot Initial => new(
|
||||
Fps: 60.0,
|
||||
FrameMilliseconds: 16.7,
|
||||
VisibleLandblocks: 0,
|
||||
TotalLandblocks: 0,
|
||||
EntityCount: 0,
|
||||
AnimatedEntityCount: 0);
|
||||
}
|
||||
|
||||
/// <summary>Non-render facts sampled only when the title cadence publishes.</summary>
|
||||
internal readonly record struct RenderFrameTitleFacts(
|
||||
int EntityCount,
|
||||
int AnimatedEntityCount,
|
||||
DerethDateTime.Calendar Calendar,
|
||||
double DayFraction);
|
||||
|
||||
internal interface IRenderFrameTitleFactsSource
|
||||
{
|
||||
RenderFrameTitleFacts Capture();
|
||||
}
|
||||
|
||||
/// <summary>Narrow host seam; diagnostics can publish chrome without retaining a window.</summary>
|
||||
internal interface IRenderFrameTitleSink
|
||||
{
|
||||
void SetTitle(string title);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameDiagnosticLog
|
||||
{
|
||||
void WriteLine(string message);
|
||||
}
|
||||
|
||||
internal sealed class ConsoleRenderFrameDiagnosticLog : IRenderFrameDiagnosticLog
|
||||
{
|
||||
public void WriteLine(string message) => Console.WriteLine(message);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameResourceDiagnosticsSource
|
||||
{
|
||||
RenderFrameResourceDiagnosticsSnapshot Capture();
|
||||
}
|
||||
|
||||
internal readonly record struct VfxStreamResourceDiagnostics(
|
||||
int ActiveEmitters,
|
||||
int ActiveParticles,
|
||||
int ActiveBindings);
|
||||
|
||||
internal readonly record struct DynamicBufferResourceDiagnostics(
|
||||
int WorldBufferSets,
|
||||
int EnvCellBufferSets,
|
||||
int ParticleBufferSets,
|
||||
long ParticleBufferBytes,
|
||||
long UiTextBufferBytes,
|
||||
long PortalDepthMaskBufferBytes,
|
||||
int ClipBufferSets,
|
||||
int TerrainIndirectBuffers,
|
||||
int SceneLightingBuffers);
|
||||
|
||||
internal readonly record struct MeshStreamResourceDiagnostics(
|
||||
int RenderData,
|
||||
int AtlasArrays,
|
||||
int UnusedLru,
|
||||
long EstimatedBytes,
|
||||
long GlobalUploadCount,
|
||||
long GlobalUploadedBytes,
|
||||
int FrameUploadCount,
|
||||
long FrameUploadBytes,
|
||||
long FrameArrayAllocationBytes,
|
||||
long FramePlannedMipmapBytes,
|
||||
int FrameNewArrayCount,
|
||||
long FrameBufferUploadBytes,
|
||||
long FrameBufferAllocationBytes,
|
||||
long FrameBufferCopyBytes,
|
||||
int FrameNewBufferCount,
|
||||
int StagedUploadBacklog,
|
||||
long StagedUploadBytes,
|
||||
bool StagingAtHighWater,
|
||||
int FrameStaleDiscardCount,
|
||||
int CpuMeshCacheCount,
|
||||
long CpuMeshCacheBytes,
|
||||
int FrameMipmapArrayCount,
|
||||
long FrameMipmapBytes,
|
||||
long GlobalCapacityBytes,
|
||||
long GlobalPhysicalCapacityBytes,
|
||||
bool GlobalMigrationInProgress);
|
||||
|
||||
internal readonly record struct TextureStreamResourceDiagnostics(
|
||||
int OwnedBindlessTextures,
|
||||
int TextureOwners,
|
||||
int ActiveParticleTextures,
|
||||
int ParticleTextureOwners,
|
||||
int CachedParticleTextures,
|
||||
int CachedUnownedParticleTextures,
|
||||
long CachedUnownedParticleTextureBytes,
|
||||
int CachedCompositeTextures,
|
||||
int CachedUnownedComposites,
|
||||
long CachedUnownedCompositeBytes,
|
||||
int CompositeAtlases,
|
||||
long CompositeAtlasBytes,
|
||||
int FrameCompositeUploadCount,
|
||||
long FrameCompositeUploadBytes,
|
||||
int CompositeWarmupPending);
|
||||
|
||||
internal readonly record struct ProcessResourceDiagnostics(
|
||||
long ManagedBytes,
|
||||
long ManagedCommittedBytes,
|
||||
long TrackedGpuBytes,
|
||||
int TrackedGpuBuffers,
|
||||
int TrackedGpuTextures);
|
||||
|
||||
/// <summary>
|
||||
/// Immutable resource facts captured only when explicit UI-probe dumping is enabled.
|
||||
/// Grouping keeps the diagnostics controller independent from every canonical renderer,
|
||||
/// VFX, mesh, texture, and process owner used to produce the values.
|
||||
/// </summary>
|
||||
internal readonly record struct RenderFrameResourceDiagnosticsSnapshot(
|
||||
VfxStreamResourceDiagnostics Vfx,
|
||||
DynamicBufferResourceDiagnostics DynamicBuffers,
|
||||
MeshStreamResourceDiagnostics Mesh,
|
||||
TextureStreamResourceDiagnostics Textures,
|
||||
ProcessResourceDiagnostics Process);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes post-presentation frame statistics. The caller invokes this only after
|
||||
/// screenshot capture, preserving the accepted screenshot-before-title/resource order.
|
||||
/// </summary>
|
||||
internal sealed class RenderFrameDiagnosticsController : IRenderFrameDiagnosticsPhase
|
||||
{
|
||||
internal const double PublicationIntervalSeconds = 0.5;
|
||||
|
||||
private readonly IRenderFrameTitleFactsSource _titleFacts;
|
||||
private readonly IRenderFrameTitleSink _titleSink;
|
||||
private readonly IRenderFrameResourceDiagnosticsSource? _resources;
|
||||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private readonly bool _publishResourceDiagnostics;
|
||||
|
||||
private double _elapsedSeconds;
|
||||
private int _frameCount;
|
||||
|
||||
public RenderFrameDiagnosticsSnapshot Snapshot { get; private set; } =
|
||||
RenderFrameDiagnosticsSnapshot.Initial;
|
||||
|
||||
public RenderFrameDiagnosticsController(
|
||||
IRenderFrameTitleFactsSource titleFacts,
|
||||
IRenderFrameTitleSink titleSink,
|
||||
IRenderFrameDiagnosticLog log,
|
||||
bool publishResourceDiagnostics,
|
||||
IRenderFrameResourceDiagnosticsSource? resources = null)
|
||||
{
|
||||
_titleFacts = titleFacts ?? throw new ArgumentNullException(nameof(titleFacts));
|
||||
_titleSink = titleSink ?? throw new ArgumentNullException(nameof(titleSink));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
_publishResourceDiagnostics = publishResourceDiagnostics;
|
||||
_resources = publishResourceDiagnostics
|
||||
? resources ?? throw new ArgumentNullException(nameof(resources))
|
||||
: resources;
|
||||
}
|
||||
|
||||
public void Publish(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
_elapsedSeconds += input.DeltaSeconds;
|
||||
_frameCount++;
|
||||
if (_elapsedSeconds < PublicationIntervalSeconds)
|
||||
return;
|
||||
|
||||
double averageFrameMilliseconds = _elapsedSeconds / _frameCount * 1000.0;
|
||||
double fps = _frameCount / _elapsedSeconds;
|
||||
RenderFrameTitleFacts facts = _titleFacts.Capture();
|
||||
|
||||
// Keep external publication ahead of the cached values and accumulator reset.
|
||||
// If either sink fails, the controller retains the accumulated window exactly as
|
||||
// the previous GameWindow body did and a later successful frame can retry it.
|
||||
_titleSink.SetTitle(FormatTitle(
|
||||
fps,
|
||||
averageFrameMilliseconds,
|
||||
outcome.World.VisibleLandblocks,
|
||||
outcome.World.TotalLandblocks,
|
||||
facts));
|
||||
|
||||
if (_publishResourceDiagnostics)
|
||||
{
|
||||
RenderFrameResourceDiagnosticsSnapshot resources = _resources!.Capture();
|
||||
_log.WriteLine(FormatGpuStream(resources));
|
||||
}
|
||||
|
||||
Snapshot = new RenderFrameDiagnosticsSnapshot(
|
||||
fps,
|
||||
averageFrameMilliseconds,
|
||||
outcome.World.VisibleLandblocks,
|
||||
outcome.World.TotalLandblocks,
|
||||
facts.EntityCount,
|
||||
facts.AnimatedEntityCount);
|
||||
_elapsedSeconds = 0;
|
||||
_frameCount = 0;
|
||||
}
|
||||
|
||||
internal static string FormatTitle(
|
||||
double fps,
|
||||
double averageFrameMilliseconds,
|
||||
int visibleLandblocks,
|
||||
int totalLandblocks,
|
||||
RenderFrameTitleFacts facts) =>
|
||||
$"acdream | {fps:F0} fps | {averageFrameMilliseconds:F1} ms | "
|
||||
+ $"lb {visibleLandblocks}/{totalLandblocks} | "
|
||||
+ $"ent {facts.EntityCount}/anim {facts.AnimatedEntityCount} | "
|
||||
+ $"PY{facts.Calendar.Year} {facts.Calendar.Month} {facts.Calendar.Day} "
|
||||
+ $"{facts.Calendar.Hour} (df={facts.DayFraction:F4})";
|
||||
|
||||
internal static string FormatGpuStream(RenderFrameResourceDiagnosticsSnapshot snapshot)
|
||||
{
|
||||
VfxStreamResourceDiagnostics vfx = snapshot.Vfx;
|
||||
DynamicBufferResourceDiagnostics buffers = snapshot.DynamicBuffers;
|
||||
MeshStreamResourceDiagnostics mesh = snapshot.Mesh;
|
||||
TextureStreamResourceDiagnostics textures = snapshot.Textures;
|
||||
ProcessResourceDiagnostics process = snapshot.Process;
|
||||
return
|
||||
$"[gpu-stream] emit={vfx.ActiveEmitters} "
|
||||
+ $"particles={vfx.ActiveParticles} "
|
||||
+ $"bindings={vfx.ActiveBindings} "
|
||||
+ $"wbSets={buffers.WorldBufferSets} "
|
||||
+ $"cellSets={buffers.EnvCellBufferSets} "
|
||||
+ $"particleSets={buffers.ParticleBufferSets}/{buffers.ParticleBufferBytes}B "
|
||||
+ $"uiBytes={buffers.UiTextBufferBytes} "
|
||||
+ $"portalBytes={buffers.PortalDepthMaskBufferBytes} "
|
||||
+ $"clipSets={buffers.ClipBufferSets} "
|
||||
+ $"terrainBuffers={buffers.TerrainIndirectBuffers} "
|
||||
+ $"lightBuffers={buffers.SceneLightingBuffers} "
|
||||
+ $"mesh={mesh.RenderData}/{mesh.AtlasArrays}/{mesh.UnusedLru} "
|
||||
+ $"meshEst={mesh.EstimatedBytes} "
|
||||
+ $"meshUpload={mesh.GlobalUploadCount}/{mesh.GlobalUploadedBytes} "
|
||||
+ $"uploadFrame={mesh.FrameUploadCount}/{mesh.FrameUploadBytes}/"
|
||||
+ $"{mesh.FrameArrayAllocationBytes}/{mesh.FramePlannedMipmapBytes}/"
|
||||
+ $"{mesh.FrameNewArrayCount} "
|
||||
+ $"bufferFrame={mesh.FrameBufferUploadBytes}/{mesh.FrameBufferAllocationBytes}/"
|
||||
+ $"{mesh.FrameBufferCopyBytes}/{mesh.FrameNewBufferCount} "
|
||||
+ $"staging={mesh.StagedUploadBacklog}/{mesh.StagedUploadBytes}/"
|
||||
+ $"{mesh.StagingAtHighWater}/{mesh.FrameStaleDiscardCount} "
|
||||
+ $"cpuMesh={mesh.CpuMeshCacheCount}/{mesh.CpuMeshCacheBytes} "
|
||||
+ $"mipmapFrame={mesh.FrameMipmapArrayCount}/{mesh.FrameMipmapBytes} "
|
||||
+ $"meshCap={mesh.GlobalCapacityBytes} "
|
||||
+ $"meshPhys={mesh.GlobalPhysicalCapacityBytes}/{mesh.GlobalMigrationInProgress} "
|
||||
+ $"gpuTrack={process.TrackedGpuBytes}/{process.TrackedGpuBuffers}/"
|
||||
+ $"{process.TrackedGpuTextures} "
|
||||
+ $"managed={process.ManagedBytes}/{process.ManagedCommittedBytes} "
|
||||
+ $"ownedTextures={textures.OwnedBindlessTextures}/{textures.TextureOwners} "
|
||||
+ $"particleTextures={textures.ActiveParticleTextures}/"
|
||||
+ $"{textures.ParticleTextureOwners}/{textures.CachedParticleTextures}/"
|
||||
+ $"{textures.CachedUnownedParticleTextures}/"
|
||||
+ $"{textures.CachedUnownedParticleTextureBytes} "
|
||||
+ $"compositeCache={textures.CachedCompositeTextures}/"
|
||||
+ $"{textures.CachedUnownedComposites}/{textures.CachedUnownedCompositeBytes} "
|
||||
+ $"compositeAtlas={textures.CompositeAtlases}/{textures.CompositeAtlasBytes} "
|
||||
+ $"compositeUpload={textures.FrameCompositeUploadCount}/"
|
||||
+ $"{textures.FrameCompositeUploadBytes}/{textures.CompositeWarmupPending}";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue