Borrow the exact prior-completed landscape visibility transaction and S2 CELLARRAY owner for the opt-in IA-24 directional-shadow pack. Select terrain by authored 1..64 cells, ordinary casters by CELLARRAY, and buildings by outdoor EffectCellId with no fallback. Keep retained caster/material/terrain topology stable across visibility-only frames. Publish exact arbitrary active instance runs and bounded terrain commands through separate selection sequences; preserve transform-journal, fade/retry, deferral, shader/RHI, ordinary world, and pack-off behavior. Amend IA-24 and the S5 ledger. Pre-commit gates: Release solution build 0 warnings/0 errors; focused visibility/frame/caster/prepared/GPU/terrain/pack lane 137/137; warmed caster/prepared/terrain selectors 0 B; git diff --check clean. Official hermetic and InstalledDat evidence intentionally run post-commit from this exact clean tree. Mutation evidence (each restored exactly): (1) CELLARRAY->Parent first failed PriorLandscapeSelection expected [201,202,203,205], actual [204,205]. (2) all resident terrain first failed Assert.Single with 3 commands. (3) building EffectCell->anchor first failed expected trailing 205, actual 206. (4) admit missing membership first failed with extra 204. (5) completed->building scratch first failed completed-view Assert.True, expected true/actual false. (6) selection advanced BuildSequence first failed expected 1/actual 2. (7) alternating->prefix first failed active command count expected 3/actual 1. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
878 lines
39 KiB
C#
878 lines
39 KiB
C#
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Runtime;
|
|
using AcDream.App.Settings;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record FrameRootDependencies(
|
|
RuntimeOptions Options,
|
|
GameRuntime Runtime,
|
|
GameWindowGraphics Graphics,
|
|
IWindow Window,
|
|
IInputContext Input,
|
|
WorldTimeService WorldTime,
|
|
WeatherSystem Weather,
|
|
LightManager Lighting,
|
|
WorldEnvironmentController WorldEnvironment,
|
|
PhysicsEngine PhysicsEngine,
|
|
CellVisibility CellVisibility,
|
|
LocalPlayerModeState PlayerMode,
|
|
LocalPlayerIdentityState PlayerIdentity,
|
|
ChaseCameraInputState ChaseCameraInput,
|
|
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,
|
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
|
|
UpdateFrameClock UpdateClock,
|
|
GameFrameGraphSlot FrameGraphs,
|
|
Action<string> Log,
|
|
AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource?
|
|
RenderPackDiagnostics = null)
|
|
{
|
|
public RuntimeLocalPlayerMovementState PlayerController =>
|
|
Runtime.MovementOwner;
|
|
|
|
public SelectionState Selection => Runtime.ActionOwner.Selection;
|
|
|
|
public CombatState Combat => Runtime.ActionOwner.Combat;
|
|
}
|
|
|
|
internal sealed record FrameRootResult(
|
|
UpdateFrameOrchestrator Update,
|
|
RenderFrameOrchestrator Render,
|
|
FrameRootRuntimeBindings RuntimeBindings,
|
|
IDisposable FrameGraphPublication,
|
|
LiveSessionHost SessionHost,
|
|
CurrentGameRuntimeAdapter GameRuntime);
|
|
|
|
internal interface IGameWindowFrameRootPublication
|
|
{
|
|
void PublishFrameRoots(FrameRootResult result);
|
|
}
|
|
|
|
internal enum FrameRootCompositionPoint
|
|
{
|
|
RenderResourcesCreated,
|
|
WorldRendererCreated,
|
|
LifecycleAutomationBound,
|
|
RenderRootCreated,
|
|
UpdateRootCreated,
|
|
FrameGraphPublished,
|
|
ResultPublished,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact late edges created with the frame roots. Completed detach operations
|
|
/// are removed immediately so shutdown retry never replays them.
|
|
/// </summary>
|
|
internal sealed class FrameRootRuntimeBindings : IDisposable
|
|
{
|
|
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
|
private bool _deactivationStarted;
|
|
|
|
public void Adopt(string name, IDisposable binding)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(binding);
|
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
|
_bindings.Add((name, binding));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_deactivationStarted && _bindings.Count == 0)
|
|
return;
|
|
_deactivationStarted = true;
|
|
|
|
List<Exception>? failures = null;
|
|
for (int i = _bindings.Count - 1; i >= 0; i--)
|
|
{
|
|
(string name, IDisposable binding) = _bindings[i];
|
|
try
|
|
{
|
|
binding.Dispose();
|
|
_bindings.RemoveAt(i);
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
(failures ??= []).Add(new InvalidOperationException(
|
|
$"Frame-root binding '{name}' did not detach.",
|
|
failure));
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
"Frame-root binding cleanup remains incomplete.",
|
|
failures);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class FrameRootCompositionPhase
|
|
: IFrameRootCompositionPhase<
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult,
|
|
FrameRootResult>
|
|
{
|
|
private readonly FrameRootDependencies _dependencies;
|
|
private readonly IGameWindowFrameRootPublication _publication;
|
|
private readonly Action<FrameRootCompositionPoint>? _faultInjection;
|
|
|
|
public FrameRootCompositionPhase(
|
|
FrameRootDependencies dependencies,
|
|
IGameWindowFrameRootPublication publication,
|
|
Action<FrameRootCompositionPoint>? faultInjection = null)
|
|
{
|
|
_dependencies = dependencies
|
|
?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication
|
|
?? throw new ArgumentNullException(nameof(publication));
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public FrameRootResult Compose(
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform,
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
ArgumentNullException.ThrowIfNull(world);
|
|
ArgumentNullException.ThrowIfNull(interaction);
|
|
ArgumentNullException.ThrowIfNull(live);
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
if (!ReferenceEquals(_dependencies.Graphics, platform.Graphics)
|
|
|| !ReferenceEquals(_dependencies.Input, platform.Input))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Frame-root dependencies do not match the ordered platform result.");
|
|
}
|
|
|
|
var scope = new CompositionAcquisitionScope();
|
|
FrameRootRuntimeBindings? bindings = null;
|
|
bool bindingsOwnedByScope = false;
|
|
try
|
|
{
|
|
FrameRootResult result = ComposeCore(
|
|
host,
|
|
content,
|
|
settings,
|
|
world,
|
|
interaction,
|
|
live,
|
|
session,
|
|
scope,
|
|
ref bindings,
|
|
ref bindingsOwnedByScope);
|
|
scope.Complete();
|
|
return result;
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
if (bindings is not null && !bindingsOwnedByScope)
|
|
{
|
|
scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
}
|
|
|
|
scope.RollbackAndThrow(failure);
|
|
throw new System.Diagnostics.UnreachableException();
|
|
}
|
|
}
|
|
|
|
private FrameRootResult ComposeCore(
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings,
|
|
WorldRenderResult world,
|
|
InteractionRetainedUiResult interaction,
|
|
LivePresentationResult live,
|
|
SessionPlayerResult session,
|
|
CompositionAcquisitionScope scope,
|
|
ref FrameRootRuntimeBindings? bindings,
|
|
ref bool bindingsOwnedByScope)
|
|
{
|
|
FrameRootDependencies d = _dependencies;
|
|
bindings = new FrameRootRuntimeBindings();
|
|
WorldRenderFoundation foundation = world.Foundation;
|
|
// Campaign V slice V6h: the frame root's raw-GL render graph fork was
|
|
// deleted at slice V11. The graph is the clear pass, the private-
|
|
// presentation phase, and the retained UI inside it — the client's
|
|
// own frame, drawn entirely through the RHI.
|
|
var renderLoginState = new RenderLoginStateSource(
|
|
d.Options.LiveMode,
|
|
d.PlayerMode);
|
|
var teleportRenderState =
|
|
new LocalPlayerTeleportRenderStateSource(
|
|
session.LocalTeleport,
|
|
renderLoginState);
|
|
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
|
|
// world pass rather than a pass of its own, so the two phases share this
|
|
// one value. See VulkanWorldScenePhase for why the merge is required
|
|
// rather than tidier.
|
|
var vulkanClear = new AcDream.App.Rendering.Gpu.Vk.VulkanBackbufferClearState();
|
|
var renderFrameLivePreparation =
|
|
new RuntimeRenderFrameLivePreparation(
|
|
foundation.TextureCache,
|
|
foundation.MeshAdapter,
|
|
session.WorldReveal,
|
|
teleportRenderState,
|
|
renderLoginState,
|
|
new LiveLoginRevealCellSource(
|
|
live.LiveEntities,
|
|
d.PlayerIdentity),
|
|
live.ParticleRenderer,
|
|
d.FrameProfiler,
|
|
d.FrameDiagnosticsEnabled);
|
|
IRenderFrameClearPhase clearPhase =
|
|
new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase(
|
|
d.WorldTime,
|
|
d.Weather,
|
|
teleportRenderState,
|
|
d.ParticleVisibility,
|
|
vulkanClear);
|
|
var renderFrameResources = new RenderFrameResourceController(
|
|
host.FrameSlots,
|
|
new RuntimeRenderFrameBeginResources(
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
live.EnvCellRenderer,
|
|
live.PortalDepthMask,
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
foundation.SceneLighting),
|
|
clearPhase,
|
|
renderFrameLivePreparation);
|
|
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
|
|
|
|
AcDream.App.Rendering.Packs.RenderPackController? renderPackController = null;
|
|
AcDream.App.Rendering.Packs.RenderPackSelectionBinding? renderPackSelection = null;
|
|
AcDream.App.Rendering.Packs.AtmosphericFrameInputState? atmosphericInputs = null;
|
|
if (settings.RenderPacks is { } renderPackCatalog)
|
|
{
|
|
renderPackController = new AcDream.App.Rendering.Packs.RenderPackController(
|
|
renderPackCatalog.Snapshot,
|
|
new AcDream.App.Rendering.Packs.AtmosphericRenderPackRuntimeFactory(
|
|
host.GpuDevice,
|
|
// Campaign VM VM6: the same ACDREAM_SKY_PHASE_SECONDS pin
|
|
// SkyRenderer uses, so the foliage-wind clock is
|
|
// deterministic under the same differential/offline
|
|
// gates — see ResolveFoliageWind's doc comment.
|
|
d.Options.SkyAnimationPhaseSeconds),
|
|
new AcDream.App.Rendering.Packs.RenderPackReceiverPipelineCoordinator(
|
|
foundation.Terrain!,
|
|
live.DrawDispatcher!),
|
|
AcDream.App.Rendering.Packs.ThreadPoolRenderPackPreparationScheduler.Instance,
|
|
renderPackCatalog);
|
|
bindings.Adopt("render-pack controller", renderPackController);
|
|
renderPackSelection = new AcDream.App.Rendering.Packs.RenderPackSelectionBinding(
|
|
d.Settings,
|
|
renderPackController,
|
|
d.Log);
|
|
bindings.Adopt("render-pack selection", renderPackSelection);
|
|
if (d.RenderPackDiagnostics is { } renderPackDiagnostics)
|
|
{
|
|
bindings.Adopt(
|
|
"render-pack diagnostics",
|
|
renderPackDiagnostics.BindOwned(renderPackController));
|
|
}
|
|
atmosphericInputs = new AcDream.App.Rendering.Packs.AtmosphericFrameInputState();
|
|
|
|
// Enhanced night sky (register IA-26, user-directed 2026-08-23):
|
|
// while the Atmospheric pack is the active runtime, SkyRenderer
|
|
// swaps the DAT star layer for sky.frag's procedural
|
|
// view-direction starfield. Same-composition lifetimes on both
|
|
// sides, and the delegate re-reads ActiveRuntime per draw, so a
|
|
// pack activate/deactivate flips the sky the same frame.
|
|
if (live.SkyRenderer is { } skyForNightSky)
|
|
{
|
|
var nightSkyController = renderPackController;
|
|
skyForNightSky.EnhancedNightSkyActive = () =>
|
|
nightSkyController.ActiveRuntime?.Descriptor.Id
|
|
== AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.Id;
|
|
}
|
|
}
|
|
|
|
var renderWeatherFrame = new RenderWeatherFrameController(
|
|
d.WorldTime,
|
|
d.Weather);
|
|
var skyPesFrame = new SkyPesFrameController(
|
|
content.ScriptRunner,
|
|
content.ParticleSink,
|
|
d.EffectPoses,
|
|
live.EntityEffects,
|
|
d.Log);
|
|
IWorldSceneFramePhase? worldSceneRenderer = null;
|
|
// Campaign FW4: the retail walk reads RenderSceneShadowRuntime
|
|
// directly. The old current-partition oracle, shadow comparison, and
|
|
// packed PView frame product no longer participate in production or
|
|
// automation; their null snapshot slots remain until the diagnostic
|
|
// contract cleanup later in this campaign.
|
|
CurrentRenderSceneOracle? currentRenderSceneOracle = null;
|
|
RenderSceneShadowComparisonController? renderSceneShadowComparison = null;
|
|
{
|
|
// Campaign V slice V6j: the world scene is composed on BOTH arms.
|
|
// Every renderer below now exists on Vulkan too; what forks is one
|
|
// pass surface, one state restorer, one GL-state reader, and the
|
|
// three renderers that stay raw GL until their own slices — sky,
|
|
// particles and the portal depth mask, which the executors already
|
|
// accept as absent.
|
|
WorldRenderDiagnostics worldRenderDiagnostics =
|
|
host.WorldRenderDiagnostics
|
|
?? new WorldRenderDiagnostics(
|
|
NullRenderGlStateReader.Instance,
|
|
d.RenderDiagnosticLog);
|
|
IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
|
|
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
|
|
IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
|
|
worldPassScope
|
|
?? throw new InvalidOperationException(
|
|
"The graphics backend must publish a world pass scope."),
|
|
host.GpuFrameLifetime,
|
|
live.ClipFrame);
|
|
var worldFrameEnvironment =
|
|
new RuntimeWorldFrameEnvironmentPreparation(
|
|
d.Options,
|
|
d.WorldTime,
|
|
d.Lighting,
|
|
live.DrawDispatcher!,
|
|
live.EnvCellRenderer!,
|
|
foundation.SceneLighting!,
|
|
d.RenderRange,
|
|
skyPesFrame,
|
|
persistentDaylight: () =>
|
|
d.Runtime.CharacterOwner.Options.GetOptionBit(
|
|
CharacterOptionId.PersistentAtDay));
|
|
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),
|
|
new RuntimeDirectionalShadowCellMembership(
|
|
d.PhysicsEngine.ShadowObjects));
|
|
var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController(
|
|
d.FrameDiagnosticsEnabled,
|
|
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(
|
|
worldPassSurface,
|
|
worldFrameGlState,
|
|
live.ClipFrame,
|
|
foundation.Terrain,
|
|
live.EnvCellRenderer!,
|
|
live.DrawDispatcher!,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer,
|
|
live.PortalDepthMask,
|
|
d.RetailAlphaQueue,
|
|
worldRenderDiagnostics,
|
|
terrainDrawDiagnostics);
|
|
var worldSceneDiagnostics = new WorldSceneDiagnosticsController(
|
|
worldRenderDiagnostics,
|
|
new RuntimeWorldScenePViewDiagnosticSource(
|
|
d.PlayerController,
|
|
d.PhysicsEngine,
|
|
d.CellVisibility),
|
|
d.WorldSceneDebugState,
|
|
// Campaign V slice V6j: no debug-line renderer on the Vulkan arm.
|
|
// DrawAndPublish flushes it INSIDE the world phase and that
|
|
// renderer opens its own pass, which the frame's one backbuffer
|
|
// pass forbids. The collision-wireframe toggle was DevTools-only
|
|
// and the ImGui DevTools frontend is gone as of Campaign V slice
|
|
// V11, so nothing is lost — composing it would throw on the
|
|
// first wireframe frame rather than silently misdraw (plan
|
|
// §5.5.14 item 7).
|
|
null,
|
|
d.PhysicsEngine,
|
|
d.PlayerMode,
|
|
d.PlayerController,
|
|
d.DebugVmRenderFacts,
|
|
debugVmConsumerActive: false);
|
|
var worldScenePasses = new WorldScenePassExecutor(
|
|
worldPassSurface,
|
|
worldFrameGlState,
|
|
live.ClipFrame,
|
|
live.DrawDispatcher!,
|
|
live.EnvCellRenderer!,
|
|
foundation.Terrain,
|
|
terrainDrawDiagnostics,
|
|
live.SkyRenderer,
|
|
content.ParticleSystem,
|
|
live.ParticleRenderer);
|
|
// The retained scene is the walk's production object source. The
|
|
// old dispatcher/selection observer remains detached.
|
|
live.DrawDispatcher!.SetCurrentRenderSceneObserver(null);
|
|
live.SelectionScene.SetCurrentRenderSceneObserver(null);
|
|
worldSceneRenderer = new WorldSceneRenderer(
|
|
renderFrameResources,
|
|
renderLoginState,
|
|
d.WorldEnvironment,
|
|
worldRenderFrameBuilder,
|
|
new RuntimeWorldSceneEntitySource(live.WorldState),
|
|
live.SelectionScene,
|
|
d.RetailAlphaQueue,
|
|
d.ParticleVisibility,
|
|
new WorldScenePViewRenderer(
|
|
new RetailPViewRenderer(
|
|
live.RenderSceneShadow
|
|
?? throw new InvalidOperationException(
|
|
"The retail frame walk requires the retained render scene."),
|
|
// Campaign FW3.2b-2: the walk's production world
|
|
// data, published/retired with each landblock by the
|
|
// render publisher (FW3.1).
|
|
live.LandblockPipeline.RenderPublisher?.WalkBuildings
|
|
?? throw new InvalidOperationException(
|
|
"The retail frame walk requires the building registry."),
|
|
live.LandblockPipeline.RenderPublisher?.WalkLandscape
|
|
?? throw new InvalidOperationException(
|
|
"The retail frame walk requires the landscape registry."),
|
|
d.CellVisibility,
|
|
d.PhysicsEngine.ShadowObjects),
|
|
retailPViewPassExecutor),
|
|
retailPViewCells,
|
|
worldScenePasses,
|
|
d.RenderRange,
|
|
worldSceneDiagnostics,
|
|
live.WorldAvailability,
|
|
atmosphericInputs);
|
|
// The world renderer runs INSIDE the frame's one backbuffer pass,
|
|
// which this phase opens, publishes on the scope, and closes.
|
|
worldSceneRenderer =
|
|
new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase(
|
|
host.GpuFrameLifetime,
|
|
vulkanClear,
|
|
() => d.Graphics.Vulkan?.SampleCount ?? 1,
|
|
(d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
|
|
?? throw new InvalidOperationException(
|
|
"The Vulkan world phase requires the Vulkan graphics handle."),
|
|
worldSceneRenderer,
|
|
renderPackController,
|
|
atmosphericInputs,
|
|
renderPackSelection is null
|
|
? null
|
|
: renderPackSelection.ApplyAtFrameBoundary,
|
|
live.RenderSceneShadow,
|
|
live.DrawDispatcher,
|
|
foundation.Terrain);
|
|
}
|
|
Fault(FrameRootCompositionPoint.WorldRendererCreated);
|
|
WorldLifecycleAutomationController? lifecycleAutomation = null;
|
|
if (interaction.RetainedUi?.Screenshots is { } screenshots
|
|
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
|
|
{
|
|
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings?
|
|
automationLastEnhancedSelection = null;
|
|
|
|
(bool Succeeded, string Error) SaveAutomationRenderPackSelection(
|
|
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings selection)
|
|
{
|
|
d.Settings.SaveDisplay(d.Settings.Display with
|
|
{
|
|
RenderPack = selection,
|
|
});
|
|
return d.Settings.Display.RenderPack == selection
|
|
? (true, string.Empty)
|
|
: (false, $"render-pack selection '{selection.PresetId}' was not persisted");
|
|
}
|
|
|
|
(bool Succeeded, string Error) SelectAutomationRenderPack(string preset)
|
|
{
|
|
var selection = string.Equals(
|
|
preset,
|
|
"retail",
|
|
StringComparison.Ordinal)
|
|
? AcDream.UI.Abstractions.Panels.Settings
|
|
.RenderPackSelectionSettings.Retail
|
|
: new AcDream.UI.Abstractions.Panels.Settings
|
|
.RenderPackSelectionSettings(
|
|
AcDream.App.Rendering.Packs
|
|
.BuiltInAtmosphericRenderPack.Id,
|
|
"1.0.0",
|
|
preset);
|
|
return SaveAutomationRenderPackSelection(selection);
|
|
}
|
|
|
|
(bool Succeeded, string Error) DisableAutomationRenderPack()
|
|
{
|
|
var current = d.Settings.Display.RenderPack;
|
|
if (!current.IsRetail)
|
|
automationLastEnhancedSelection = current;
|
|
return SaveAutomationRenderPackSelection(
|
|
AcDream.UI.Abstractions.Panels.Settings
|
|
.RenderPackSelectionSettings.Retail);
|
|
}
|
|
|
|
(bool Succeeded, string Error) ReenableAutomationRenderPack()
|
|
{
|
|
return automationLastEnhancedSelection is { } selection
|
|
? SaveAutomationRenderPackSelection(selection)
|
|
: (false, "render-pack re-enable requires a prior enhanced selection");
|
|
}
|
|
|
|
var resourceSnapshots =
|
|
new WorldLifecycleResourceSnapshotSource(
|
|
live.WorldState,
|
|
d.Animations,
|
|
live.FrameDiagnostics,
|
|
live.LiveEntities,
|
|
session.Streaming,
|
|
content.ParticleSystem,
|
|
content.ParticleSink,
|
|
live.EntityEffects,
|
|
live.Lights,
|
|
content.ScriptRunner,
|
|
foundation.MeshAdapter,
|
|
foundation.TextureCache,
|
|
live.DrawDispatcher,
|
|
d.FrameProfiler,
|
|
content.Dats,
|
|
foundation.Residency,
|
|
d.PhysicsEngine.DataCache
|
|
?? throw new InvalidOperationException(
|
|
"Lifecycle automation requires the canonical physics cache."),
|
|
currentRenderSceneOracle,
|
|
renderSceneShadowComparison);
|
|
lifecycleAutomation =
|
|
new WorldLifecycleAutomationController(
|
|
() => session.WorldReveal.Snapshot,
|
|
() => d.WorldEnvironment.Runtime.Ownership,
|
|
() => live.WorldTransit.Ownership,
|
|
() => session.WorldReveal.PortalMaterializationCount,
|
|
resourceSnapshots.Capture,
|
|
screenshots,
|
|
artifactDirectory,
|
|
message => d.Log("[UI-PROBE] " + message),
|
|
() => renderPackController?.MinimumPerformanceSampleCount ?? 0,
|
|
() =>
|
|
{
|
|
if (renderPackController is null)
|
|
{
|
|
return (
|
|
false,
|
|
"render-pack performance automation is unavailable");
|
|
}
|
|
bool reset = renderPackController.TryResetPerformanceEvidence(
|
|
out string error);
|
|
return (reset, error);
|
|
},
|
|
() => renderPackController?.Snapshot.State ==
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail,
|
|
getRenderPackStatus: () =>
|
|
{
|
|
AcDream.App.Rendering.Packs.RenderPackActivationSnapshot snapshot =
|
|
renderPackController?.Snapshot
|
|
?? new AcDream.App.Rendering.Packs.RenderPackActivationSnapshot(
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail,
|
|
AcDream.UI.Abstractions.Panels.Settings
|
|
.RenderPackSelectionSettings.Retail,
|
|
ActivePackDisplayName: null,
|
|
Reason: null,
|
|
ActivationGeneration: 0);
|
|
var state = snapshot.State switch
|
|
{
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail =>
|
|
AcDream.App.UI.Testing
|
|
.RetailUiAutomationRenderPackState.Retail,
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.CandidatePending =>
|
|
AcDream.App.UI.Testing
|
|
.RetailUiAutomationRenderPackState.CandidatePending,
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.Active =>
|
|
AcDream.App.UI.Testing
|
|
.RetailUiAutomationRenderPackState.Active,
|
|
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail =>
|
|
AcDream.App.UI.Testing
|
|
.RetailUiAutomationRenderPackState.FailedToRetail,
|
|
_ => throw new ArgumentOutOfRangeException(),
|
|
};
|
|
return new AcDream.App.UI.Testing
|
|
.RetailUiAutomationRenderPackStatus(
|
|
state,
|
|
snapshot.Selection.PackId,
|
|
snapshot.Selection.PresetId,
|
|
snapshot.ActivationGeneration,
|
|
snapshot.Reason);
|
|
},
|
|
selectRenderPack: SelectAutomationRenderPack,
|
|
disableRenderPack: DisableAutomationRenderPack,
|
|
reenableRenderPack: ReenableAutomationRenderPack,
|
|
getFramebufferSize: () =>
|
|
{
|
|
var size = d.Window.FramebufferSize;
|
|
return (size.X, size.Y);
|
|
},
|
|
resizeFramebuffer: (width, height) =>
|
|
{
|
|
if (d.Settings.Display.Fullscreen)
|
|
{
|
|
return (
|
|
false,
|
|
"automation framebuffer resize requires windowed mode");
|
|
}
|
|
string resolution = $"{width}x{height}";
|
|
d.Settings.SaveDisplay(d.Settings.Display with
|
|
{
|
|
Resolution = resolution,
|
|
});
|
|
return string.Equals(
|
|
d.Settings.Display.Resolution,
|
|
resolution,
|
|
StringComparison.Ordinal)
|
|
? (true, string.Empty)
|
|
: (false, $"framebuffer resize '{resolution}' was not persisted");
|
|
},
|
|
requestClientClose: d.Window.Close);
|
|
bindings.Adopt(
|
|
"world lifecycle automation owner",
|
|
lifecycleAutomation);
|
|
bindings.Adopt(
|
|
"world lifecycle automation binding",
|
|
interaction.LateBindings.Automation.Bind(
|
|
lifecycleAutomation));
|
|
}
|
|
else if (interaction.RetainedUi is not null)
|
|
{
|
|
// #415: without ACDREAM_AUTOMATION_ARTIFACT_DIR the deferred
|
|
// automation wrapper stayed unbound, so a probe script's
|
|
// `wait world-ready/world-visible/materialized` verbs read
|
|
// false forever and timed out even while the world revealed.
|
|
// The wait verbs need only the reveal facts — bind them always;
|
|
// checkpoint/screenshot verbs keep requiring the artifact
|
|
// directory and now report that instead of a generic timeout.
|
|
bindings.Adopt(
|
|
"world reveal facts automation binding",
|
|
interaction.LateBindings.Automation.Bind(
|
|
new WorldRevealFactsAutomationRuntime(
|
|
() => session.WorldReveal.Snapshot,
|
|
() => session.WorldReveal.PortalMaterializationCount)));
|
|
}
|
|
Fault(FrameRootCompositionPoint.LifecycleAutomationBound);
|
|
|
|
IRetainedGameplayUiFrame? retainedGameplayUi =
|
|
d.Options.RetailUi && interaction.RetainedUi is { } retained
|
|
? new RetainedGameplayUiFrame(retained.Runtime, d.Input)
|
|
: null;
|
|
IPrivateFrameScreenshot? privateScreenshot =
|
|
interaction.RetainedUi?.Screenshots is { } frameScreenshots
|
|
? new PrivateFrameScreenshot(frameScreenshots)
|
|
: null;
|
|
var privatePresentation = new PrivatePresentationRenderer(
|
|
new LocalPlayerPortalViewport(
|
|
session.LocalTeleport,
|
|
host.CameraController),
|
|
renderFrameResources,
|
|
new PrivateEntityViewportFrameGroup(
|
|
live.PaperdollPresenter,
|
|
live.CreatureAppraisalPresenter,
|
|
live.ChargenPreviewController,
|
|
live.SummaryPreviewController),
|
|
retainedGameplayUi,
|
|
// The ImGui developer-tools frontend was removed at Campaign V
|
|
// slice V11; this optional hook is unbound until a follow-up
|
|
// re-homes it onto the retained UI (IDevToolsFrameLifecycle stays
|
|
// as the seam).
|
|
devTools: null,
|
|
privateScreenshot);
|
|
var framePreparation = new RenderFramePreparationController(
|
|
renderFrameResources,
|
|
devTools: null,
|
|
renderWeatherFrame,
|
|
live.PaperdollPresenter);
|
|
IRenderFramePostDiagnosticsPhase postDiagnostics =
|
|
renderSceneShadowComparison is not null
|
|
&& lifecycleAutomation is not null
|
|
? new SerialRenderFramePostDiagnosticsPhase(
|
|
renderSceneShadowComparison,
|
|
lifecycleAutomation)
|
|
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
|
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
|
if (RenderPresentationDiagnostics.ProbeLoginFrames)
|
|
{
|
|
// Enter-world gate round (2026-08-17): the per-frame presentation
|
|
// classification probe (world/tunnel/black/void transitions). The
|
|
// tunnel fact is the controller's RAW portal-scene visibility, not
|
|
// the composed render-state source, so the probe can distinguish
|
|
// "covered black" from "tunnel scene drawn".
|
|
var loginFrameProbe = new LoginPresentationFrameProbe(
|
|
() => session.LocalTeleport.IsPortalViewportVisible,
|
|
renderLoginState,
|
|
d.Log);
|
|
postDiagnostics =
|
|
postDiagnostics is NullRenderFramePostDiagnosticsPhase
|
|
? loginFrameProbe
|
|
: new SerialRenderFramePostDiagnosticsPhase(
|
|
postDiagnostics,
|
|
loginFrameProbe);
|
|
}
|
|
var renderFrame = new RenderFrameOrchestrator(
|
|
host.GpuFrameLifetime,
|
|
// Campaign V slice V8: the Vulkan arm measures the frame bracket
|
|
// through its own timestamp scope — see VulkanFrameGpuMeasurement.
|
|
// The raw-GL adapter (FrameProfilerGpuMeasurement) was deleted at
|
|
// slice V11.
|
|
d.Graphics.Vulkan is { } vulkanGraphics
|
|
? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement(
|
|
d.FrameProfiler,
|
|
vulkanGraphics.Device)
|
|
: AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance,
|
|
framePreparation,
|
|
worldSceneRenderer,
|
|
privatePresentation,
|
|
live.FrameDiagnostics,
|
|
postDiagnostics,
|
|
NullRenderFrameFailureRecovery.Instance);
|
|
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
|
|
|
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
|
session.LiveObjectFrame,
|
|
live.WorldState,
|
|
session.SessionHost,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler,
|
|
live.WorldAvailability,
|
|
live.RenderSceneShadow?.LiveProjections,
|
|
session.PlacementProjectionRetry);
|
|
var cameraFrame = new CameraFrameController(
|
|
host.CameraController,
|
|
d.InputCapture,
|
|
d.CameraInput,
|
|
session.LocalPlayerFrameRuntime,
|
|
d.ChaseCameraInput,
|
|
session.LocalPlayerFrame,
|
|
session.LiveSpatialReconciler,
|
|
new AcDream.App.Combat.CombatCameraTargetSource(
|
|
// D7 Group-C re-point (Campaign OP OP4): server bit, not
|
|
// the client-local GameplaySettings record — see
|
|
// CharacterOptionCombatSettingsSource's doc comment.
|
|
new AcDream.App.Combat.CharacterOptionCombatSettingsSource(
|
|
d.Runtime.CharacterOwner.Options),
|
|
d.Combat,
|
|
d.Selection,
|
|
live.SelectionQuery));
|
|
var updateFrame = new UpdateFrameOrchestrator(
|
|
new LiveEntityTeardownFramePhase(live.LiveEntities),
|
|
new ConsoleUpdateFrameFailureSink(),
|
|
d.UpdateClock,
|
|
new PhysicsScriptClockPublisher(content.ScriptRunner),
|
|
session.StreamingFrame,
|
|
session.GameplayInput,
|
|
liveFrameCoordinator,
|
|
new LiveEntityLivenessFramePhase(
|
|
session.Liveness,
|
|
new StopwatchClientMonotonicTimeSource()),
|
|
session.LocalTeleport,
|
|
new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry),
|
|
cameraFrame,
|
|
new RenderSceneUpdateCommitPhase(live.RenderSceneShadow),
|
|
live.WorldAvailability);
|
|
Fault(FrameRootCompositionPoint.UpdateRootCreated);
|
|
|
|
var bindingsLease = scope.Own(
|
|
"frame-root runtime bindings",
|
|
bindings,
|
|
static value => value.Dispose());
|
|
bindingsOwnedByScope = true;
|
|
IDisposable frameGraphPublication = d.FrameGraphs.PublishOwned(
|
|
updateFrame,
|
|
renderFrame);
|
|
var graphLease = scope.Own(
|
|
"game frame graph publication",
|
|
frameGraphPublication,
|
|
static value => value.Dispose());
|
|
Fault(FrameRootCompositionPoint.FrameGraphPublished);
|
|
|
|
var result = new FrameRootResult(
|
|
updateFrame,
|
|
renderFrame,
|
|
bindings,
|
|
frameGraphPublication,
|
|
session.SessionHost,
|
|
session.GameRuntime);
|
|
_publication.PublishFrameRoots(result);
|
|
graphLease.Transfer();
|
|
bindingsLease.Transfer();
|
|
Fault(FrameRootCompositionPoint.ResultPublished);
|
|
return result;
|
|
}
|
|
|
|
private void Fault(FrameRootCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|