278 lines
11 KiB
C#
278 lines
11 KiB
C#
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,
|
|
long PreparedProbes,
|
|
long PreparedReads,
|
|
long PreparedLoaded,
|
|
long PreparedMissing,
|
|
long PreparedCorrupt);
|
|
|
|
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,
|
|
IRenderFrameDiagnosticsSnapshotSource
|
|
{
|
|
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} "
|
|
+ $"prepared={mesh.PreparedProbes}/{mesh.PreparedReads}/"
|
|
+ $"{mesh.PreparedLoaded}/{mesh.PreparedMissing}/{mesh.PreparedCorrupt} "
|
|
+ $"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}";
|
|
}
|
|
}
|