acdream/src/AcDream.App/Composition/FrameRootComposition.cs
Erik d9446030e6 feat(streaming): shadow-publish flat collision assets
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 16:38:54 +02:00

579 lines
22 KiB
C#

using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
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),
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);
CurrentRenderSceneOracle? currentRenderSceneOracle =
interaction.RetainedUi?.Screenshots is not null
&& d.Options.AutomationArtifactDirectory is not null
? new CurrentRenderSceneOracle()
: null;
RenderSceneShadowComparisonController? renderSceneShadowComparison =
currentRenderSceneOracle is not null
&& live.RenderSceneShadow is not null
? new RenderSceneShadowComparisonController(
live.RenderSceneShadow,
currentRenderSceneOracle,
message => d.Log("[UI-PROBE] " + message),
acknowledgeDirty: false)
: null;
RenderScenePViewFrameProductController? renderFrameProduct =
live.RenderSceneShadow is not null
? new RenderScenePViewFrameProductController(
live.RenderSceneShadow,
currentRenderSceneOracle,
message => d.Log("[UI-PROBE] " + message),
live.DrawDispatcher)
: null;
// After G4 the retained product is the production object source.
// The old dispatcher/selection observer is intentionally detached;
// automation still compares the independently built PView route list.
live.DrawDispatcher.SetCurrentRenderSceneObserver(null);
live.SelectionScene.SetCurrentRenderSceneObserver(null);
var worldSceneRenderer = new WorldSceneRenderer(
renderFrameResources,
renderLoginState,
d.WorldEnvironment,
worldRenderFrameBuilder,
new RuntimeWorldSceneEntitySource(live.WorldState),
live.SelectionScene,
d.RetailAlphaQueue,
d.ParticleVisibility,
new WorldScenePViewRenderer(
new RetailPViewRenderer(
currentRenderSceneOracle,
renderFrameProduct),
retailPViewPassExecutor,
retailPViewPassExecutor),
retailPViewCells,
worldScenePasses,
d.RenderRange,
worldSceneDiagnostics,
live.WorldAvailability);
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,
foundation.Residency,
d.PhysicsEngine.DataCache
?? throw new InvalidOperationException(
"Lifecycle automation requires the canonical physics cache."),
currentRenderSceneOracle,
renderSceneShadowComparison,
renderFrameProduct);
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);
IRenderFramePostDiagnosticsPhase postDiagnostics =
renderSceneShadowComparison is not null
&& lifecycleAutomation is not null
? new SerialRenderFramePostDiagnosticsPhase(
renderSceneShadowComparison,
lifecycleAutomation)
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
?? NullRenderFramePostDiagnosticsPhase.Instance;
var renderFrame = new RenderFrameOrchestrator(
host.GpuFrameFlights,
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
framePreparation,
worldSceneRenderer,
privatePresentation,
live.FrameDiagnostics,
postDiagnostics,
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
?? NullRenderFrameFailureRecovery.Instance);
Fault(FrameRootCompositionPoint.RenderRootCreated);
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
session.LiveObjectFrame,
live.WorldState,
session.LiveSession,
session.LocalPlayerFrame,
session.LiveSpatialReconciler,
live.WorldAvailability,
live.RenderSceneShadow?.LiveProjections);
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,
new RenderSceneUpdateCommitPhase(live.RenderSceneShadow),
live.WorldAvailability);
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);
}