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
|
I.7 now owns streaming/session/hydration/local-player/teleport startup and
|
||||||
every late edge explicitly. I.8a now owns the focused session factory,
|
every late edge explicitly. I.8a now owns the focused session factory,
|
||||||
command targets, and sole gameplay input subscriber before frame publication.
|
command targets, and sole gameplay input subscriber before frame publication.
|
||||||
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
I.8b now owns the complete update/render construction, exact atomic frame
|
||||||
handoffs and the class is 2,149 raw lines. I.8b–I.9 plus the remaining
|
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
|
shutdown/closeout
|
||||||
cleanup is active in
|
cleanup is active in
|
||||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
[`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
|
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
|
gone. I.8a moves the exact live-session reset/router graph, combat and
|
||||||
diagnostic command targets, and sole gameplay input subscriber into Phase 7
|
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
|
before frame publication. I.8b moves the complete update/render construction
|
||||||
remain active, with atomic frame-root publication next. The App gate passes
|
body into `FrameRootCompositionPhase`, publishes the pair through an exact
|
||||||
3,424 tests / 3 intentional skips and the complete Release suite passes 7,796
|
lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
|
||||||
tests / 5 intentional skips. The clean solution build retains only the 17
|
1,919 raw lines. I.8c–L remain active, with terminal session start next. The
|
||||||
test-project warnings tracked by #228; the I.8a behavior, architecture, and
|
App gate passes 3,429 tests / 3 intentional skips and the complete Release suite
|
||||||
adversarial reviews are clean.
|
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
|
### 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.
|
resources have transactional phase ownership and exact-owner late bindings.
|
||||||
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
||||||
including explicit reversible ownership for every late edge. `GameWindow` is
|
including explicit reversible ownership for every late edge. `GameWindow` is
|
||||||
now 2,149 raw lines after I.8a, which composes the session factory, command
|
now 1,919 raw lines after I.8b, which also composes and atomically publishes the
|
||||||
targets, and sole gameplay input subscriber before frame publication. I.8b–I.9,
|
complete update/render pair and extracts lifecycle resource sampling. I.8c–I.9,
|
||||||
shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The
|
shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The
|
||||||
App gate passes 3,424 tests / 3 intentional skips, the complete Release suite
|
App gate passes 3,429 tests / 3 intentional skips, the complete Release suite
|
||||||
passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
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
|
This is a behavior-preserving structural program. Severe regressions still get
|
||||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
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,
|
ownership and exact-owner late bindings. I.7 now owns the complete streaming,
|
||||||
session, hydration, local-player, combat, and teleport startup graph, including
|
session, hydration, local-player, combat, and teleport startup graph, including
|
||||||
named reversible late edges. I.8a adds the focused live-session runtime factory
|
named reversible late edges. I.8a adds the focused live-session runtime factory
|
||||||
and attaches command/input owners before frame publication. `GameWindow` is
|
and attaches command/input owners before frame publication. I.8b composes and
|
||||||
2,149 raw lines. I.8b–I.9 and checkpoints J–L remain active. Issue #232 tracks
|
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.
|
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
|
The App gate passes 3,429 tests / 3 skips, the complete Release suite passes
|
||||||
7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
7,801 tests / 5 skips, and the I.8b corrected-diff review is clean.
|
||||||
|
|
||||||
Carried:
|
Carried:
|
||||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
#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
|
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).
|
[`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
|
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;
|
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
|
I.8b composes the unchanged update/render graph in a focused phase, publishes
|
||||||
active cut.
|
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.
|
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
||||||
|
|
||||||
## 6. Automated acceptance
|
## 6. Automated acceptance
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
|
# 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`
|
**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
|
## 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
|
Add `FrameRootCompositionPhase` implementing the existing
|
||||||
`IFrameRootCompositionPhase` contract. Its dependencies are focused App owners;
|
`IFrameRootCompositionPhase` contract. Its dependencies are focused App owners;
|
||||||
prior phase results supply all resources created during startup.
|
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
|
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
||||||
explicit single ownership and transfer seams.
|
explicit single ownership and transfer seams.
|
||||||
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
||||||
transactional partial-acquisition rollback. I.1–I.7 and I.8a are complete;
|
transactional partial-acquisition rollback. I.1–I.8b are complete; I.8c
|
||||||
I.8b atomic frame-root publication is active. Detailed plan:
|
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).
|
[`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
|
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
||||||
owner and prove all partial-load/reentrant/retry paths.
|
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);
|
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(
|
TResult Compose(
|
||||||
THost host,
|
THost host,
|
||||||
|
TContent content,
|
||||||
TSettings settings,
|
TSettings settings,
|
||||||
TWorld world,
|
TWorld world,
|
||||||
TInteraction interaction,
|
TInteraction interaction,
|
||||||
|
|
@ -79,7 +80,7 @@ internal sealed class GameWindowCompositionPipeline<
|
||||||
private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction;
|
private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction;
|
||||||
private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live;
|
private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live;
|
||||||
private readonly ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> _session;
|
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;
|
private readonly ISessionStartCompositionPhase<TFrame> _start;
|
||||||
|
|
||||||
public GameWindowCompositionPipeline(
|
public GameWindowCompositionPipeline(
|
||||||
|
|
@ -90,7 +91,7 @@ internal sealed class GameWindowCompositionPipeline<
|
||||||
IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction,
|
IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction,
|
||||||
ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live,
|
ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live,
|
||||||
ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> session,
|
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)
|
ISessionStartCompositionPhase<TFrame> start)
|
||||||
{
|
{
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
|
|
@ -113,7 +114,14 @@ internal sealed class GameWindowCompositionPipeline<
|
||||||
TInteraction interaction = _interaction.Compose(host, content, settings, world);
|
TInteraction interaction = _interaction.Compose(host, content, settings, world);
|
||||||
TLive live = _live.Compose(host, content, world, interaction);
|
TLive live = _live.Compose(host, content, world, interaction);
|
||||||
TSession session = _session.Compose(host, content, world, interaction, live);
|
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);
|
_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 bool IsPublished => Volatile.Read(ref _pair) is not null;
|
||||||
|
|
||||||
public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render)
|
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(update);
|
||||||
ArgumentNullException.ThrowIfNull(render);
|
ArgumentNullException.ThrowIfNull(render);
|
||||||
var pair = new FrameGraphPair(update, render);
|
var pair = new FrameGraphPair(update, render);
|
||||||
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
|
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
|
||||||
throw new InvalidOperationException("A game frame graph is already published.");
|
throw new InvalidOperationException("A game frame graph is already published.");
|
||||||
|
return pair;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Tick(UpdateFrameInput input)
|
public bool Tick(UpdateFrameInput input)
|
||||||
|
|
@ -64,4 +80,22 @@ internal sealed class GameFrameGraphSlot
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _pair, null);
|
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,
|
IGameWindowWorldRenderPublication,
|
||||||
IGameWindowInteractionRetainedUiPublication,
|
IGameWindowInteractionRetainedUiPublication,
|
||||||
IGameWindowLivePresentationPublication,
|
IGameWindowLivePresentationPublication,
|
||||||
IGameWindowSessionPlayerPublication
|
IGameWindowSessionPlayerPublication,
|
||||||
|
IGameWindowFrameRootPublication
|
||||||
{
|
{
|
||||||
private static double ClientTimerNow() =>
|
private static double ClientTimerNow() =>
|
||||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||||
|
|
@ -103,6 +104,8 @@ public sealed class GameWindow :
|
||||||
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
||||||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||||
|
private FrameRootRuntimeBindings? _frameRootBindings;
|
||||||
|
private IDisposable? _frameGraphPublication;
|
||||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||||
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
||||||
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
||||||
|
|
@ -1040,6 +1043,23 @@ public sealed class GameWindow :
|
||||||
_sessionPlayerBindings = result.RuntimeBindings;
|
_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>(
|
private static void PublishCompositionOwner<T>(
|
||||||
ref T? destination,
|
ref T? destination,
|
||||||
T value,
|
T value,
|
||||||
|
|
@ -1355,268 +1375,53 @@ public sealed class GameWindow :
|
||||||
interactionUi,
|
interactionUi,
|
||||||
livePresentation);
|
livePresentation);
|
||||||
|
|
||||||
var teleportRenderState =
|
FrameRootResult frameRoots = new FrameRootCompositionPhase(
|
||||||
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
new FrameRootDependencies(
|
||||||
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(
|
|
||||||
_options,
|
_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!,
|
_gl!,
|
||||||
renderFrameGlState,
|
_window!,
|
||||||
new AcDream.App.Rendering.SilkRetailPViewFramebufferSource(
|
_input!,
|
||||||
_window!),
|
WorldTime,
|
||||||
_clipFrame!,
|
Weather,
|
||||||
_terrain,
|
Lighting,
|
||||||
_envCellRenderer!,
|
_worldEnvironment,
|
||||||
_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,
|
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
|
_cellVisibility,
|
||||||
_localPlayerMode,
|
_localPlayerMode,
|
||||||
|
_localPlayerIdentity,
|
||||||
|
_chaseCameraInput,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
_debugVmRenderFacts,
|
_liveWorldOrigin,
|
||||||
_debugVm is not null);
|
_particleVisibility,
|
||||||
var worldScenePasses = new AcDream.App.Rendering.WorldScenePassExecutor(
|
_effectPoses,
|
||||||
_gl!,
|
_renderRange,
|
||||||
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(
|
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
Combat,
|
_displayFramePacing,
|
||||||
|
_worldSceneDebugState,
|
||||||
|
_retailAlphaQueue,
|
||||||
|
_frameProfiler,
|
||||||
|
_frameDiag,
|
||||||
|
_renderDiagnosticLog,
|
||||||
|
_debugVmRenderFacts,
|
||||||
|
_inputCapture,
|
||||||
|
_cameraInput,
|
||||||
_selection,
|
_selection,
|
||||||
_worldSelectionQuery!));
|
_animatedEntities,
|
||||||
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
_updateFrameClock,
|
||||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
Combat,
|
||||||
livePresentation.LiveEntities),
|
_frameGraphs,
|
||||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
Console.WriteLine),
|
||||||
_updateFrameClock,
|
this).Compose(
|
||||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
hostInputCamera,
|
||||||
sessionPlayer.StreamingFrame,
|
contentEffectsAudio,
|
||||||
sessionPlayer.GameplayInput,
|
settingsDevTools,
|
||||||
liveFrameCoordinator,
|
worldRender,
|
||||||
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
interactionUi,
|
||||||
sessionPlayer.Liveness,
|
livePresentation,
|
||||||
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
sessionPlayer);
|
||||||
sessionPlayer.LocalTeleport,
|
|
||||||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
|
||||||
sessionPlayer.PlayerModeAutoEntry),
|
|
||||||
cameraFrame);
|
|
||||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
|
||||||
|
|
||||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||||
sessionPlayer.SessionHost.Start(_options);
|
frameRoots.SessionHost.Start(_options);
|
||||||
switch (liveStart.Status)
|
switch (liveStart.Status)
|
||||||
{
|
{
|
||||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||||
|
|
@ -1649,54 +1454,6 @@ public sealed class GameWindow :
|
||||||
out _);
|
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
|
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
||||||
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
// 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
|
// 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", () =>
|
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", () =>
|
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>,
|
IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>,
|
||||||
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>,
|
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>,
|
||||||
ISessionPlayerCompositionPhase<Token, 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>
|
ISessionStartCompositionPhase<Token>
|
||||||
{
|
{
|
||||||
public Token Platform { get; } = new(0);
|
public Token Platform { get; } = new(0);
|
||||||
|
|
@ -155,8 +155,9 @@ public sealed class GameWindowCompositionPipelineTests
|
||||||
return _session;
|
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 host,
|
||||||
|
Token content,
|
||||||
Token settings,
|
Token settings,
|
||||||
Token world,
|
Token world,
|
||||||
Token interaction,
|
Token interaction,
|
||||||
|
|
@ -164,6 +165,7 @@ public sealed class GameWindowCompositionPipelineTests
|
||||||
Token session)
|
Token session)
|
||||||
{
|
{
|
||||||
Assert.Same(_host, host);
|
Assert.Same(_host, host);
|
||||||
|
Assert.Same(_content, content);
|
||||||
Assert.Same(_settings, settings);
|
Assert.Same(_settings, settings);
|
||||||
Assert.Same(_world, world);
|
Assert.Same(_world, world);
|
||||||
Assert.Same(_interaction, interaction);
|
Assert.Same(_interaction, interaction);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"Composition",
|
"Composition",
|
||||||
"SessionPlayerComposition.cs"));
|
"SessionPlayerComposition.cs"));
|
||||||
string source = GameWindowSource();
|
string source = GameWindowSource();
|
||||||
|
string framePhase = FrameRootSource();
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
phase,
|
phase,
|
||||||
|
|
@ -57,10 +58,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"new SessionPlayerCompositionPhase(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(",
|
"new FrameRootCompositionPhase(");
|
||||||
"new AcDream.App.Rendering.RenderFrameResourceController(",
|
AssertAppearsInOrder(
|
||||||
"new AcDream.App.Rendering.PrivatePresentationRenderer(",
|
framePhase,
|
||||||
"new AcDream.App.Rendering.RenderFrameOrchestrator(");
|
"new LocalPlayerTeleportRenderStateSource(",
|
||||||
|
"new RenderFrameResourceController(",
|
||||||
|
"new PrivatePresentationRenderer(",
|
||||||
|
"new RenderFrameOrchestrator(");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -111,10 +115,11 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"Composition",
|
"Composition",
|
||||||
"SettingsDevToolsComposition.cs"));
|
"SettingsDevToolsComposition.cs"));
|
||||||
Assert.Contains("new DevToolsFramePresenter(", settingsComposition);
|
Assert.Contains("new DevToolsFramePresenter(", settingsComposition);
|
||||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameResourceController(", source);
|
string framePhase = FrameRootSource();
|
||||||
Assert.Contains("new AcDream.App.Rendering.RenderWeatherFrameController(", source);
|
Assert.Contains("new RenderFrameResourceController(", framePhase);
|
||||||
Assert.Contains("new AcDream.App.Rendering.PrivatePresentationRenderer(", source);
|
Assert.Contains("new RenderWeatherFrameController(", framePhase);
|
||||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source);
|
Assert.Contains("new PrivatePresentationRenderer(", framePhase);
|
||||||
|
Assert.Contains("new RenderFrameOrchestrator(", framePhase);
|
||||||
Assert.Contains("new DisplayFramePacingController(", source);
|
Assert.Contains("new DisplayFramePacingController(", source);
|
||||||
string pointerSource = File.ReadAllText(Path.Combine(
|
string pointerSource = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -142,7 +147,8 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||||
"_frameGraphs.Withdraw();",
|
"publication.Dispose();",
|
||||||
|
"bindings.Dispose();",
|
||||||
"new ResourceShutdownStage(\"session dependents\"");
|
"new ResourceShutdownStage(\"session dependents\"");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
|
|
@ -150,7 +156,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"new(\"frame profiler\", _frameProfiler.Dispose)");
|
"new(\"frame profiler\", _frameProfiler.Dispose)");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"_frameGraphs.Withdraw();",
|
"publication.Dispose();",
|
||||||
"owner.DisposeFrontend()",
|
"owner.DisposeFrontend()",
|
||||||
"new ResourceShutdownStage(\"input\"",
|
"new ResourceShutdownStage(\"input\"",
|
||||||
"_input?.Dispose();",
|
"_input?.Dispose();",
|
||||||
|
|
@ -186,7 +192,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"private void OnRender(double deltaSeconds)",
|
"private void OnRender(double deltaSeconds)",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
int end = source.IndexOf(
|
int end = source.IndexOf(
|
||||||
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot",
|
"private void OnFramebufferResize(",
|
||||||
start,
|
start,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.True(start >= 0 && end > start);
|
Assert.True(start >= 0 && end > start);
|
||||||
|
|
@ -216,12 +222,12 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
||||||
{
|
{
|
||||||
string source = GameWindowSource();
|
string source = FrameRootSource();
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new AcDream.App.Rendering.TerrainDrawDiagnosticsController(",
|
"new TerrainDrawDiagnosticsController(",
|
||||||
source);
|
source);
|
||||||
Assert.Contains("new AcDream.App.Rendering.WorldScenePassExecutor(", source);
|
Assert.Contains("new WorldScenePassExecutor(", source);
|
||||||
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
|
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
|
||||||
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
|
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +246,13 @@ public sealed class GameWindowRenderLeafCompositionTests
|
||||||
"Composition",
|
"Composition",
|
||||||
"LivePresentationComposition.cs"));
|
"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(
|
private static string Source(string fileName) => File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,21 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"new LivePresentationCompositionPhase(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"this).Compose(",
|
"this).Compose(",
|
||||||
"new SessionPlayerCompositionPhase(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"var renderFrameOrchestrator =",
|
"FrameRootResult frameRoots = new FrameRootCompositionPhase(",
|
||||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
"frameRoots.SessionHost.Start(_options)");
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
|
||||||
"sessionPlayer.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(
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -86,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"_publication.PublishSessionPlayer(result);");
|
"_publication.PublishSessionPlayer(result);");
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
body,
|
body,
|
||||||
"sessionPlayer.SessionHost.Start(_options)"));
|
"frameRoots.SessionHost.Start(_options)"));
|
||||||
string phaseOne = Slice(
|
string phaseOne = Slice(
|
||||||
File.ReadAllText(Path.Combine(
|
File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -107,7 +118,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
phaseOne,
|
phaseOne,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
int start = body.IndexOf(
|
int start = body.IndexOf(
|
||||||
"sessionPlayer.SessionHost.Start(_options)",
|
"frameRoots.SessionHost.Start(_options)",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
string postStart = body[start..];
|
string postStart = body[start..];
|
||||||
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
||||||
|
|
@ -331,7 +342,8 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"new SessionPlayerCompositionPhase(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"sessionPlayer.SessionHost.Start(_options)");
|
"new FrameRootCompositionPhase(",
|
||||||
|
"frameRoots.SessionHost.Start(_options)");
|
||||||
string sessionPhase = File.ReadAllText(Path.Combine(
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -385,7 +397,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
string render = Slice(
|
string render = Slice(
|
||||||
source,
|
source,
|
||||||
"private void OnRender(double deltaSeconds)",
|
"private void OnRender(double deltaSeconds)",
|
||||||
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot");
|
"private void OnFramebufferResize(");
|
||||||
string focus = Slice(
|
string focus = Slice(
|
||||||
source,
|
source,
|
||||||
"private void OnFocusChanged(bool focused)",
|
"private void OnFocusChanged(bool focused)",
|
||||||
|
|
@ -524,6 +536,12 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"SessionPlayerComposition.cs"));
|
"SessionPlayerComposition.cs"));
|
||||||
|
string framePhase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
livePhase,
|
livePhase,
|
||||||
|
|
@ -536,7 +554,12 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"new LivePresentationCompositionPhase(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"new SessionPlayerCompositionPhase(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
|
"new FrameRootCompositionPhase(");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
framePhase,
|
||||||
|
"new RenderFrameOrchestrator(",
|
||||||
|
"new UpdateFrameOrchestrator(",
|
||||||
|
"d.FrameGraphs.PublishOwned(");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
sessionPhase,
|
sessionPhase,
|
||||||
"d.PortalTunnelFallback.Transfer(",
|
"d.PortalTunnelFallback.Transfer(",
|
||||||
|
|
@ -550,7 +573,8 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"TerrainModernRenderer terrain = AcquireAndPublish(");
|
"TerrainModernRenderer terrain = AcquireAndPublish(");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
shutdown,
|
shutdown,
|
||||||
"_frameGraphs.Withdraw();",
|
"publication.Dispose();",
|
||||||
|
"bindings.Dispose();",
|
||||||
"_retailUiLease.Dispose();",
|
"_retailUiLease.Dispose();",
|
||||||
"_localPlayerTeleport?.Dispose();",
|
"_localPlayerTeleport?.Dispose();",
|
||||||
"_portalTunnelFallback.ReleaseFallback();",
|
"_portalTunnelFallback.ReleaseFallback();",
|
||||||
|
|
|
||||||
|
|
@ -225,11 +225,11 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Composition",
|
||||||
"GameWindow.cs"));
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source);
|
Assert.Contains("new RetailPViewPassExecutor(", source);
|
||||||
Assert.Contains("new AcDream.App.Rendering.WorldScenePViewRenderer(", source);
|
Assert.Contains("new WorldScenePViewRenderer(", source);
|
||||||
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
|
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
|
||||||
Assert.DoesNotContain("RetailPViewDrawContext", source);
|
Assert.DoesNotContain("RetailPViewDrawContext", source);
|
||||||
Assert.DoesNotContain("DrawLandscapeSlice =", source);
|
Assert.DoesNotContain("DrawLandscapeSlice =", source);
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,28 @@ public sealed class RuntimeResourceSlotTests
|
||||||
Assert.Equal(["update:2"], calls);
|
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]
|
[Fact]
|
||||||
public void OwnedSlotRejectsReentrantAcquireWithoutLosingOuterResource()
|
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()
|
public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition()
|
||||||
{
|
{
|
||||||
string gameWindow = GameWindowSource();
|
string gameWindow = GameWindowSource();
|
||||||
|
string frameRoot = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"FrameRootComposition.cs"));
|
||||||
string worldScene = File.ReadAllText(Path.Combine(
|
string worldScene = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -357,11 +363,14 @@ public sealed class WorldRenderFrameBuilderTests
|
||||||
"_passes.DrawFlatTerrain(",
|
"_passes.DrawFlatTerrain(",
|
||||||
"NormalWorldDrawn: true");
|
"NormalWorldDrawn: true");
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
gameWindow,
|
frameRoot,
|
||||||
"var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(",
|
"var skyPesFrame = new SkyPesFrameController(",
|
||||||
"var worldRenderFrameBuilder =",
|
"var worldRenderFrameBuilder =",
|
||||||
"new AcDream.App.Rendering.RenderFrameOrchestrator(",
|
"new RenderFrameOrchestrator(",
|
||||||
"_frameGraphs.Withdraw();",
|
"d.FrameGraphs.PublishOwned(");
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
gameWindow,
|
||||||
|
"publication.Dispose();",
|
||||||
"new(\"equipped children\"",
|
"new(\"equipped children\"",
|
||||||
"new(\"effect network state\"",
|
"new(\"effect network state\"",
|
||||||
"new(\"audio\"",
|
"new(\"audio\"",
|
||||||
|
|
|
||||||
|
|
@ -279,11 +279,11 @@ public sealed class WorldSceneRendererTests
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Rendering",
|
"Composition",
|
||||||
"GameWindow.cs"));
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new AcDream.App.Rendering.WorldSceneRenderer(", source);
|
Assert.Contains("new WorldSceneRenderer(", source);
|
||||||
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source);
|
Assert.Contains("new RenderFrameOrchestrator(", source);
|
||||||
Assert.DoesNotContain("_worldSceneRenderer", source);
|
Assert.DoesNotContain("_worldSceneRenderer", source);
|
||||||
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
|
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
|
||||||
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);
|
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);
|
||||||
|
|
|
||||||
|
|
@ -276,10 +276,16 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Update",
|
"Update",
|
||||||
"UpdateFrameRuntimeAdapters.cs"));
|
"UpdateFrameRuntimeAdapters.cs"));
|
||||||
|
string frameRoot = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
Assert.Equal(0, CountOccurrences(source, "PublishTime("));
|
Assert.Equal(0, CountOccurrences(source, "PublishTime("));
|
||||||
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
|
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
|
||||||
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source,
|
Assert.Contains("new PhysicsScriptClockPublisher(", frameRoot,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,10 +355,16 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"SessionPlayerComposition.cs"));
|
"SessionPlayerComposition.cs"));
|
||||||
|
string frameRoot = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new LiveObjectFrameController(", sessionPlayer);
|
Assert.Contains("new LiveObjectFrameController(", sessionPlayer);
|
||||||
Assert.Contains("new LiveSpatialPresentationReconciler(", 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("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
@ -375,7 +387,6 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"SessionPlayerComposition.cs"));
|
"SessionPlayerComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains("new StreamingFrameController(", sessionPlayer);
|
Assert.Contains("new StreamingFrameController(", sessionPlayer);
|
||||||
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
|
|
@ -492,6 +503,12 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"SessionPlayerComposition.cs"));
|
"SessionPlayerComposition.cs"));
|
||||||
|
string frameRoot = File.ReadAllText(Path.Combine(
|
||||||
|
root,
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"FrameRootComposition.cs"));
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"new LocalPlayerTeleportController(",
|
"new LocalPlayerTeleportController(",
|
||||||
|
|
@ -513,10 +530,10 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource,
|
Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
frameRoot,
|
||||||
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
"new LiveEntityLivenessFramePhase(",
|
||||||
"sessionPlayer.LocalTeleport,",
|
"session.LocalTeleport,",
|
||||||
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
"new PlayerModeAutoEntryFramePhase(",
|
||||||
"cameraFrame);");
|
"cameraFrame);");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue