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
|
|
@ -34,8 +34,10 @@ What does NOT go here:
|
|||
I.7 now owns streaming/session/hydration/local-player/teleport startup and
|
||||
every late edge explicitly. I.8a now owns the focused session factory,
|
||||
command targets, and sole gameplay input subscriber before frame publication.
|
||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
||||
handoffs and the class is 2,149 raw lines. I.8b–I.9 plus the remaining
|
||||
I.8b now owns the complete update/render construction, exact atomic frame
|
||||
publication, and lifecycle resource snapshot source. `GameWindow.OnUpdate`
|
||||
and `GameWindow.OnRender` are typed orchestration handoffs and the class is
|
||||
1,919 raw lines. I.8c–I.9 plus the remaining
|
||||
shutdown/closeout
|
||||
cleanup is active in
|
||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||
|
|
|
|||
|
|
@ -736,12 +736,14 @@ local-player/teleport construction phase and every late edge through named
|
|||
exact-owner tokens; the former 432-line inline body and spawn-claim memo are
|
||||
gone. I.8a moves the exact live-session reset/router graph, combat and
|
||||
diagnostic command targets, and sole gameplay input subscriber into Phase 7
|
||||
before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8b–L
|
||||
remain active, with atomic frame-root publication next. The App gate passes
|
||||
3,424 tests / 3 intentional skips and the complete Release suite passes 7,796
|
||||
tests / 5 intentional skips. The clean solution build retains only the 17
|
||||
test-project warnings tracked by #228; the I.8a behavior, architecture, and
|
||||
adversarial reviews are clean.
|
||||
before frame publication. I.8b moves the complete update/render construction
|
||||
body into `FrameRootCompositionPhase`, publishes the pair through an exact
|
||||
lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
|
||||
1,919 raw lines. I.8c–L remain active, with terminal session start next. The
|
||||
App gate passes 3,429 tests / 3 intentional skips and the complete Release suite
|
||||
passes 7,801 tests / 5 intentional skips. The clean solution build retains only
|
||||
the 17 test-project warnings tracked by #228; the I.8b corrected-diff review is
|
||||
clean.
|
||||
|
||||
### 4.4 Exit criteria
|
||||
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
|
|||
resources have transactional phase ownership and exact-owner late bindings.
|
||||
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
||||
including explicit reversible ownership for every late edge. `GameWindow` is
|
||||
now 2,149 raw lines after I.8a, which composes the session factory, command
|
||||
targets, and sole gameplay input subscriber before frame publication. I.8b–I.9,
|
||||
now 1,919 raw lines after I.8b, which also composes and atomically publishes the
|
||||
complete update/render pair and extracts lifecycle resource sampling. I.8c–I.9,
|
||||
shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The
|
||||
App gate passes 3,424 tests / 3 intentional skips, the complete Release suite
|
||||
passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
||||
App gate passes 3,429 tests / 3 intentional skips, the complete Release suite
|
||||
passes 7,801 tests / 5 skips, and the I.8b corrected-diff review is clean.
|
||||
|
||||
This is a behavior-preserving structural program. Severe regressions still get
|
||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
||||
|
|
|
|||
|
|
@ -60,11 +60,13 @@ publishers, and portal/sky/particle resources have transactional phase
|
|||
ownership and exact-owner late bindings. I.7 now owns the complete streaming,
|
||||
session, hydration, local-player, combat, and teleport startup graph, including
|
||||
named reversible late edges. I.8a adds the focused live-session runtime factory
|
||||
and attaches command/input owners before frame publication. `GameWindow` is
|
||||
2,149 raw lines. I.8b–I.9 and checkpoints J–L remain active. Issue #232 tracks
|
||||
and attaches command/input owners before frame publication. I.8b composes and
|
||||
atomically publishes the complete frame pair and extracts lifecycle resource
|
||||
sampling. `GameWindow` is 1,919 raw lines. I.8c–I.9 and checkpoints J–L remain
|
||||
active. Issue #232 tracks
|
||||
process-residency variance in the soak without loosening its leak threshold.
|
||||
The App gate passes 3,424 tests / 3 skips, the complete Release suite passes
|
||||
7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
||||
The App gate passes 3,429 tests / 3 skips, the complete Release suite passes
|
||||
7,801 tests / 5 skips, and the I.8b corrected-diff review is clean.
|
||||
|
||||
Carried:
|
||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||
|
|
|
|||
|
|
@ -294,9 +294,11 @@ ledger only after the complete pipeline is live.
|
|||
detailed session-binding, atomic-root, and terminal-start plan is
|
||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md).
|
||||
I.8a is complete: the session factory, command targets, and sole gameplay
|
||||
input subscriber now belong to Phase 7 and publish before any frame root;
|
||||
`GameWindow.cs` is 2,149 raw lines. I.8b atomic frame-root composition is the
|
||||
active cut.
|
||||
input subscriber now belong to Phase 7 and publish before any frame root.
|
||||
I.8b composes the unchanged update/render graph in a focused phase, publishes
|
||||
the pair through an exact owned slot lease, and moves lifecycle resource
|
||||
sampling out of the window. `GameWindow.cs` is 1,919 raw lines. I.8c terminal
|
||||
session start is the active cut.
|
||||
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
||||
|
||||
## 6. Automated acceptance
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
|
||||
|
||||
**Status:** Active — I.8a complete; I.8b frame-root composition next
|
||||
**Status:** Active — I.8a–I.8b complete; I.8c terminal start next
|
||||
|
||||
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
||||
|
||||
|
|
@ -81,6 +81,16 @@ Phase-7 suffix in reverse order.
|
|||
|
||||
## 3. I.8b — atomic frame-root composition
|
||||
|
||||
**Completed 2026-07-22.** `FrameRootCompositionPhase` now constructs the
|
||||
unchanged render and update graphs from explicit prior-phase results and
|
||||
focused runtime owners. `GameFrameGraphSlot.PublishOwned` publishes and
|
||||
withdraws one exact pair. Optional lifecycle automation has a Phase-8 binding
|
||||
owner, and `WorldLifecycleResourceSnapshotSource` samples the same canonical
|
||||
owners without window callbacks or mirrored counters. The Phase-8 contract now
|
||||
receives the Phase-2 content result explicitly because scripts and particles
|
||||
are real frame dependencies; this removes a hidden reach-back through the
|
||||
window shell.
|
||||
|
||||
Add `FrameRootCompositionPhase` implementing the existing
|
||||
`IFrameRootCompositionPhase` contract. Its dependencies are focused App owners;
|
||||
prior phase results supply all resources created during startup.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ audit.
|
|||
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
||||
explicit single ownership and transfer seams.
|
||||
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
||||
transactional partial-acquisition rollback. I.1–I.7 and I.8a are complete;
|
||||
I.8b atomic frame-root publication is active. Detailed plan:
|
||||
transactional partial-acquisition rollback. I.1–I.8b are complete; I.8c
|
||||
terminal session start is active. Detailed plan:
|
||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
|
||||
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
||||
owner and prove all partial-load/reentrant/retry paths.
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
|
@ -40,10 +40,11 @@ internal interface ISessionPlayerCompositionPhase<in THost, in TContent, in TWor
|
|||
TLive live);
|
||||
}
|
||||
|
||||
internal interface IFrameRootCompositionPhase<in THost, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult>
|
||||
internal interface IFrameRootCompositionPhase<in THost, in TContent, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult>
|
||||
{
|
||||
TResult Compose(
|
||||
THost host,
|
||||
TContent content,
|
||||
TSettings settings,
|
||||
TWorld world,
|
||||
TInteraction interaction,
|
||||
|
|
@ -79,7 +80,7 @@ internal sealed class GameWindowCompositionPipeline<
|
|||
private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction;
|
||||
private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live;
|
||||
private readonly ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> _session;
|
||||
private readonly IFrameRootCompositionPhase<THost, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame;
|
||||
private readonly IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame;
|
||||
private readonly ISessionStartCompositionPhase<TFrame> _start;
|
||||
|
||||
public GameWindowCompositionPipeline(
|
||||
|
|
@ -90,7 +91,7 @@ internal sealed class GameWindowCompositionPipeline<
|
|||
IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction,
|
||||
ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live,
|
||||
ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> session,
|
||||
IFrameRootCompositionPhase<THost, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
|
||||
IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
|
||||
ISessionStartCompositionPhase<TFrame> start)
|
||||
{
|
||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
|
|
@ -113,7 +114,14 @@ internal sealed class GameWindowCompositionPipeline<
|
|||
TInteraction interaction = _interaction.Compose(host, content, settings, world);
|
||||
TLive live = _live.Compose(host, content, world, interaction);
|
||||
TSession session = _session.Compose(host, content, world, interaction, live);
|
||||
TFrame frame = _frame.Compose(host, settings, world, interaction, live, session);
|
||||
TFrame frame = _frame.Compose(
|
||||
host,
|
||||
content,
|
||||
settings,
|
||||
world,
|
||||
interaction,
|
||||
live,
|
||||
session);
|
||||
_start.Start(frame);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Vfx;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Samples the canonical owners used by the connected lifecycle gate. This is
|
||||
/// deliberately a read-only view: it owns no counters and keeps no mirrored
|
||||
/// resource state between checkpoints.
|
||||
/// </summary>
|
||||
internal sealed class WorldLifecycleResourceSnapshotSource
|
||||
{
|
||||
private readonly GpuWorldState _world;
|
||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState>
|
||||
_animations;
|
||||
private readonly RenderFrameDiagnosticsController _renderDiagnostics;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly StreamingController _streaming;
|
||||
private readonly ParticleSystem _particles;
|
||||
private readonly ParticleHookSink _particleSink;
|
||||
private readonly EntityEffectController _effects;
|
||||
private readonly LiveEntityLightController _lights;
|
||||
private readonly PhysicsScriptRunner _scripts;
|
||||
private readonly WbMeshAdapter _meshes;
|
||||
private readonly TextureCache _textures;
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly FrameProfiler _frameProfiler;
|
||||
|
||||
public WorldLifecycleResourceSnapshotSource(
|
||||
GpuWorldState world,
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
||||
RenderFrameDiagnosticsController renderDiagnostics,
|
||||
LiveEntityRuntime liveEntities,
|
||||
StreamingController streaming,
|
||||
ParticleSystem particles,
|
||||
ParticleHookSink particleSink,
|
||||
EntityEffectController effects,
|
||||
LiveEntityLightController lights,
|
||||
PhysicsScriptRunner scripts,
|
||||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
WbDrawDispatcher dispatcher,
|
||||
FrameProfiler frameProfiler)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations
|
||||
?? throw new ArgumentNullException(nameof(animations));
|
||||
_renderDiagnostics = renderDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(renderDiagnostics));
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_streaming = streaming
|
||||
?? throw new ArgumentNullException(nameof(streaming));
|
||||
_particles = particles
|
||||
?? throw new ArgumentNullException(nameof(particles));
|
||||
_particleSink = particleSink
|
||||
?? throw new ArgumentNullException(nameof(particleSink));
|
||||
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||
_scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
|
||||
_meshes = meshes ?? throw new ArgumentNullException(nameof(meshes));
|
||||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||||
_dispatcher = dispatcher
|
||||
?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_frameProfiler = frameProfiler
|
||||
?? throw new ArgumentNullException(nameof(frameProfiler));
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture()
|
||||
{
|
||||
ObjectMeshManager meshManager = _meshes.MeshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"Lifecycle snapshots require the composed modern mesh manager.");
|
||||
var mesh = meshManager.Diagnostics;
|
||||
RenderFrameDiagnosticsSnapshot render = _renderDiagnostics.Snapshot;
|
||||
GCMemoryInfo memory = GC.GetGCMemoryInfo();
|
||||
return new WorldLifecycleResourceSnapshot(
|
||||
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
||||
WorldEntities: _world.Entities.Count,
|
||||
AnimatedEntities: _animations.Count,
|
||||
VisibleLandblocks: render.VisibleLandblocks,
|
||||
TotalLandblocks: render.TotalLandblocks,
|
||||
LiveEntities: _liveEntities.Count,
|
||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||
Particles: _particles.ActiveParticleCount,
|
||||
ParticleBindings: _particleSink.ActiveBindingCount,
|
||||
ParticleOwners: _particleSink.TrackedOwnerCount,
|
||||
EffectOwners: _effects.ReadyOwnerCount,
|
||||
LightOwners: _lights.TrackedOwnerCount,
|
||||
ScriptOwners: _scripts.ActiveOwnerCount,
|
||||
ActiveScripts: _scripts.ActiveScriptCount,
|
||||
MeshRenderData: mesh.RenderData,
|
||||
MeshAtlasArrays: mesh.AtlasArrays,
|
||||
MeshEstimatedBytes: mesh.EstimatedBytes,
|
||||
StagedMeshUploads: _meshes.StagedUploadBacklog,
|
||||
StagedMeshBytes: _meshes.StagedUploadBytes,
|
||||
TrackedGpuBytes: GpuMemoryTracker.AllocatedBytes,
|
||||
TrackedGpuBuffers: GpuMemoryTracker.BufferCount,
|
||||
TrackedGpuTextures: GpuMemoryTracker.TextureCount,
|
||||
OwnedCompositeTextures: _textures.OwnedBindlessTextureCount,
|
||||
CompositeTextureOwners: _textures.TextureOwnerCount,
|
||||
ActiveParticleTextures: _textures.ActiveParticleTextureCount,
|
||||
ParticleTextureOwners: _textures.ParticleTextureOwnerCount,
|
||||
CompositeWarmupPending:
|
||||
_dispatcher.LastCompositeWarmupPendingCount,
|
||||
ManagedBytes: GC.GetTotalMemory(forceFullCollection: false),
|
||||
ManagedCommittedBytes: memory.TotalCommittedBytes,
|
||||
Fps: render.Fps,
|
||||
FrameMilliseconds: render.FrameMilliseconds,
|
||||
LastFrameProfile: _frameProfiler.LastReport);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,12 +29,28 @@ internal sealed class GameFrameGraphSlot
|
|||
public bool IsPublished => Volatile.Read(ref _pair) is not null;
|
||||
|
||||
public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render)
|
||||
{
|
||||
_ = PublishCore(update, render);
|
||||
}
|
||||
|
||||
public IDisposable PublishOwned(
|
||||
IGameUpdateFrameRoot update,
|
||||
IGameRenderFrameRoot render)
|
||||
{
|
||||
FrameGraphPair pair = PublishCore(update, render);
|
||||
return new Publication(this, pair);
|
||||
}
|
||||
|
||||
private FrameGraphPair PublishCore(
|
||||
IGameUpdateFrameRoot update,
|
||||
IGameRenderFrameRoot render)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(update);
|
||||
ArgumentNullException.ThrowIfNull(render);
|
||||
var pair = new FrameGraphPair(update, render);
|
||||
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
|
||||
throw new InvalidOperationException("A game frame graph is already published.");
|
||||
return pair;
|
||||
}
|
||||
|
||||
public bool Tick(UpdateFrameInput input)
|
||||
|
|
@ -64,4 +80,22 @@ internal sealed class GameFrameGraphSlot
|
|||
{
|
||||
Interlocked.Exchange(ref _pair, null);
|
||||
}
|
||||
|
||||
private void Withdraw(FrameGraphPair expected) =>
|
||||
Interlocked.CompareExchange(ref _pair, null, expected);
|
||||
|
||||
private sealed class Publication : IDisposable
|
||||
{
|
||||
private GameFrameGraphSlot? _owner;
|
||||
private readonly FrameGraphPair _expected;
|
||||
|
||||
public Publication(GameFrameGraphSlot owner, FrameGraphPair expected)
|
||||
{
|
||||
_owner = owner;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Withdraw(_expected);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ public sealed class GameWindow :
|
|||
IGameWindowWorldRenderPublication,
|
||||
IGameWindowInteractionRetainedUiPublication,
|
||||
IGameWindowLivePresentationPublication,
|
||||
IGameWindowSessionPlayerPublication
|
||||
IGameWindowSessionPlayerPublication,
|
||||
IGameWindowFrameRootPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -103,6 +104,8 @@ public sealed class GameWindow :
|
|||
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||
private FrameRootRuntimeBindings? _frameRootBindings;
|
||||
private IDisposable? _frameGraphPublication;
|
||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
||||
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
||||
|
|
@ -1040,6 +1043,23 @@ public sealed class GameWindow :
|
|||
_sessionPlayerBindings = result.RuntimeBindings;
|
||||
}
|
||||
|
||||
void IGameWindowFrameRootPublication.PublishFrameRoots(
|
||||
FrameRootResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
if (_worldLifecycleAutomation is not null
|
||||
|| _frameRootBindings is not null
|
||||
|| _frameGraphPublication is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns frame roots.");
|
||||
}
|
||||
|
||||
_worldLifecycleAutomation = result.LifecycleAutomation;
|
||||
_frameRootBindings = result.RuntimeBindings;
|
||||
_frameGraphPublication = result.FrameGraphPublication;
|
||||
}
|
||||
|
||||
private static void PublishCompositionOwner<T>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
|
|
@ -1355,268 +1375,53 @@ public sealed class GameWindow :
|
|||
interactionUi,
|
||||
livePresentation);
|
||||
|
||||
var teleportRenderState =
|
||||
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
||||
sessionPlayer.LocalTeleport);
|
||||
var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource(
|
||||
_options.LiveMode,
|
||||
_localPlayerMode);
|
||||
var renderFrameGlState = new AcDream.App.Rendering.RenderFrameGlStateController(
|
||||
new AcDream.App.Rendering.SilkRenderFrameGlStateApi(_gl!));
|
||||
var renderFrameLivePreparation =
|
||||
new AcDream.App.Rendering.RuntimeRenderFrameLivePreparation(
|
||||
_textureCache,
|
||||
_wbMeshAdapter,
|
||||
_worldReveal,
|
||||
teleportRenderState,
|
||||
renderLoginState,
|
||||
new AcDream.App.Rendering.LiveLoginRevealCellSource(
|
||||
_liveEntities!,
|
||||
_localPlayerIdentity),
|
||||
_particleRenderer,
|
||||
_frameProfiler,
|
||||
_frameDiag);
|
||||
var renderFrameResources =
|
||||
new AcDream.App.Rendering.RenderFrameResourceController(
|
||||
_gpuFrameFlights!,
|
||||
new AcDream.App.Rendering.RuntimeRenderFrameBeginResources(
|
||||
_textureCache,
|
||||
_wbDrawDispatcher,
|
||||
_envCellRenderer,
|
||||
_portalDepthMask,
|
||||
_textRenderer,
|
||||
_uiHost?.TextRenderer,
|
||||
_clipFrame,
|
||||
_terrain,
|
||||
_sceneLightingUbo,
|
||||
_frameProfiler,
|
||||
_gl!),
|
||||
new AcDream.App.Rendering.RuntimeRenderFrameClearPhase(
|
||||
_gl!,
|
||||
WorldTime,
|
||||
Weather,
|
||||
teleportRenderState,
|
||||
_particleVisibility,
|
||||
worldRenderDiagnostics,
|
||||
renderFrameGlState),
|
||||
renderFrameLivePreparation);
|
||||
var renderWeatherFrame =
|
||||
new AcDream.App.Rendering.RenderWeatherFrameController(
|
||||
WorldTime,
|
||||
Weather);
|
||||
var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(
|
||||
_scriptRunner!,
|
||||
_particleSink!,
|
||||
_effectPoses,
|
||||
_entityEffects);
|
||||
var worldFrameEnvironment =
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameEnvironmentPreparation(
|
||||
FrameRootResult frameRoots = new FrameRootCompositionPhase(
|
||||
new FrameRootDependencies(
|
||||
_options,
|
||||
WorldTime,
|
||||
Lighting,
|
||||
_wbDrawDispatcher,
|
||||
_envCellRenderer,
|
||||
_sceneLightingUbo,
|
||||
_renderRange,
|
||||
skyPesFrame);
|
||||
var worldRenderFrameBuilder =
|
||||
new AcDream.App.Rendering.WorldRenderFrameBuilder(
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
|
||||
_cameraController!,
|
||||
sessionPlayer.LocalTeleport),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation(
|
||||
_retailSelectionScene,
|
||||
_particleVisibility,
|
||||
_terrain,
|
||||
_worldReveal,
|
||||
_envCellFrustum),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameSettingsPreview(
|
||||
_runtimeSettings,
|
||||
_audioEngine,
|
||||
_cameraController!,
|
||||
_displayFramePacing),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameRootSource(
|
||||
_physicsEngine,
|
||||
_cellVisibility,
|
||||
_localPlayerMode,
|
||||
_chaseCameraInput,
|
||||
_playerControllerSlot,
|
||||
_liveWorldOrigin),
|
||||
worldFrameEnvironment,
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameAnimatedEntitySource(
|
||||
_animatedEntities,
|
||||
_staticAnimationScheduler,
|
||||
_equippedChildRenderer),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource(
|
||||
_landblockPresentationPipeline!,
|
||||
_cellVisibility));
|
||||
var terrainDrawDiagnostics =
|
||||
new AcDream.App.Rendering.TerrainDrawDiagnosticsController(
|
||||
_frameDiag,
|
||||
worldRenderDiagnostics,
|
||||
new AcDream.App.Rendering.RuntimeFramePipelineDiagnosticFactsSource(
|
||||
_terrain,
|
||||
_landblockPresentationPipeline,
|
||||
renderFrameLivePreparation,
|
||||
_wbDrawDispatcher,
|
||||
_streamingController,
|
||||
_liveEntities!,
|
||||
_worldState),
|
||||
_renderDiagnosticLog);
|
||||
var retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource(
|
||||
_cellVisibility);
|
||||
var retailPViewPassExecutor =
|
||||
new AcDream.App.Rendering.RetailPViewPassExecutor(
|
||||
_gl!,
|
||||
renderFrameGlState,
|
||||
new AcDream.App.Rendering.SilkRetailPViewFramebufferSource(
|
||||
_window!),
|
||||
_clipFrame!,
|
||||
_terrain,
|
||||
_envCellRenderer!,
|
||||
_wbDrawDispatcher!,
|
||||
_skyRenderer,
|
||||
_particleSystem,
|
||||
_particleRenderer,
|
||||
_portalDepthMask,
|
||||
_retailAlphaQueue,
|
||||
worldRenderDiagnostics,
|
||||
terrainDrawDiagnostics);
|
||||
var worldSceneDiagnostics =
|
||||
new AcDream.App.Rendering.WorldSceneDiagnosticsController(
|
||||
worldRenderDiagnostics,
|
||||
new AcDream.App.Rendering.RuntimeWorldScenePViewDiagnosticSource(
|
||||
_playerControllerSlot,
|
||||
_physicsEngine,
|
||||
_cellVisibility),
|
||||
_worldSceneDebugState,
|
||||
_debugLines,
|
||||
_window!,
|
||||
_input!,
|
||||
WorldTime,
|
||||
Weather,
|
||||
Lighting,
|
||||
_worldEnvironment,
|
||||
_physicsEngine,
|
||||
_cellVisibility,
|
||||
_localPlayerMode,
|
||||
_localPlayerIdentity,
|
||||
_chaseCameraInput,
|
||||
_playerControllerSlot,
|
||||
_debugVmRenderFacts,
|
||||
_debugVm is not null);
|
||||
var worldScenePasses = new AcDream.App.Rendering.WorldScenePassExecutor(
|
||||
_gl!,
|
||||
renderFrameGlState,
|
||||
_clipFrame!,
|
||||
_wbDrawDispatcher!,
|
||||
_envCellRenderer!,
|
||||
_terrain,
|
||||
terrainDrawDiagnostics,
|
||||
_skyRenderer,
|
||||
_particleSystem,
|
||||
_particleRenderer);
|
||||
var worldSceneRenderer = new AcDream.App.Rendering.WorldSceneRenderer(
|
||||
renderFrameResources,
|
||||
renderLoginState,
|
||||
_worldEnvironment,
|
||||
worldRenderFrameBuilder,
|
||||
new AcDream.App.Rendering.RuntimeWorldSceneEntitySource(_worldState),
|
||||
_retailSelectionScene,
|
||||
_retailAlphaQueue,
|
||||
_particleVisibility,
|
||||
new AcDream.App.Rendering.WorldScenePViewRenderer(
|
||||
new AcDream.App.Rendering.RetailPViewRenderer(),
|
||||
retailPViewPassExecutor,
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
worldScenePasses,
|
||||
_renderRange,
|
||||
worldSceneDiagnostics);
|
||||
if (_frameScreenshots is { } screenshots
|
||||
&& _options.AutomationArtifactDirectory is { } artifactDirectory)
|
||||
{
|
||||
void UiProbeLog(string message) =>
|
||||
Console.WriteLine("[UI-PROBE] " + message);
|
||||
_worldLifecycleAutomation =
|
||||
new AcDream.App.Diagnostics.WorldLifecycleAutomationController(
|
||||
() => _worldReveal?.Snapshot ?? default,
|
||||
() => _worldReveal?.PortalMaterializationCount ?? 0,
|
||||
CaptureWorldLifecycleResourceSnapshot,
|
||||
screenshots,
|
||||
artifactDirectory,
|
||||
UiProbeLog);
|
||||
interactionUi.LateBindings.AdoptLateOwnerBinding(
|
||||
"world lifecycle automation",
|
||||
interactionUi.LateBindings.Automation.Bind(
|
||||
_worldLifecycleAutomation));
|
||||
}
|
||||
AcDream.App.Rendering.IRetainedGameplayUiFrame? retainedGameplayUi =
|
||||
_options.RetailUi && _retailUiRuntime is not null
|
||||
? new AcDream.App.Rendering.RetainedGameplayUiFrame(
|
||||
_retailUiRuntime,
|
||||
_input)
|
||||
: null;
|
||||
AcDream.App.Rendering.IPrivateFrameScreenshot? privateScreenshot =
|
||||
_frameScreenshots is not null
|
||||
? new AcDream.App.Rendering.PrivateFrameScreenshot(
|
||||
_frameScreenshots)
|
||||
: null;
|
||||
var privatePresentation =
|
||||
new AcDream.App.Rendering.PrivatePresentationRenderer(
|
||||
new AcDream.App.Rendering.LocalPlayerPortalViewport(
|
||||
sessionPlayer.LocalTeleport,
|
||||
_cameraController!),
|
||||
renderFrameResources,
|
||||
_paperdollFramePresenter,
|
||||
retainedGameplayUi,
|
||||
_devToolsFramePresenter,
|
||||
privateScreenshot);
|
||||
var framePreparation =
|
||||
new AcDream.App.Rendering.RenderFramePreparationController(
|
||||
renderFrameResources,
|
||||
_devToolsFramePresenter,
|
||||
renderWeatherFrame);
|
||||
var renderFrameOrchestrator =
|
||||
new AcDream.App.Rendering.RenderFrameOrchestrator(
|
||||
_gpuFrameFlights!,
|
||||
framePreparation,
|
||||
worldSceneRenderer,
|
||||
privatePresentation,
|
||||
_renderFrameDiagnostics!,
|
||||
(AcDream.App.Rendering.IRenderFrameFailureRecovery?)
|
||||
_devToolsFramePresenter
|
||||
?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance);
|
||||
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||
sessionPlayer.LiveObjectFrame,
|
||||
_worldState,
|
||||
sessionPlayer.LiveSession,
|
||||
sessionPlayer.LocalPlayerFrame,
|
||||
sessionPlayer.LiveSpatialReconciler);
|
||||
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||
_cameraController!,
|
||||
_inputCapture,
|
||||
_cameraInput,
|
||||
sessionPlayer.LocalPlayerFrameRuntime,
|
||||
_chaseCameraInput,
|
||||
sessionPlayer.LocalPlayerFrame,
|
||||
sessionPlayer.LiveSpatialReconciler,
|
||||
new AcDream.App.Combat.CombatCameraTargetSource(
|
||||
_liveWorldOrigin,
|
||||
_particleVisibility,
|
||||
_effectPoses,
|
||||
_renderRange,
|
||||
_runtimeSettings,
|
||||
Combat,
|
||||
_displayFramePacing,
|
||||
_worldSceneDebugState,
|
||||
_retailAlphaQueue,
|
||||
_frameProfiler,
|
||||
_frameDiag,
|
||||
_renderDiagnosticLog,
|
||||
_debugVmRenderFacts,
|
||||
_inputCapture,
|
||||
_cameraInput,
|
||||
_selection,
|
||||
_worldSelectionQuery!));
|
||||
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||
livePresentation.LiveEntities),
|
||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||
_updateFrameClock,
|
||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||
sessionPlayer.StreamingFrame,
|
||||
sessionPlayer.GameplayInput,
|
||||
liveFrameCoordinator,
|
||||
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
||||
sessionPlayer.Liveness,
|
||||
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
||||
sessionPlayer.LocalTeleport,
|
||||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||
sessionPlayer.PlayerModeAutoEntry),
|
||||
cameraFrame);
|
||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
||||
_animatedEntities,
|
||||
_updateFrameClock,
|
||||
Combat,
|
||||
_frameGraphs,
|
||||
Console.WriteLine),
|
||||
this).Compose(
|
||||
hostInputCamera,
|
||||
contentEffectsAudio,
|
||||
settingsDevTools,
|
||||
worldRender,
|
||||
interactionUi,
|
||||
livePresentation,
|
||||
sessionPlayer);
|
||||
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
sessionPlayer.SessionHost.Start(_options);
|
||||
frameRoots.SessionHost.Start(_options);
|
||||
switch (liveStart.Status)
|
||||
{
|
||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||
|
|
@ -1649,54 +1454,6 @@ public sealed class GameWindow :
|
|||
out _);
|
||||
}
|
||||
|
||||
private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot
|
||||
CaptureWorldLifecycleResourceSnapshot()
|
||||
{
|
||||
var meshManager = _wbMeshAdapter?.MeshManager;
|
||||
var mesh = meshManager?.Diagnostics ?? default;
|
||||
AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot render =
|
||||
_renderFrameDiagnostics?.Snapshot
|
||||
?? AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot.Initial;
|
||||
GCMemoryInfo memory = GC.GetGCMemoryInfo();
|
||||
return new AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot(
|
||||
LoadedLandblocks: _worldState.LoadedLandblockIds.Count,
|
||||
WorldEntities: _worldState.Entities.Count,
|
||||
AnimatedEntities: _animatedEntities.Count,
|
||||
VisibleLandblocks: render.VisibleLandblocks,
|
||||
TotalLandblocks: render.TotalLandblocks,
|
||||
LiveEntities: _liveEntities?.Count ?? 0,
|
||||
MaterializedLiveEntities: _liveEntities?.MaterializedCount ?? 0,
|
||||
PendingLiveTeardowns: _liveEntities?.PendingTeardownCount ?? 0,
|
||||
PendingLandblockRetirements:
|
||||
_streamingController?.PendingRetirementCount ?? 0,
|
||||
ParticleEmitters: _particleSystem?.ActiveEmitterCount ?? 0,
|
||||
Particles: _particleSystem?.ActiveParticleCount ?? 0,
|
||||
ParticleBindings: _particleSink?.ActiveBindingCount ?? 0,
|
||||
ParticleOwners: _particleSink?.TrackedOwnerCount ?? 0,
|
||||
EffectOwners: _entityEffects?.ReadyOwnerCount ?? 0,
|
||||
LightOwners: _liveEntityLights?.TrackedOwnerCount ?? 0,
|
||||
ScriptOwners: _scriptRunner?.ActiveOwnerCount ?? 0,
|
||||
ActiveScripts: _scriptRunner?.ActiveScriptCount ?? 0,
|
||||
MeshRenderData: mesh.RenderData,
|
||||
MeshAtlasArrays: mesh.AtlasArrays,
|
||||
MeshEstimatedBytes: mesh.EstimatedBytes,
|
||||
StagedMeshUploads: _wbMeshAdapter?.StagedUploadBacklog ?? 0,
|
||||
StagedMeshBytes: _wbMeshAdapter?.StagedUploadBytes ?? 0,
|
||||
TrackedGpuBytes: AcDream.App.Rendering.Wb.GpuMemoryTracker.AllocatedBytes,
|
||||
TrackedGpuBuffers: AcDream.App.Rendering.Wb.GpuMemoryTracker.BufferCount,
|
||||
TrackedGpuTextures: AcDream.App.Rendering.Wb.GpuMemoryTracker.TextureCount,
|
||||
OwnedCompositeTextures: _textureCache?.OwnedBindlessTextureCount ?? 0,
|
||||
CompositeTextureOwners: _textureCache?.TextureOwnerCount ?? 0,
|
||||
ActiveParticleTextures: _textureCache?.ActiveParticleTextureCount ?? 0,
|
||||
ParticleTextureOwners: _textureCache?.ParticleTextureOwnerCount ?? 0,
|
||||
CompositeWarmupPending: _wbDrawDispatcher?.LastCompositeWarmupPendingCount ?? 0,
|
||||
ManagedBytes: GC.GetTotalMemory(forceFullCollection: false),
|
||||
ManagedCommittedBytes: memory.TotalCommittedBytes,
|
||||
Fps: render.Fps,
|
||||
FrameMilliseconds: render.FrameMilliseconds,
|
||||
LastFrameProfile: _frameProfiler.LastReport);
|
||||
}
|
||||
|
||||
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
||||
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
||||
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
||||
|
|
@ -1871,7 +1628,20 @@ public sealed class GameWindow :
|
|||
[
|
||||
new("world frame composition", () =>
|
||||
{
|
||||
_frameGraphs.Withdraw();
|
||||
IDisposable? publication = _frameGraphPublication;
|
||||
if (publication is null)
|
||||
return;
|
||||
publication.Dispose();
|
||||
_frameGraphPublication = null;
|
||||
}),
|
||||
new("frame-root bindings", () =>
|
||||
{
|
||||
FrameRootRuntimeBindings? bindings = _frameRootBindings;
|
||||
if (bindings is null)
|
||||
return;
|
||||
bindings.Dispose();
|
||||
_frameRootBindings = null;
|
||||
_worldLifecycleAutomation = null;
|
||||
}),
|
||||
new("session/player bindings", () =>
|
||||
{
|
||||
|
|
|
|||
126
tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs
Normal file
126
tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System.Reflection;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
public sealed class FrameRootCompositionTests
|
||||
{
|
||||
[Fact]
|
||||
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var bindings = new FrameRootRuntimeBindings();
|
||||
bindings.Adopt("first", new RetryBinding("first", calls, 0));
|
||||
bindings.Adopt("second", new RetryBinding("second", calls, 1));
|
||||
|
||||
Assert.Throws<AggregateException>(bindings.Dispose);
|
||||
Assert.Equal(["second", "first"], calls);
|
||||
|
||||
bindings.Dispose();
|
||||
bindings.Dispose();
|
||||
|
||||
Assert.Equal(["second", "first", "second"], calls);
|
||||
Assert.Throws<ObjectDisposedException>(() =>
|
||||
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionPhasePublishesOnlyAfterBothRootsExist()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new RenderFrameResourceController(",
|
||||
"new WorldSceneRenderer(",
|
||||
"new RenderFrameOrchestrator(",
|
||||
"new RetailLiveFrameCoordinator(",
|
||||
"new UpdateFrameOrchestrator(",
|
||||
"d.FrameGraphs.PublishOwned(",
|
||||
"_publication.PublishFrameRoots(result);",
|
||||
"graphLease.Transfer();",
|
||||
"bindingsLease.Transfer();");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
Assert.Contains("new FrameRootCompositionPhase(", source);
|
||||
Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source);
|
||||
Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source);
|
||||
Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source);
|
||||
Assert.DoesNotContain("_frameGraphs.Publish(", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FramePhaseAndSnapshotSourceRetainNoWindowOwner()
|
||||
{
|
||||
Assert.DoesNotContain(
|
||||
typeof(FrameRootCompositionPhase).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.FieldType == typeof(GameWindow));
|
||||
|
||||
string snapshots = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Diagnostics",
|
||||
"WorldLifecycleResourceSnapshotSource.cs"));
|
||||
Assert.DoesNotContain("GameWindow", snapshots, StringComparison.Ordinal);
|
||||
Assert.Contains("_liveEntities.PendingTeardownCount", snapshots);
|
||||
Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots);
|
||||
Assert.Contains("_frameProfiler.LastReport", snapshots);
|
||||
}
|
||||
|
||||
private sealed class RetryBinding(
|
||||
string name,
|
||||
List<string> calls,
|
||||
int failures) : IDisposable
|
||||
{
|
||||
private int _failures = failures;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
calls.Add(name);
|
||||
if (_failures-- > 0)
|
||||
throw new InvalidOperationException("retry");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] values)
|
||||
{
|
||||
int cursor = 0;
|
||||
foreach (string value in values)
|
||||
{
|
||||
int found = source.IndexOf(value, cursor, StringComparison.Ordinal);
|
||||
Assert.True(found >= 0, $"Missing expected source fragment: {value}");
|
||||
cursor = found + value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ public sealed class GameWindowCompositionPipelineTests
|
|||
IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>,
|
||||
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>,
|
||||
ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token>,
|
||||
IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token>,
|
||||
IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>,
|
||||
ISessionStartCompositionPhase<Token>
|
||||
{
|
||||
public Token Platform { get; } = new(0);
|
||||
|
|
@ -155,8 +155,9 @@ public sealed class GameWindowCompositionPipelineTests
|
|||
return _session;
|
||||
}
|
||||
|
||||
Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token>.Compose(
|
||||
Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>.Compose(
|
||||
Token host,
|
||||
Token content,
|
||||
Token settings,
|
||||
Token world,
|
||||
Token interaction,
|
||||
|
|
@ -164,6 +165,7 @@ public sealed class GameWindowCompositionPipelineTests
|
|||
Token session)
|
||||
{
|
||||
Assert.Same(_host, host);
|
||||
Assert.Same(_content, content);
|
||||
Assert.Same(_settings, settings);
|
||||
Assert.Same(_world, world);
|
||||
Assert.Same(_interaction, interaction);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
string source = GameWindowSource();
|
||||
string framePhase = FrameRootSource();
|
||||
|
||||
AssertAppearsInOrder(
|
||||
phase,
|
||||
|
|
@ -57,10 +58,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new SessionPlayerCompositionPhase(",
|
||||
"new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(",
|
||||
"new AcDream.App.Rendering.RenderFrameResourceController(",
|
||||
"new AcDream.App.Rendering.PrivatePresentationRenderer(",
|
||||
"new AcDream.App.Rendering.RenderFrameOrchestrator(");
|
||||
"new FrameRootCompositionPhase(");
|
||||
AssertAppearsInOrder(
|
||||
framePhase,
|
||||
"new LocalPlayerTeleportRenderStateSource(",
|
||||
"new RenderFrameResourceController(",
|
||||
"new PrivatePresentationRenderer(",
|
||||
"new RenderFrameOrchestrator(");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -111,10 +115,11 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"Composition",
|
||||
"SettingsDevToolsComposition.cs"));
|
||||
Assert.Contains("new DevToolsFramePresenter(", settingsComposition);
|
||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameResourceController(", source);
|
||||
Assert.Contains("new AcDream.App.Rendering.RenderWeatherFrameController(", source);
|
||||
Assert.Contains("new AcDream.App.Rendering.PrivatePresentationRenderer(", source);
|
||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source);
|
||||
string framePhase = FrameRootSource();
|
||||
Assert.Contains("new RenderFrameResourceController(", framePhase);
|
||||
Assert.Contains("new RenderWeatherFrameController(", framePhase);
|
||||
Assert.Contains("new PrivatePresentationRenderer(", framePhase);
|
||||
Assert.Contains("new RenderFrameOrchestrator(", framePhase);
|
||||
Assert.Contains("new DisplayFramePacingController(", source);
|
||||
string pointerSource = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
|
|
@ -142,7 +147,8 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||
"_frameGraphs.Withdraw();",
|
||||
"publication.Dispose();",
|
||||
"bindings.Dispose();",
|
||||
"new ResourceShutdownStage(\"session dependents\"");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
|
|
@ -150,7 +156,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"new(\"frame profiler\", _frameProfiler.Dispose)");
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_frameGraphs.Withdraw();",
|
||||
"publication.Dispose();",
|
||||
"owner.DisposeFrontend()",
|
||||
"new ResourceShutdownStage(\"input\"",
|
||||
"_input?.Dispose();",
|
||||
|
|
@ -186,7 +192,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"private void OnRender(double deltaSeconds)",
|
||||
StringComparison.Ordinal);
|
||||
int end = source.IndexOf(
|
||||
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot",
|
||||
"private void OnFramebufferResize(",
|
||||
start,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
|
|
@ -216,12 +222,12 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
[Fact]
|
||||
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
string source = FrameRootSource();
|
||||
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Rendering.TerrainDrawDiagnosticsController(",
|
||||
"new TerrainDrawDiagnosticsController(",
|
||||
source);
|
||||
Assert.Contains("new AcDream.App.Rendering.WorldScenePassExecutor(", source);
|
||||
Assert.Contains("new WorldScenePassExecutor(", source);
|
||||
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
|
||||
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
|
||||
}
|
||||
|
|
@ -240,6 +246,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
"Composition",
|
||||
"LivePresentationComposition.cs"));
|
||||
|
||||
private static string FrameRootSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
|
|||
|
|
@ -65,10 +65,21 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"new LivePresentationCompositionPhase(",
|
||||
"this).Compose(",
|
||||
"new SessionPlayerCompositionPhase(",
|
||||
"var renderFrameOrchestrator =",
|
||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
||||
"sessionPlayer.SessionHost.Start(_options)");
|
||||
"FrameRootResult frameRoots = new FrameRootCompositionPhase(",
|
||||
"frameRoots.SessionHost.Start(_options)");
|
||||
|
||||
string framePhase = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
AssertAppearsInOrder(
|
||||
framePhase,
|
||||
"new RenderFrameOrchestrator(",
|
||||
"new UpdateFrameOrchestrator(",
|
||||
"d.FrameGraphs.PublishOwned(",
|
||||
"_publication.PublishFrameRoots(result);");
|
||||
|
||||
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
|
|
@ -86,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_publication.PublishSessionPlayer(result);");
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
body,
|
||||
"sessionPlayer.SessionHost.Start(_options)"));
|
||||
"frameRoots.SessionHost.Start(_options)"));
|
||||
string phaseOne = Slice(
|
||||
File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
|
|
@ -107,7 +118,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
phaseOne,
|
||||
StringComparison.Ordinal);
|
||||
int start = body.IndexOf(
|
||||
"sessionPlayer.SessionHost.Start(_options)",
|
||||
"frameRoots.SessionHost.Start(_options)",
|
||||
StringComparison.Ordinal);
|
||||
string postStart = body[start..];
|
||||
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
||||
|
|
@ -331,7 +342,8 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
||||
"new WorldRenderCompositionPhase(",
|
||||
"new SessionPlayerCompositionPhase(",
|
||||
"sessionPlayer.SessionHost.Start(_options)");
|
||||
"new FrameRootCompositionPhase(",
|
||||
"frameRoots.SessionHost.Start(_options)");
|
||||
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
@ -385,7 +397,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
string render = Slice(
|
||||
source,
|
||||
"private void OnRender(double deltaSeconds)",
|
||||
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot");
|
||||
"private void OnFramebufferResize(");
|
||||
string focus = Slice(
|
||||
source,
|
||||
"private void OnFocusChanged(bool focused)",
|
||||
|
|
@ -524,6 +536,12 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
string framePhase = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
AssertAppearsInOrder(
|
||||
livePhase,
|
||||
|
|
@ -536,7 +554,12 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"new WorldRenderCompositionPhase(",
|
||||
"new LivePresentationCompositionPhase(",
|
||||
"new SessionPlayerCompositionPhase(",
|
||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
||||
"new FrameRootCompositionPhase(");
|
||||
AssertAppearsInOrder(
|
||||
framePhase,
|
||||
"new RenderFrameOrchestrator(",
|
||||
"new UpdateFrameOrchestrator(",
|
||||
"d.FrameGraphs.PublishOwned(");
|
||||
AssertAppearsInOrder(
|
||||
sessionPhase,
|
||||
"d.PortalTunnelFallback.Transfer(",
|
||||
|
|
@ -550,7 +573,8 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"TerrainModernRenderer terrain = AcquireAndPublish(");
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"_frameGraphs.Withdraw();",
|
||||
"publication.Dispose();",
|
||||
"bindings.Dispose();",
|
||||
"_retailUiLease.Dispose();",
|
||||
"_localPlayerTeleport?.Dispose();",
|
||||
"_portalTunnelFallback.ReleaseFallback();",
|
||||
|
|
|
|||
|
|
@ -225,11 +225,11 @@ public sealed class RetailPViewPassExecutorTests
|
|||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source);
|
||||
Assert.Contains("new AcDream.App.Rendering.WorldScenePViewRenderer(", source);
|
||||
Assert.Contains("new RetailPViewPassExecutor(", source);
|
||||
Assert.Contains("new WorldScenePViewRenderer(", source);
|
||||
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
|
||||
Assert.DoesNotContain("RetailPViewDrawContext", source);
|
||||
Assert.DoesNotContain("DrawLandscapeSlice =", source);
|
||||
|
|
|
|||
|
|
@ -152,6 +152,28 @@ public sealed class RuntimeResourceSlotTests
|
|||
Assert.Equal(["update:2"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnedFramePublicationWithdrawsOnlyItsExactPair()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var slot = new GameFrameGraphSlot();
|
||||
IDisposable stale = slot.PublishOwned(
|
||||
new RecordingUpdateRoot(calls),
|
||||
new RecordingRenderRoot(calls));
|
||||
|
||||
stale.Dispose();
|
||||
IDisposable current = slot.PublishOwned(
|
||||
new RecordingUpdateRoot(calls),
|
||||
new RecordingRenderRoot(calls));
|
||||
stale.Dispose();
|
||||
|
||||
Assert.True(slot.Tick(new UpdateFrameInput(3)));
|
||||
current.Dispose();
|
||||
current.Dispose();
|
||||
Assert.False(slot.Tick(new UpdateFrameInput(4)));
|
||||
Assert.Equal(["update:3"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnedSlotRejectsReentrantAcquireWithoutLosingOuterResource()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -328,6 +328,12 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition()
|
||||
{
|
||||
string gameWindow = GameWindowSource();
|
||||
string frameRoot = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
string worldScene = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
|
|
@ -357,11 +363,14 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
"_passes.DrawFlatTerrain(",
|
||||
"NormalWorldDrawn: true");
|
||||
AssertAppearsInOrder(
|
||||
gameWindow,
|
||||
"var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(",
|
||||
frameRoot,
|
||||
"var skyPesFrame = new SkyPesFrameController(",
|
||||
"var worldRenderFrameBuilder =",
|
||||
"new AcDream.App.Rendering.RenderFrameOrchestrator(",
|
||||
"_frameGraphs.Withdraw();",
|
||||
"new RenderFrameOrchestrator(",
|
||||
"d.FrameGraphs.PublishOwned(");
|
||||
AssertAppearsInOrder(
|
||||
gameWindow,
|
||||
"publication.Dispose();",
|
||||
"new(\"equipped children\"",
|
||||
"new(\"effect network state\"",
|
||||
"new(\"audio\"",
|
||||
|
|
|
|||
|
|
@ -279,11 +279,11 @@ public sealed class WorldSceneRendererTests
|
|||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Rendering.WorldSceneRenderer(", source);
|
||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source);
|
||||
Assert.Contains("new WorldSceneRenderer(", source);
|
||||
Assert.Contains("new RenderFrameOrchestrator(", source);
|
||||
Assert.DoesNotContain("_worldSceneRenderer", source);
|
||||
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
|
||||
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);
|
||||
|
|
|
|||
|
|
@ -276,10 +276,16 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Update",
|
||||
"UpdateFrameRuntimeAdapters.cs"));
|
||||
string frameRoot = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
Assert.Equal(0, CountOccurrences(source, "PublishTime("));
|
||||
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
|
||||
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source,
|
||||
Assert.Contains("new PhysicsScriptClockPublisher(", frameRoot,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
|
@ -349,10 +355,16 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
string frameRoot = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
Assert.Contains("new LiveObjectFrameController(", sessionPlayer);
|
||||
Assert.Contains("new LiveSpatialPresentationReconciler(", sessionPlayer);
|
||||
Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source);
|
||||
Assert.Contains("new RetailLiveFrameCoordinator(", frameRoot);
|
||||
Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
||||
StringComparison.Ordinal);
|
||||
|
|
@ -375,7 +387,6 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
|
||||
Assert.Contains("new StreamingFrameController(", sessionPlayer);
|
||||
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
|
|
@ -492,6 +503,12 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Composition",
|
||||
"SessionPlayerComposition.cs"));
|
||||
string frameRoot = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"FrameRootComposition.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"new LocalPlayerTeleportController(",
|
||||
|
|
@ -513,10 +530,10 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
||||
"sessionPlayer.LocalTeleport,",
|
||||
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
||||
frameRoot,
|
||||
"new LiveEntityLivenessFramePhase(",
|
||||
"session.LocalTeleport,",
|
||||
"new PlayerModeAutoEntryFramePhase(",
|
||||
"cameraFrame);");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue