refactor(app): compose atomic frame roots
Move the complete update/render construction graph into a typed Phase-8 owner, explicitly carry the content dependencies it consumes, and publish both roots through one exact lease. Extract lifecycle resource sampling and frame-owned late bindings so partial startup and shutdown withdraw the same generation without window callbacks. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
826f9ea9b5
commit
3628aeb520
21 changed files with 1052 additions and 374 deletions
516
src/AcDream.App/Composition/FrameRootComposition.cs
Normal file
516
src/AcDream.App/Composition/FrameRootComposition.cs
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
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,
|
||||
WorldLifecycleAutomationController? LifecycleAutomation,
|
||||
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<
|
||||
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(
|
||||
HostInputCameraResult host,
|
||||
ContentEffectsAudioResult content,
|
||||
SettingsDevToolsResult settings,
|
||||
WorldRenderResult world,
|
||||
InteractionRetainedUiResult interaction,
|
||||
LivePresentationResult live,
|
||||
SessionPlayerResult session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
ArgumentNullException.ThrowIfNull(interaction);
|
||||
ArgumentNullException.ThrowIfNull(live);
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
|
||||
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);
|
||||
lifecycleAutomation =
|
||||
new WorldLifecycleAutomationController(
|
||||
() => session.WorldReveal.Snapshot,
|
||||
() => session.WorldReveal.PortalMaterializationCount,
|
||||
resourceSnapshots.Capture,
|
||||
screenshots,
|
||||
artifactDirectory,
|
||||
message => d.Log("[UI-PROBE] " + message));
|
||||
bindings.Adopt(
|
||||
"world lifecycle automation",
|
||||
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,
|
||||
live.PaperdollPresenter,
|
||||
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,
|
||||
(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,
|
||||
lifecycleAutomation,
|
||||
bindings,
|
||||
frameGraphPublication,
|
||||
session.SessionHost);
|
||||
_publication.PublishFrameRoots(result);
|
||||
graphLease.Transfer();
|
||||
bindingsLease.Transfer();
|
||||
Fault(FrameRootCompositionPoint.ResultPublished);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Fault(FrameRootCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue