An adversarial performance review found our own instruments cannot measure the project's own performance gates: - FrameProfiler aggregated CPU/GPU/alloc/stage samples into ~5-second windows and reset the ring buffers after each report, so route-wide p50/p95/p99 distributions across a whole soak could not be reconstructed after the fact. ACDREAM_FRAME_HISTORY=<path> now opts into a separate per-frame history (one record per frame, ~72 bytes/record, accumulated in memory with zero frame-thread I/O) that a shutdown-only Dispose() writes as CSV. The aggregated [frame-prof] report format and its existing metrics are unchanged. - The canonical checkpoint JSON tracked cache residency (entry/byte counts) but never LOH size/fragmentation, process-wide allocated bytes, or cache hit/miss/eviction traffic — a committed audit JSON showed 65% LOH fragmentation that no tracked instrument recorded, and "does a revisit portal hit or miss the caches" was unanswerable from an artifact alone. WorldLifecycleResourceSnapshot now carries loh_size_bytes/loh_fragmentation_bytes (GCMemoryInfo.GenerationInfo index 3), process_total_allocated_bytes (GC.GetTotalAllocatedBytes), and Interlocked hit/miss/eviction counters for the CPU mesh cache, decoded-texture cache, and the four bounded DAT-object caches (portal/cell/highRes/language, aggregated). - run-connected-r6-soak.ps1 unconditionally forced ACDREAM_UNCAPPED_RENDER=1 with no capped mode, while its sibling lifecycle-gate script correctly gated it behind a switch. Added -Uncapped (default capped, matching the sibling script's pattern), fixed the stationary dwell (12s -> 26s, past the 25s LiveEntityLivenessController deadline the adjacent comment already cited), and now write an env-disclosure.json into the automation artifact directory before every launch listing every ACDREAM_* var the script sets plus -Uncapped, since the prior audit could only see ACDREAM_DUMP_MOVE_TRUTH and nothing else was ever recorded anywhere. Cache counters are wired via the existing composition path (ObjectMeshManager already owns the CPU mesh cache and the mesh extractor directly; content.Dats is threaded into WorldLifecycleResourceSnapshotSource the same way every other composition consumer receives it). The DAT-object cache lives behind IDatReaderWriter, a third-party interface from the DatReaderWriter package that cannot be extended; RuntimeDatCollection (the one production implementation) exposes the aggregate stats directly and a pattern match reads them, degrading to zero for any test double — no new static registry was introduced (GpuMemoryTracker remains the one precedented process-wide static). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 1da2c33c875b41fa383dd79694ee2765f0e21896)
531 lines
19 KiB
C#
531 lines
19 KiB
C#
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Settings;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.OpenGL;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record FrameRootDependencies(
|
|
RuntimeOptions Options,
|
|
GL Gl,
|
|
IWindow Window,
|
|
IInputContext Input,
|
|
WorldTimeService WorldTime,
|
|
WeatherSystem Weather,
|
|
LightManager Lighting,
|
|
WorldEnvironmentController WorldEnvironment,
|
|
PhysicsEngine PhysicsEngine,
|
|
CellVisibility CellVisibility,
|
|
LocalPlayerModeState PlayerMode,
|
|
LocalPlayerIdentityState PlayerIdentity,
|
|
ChaseCameraInputState ChaseCameraInput,
|
|
LocalPlayerControllerSlot PlayerController,
|
|
LiveWorldOriginState WorldOrigin,
|
|
ParticleVisibilityController ParticleVisibility,
|
|
EntityEffectPoseRegistry EffectPoses,
|
|
WorldRenderRangeState RenderRange,
|
|
RuntimeSettingsController Settings,
|
|
DisplayFramePacingController DisplayFramePacing,
|
|
WorldSceneDebugState WorldSceneDebugState,
|
|
RetailAlphaQueue RetailAlphaQueue,
|
|
FrameProfiler FrameProfiler,
|
|
bool FrameDiagnosticsEnabled,
|
|
IRenderFrameDiagnosticLog RenderDiagnosticLog,
|
|
DebugVmRenderFactsPublisher DebugVmRenderFacts,
|
|
IInputCaptureSource InputCapture,
|
|
DispatcherCameraInputSource CameraInput,
|
|
SelectionState Selection,
|
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
|
|
UpdateFrameClock UpdateClock,
|
|
CombatState Combat,
|
|
GameFrameGraphSlot FrameGraphs,
|
|
Action<string> Log);
|
|
|
|
internal sealed record FrameRootResult(
|
|
UpdateFrameOrchestrator Update,
|
|
RenderFrameOrchestrator Render,
|
|
FrameRootRuntimeBindings RuntimeBindings,
|
|
IDisposable FrameGraphPublication,
|
|
AcDream.App.Net.LiveSessionHost SessionHost);
|
|
|
|
internal interface IGameWindowFrameRootPublication
|
|
{
|
|
void PublishFrameRoots(FrameRootResult result);
|
|
}
|
|
|
|
internal enum FrameRootCompositionPoint
|
|
{
|
|
RenderResourcesCreated,
|
|
WorldRendererCreated,
|
|
LifecycleAutomationBound,
|
|
RenderRootCreated,
|
|
UpdateRootCreated,
|
|
FrameGraphPublished,
|
|
ResultPublished,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact late edges created with the frame roots. Completed detach operations
|
|
/// are removed immediately so shutdown retry never replays them.
|
|
/// </summary>
|
|
internal sealed class FrameRootRuntimeBindings : IDisposable
|
|
{
|
|
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
|
private bool _deactivationStarted;
|
|
|
|
public void Adopt(string name, IDisposable binding)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(binding);
|
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
|
_bindings.Add((name, binding));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_deactivationStarted && _bindings.Count == 0)
|
|
return;
|
|
_deactivationStarted = true;
|
|
|
|
List<Exception>? failures = null;
|
|
for (int i = _bindings.Count - 1; i >= 0; i--)
|
|
{
|
|
(string name, IDisposable binding) = _bindings[i];
|
|
try
|
|
{
|
|
binding.Dispose();
|
|
_bindings.RemoveAt(i);
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
(failures ??= []).Add(new InvalidOperationException(
|
|
$"Frame-root binding '{name}' did not detach.",
|
|
failure));
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
"Frame-root binding cleanup remains incomplete.",
|
|
failures);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class FrameRootCompositionPhase
|
|
: IFrameRootCompositionPhase<
|
|
GameWindowPlatformResult<GL, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult,
|
|
FrameRootResult>
|
|
{
|
|
private readonly FrameRootDependencies _dependencies;
|
|
private readonly IGameWindowFrameRootPublication _publication;
|
|
private readonly Action<FrameRootCompositionPoint>? _faultInjection;
|
|
|
|
public FrameRootCompositionPhase(
|
|
FrameRootDependencies dependencies,
|
|
IGameWindowFrameRootPublication publication,
|
|
Action<FrameRootCompositionPoint>? faultInjection = null)
|
|
{
|
|
_dependencies = dependencies
|
|
?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication
|
|
?? throw new ArgumentNullException(nameof(publication));
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public FrameRootResult Compose(
|
|
GameWindowPlatformResult<GL, IInputContext> platform,
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
ArgumentNullException.ThrowIfNull(world);
|
|
ArgumentNullException.ThrowIfNull(interaction);
|
|
ArgumentNullException.ThrowIfNull(live);
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)
|
|
|| !ReferenceEquals(_dependencies.Input, platform.Input))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Frame-root dependencies do not match the ordered platform result.");
|
|
}
|
|
|
|
var scope = new CompositionAcquisitionScope();
|
|
FrameRootRuntimeBindings? bindings = null;
|
|
bool bindingsOwnedByScope = false;
|
|
try
|
|
{
|
|
FrameRootResult result = ComposeCore(
|
|
host,
|
|
content,
|
|
settings,
|
|
world,
|
|
interaction,
|
|
live,
|
|
session,
|
|
scope,
|
|
ref bindings,
|
|
ref bindingsOwnedByScope);
|
|
scope.Complete();
|
|
return result;
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
if (bindings is not null && !bindingsOwnedByScope)
|
|
{
|
|
scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
}
|
|
|
|
scope.RollbackAndThrow(failure);
|
|
throw new System.Diagnostics.UnreachableException();
|
|
}
|
|
}
|
|
|
|
private FrameRootResult ComposeCore(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session,
|
|
CompositionAcquisitionScope scope,
|
|
ref FrameRootRuntimeBindings? bindings,
|
|
ref bool bindingsOwnedByScope)
|
|
{
|
|
FrameRootDependencies d = _dependencies;
|
|
WorldRenderFoundation foundation = world.Foundation;
|
|
var teleportRenderState =
|
|
new LocalPlayerTeleportRenderStateSource(session.LocalTeleport);
|
|
var renderLoginState = new RenderLoginStateSource(
|
|
d.Options.LiveMode,
|
|
d.PlayerMode);
|
|
var renderFrameGlState = new RenderFrameGlStateController(
|
|
new SilkRenderFrameGlStateApi(d.Gl));
|
|
var renderFrameLivePreparation =
|
|
new RuntimeRenderFrameLivePreparation(
|
|
foundation.TextureCache,
|
|
foundation.MeshAdapter,
|
|
session.WorldReveal,
|
|
teleportRenderState,
|
|
renderLoginState,
|
|
new LiveLoginRevealCellSource(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity),
|
|
live.ParticleRenderer,
|
|
d.FrameProfiler,
|
|
d.FrameDiagnosticsEnabled);
|
|
var renderFrameResources = new RenderFrameResourceController(
|
|
host.GpuFrameFlights,
|
|
new RuntimeRenderFrameBeginResources(
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
live.PortalDepthMask,
|
|
foundation.TextRenderer,
|
|
interaction.RetainedUi?.Host.TextRenderer,
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
foundation.SceneLighting,
|
|
d.FrameProfiler,
|
|
d.Gl),
|
|
new RuntimeRenderFrameClearPhase(
|
|
d.Gl,
|
|
d.WorldTime,
|
|
d.Weather,
|
|
teleportRenderState,
|
|
d.ParticleVisibility,
|
|
host.WorldRenderDiagnostics,
|
|
renderFrameGlState),
|
|
renderFrameLivePreparation);
|
|
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
|
|
|
|
var renderWeatherFrame = new RenderWeatherFrameController(
|
|
d.WorldTime,
|
|
d.Weather);
|
|
var skyPesFrame = new SkyPesFrameController(
|
|
content.ScriptRunner,
|
|
content.ParticleSink,
|
|
d.EffectPoses,
|
|
live.EntityEffects);
|
|
var worldFrameEnvironment =
|
|
new RuntimeWorldFrameEnvironmentPreparation(
|
|
d.Options,
|
|
d.WorldTime,
|
|
d.Lighting,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
foundation.SceneLighting,
|
|
d.RenderRange,
|
|
skyPesFrame);
|
|
var worldRenderFrameBuilder = new WorldRenderFrameBuilder(
|
|
new RuntimeWorldFrameCameraSource(
|
|
host.CameraController,
|
|
session.LocalTeleport),
|
|
new RuntimeWorldFrameVisibilityPreparation(
|
|
live.SelectionScene,
|
|
d.ParticleVisibility,
|
|
foundation.Terrain,
|
|
session.WorldReveal,
|
|
live.EnvCellFrustum),
|
|
new RuntimeWorldFrameSettingsPreview(
|
|
d.Settings,
|
|
content.Audio?.Engine,
|
|
host.CameraController,
|
|
d.DisplayFramePacing),
|
|
new RuntimeWorldFrameRootSource(
|
|
d.PhysicsEngine,
|
|
d.CellVisibility,
|
|
d.PlayerMode,
|
|
d.ChaseCameraInput,
|
|
d.PlayerController,
|
|
d.WorldOrigin),
|
|
worldFrameEnvironment,
|
|
new RuntimeWorldFrameAnimatedEntitySource(
|
|
d.Animations,
|
|
live.StaticAnimationScheduler,
|
|
live.EquippedChildren),
|
|
new RuntimeWorldFrameBuildingSource(
|
|
live.LandblockPipeline,
|
|
d.CellVisibility));
|
|
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
|
|
d.FrameDiagnosticsEnabled,
|
|
host.WorldRenderDiagnostics,
|
|
new RuntimeFramePipelineDiagnosticFactsSource(
|
|
foundation.Terrain,
|
|
live.LandblockPipeline,
|
|
renderFrameLivePreparation,
|
|
live.DrawDispatcher,
|
|
session.Streaming,
|
|
live.LiveEntities,
|
|
live.WorldState),
|
|
d.RenderDiagnosticLog);
|
|
var retailPViewCells = new RetailPViewCellSource(d.CellVisibility);
|
|
var retailPViewPassExecutor = new RetailPViewPassExecutor(
|
|
d.Gl,
|
|
renderFrameGlState,
|
|
new SilkRetailPViewFramebufferSource(d.Window),
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
live.EnvCellRenderer,
|
|
live.DrawDispatcher,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer,
|
|
live.PortalDepthMask,
|
|
d.RetailAlphaQueue,
|
|
host.WorldRenderDiagnostics,
|
|
terrainDrawDiagnostics);
|
|
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
|
|
host.WorldRenderDiagnostics,
|
|
new RuntimeWorldScenePViewDiagnosticSource(
|
|
d.PlayerController,
|
|
d.PhysicsEngine,
|
|
d.CellVisibility),
|
|
d.WorldSceneDebugState,
|
|
foundation.DebugLines,
|
|
d.PhysicsEngine,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
d.DebugVmRenderFacts,
|
|
settings.DevTools is not null);
|
|
var worldScenePasses = new WorldScenePassExecutor(
|
|
d.Gl,
|
|
renderFrameGlState,
|
|
live.ClipFrame,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
foundation.Terrain,
|
|
terrainDrawDiagnostics,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer);
|
|
var worldSceneRenderer = new WorldSceneRenderer(
|
|
renderFrameResources,
|
|
renderLoginState,
|
|
d.WorldEnvironment,
|
|
worldRenderFrameBuilder,
|
|
new RuntimeWorldSceneEntitySource(live.WorldState),
|
|
live.SelectionScene,
|
|
d.RetailAlphaQueue,
|
|
d.ParticleVisibility,
|
|
new WorldScenePViewRenderer(
|
|
new RetailPViewRenderer(),
|
|
retailPViewPassExecutor,
|
|
retailPViewPassExecutor),
|
|
retailPViewCells,
|
|
worldScenePasses,
|
|
d.RenderRange,
|
|
worldSceneDiagnostics);
|
|
Fault(FrameRootCompositionPoint.WorldRendererCreated);
|
|
|
|
bindings = new FrameRootRuntimeBindings();
|
|
WorldLifecycleAutomationController? lifecycleAutomation = null;
|
|
if (interaction.RetainedUi?.Screenshots is { } screenshots
|
|
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
|
{
|
|
var resourceSnapshots =
|
|
new WorldLifecycleResourceSnapshotSource(
|
|
live.WorldState,
|
|
d.Animations,
|
|
live.FrameDiagnostics,
|
|
live.LiveEntities,
|
|
session.Streaming,
|
|
content.ParticleSystem,
|
|
content.ParticleSink,
|
|
live.EntityEffects,
|
|
live.Lights,
|
|
content.ScriptRunner,
|
|
foundation.MeshAdapter,
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
d.FrameProfiler,
|
|
content.Dats);
|
|
lifecycleAutomation =
|
|
new WorldLifecycleAutomationController(
|
|
() => session.WorldReveal.Snapshot,
|
|
() => session.WorldReveal.PortalMaterializationCount,
|
|
resourceSnapshots.Capture,
|
|
screenshots,
|
|
artifactDirectory,
|
|
message => d.Log("[UI-PROBE] " + message));
|
|
bindings.Adopt(
|
|
"world lifecycle automation owner",
|
|
lifecycleAutomation);
|
|
bindings.Adopt(
|
|
"world lifecycle automation binding",
|
|
interaction.LateBindings.Automation.Bind(
|
|
lifecycleAutomation));
|
|
}
|
|
Fault(FrameRootCompositionPoint.LifecycleAutomationBound);
|
|
|
|
IRetainedGameplayUiFrame? retainedGameplayUi =
|
|
d.Options.RetailUi && interaction.RetainedUi is { } retained
|
|
? new RetainedGameplayUiFrame(retained.Runtime, d.Input)
|
|
: null;
|
|
IPrivateFrameScreenshot? privateScreenshot =
|
|
interaction.RetainedUi?.Screenshots is { } frameScreenshots
|
|
? new PrivateFrameScreenshot(frameScreenshots)
|
|
: null;
|
|
var privatePresentation = new PrivatePresentationRenderer(
|
|
new LocalPlayerPortalViewport(
|
|
session.LocalTeleport,
|
|
host.CameraController),
|
|
renderFrameResources,
|
|
new PrivateEntityViewportFrameGroup(
|
|
live.PaperdollPresenter,
|
|
live.CreatureAppraisalPresenter),
|
|
retainedGameplayUi,
|
|
settings.DevTools?.Presenter,
|
|
privateScreenshot);
|
|
var framePreparation = new RenderFramePreparationController(
|
|
renderFrameResources,
|
|
settings.DevTools?.Presenter,
|
|
renderWeatherFrame);
|
|
var renderFrame = new RenderFrameOrchestrator(
|
|
host.GpuFrameFlights,
|
|
framePreparation,
|
|
worldSceneRenderer,
|
|
privatePresentation,
|
|
live.FrameDiagnostics,
|
|
(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
|
?? NullRenderFramePostDiagnosticsPhase.Instance,
|
|
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
|
?? NullRenderFrameFailureRecovery.Instance);
|
|
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
|
|
|
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
|
session.LiveObjectFrame,
|
|
live.WorldState,
|
|
session.LiveSession,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler);
|
|
var cameraFrame = new CameraFrameController(
|
|
host.CameraController,
|
|
d.InputCapture,
|
|
d.CameraInput,
|
|
session.LocalPlayerFrameRuntime,
|
|
d.ChaseCameraInput,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler,
|
|
new AcDream.App.Combat.CombatCameraTargetSource(
|
|
d.Settings,
|
|
d.Combat,
|
|
d.Selection,
|
|
live.SelectionQuery));
|
|
var updateFrame = new UpdateFrameOrchestrator(
|
|
new LiveEntityTeardownFramePhase(live.LiveEntities),
|
|
new ConsoleUpdateFrameFailureSink(),
|
|
d.UpdateClock,
|
|
new PhysicsScriptClockPublisher(content.ScriptRunner),
|
|
session.StreamingFrame,
|
|
session.GameplayInput,
|
|
liveFrameCoordinator,
|
|
new LiveEntityLivenessFramePhase(
|
|
session.Liveness,
|
|
new StopwatchClientMonotonicTimeSource()),
|
|
session.LocalTeleport,
|
|
new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry),
|
|
cameraFrame);
|
|
Fault(FrameRootCompositionPoint.UpdateRootCreated);
|
|
|
|
var bindingsLease = scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
bindingsOwnedByScope = true;
|
|
IDisposable frameGraphPublication = d.FrameGraphs.PublishOwned(
|
|
updateFrame,
|
|
renderFrame);
|
|
var graphLease = scope.Own(
|
|
"game frame graph publication",
|
|
frameGraphPublication,
|
|
static value => value.Dispose());
|
|
Fault(FrameRootCompositionPoint.FrameGraphPublished);
|
|
|
|
var result = new FrameRootResult(
|
|
updateFrame,
|
|
renderFrame,
|
|
bindings,
|
|
frameGraphPublication,
|
|
session.SessionHost);
|
|
_publication.PublishFrameRoots(result);
|
|
graphLease.Transfer();
|
|
bindingsLease.Transfer();
|
|
Fault(FrameRootCompositionPoint.ResultPublished);
|
|
return result;
|
|
}
|
|
|
|
private void Fault(FrameRootCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|