acdream/src/AcDream.App/Composition/LivePresentationComposition.cs
Erik ad5f8b68dc fix(render): Campaign V slice V7 commit 1 - world anisotropy, and the sky's second clock
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at
Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same
stop on the same instrument now measures 9.05%, and the two populations these
address are gone from the difference map rather than merely smaller.

1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE
DEVICE MAXIMUM ON GL.

RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and
the only IWorldTextureArray the Vulkan arm ever constructs -- registered its
clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as
written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own
GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets
GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident
bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler)
set it again, which is the one that actually wins.

V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that
draws through these arrays is the next slice, and it is the one that can gate a
filtering change visually." That slice was V6j, the gate is V7, and this is it.

Retail settles the question rather than the GL arm settling it.
RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler
stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at
0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's
reported cap, not a setting -- so "as much anisotropy as this device has" is
retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from
retail as well as from the shipping backend. No divergence-register row is owed
in either direction: this retires a Vulkan-only gap and lands on retail's value.

The fix asks for a ceiling rather than reading a limit back, because the pinned
RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It
does not need one: VulkanGpuSampler already clamps MaxAnisotropy to
VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at
least 16 wherever the samplerAnisotropy feature is supported -- which this
backend requires -- and 16 is where every desktop driver caps. The request and
the GL arm's read therefore land on the same number.

What it was worth, from the difference map at the same stop: the roof shingles
of both Holtburg cottages, which had been dense hatching across the whole
surface, and the stone courses of the near building are now black. Measured as
high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the
right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to
1.023; every other textured region in the frame is between 0.99 and 1.02.
Grazing-angle surfaces are where anisotropy is the whole difference, which is
why a roof was the loudest thing in the frame.

2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE.

ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth
date, which chooses the day group, the keyframe and the sun angle. The cloud
sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against
DateTime.UtcNow minus its own construction time, by design, because retail's
clouds drift with real time regardless of the date. Two launches minutes apart
therefore cannot agree about where the clouds are no matter what the route does,
and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows.

ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds ->
SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value
with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall
clock, so nothing a user or the offline gate sees changes. The differential gate
forces it on both launches alongside MSAA and the day group; the offline gate
keeps its top-280 mask, because a same-commit GL pair still has the sun to
disagree about.

This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a
workaround: it is one input to a UV offset, it is off by default, and no shipping
path reads it. The alternative on the table was -MaskTopPixels, which would have
permanently blinded the campaign's strictest instrument to the entire sky -- one
of the five surfaces the offline gate already cannot see. Rows 0-32 of the
Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up
there is roof and portal rather than cloud.

WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the
distant treeline, the player and the NPCs, and the animated portal. The portal is
phase and expected. The treeline is not filtering -- sharpness now matches within
5% and a shift search finds no sub-pixel offset -- and the two runs entered the
world at different last-logout positions (0xC95B0001 versus 0x09040008), so the
far-tier streaming history differed. That is the next thing to prove or refute.

Gates. Release build green. App tests 4,133 passed / 3 skipped against the
4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against
the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31
band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation
proven inserted by the loader: zero validation errors, zero warnings. Full
three-stop differential recorded at artifacts/v7-diff-c1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 19:14:08 +02:00

1287 lines
55 KiB
C#

using System.Numerics;
using AcDream.Content;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Audio;
using AcDream.Core.Items;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.Selection;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
internal sealed record LivePresentationDependencies(
RuntimeOptions Options,
GameWindowGraphics Graphics,
IWindow Window,
object DatLock,
RuntimeSettingsController Settings,
HostQuiescenceGate HostQuiescence,
PhysicsEngine PhysicsEngine,
PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState,
WorldEvents WorldEvents,
GameRuntime Runtime,
LiveEntityRuntimeSlot RuntimeSlot,
DeferredLiveEntityMotionRuntimeBindings MotionBindings,
DeferredEntityEffectAdvanceSource EffectAdvance,
EntityEffectPoseRegistry EffectPoses,
RemotePhysicsUpdater RemotePhysicsUpdater,
LocalPlayerShadowState LocalPlayerShadow,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
AnimationPresentationDiagnostics AnimationDiagnostics,
EntityClassificationCache ClassificationCache,
TranslucencyFadeManager TranslucencyFades,
RetailAlphaQueue RetailAlphaQueue,
CellVisibility CellVisibility,
LiveWorldOriginState WorldOrigin,
LocalPlayerIdentityState PlayerIdentity,
PointerPositionState PointerPosition,
PlayerApproachCompletionState PlayerApproachCompletions,
GameRenderResourceLifetime RenderResourceLifetime,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
AnimationHookRouter HookRouter,
IRenderFrameDiagnosticLog RenderDiagnosticLog,
WorldTimeService WorldTime,
DeferredCanonicalWorldEntityCountSource? DevWorldEntities,
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
Action<string> Log,
Action<string>? Toast)
{
public SelectionState Selection => Runtime.ActionOwner.Selection;
public RuntimeEntityObjectLifetime EntityObjects =>
Runtime.EntityObjects;
public RuntimeWorldTransitState WorldTransit => Runtime.TransitOwner;
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
}
internal sealed record LivePresentationResult(
DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle,
LiveEntityMotionRuntimeController MotionRuntime,
DeferredLiveEntityParentAcceptance ParentAcceptance,
EntitySpawnAdapter EntitySpawnAdapter,
EntityScriptActivator EntityScriptActivator,
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
RuntimeWorldTransitState WorldTransit,
WorldGenerationAvailabilityState WorldAvailability,
GpuWorldState WorldState,
RenderSceneShadowRuntime? RenderSceneShadow,
LiveEntityRuntime LiveEntities,
ProjectileController ProjectileController,
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
LiveEntityLightController Lights,
LiveEntityAnimationScheduler AnimationScheduler,
LiveEntityAnimationPresenter AnimationPresenter,
EquippedChildRenderController EquippedChildren,
EntityEffectController EntityEffects,
LiveEntityPresentationController Presentation,
RemoteTeleportController RemoteTeleport,
WbDrawDispatcher? DrawDispatcher,
RetailSelectionScene SelectionScene,
WorldSelectionQuery SelectionQuery,
SelectionInteractionController SelectionInteractions,
RetainedUiGameplayBinding? RetainedGameplay,
PaperdollViewportRenderer? PaperdollRenderer,
PaperdollFramePresenter? PaperdollPresenter,
CreatureAppraisalViewportRenderer? CreatureAppraisalRenderer,
CreatureAppraisalFramePresenter? CreatureAppraisalPresenter,
WbFrustum EnvCellFrustum,
EnvCellRenderer? EnvCellRenderer,
LandblockPresentationPipeline LandblockPipeline,
ClipFrame ClipFrame,
PortalDepthMaskRenderer? PortalDepthMask,
SkyRenderer? SkyRenderer,
ParticleRenderer? ParticleRenderer,
RenderFrameDiagnosticsController FrameDiagnostics,
LivePresentationRuntimeBindings RuntimeBindings,
DeferredLiveEntityLandblockLoadedSink LandblockLoaded);
internal interface IGameWindowLivePresentationPublication
{
void PublishLivePresentation(LivePresentationResult result);
}
internal enum LivePresentationCompositionPoint
{
CanonicalRuntimeCreated,
CanonicalRuntimeBound,
MotionRuntimeBound,
ProjectionVisibilityBound,
CorePresentationCreated,
EffectRoutingBound,
SelectionAndRadarBound,
RetainedGameplayBound,
PrivateCreatureViewportsCreated,
EnvironmentCellsCreated,
LandblockPipelineCreated,
PortalResourcesCreated,
SkyAndParticlesCreated,
DiagnosticsBound,
ResultPublished,
}
internal sealed class LivePresentationCompositionPhase
: ILivePresentationCompositionPhase<
GameWindowPlatformResult<GameWindowGraphics, Silk.NET.Input.IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult>
{
private readonly LivePresentationDependencies _dependencies;
private readonly IGameWindowLivePresentationPublication _publication;
private readonly Action<LivePresentationCompositionPoint>? _faultInjection;
public LivePresentationCompositionPhase(
LivePresentationDependencies dependencies,
IGameWindowLivePresentationPublication publication,
Action<LivePresentationCompositionPoint>? faultInjection = null)
{
_dependencies = dependencies
?? throw new ArgumentNullException(nameof(dependencies));
_publication = publication
?? throw new ArgumentNullException(nameof(publication));
_faultInjection = faultInjection;
}
public LivePresentationResult Compose(
GameWindowPlatformResult<GameWindowGraphics, Silk.NET.Input.IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
if (!ReferenceEquals(_dependencies.Graphics, platform.Graphics))
{
throw new InvalidOperationException(
"Live-presentation dependencies do not match the ordered platform result.");
}
return ComposeCore(host, content, world, interaction);
}
private LivePresentationResult ComposeCore(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction)
{
LivePresentationDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
var scope = new CompositionAcquisitionScope();
LivePresentationRuntimeBindings? bindings = null;
bool bindingsOwnedByScope = false;
try
{
CompositionAcquisitionScope.CompositionAcquisitionLease<
RenderSceneShadowRuntime>? renderSceneShadowLease = null;
renderSceneShadowLease = scope.Acquire(
"render scene",
() => new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1)),
static value => value.Dispose());
RenderSceneShadowRuntime? renderSceneShadow =
renderSceneShadowLease?.Resource;
var componentLifecycle =
new DeferredLiveEntityRuntimeComponentLifecycle();
var wbSpawnAdapter = new LandblockSpawnAdapter(
foundation.MeshAdapter
?? throw new InvalidOperationException(
"The landblock spawn ledger requires the mesh pipeline, which "
+ "Campaign V slice V6i-3 made backend-neutral."));
Setup? LoadPreparedSetup(uint sourceId)
{
if (!content.Dats.TryResolvePreferred(
sourceId,
out IDatDatabase? database,
out DatReaderWriter.Enums.DBObjType type)
|| type != DatReaderWriter.Enums.DBObjType.Setup)
{
return null;
}
return database.TryGet<Setup>(
sourceId,
out Setup? setup)
? setup
: null;
}
var setupResolver = new PreparedSetupResolver(
content.PreparedAssets,
LoadPreparedSetup,
message => Console.Error.WriteLine(
$"setup activation: {message}"));
AnimationSequencer SequencerFactory(WorldEntity entity)
{
if (setupResolver.TryResolve(
entity.SourceGfxObjOrSetupId,
out Setup? setup))
{
uint motionTableId = (uint)setup.DefaultMotionTable;
if (motionTableId != 0
&& content.Dats.Get<MotionTable>(motionTableId) is { } motionTable)
{
return new AnimationSequencer(
setup,
motionTable,
content.AnimationLoader);
}
return new AnimationSequencer(
setup,
new MotionTable(),
content.AnimationLoader);
}
return new AnimationSequencer(
new Setup(),
new MotionTable(),
NullAnimationLoader.Instance);
}
var entitySpawnAdapter = new EntitySpawnAdapter(
foundation.TextureCache,
SequencerFactory,
foundation.MeshAdapter);
EntityEffectController? entityEffects = null;
LiveEntityRuntime? liveEntities = null;
var staticRootCommitter = new StaticLiveRootCommitter(
d.RuntimeSlot,
d.PhysicsEngine.ShadowObjects,
d.WorldOrigin,
d.EffectPoses);
var staticResidency = new LiveStaticAnimationResidency(d.RuntimeSlot);
var staticAnimationScheduler =
new RetailStaticAnimatingObjectScheduler(
content.AnimationLoader,
content.AnimationHookFrames.Capture,
d.EffectPoses.Publish,
staticResidency.IsResident,
(entity, body) => _ = staticRootCommitter.Commit(entity, body),
staticResidency.ProjectionVersion);
ScriptActivationInfo? ResolveActivation(WorldEntity entity)
{
if (!setupResolver.TryResolve(
entity.SourceGfxObjOrSetupId,
out Setup? setup))
{
return null;
}
uint scriptId = setup.DefaultScript.DataId;
if (entity.IndexedPartTransforms.Count == 0)
{
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
}
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|| (liveEntities?.TryGetRecord(
entity.ServerGuid,
out LiveEntityRecord liveRecord) == true
&& (liveRecord.FinalPhysicsState
& PhysicsStateFlags.Static) != 0);
return new ScriptActivationInfo(
scriptId,
entity.IndexedPartTransforms,
EntityEffectProfile.CreateDatStatic(setup),
entity.IndexedPartAvailable,
setup,
(uint)setup.DefaultAnimation,
usesStaticAnimationWorkset);
}
var entityScriptActivator = new EntityScriptActivator(
content.ScriptRunner,
content.ParticleSink,
d.EffectPoses,
ResolveActivation,
(ownerId, entity, profile) =>
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
ownerId =>
{
entityEffects?.OnDatStaticEntityRemoved(ownerId);
content.LightingSink.UnregisterOwner(ownerId);
d.TranslucencyFades.ClearEntity(ownerId);
},
(entity, info) => staticAnimationScheduler.Register(entity, info),
staticAnimationScheduler.Unregister,
(entity, info) => staticAnimationScheduler.Rebind(entity, info));
RuntimeWorldTransitState worldTransit = d.WorldTransit;
var worldAvailability =
new WorldGenerationAvailabilityState(worldTransit);
var worldState = new GpuWorldState(
wbSpawnAdapter,
d.ClassificationCache.InvalidateLandblock,
entityScriptActivator,
worldAvailability);
bindings = new LivePresentationRuntimeBindings();
if (d.DevWorldEntities is { } devWorldEntities)
{
bindings.Adopt(
"developer world-entity source",
devWorldEntities.BindOwned(worldState));
}
var resourceOwners =
new List<CompositeLiveEntityResourceLifecycle.Owner>
{
new(
entity =>
{
if (liveEntities is null
|| !liveEntities.TryGetRecordByLocalEntityId(
entity.Id,
out LiveEntityRecord record)
|| record.ProjectionKey is not { } key)
{
throw new InvalidOperationException(
"Live presentation registration requires an exact Runtime projection key.");
}
_ = entitySpawnAdapter.OnCreate(key, entity);
},
entity => _ = entitySpawnAdapter.OnRemove(entity)),
new(
entityScriptActivator.OnCreate,
entityScriptActivator.OnRemove),
};
if (renderSceneShadow is not null)
{
resourceOwners.Add(new(
renderSceneShadow.OnLiveResourceRegistered,
renderSceneShadow.OnLiveResourceUnregistered));
}
liveEntities = new LiveEntityRuntime(
worldState,
new CompositeLiveEntityResourceLifecycle(
[.. resourceOwners]),
componentLifecycle,
d.EntityObjects);
var liveRuntimeLease = scope.Own(
"canonical live-entity runtime",
liveEntities,
static runtime => runtime.Clear());
LiveRenderProjectionJournal? liveRenderProjections =
renderSceneShadow?.BindLiveRuntime(
liveEntities,
new GpuWorldRenderTraversalOrderSource(worldState));
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
bindings.Adopt(
"canonical live-runtime slot",
d.RuntimeSlot.BindOwned(liveEntities));
Fault(LivePresentationCompositionPoint.CanonicalRuntimeBound);
var selectionInteractionSource =
new DeferredSelectionInteractionSource();
var motionRuntime = new LiveEntityMotionRuntimeController(
liveEntities,
d.PhysicsDataCache,
() => selectionInteractionSource.Current,
d.Selection,
d.WorldOrigin);
bindings.Adopt(
"live motion runtime",
d.MotionBindings.BindOwned(motionRuntime));
Fault(LivePresentationCompositionPoint.MotionRuntimeBound);
Action<LiveEntityRecord, bool> wbVisibility = (record, visible) =>
{
if (record.WorldEntity is { } entity)
entitySpawnAdapter.SetPresentationResident(entity, visible);
};
bindings.BindProjectionVisibility(
liveEntities,
wbVisibility,
"WB projection visibility");
if (liveRenderProjections is not null)
{
bindings.BindProjectionVisibility(
liveEntities,
liveRenderProjections.OnProjectionVisibilityChanged,
"shadow render projection visibility");
}
Action<LiveEntityRecord, bool> particleVisibility = (record, visible) =>
{
if (record.WorldEntity is { } entity)
content.ParticleSink.SetEntityPresentationVisible(entity.Id, visible);
};
bindings.BindProjectionVisibility(
liveEntities,
particleVisibility,
"particle projection visibility");
Fault(LivePresentationCompositionPoint.ProjectionVisibilityBound);
var projectileController = new ProjectileController(
liveEntities,
new DatProjectileSetupResolver(content.Dats, d.DatLock),
new EntityRootPosePublisher(d.EffectPoses),
d.WorldOrigin)
{
DiagnosticSink = message =>
Console.Error.WriteLine($"projectile: {message}"),
};
var projectionWithdrawal =
new LiveEntityProjectionWithdrawalController(
liveEntities,
projectileController,
d.WorldGameState,
d.WorldEvents,
d.PhysicsEngine.ShadowObjects,
d.EffectPoses,
d.LocalPlayerShadow);
var lightsLease = scope.Acquire(
"live-entity lights",
() => new LiveEntityLightController(
liveEntities,
d.EffectPoses,
content.LightingSink,
setupId => content.Dats.Get<Setup>(setupId)),
static value => value.Dispose());
var ordinaryPhysicsUpdater = new LiveEntityOrdinaryPhysicsUpdater(
d.EntityObjects.Physics,
d.MotionBindings.GetSetupCylinder);
var animationScheduler = new LiveEntityAnimationScheduler(
liveEntities,
d.PlayerIdentity,
d.RemotePhysicsUpdater,
ordinaryPhysicsUpdater,
projectileController,
new EntityRootPosePublisher(d.EffectPoses),
new AnimationHookCaptureSink(content.AnimationHookFrames));
var animationPresenter = new LiveEntityAnimationPresenter(
liveEntities,
staticAnimationScheduler,
d.EffectPoses,
new LiveAnimationPresentationContext(
liveEntities,
d.PlayerIdentity,
d.PlayerController),
d.AnimationDiagnostics,
d.Options.HidePartIndex);
var parentAcceptance = new DeferredLiveEntityParentAcceptance();
var equippedLease = scope.Acquire(
"equipped-child renderer",
() => new EquippedChildRenderController(
content.Dats,
d.DatLock,
d.EntityObjects.Objects,
liveEntities,
d.EffectPoses,
parentAcceptance.TryAccept,
(childRecord, positionVersion, projectionVersion) =>
projectionWithdrawal.WithdrawExact(
childRecord,
positionVersion,
projectionVersion,
d.PlayerIdentity.ServerGuid)),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.CorePresentationCreated);
var tableResolver = new PhysicsScriptTableResolver(
id => content.Dats.Get<PhysicsScriptTable>(id));
entityEffects = new EntityEffectController(
liveEntities,
content.ScriptRunner,
tableResolver,
d.EffectPoses,
(parentLocalId, partIndex) =>
equippedLease.Resource.FindChildLocalIdAtPart(
parentLocalId,
partIndex),
equippedLease.Resource.FindParentLocalId,
ownerId => content.Audio?.EntitySoundTables.Remove(ownerId),
(ownerId, soundTableDid) =>
{
content.Audio?.EntitySoundTables.Remove(ownerId);
if (soundTableDid is { } did)
content.Audio?.EntitySoundTables.Set(ownerId, did);
});
bindings.Adopt(
"entity-effect advance",
d.EffectAdvance.BindOwned(entityEffects));
entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}");
var partArrayLifecycle = new LiveEntityPartArrayLifecycle(
d.AnimatedEntities);
var presentationLease = scope.Acquire(
"live-entity presentation",
() => new LiveEntityPresentationController(
liveEntities,
d.PhysicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition,
new LiveEntityPartArrayEnterWorldPort(
partArrayLifecycle.HandleEnterWorld),
equippedLease.Resource.SetDirectChildrenNoDraw,
d.MotionBindings.ClearTargetForHiddenEntity,
d.WorldOrigin.GetCenter),
static value => value.Dispose());
var remoteShadowPlacement = new RemoteShadowPlacementSynchronizer(
d.RemotePhysicsUpdater,
d.WorldOrigin);
var remoteTeleportPresentation =
new RemoteTeleportPlacementPresentation(presentationLease.Resource);
var remoteTeleportLease = scope.Acquire(
"remote teleport",
() => new RemoteTeleportController(
d.PhysicsEngine,
liveEntities,
d.MotionBindings.GetSetupCylinder,
d.WorldOrigin.CellLocalForSeed,
remoteShadowPlacement.Sync,
remoteTeleportPresentation.Complete,
remoteTeleportPresentation.Begin),
static value => value.Dispose());
bindings.BindProjectionPoseReady(
equippedLease.Resource,
lightsLease.Resource.OnAttachedPoseReady);
if (liveRenderProjections is not null)
{
bindings.BindProjectionPoseReady(
equippedLease.Resource,
liveRenderProjections.OnProjectionPoseReady);
bindings.BindProjectionRemoved(
equippedLease.Resource,
liveRenderProjections.OnProjectionRemoved);
}
bindings.Adopt(
"entity-effect animation hooks",
content.HookRegistrations.RegisterOwned(entityEffects));
Fault(LivePresentationCompositionPoint.EffectRoutingBound);
return CompletePresentation(
host,
content,
world,
interaction,
componentLifecycle,
motionRuntime,
parentAcceptance,
entitySpawnAdapter,
entityScriptActivator,
staticAnimationScheduler,
worldTransit,
worldAvailability,
worldState,
renderSceneShadow,
renderSceneShadowLease,
liveEntities,
projectileController,
projectionWithdrawal,
lightsLease,
animationScheduler,
animationPresenter,
equippedLease,
entityEffects,
presentationLease,
remoteTeleportLease,
selectionInteractionSource,
bindings,
scope,
ref bindingsOwnedByScope,
liveRuntimeLease);
}
catch (Exception failure)
{
if (bindings is not null && !bindingsOwnedByScope)
{
scope.Own(
"live-presentation runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
}
scope.RollbackAndThrow(failure);
throw new System.Diagnostics.UnreachableException();
}
}
private LivePresentationResult CompletePresentation(
HostInputCameraResult host,
ContentEffectsAudioResult content,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
DeferredLiveEntityRuntimeComponentLifecycle componentLifecycle,
LiveEntityMotionRuntimeController motionRuntime,
DeferredLiveEntityParentAcceptance parentAcceptance,
EntitySpawnAdapter entitySpawnAdapter,
EntityScriptActivator entityScriptActivator,
RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
RuntimeWorldTransitState worldTransit,
WorldGenerationAvailabilityState worldAvailability,
GpuWorldState worldState,
RenderSceneShadowRuntime? renderSceneShadow,
CompositionAcquisitionScope.CompositionAcquisitionLease<
RenderSceneShadowRuntime>? renderSceneShadowLease,
LiveEntityRuntime liveEntities,
ProjectileController projectileController,
LiveEntityProjectionWithdrawalController projectionWithdrawal,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityLightController> lightsLease,
LiveEntityAnimationScheduler animationScheduler,
LiveEntityAnimationPresenter animationPresenter,
CompositionAcquisitionScope.CompositionAcquisitionLease<EquippedChildRenderController> equippedLease,
EntityEffectController entityEffects,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityPresentationController> presentationLease,
CompositionAcquisitionScope.CompositionAcquisitionLease<RemoteTeleportController> remoteTeleportLease,
DeferredSelectionInteractionSource selectionInteractionSource,
LivePresentationRuntimeBindings bindings,
CompositionAcquisitionScope scope,
ref bool bindingsOwnedByScope,
CompositionAcquisitionScope.CompositionAcquisitionLease<LiveEntityRuntime> liveRuntimeLease)
{
LivePresentationDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
AlphaScratchBudgetProfile alphaScratchBudgets =
AlphaScratchBudgetProfile.Create(
d.Options.ResidencyBudgets.AlphaScratchBytes);
// Campaign V slice V6h: every renderer below this line is still raw GL.
// On a backend without a GL context none of them is constructed, and the
// CPU owners this phase also produces — entity lifetime, motion,
// selection, effects, lights, the landblock pipeline — run unchanged.
GL? gl = d.Graphics.Gl;
var selectionScene = new RetailSelectionScene(
new RetailSelectionGeometryCache(content.Dats, d.DatLock));
// Campaign V slice V6j: the world dispatcher exists on BOTH arms. The GL
// arm is unchanged; the RHI arm records into the pass the world scene
// phase publishes on this scope.
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
var dispatcherLease = scope.Acquire(
"WB draw dispatcher",
() => gl is not null
? new WbDrawDispatcher(
gl,
foundation.MeshShader!,
foundation.TextureCache,
foundation.MeshAdapter!,
entitySpawnAdapter,
foundation.Bindless!,
d.ClassificationCache,
d.TranslucencyFades,
selectionScene,
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes)
: new WbDrawDispatcher(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"A backend without a GL context must publish a world pass scope."),
foundation.TextureCache,
foundation.MeshAdapter!,
entitySpawnAdapter,
d.ClassificationCache,
d.TranslucencyFades,
selectionScene,
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
d.EntityObjects.Objects,
selectionScene,
() => d.PlayerIdentity.ServerGuid,
interaction.LateBindings.SelectionCamera.Snapshot,
() => new Vector2(
d.PointerPosition.X,
d.PointerPosition.Y),
() => d.PlayerController.Controller is { } player
? new PlayerInteractionPose(player.CellId, player.Position)
: null,
d.MotionBindings.GetSetupCylinder,
setupId =>
{
lock (d.DatLock)
{
if (!content.Dats.TryGet<Setup>(setupId, out Setup? setup)
|| setup.SelectionSphere is not { } sphere)
{
return null;
}
return (sphere.Origin, sphere.Radius);
}
});
var radarSnapshotProvider = new RadarSnapshotProvider(
d.EntityObjects.Objects,
liveEntities,
() => liveEntities.Snapshots,
playerGuid: () => d.PlayerIdentity.ServerGuid,
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
playerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
selectedGuid: () => d.Selection.SelectedObjectId,
coordinatesOnRadar: () => d.Settings.Gameplay.CoordinatesOnRadar,
uiLocked: () => d.Settings.Gameplay.LockUI,
spatialQuery: () => worldState);
bindings.Adopt(
"radar snapshot",
interaction.LateBindings.Radar.Bind(radarSnapshotProvider));
var selectionInteractions = new SelectionInteractionController(
d.Selection,
selectionQuery,
interaction.ItemInteraction,
new WorldSessionSelectionInteractionTransport(
() => interaction.LateBindings.Session.CurrentSession),
new PlayerInteractionMovementSink(
() => d.PlayerController.Controller,
d.PlayerApproachCompletions),
d.Toast,
d.PlayerApproachCompletions);
selectionInteractionSource.Bind(selectionInteractions);
bindings.Adopt(
"world selection",
interaction.LateBindings.Selection.Bind(
selectionQuery,
selectionInteractions));
Fault(LivePresentationCompositionPoint.SelectionAndRadarBound);
CompositionAcquisitionScope.CompositionAcquisitionLease<
RetainedUiGameplayBinding>? retainedGameplayLease = null;
if (interaction.RetainedUi is { } retainedUi)
{
retainedGameplayLease = scope.Acquire(
"retained gameplay binding",
() => RetainedUiGameplayBinding.Create(
retainedUi.Host.Root,
(item, x, y) =>
selectionInteractions.PlaceDraggedItem(item, x, y),
d.HostQuiescence),
static value => value.Dispose());
retainedGameplayLease.Resource.Attach();
}
Fault(LivePresentationCompositionPoint.RetainedGameplayBound);
if (dispatcherLease.Resource is { } alphaDispatcher)
{
alphaDispatcher.AlphaToCoverage =
d.Settings.ResolvedQuality.AlphaToCoverage;
}
CompositionAcquisitionScope.CompositionAcquisitionLease<
PaperdollViewportRenderer>? paperdollLease = null;
PaperdollFramePresenter? paperdollPresenter = null;
// Campaign V slice V6l: both retained-UI viewports exist on BOTH arms.
// The RHI arm needed two backend fixes first — a layered sampled view per
// render target, so a colour attachment can legally enter the
// sampler2DArray table, and sample-count pipeline variants in
// WbDrawDispatcher, because a world pipeline is built at the backbuffer's
// count and an offscreen target is single-sampled by contract (plan
// §5.5.16 defect 3).
if (dispatcherLease.Resource is { } paperdollDispatcher
&& interaction.RetainedUi?.Runtime.PaperdollViewportWidget is { } viewport
&& interaction.RetainedUi.Runtime.InventoryFrame is { } inventoryFrame)
{
paperdollLease = scope.Acquire(
"paperdoll viewport",
() => new PaperdollViewportRenderer(
gl,
worldPassScope,
host.GpuDevice,
host.GpuFrameLifetime,
paperdollDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!),
static value => value.Dispose());
IUiViewportRenderer? previousRenderer = viewport.Renderer;
viewport.Renderer = paperdollLease.Resource;
bindings.AdoptRelease(
"paperdoll viewport target",
() =>
{
if (ReferenceEquals(viewport.Renderer, paperdollLease.Resource))
viewport.Renderer = previousRenderer;
});
paperdollPresenter = new PaperdollFramePresenter(
paperdollLease.Resource,
new RetailPaperdollFrameView(
viewport,
new PaperdollInventoryVisibility(inventoryFrame)),
new RetailPaperdollDollFactory(
new LivePaperdollEntityLookup(liveEntities),
d.PlayerIdentity,
new RetailPaperdollPoseApplicator(
content.Dats,
content.AnimationLoader,
d.DatLock)));
}
CompositionAcquisitionScope.CompositionAcquisitionLease<
CreatureAppraisalViewportRenderer>? creatureAppraisalLease = null;
CreatureAppraisalFramePresenter? creatureAppraisalPresenter = null;
if (dispatcherLease.Resource is { } appraisalDispatcher
&& interaction.RetainedUi?.Runtime.CreatureAppraisalViewportWidget
is { } creatureViewport
&& interaction.RetainedUi.Runtime.ExaminationFrame
is { } examinationFrame
&& interaction.RetainedUi.Runtime.AppraisalController
is { } appraisalController)
{
creatureAppraisalLease = scope.Acquire(
"creature appraisal viewport",
() => new CreatureAppraisalViewportRenderer(
gl,
worldPassScope,
host.GpuDevice,
host.GpuFrameLifetime,
appraisalDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!),
static value => value.Dispose());
IUiViewportRenderer? previousRenderer = creatureViewport.Renderer;
creatureViewport.Renderer = creatureAppraisalLease.Resource;
bindings.AdoptRelease(
"creature appraisal viewport target",
() =>
{
if (ReferenceEquals(
creatureViewport.Renderer,
creatureAppraisalLease.Resource))
{
creatureViewport.Renderer = previousRenderer;
}
});
creatureAppraisalPresenter = new CreatureAppraisalFramePresenter(
creatureAppraisalLease.Resource,
new RetailCreatureAppraisalFrameView(
creatureViewport,
examinationFrame,
appraisalController),
new RetailCreatureAppraisalCloneFactory(
new LiveCreatureAppraisalEntityLookup(liveEntities)));
}
Fault(LivePresentationCompositionPoint.PrivateCreatureViewportsCreated);
var envCellFrustum = new WbFrustum();
var envCellLease = scope.Acquire(
"environment-cell renderer",
() => gl is not null
? new EnvCellRenderer(
gl,
foundation.MeshAdapter!.MeshManager!,
envCellFrustum)
: new EnvCellRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"A backend without a GL context must publish a world pass scope."),
foundation.MeshAdapter!.MeshManager!,
envCellFrustum),
static value => value.Dispose());
// The RHI arm's three pipelines ARE its program, built at construction,
// so only the GL arm has a second initialisation step.
if (foundation.MeshShader is { } envCellShader)
envCellLease.Resource.Initialize(envCellShader);
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
// The streaming pipeline itself is backend-neutral and runs on both
// arms: landblocks load, heightfields and collision build, and the
// spatial index fills. Only publication into GPU state is renderer-
// owned, so the Vulkan arm publishes into nothing until the world arm
// lands.
TerrainModernRenderer? terrainRenderer = foundation.Terrain;
EnvCellRenderer? envCells = envCellLease.Resource;
var landblockRenderPublisher = new LandblockRenderPublisher(
(landblockId, meshData, origin) =>
terrainRenderer?.AddLandblockWithMesh(
landblockId,
meshData,
origin),
landblockId => terrainRenderer?.RemoveLandblock(landblockId),
d.CellVisibility,
worldState,
prepareEnvCells: build =>
{
if (foundation.MeshAdapter?.MeshManager is { } envCellMeshes)
EnvCellMeshPreparationScheduler.Schedule(build, envCellMeshes);
},
removeEnvCells: landblockId => envCells?.RemoveLandblock(landblockId),
envCellPublisher: envCells);
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
d.EntityObjects.Physics,
world.TerrainBuild.HeightTable);
var landblockStaticPublisher =
new LandblockStaticPresentationPublisher(
content.LightingSink,
d.TranslucencyFades,
d.WorldGameState,
d.WorldEvents);
var landblockRetirementOwner =
new LandblockPresentationRetirementOwner(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPublisher,
content.LightingSink,
d.TranslucencyFades);
var landblockLoaded = new DeferredLiveEntityLandblockLoadedSink();
var landblockPipeline = new LandblockPresentationPipeline(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPublisher,
worldState,
landblockRetirementOwner,
landblockLoaded.OnLandblockLoaded,
landblockRenderPublisher.PrepareAfterRenderPins,
renderSceneShadow?.StaticProjections);
Fault(LivePresentationCompositionPoint.LandblockPipelineCreated);
var clipFrameLease = scope.Acquire(
"portal clip frame",
ClipFrame.NoClip,
static value => value.Dispose());
// Campaign V slice V6l: the portal depth mask exists on BOTH arms. The
// GL arm keeps its inline program and raw draws; the RHI arm compiles
// portal_depth from SPIR-V into three pipelines and records into the
// pass the world scene phase publishes on this scope.
var portalDepthLease = scope.Acquire(
"portal depth mask",
() => gl is not null
? new PortalDepthMaskRenderer(gl)
: new PortalDepthMaskRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"A backend without a GL context must publish a world pass scope.")),
static value => value.Dispose());
CompositionAcquisitionScope.CompositionAcquisitionLease<
PortalWaitNoticeController>? portalWaitNoticeLease = null;
if (interaction.RetainedUi is { } portalRetainedUi)
{
portalWaitNoticeLease = scope.Acquire(
"portal wait notice",
() => new PortalWaitNoticeController(
portalRetainedUi.Host.Root),
static value => value.Dispose());
}
Action<string?>? displayPortalWaitNotice =
portalWaitNoticeLease is { } waitNoticeLease
? waitNoticeLease.Resource.Set
: null;
CompositionAcquisitionScope.CompositionAcquisitionLease<
PortalTunnelPresentation>? portalTunnelLease = null;
// Campaign V slice V6m: portal space exists on BOTH arms. The GL arm is
// unchanged; the RHI arm opens a backbuffer pass of its own and publishes
// it on the scope for the span of the draw, the way the two offscreen
// viewports do. It was the last raw-GL world-adjacent renderer, so the
// Vulkan arm no longer composes a portal-less teleport presentation.
if (dispatcherLease.Resource is { } portalDispatcher)
{
PortalTunnelPresentation portalTunnel;
try
{
portalTunnel = d.PortalTunnelFallback.AcquirePrepared(
() => PortalTunnelPresentation.CreateRequired(
gl,
worldPassScope,
host.GpuFrameLifetime,
content.Dats,
content.AnimationLoader,
d.HookRouter,
portalDispatcher,
foundation.SceneLighting!,
foundation.MeshAdapter!,
displayPortalWaitNotice,
portalWaitNoticeLease?.Resource),
static tunnel => tunnel.PrepareResources());
}
catch (Exception acquisitionFailure)
{
try
{
d.PortalTunnelFallback.ReleaseFallback();
}
catch (Exception cleanupFailure)
{
throw new AggregateException(
"Portal-tunnel construction and fallback rollback both failed.",
acquisitionFailure,
cleanupFailure);
}
throw;
}
portalTunnelLease = scope.Own(
"portal tunnel fallback",
portalTunnel,
_ => d.PortalTunnelFallback.ReleaseFallback());
}
Fault(LivePresentationCompositionPoint.PortalResourcesCreated);
AcDream.App.Rendering.Shader? skyShader = gl is null
? null
: d.RenderResourceLifetime.AcquireSkyShader(
() => new AcDream.App.Rendering.Shader(
gl,
Path.Combine(foundation.ShadersDirectory, "sky.vert"),
Path.Combine(foundation.ShadersDirectory, "sky.frag"),
// Campaign V slice V6e: sky reads the shared texture table and
// ACDREAM_UBO_SET now, both of which common.glsl declares.
includeCommonPreamble: true));
var skyShaderLease = skyShader is null
? null
: scope.Own(
"sky shader lifetime",
skyShader,
_ => d.RenderResourceLifetime.ReleaseSkyShader());
// Campaign V slice V6k: the sky exists on BOTH arms. The GL arm is
// unchanged except for where its table slots come from; the RHI arm
// compiles the sky pair from SPIR-V and records into the pass the world
// scene phase publishes on this scope.
var skyLease = scope.Acquire(
"sky renderer",
() => gl is not null
? new SkyRenderer(
gl,
content.Dats,
skyShader!,
foundation.TextureCache,
foundation.Samplers!,
// Slice V6e: the sky samples through the shared texture table,
// so it needs the same bindless entry point the world path uses.
foundation.Bindless!,
// Slice V6k: and V4t's world-handle seam on the device, which
// retired the last per-renderer GlBindlessHandleTable.
(AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)host.GpuDevice)
{
// Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS
// is set, which is every run but a differential gate's.
AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
}
: new SkyRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"A backend without a GL context must publish a world pass scope."),
content.Dats,
foundation.TextureCache)
{
AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
},
static value => value.Dispose());
// Campaign V slice V6l: particles exist on BOTH arms. The GL arm is
// unchanged; the RHI arm compiles the two particle pairs from SPIR-V,
// draws instances through the vertex binding the V6l contract amendment
// added, and records into the pass the world scene phase publishes on
// this scope.
var particleLease = scope.AcquireOptional(
"particle renderer",
() => gl is not null
? new ParticleRenderer(
gl,
foundation.ShadersDirectory,
content.ParticleSystem,
foundation.TextureCache,
content.Dats,
foundation.MeshAdapter!,
d.RetailAlphaQueue,
alphaScratchBudgets.ParticleBytes)
: new ParticleRenderer(
host.GpuDevice,
host.GpuFrameLifetime,
worldPassScope
?? throw new InvalidOperationException(
"A backend without a GL context must publish a world pass scope."),
content.ParticleSystem,
foundation.TextureCache,
content.Dats,
foundation.MeshAdapter!,
d.RetailAlphaQueue,
alphaScratchBudgets.ParticleBytes),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated);
IRenderFrameResourceDiagnosticsSource? resourceDiagnostics =
d.Options.UiProbeDump
&& dispatcherLease.Resource is { } diagnosticDispatcher
&& envCellLease.Resource is { } diagnosticEnvCells
&& particleLease.Resource is { } diagnosticParticles
&& portalDepthLease.Resource is { } diagnosticPortalDepth
? new RuntimeRenderFrameResourceDiagnosticsSource(
content.ParticleSystem,
content.ParticleSink,
diagnosticDispatcher,
diagnosticEnvCells,
diagnosticParticles,
interaction.RetainedUi?.Host.TextRenderer,
diagnosticPortalDepth,
clipFrameLease.Resource,
foundation.Terrain!,
foundation.SceneLighting!,
foundation.MeshAdapter!,
foundation.TextureCache,
content.PreparedAssets)
: null;
var frameDiagnostics = new RenderFrameDiagnosticsController(
new RuntimeRenderFrameTitleFactsSource(
worldState,
d.AnimatedEntities,
d.WorldTime),
new SilkRenderFrameTitleSink(d.Window),
d.RenderDiagnosticLog,
d.Options.UiProbeDump,
resourceDiagnostics);
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
{
bindings.Adopt(
"developer frame diagnostics",
devFrameDiagnostics.BindOwned(frameDiagnostics));
}
bindings.Adopt(
"retained-UI frame diagnostics",
d.UiFrameDiagnostics.BindOwned(frameDiagnostics));
Fault(LivePresentationCompositionPoint.DiagnosticsBound);
var bindingsLease = scope.Own(
"live-presentation runtime bindings",
bindings,
static value => value.Dispose());
bindingsOwnedByScope = true;
var result = new LivePresentationResult(
componentLifecycle,
motionRuntime,
parentAcceptance,
entitySpawnAdapter,
entityScriptActivator,
staticAnimationScheduler,
worldTransit,
worldAvailability,
worldState,
renderSceneShadow,
liveEntities,
projectileController,
projectionWithdrawal,
lightsLease.Resource,
animationScheduler,
animationPresenter,
equippedLease.Resource,
entityEffects,
presentationLease.Resource,
remoteTeleportLease.Resource,
dispatcherLease.Resource,
selectionScene,
selectionQuery,
selectionInteractions,
retainedGameplayLease?.Resource,
paperdollLease?.Resource,
paperdollPresenter,
creatureAppraisalLease?.Resource,
creatureAppraisalPresenter,
envCellFrustum,
envCellLease.Resource,
landblockPipeline,
clipFrameLease.Resource,
portalDepthLease.Resource,
skyLease.Resource,
particleLease.Resource,
frameDiagnostics,
bindings,
landblockLoaded);
foundation.Residency.RegisterDomainSource(
new DelegateResidencyDomainSource(
ResidencyDomain.AlphaScratch,
() => new ResidencyDomainSnapshot(
ResidencyDomain.AlphaScratch,
EntryCount: 3,
OwnerCount: 3,
Charges: new ResidencyCharges(
ScratchBytes: checked(
d.RetailAlphaQueue.RetainedScratchBytes
+ (dispatcherLease.Resource?.RetainedAlphaScratchBytes ?? 0)
+ (particleLease.Resource?.RetainedAlphaScratchBytes ?? 0))),
BudgetBytes: alphaScratchBudgets.TotalBytes)));
_publication.PublishLivePresentation(result);
liveRuntimeLease.Transfer();
renderSceneShadowLease?.Transfer();
lightsLease.Transfer();
equippedLease.Transfer();
presentationLease.Transfer();
remoteTeleportLease.Transfer();
dispatcherLease.Transfer();
retainedGameplayLease?.Transfer();
paperdollLease?.Transfer();
creatureAppraisalLease?.Transfer();
envCellLease.Transfer();
clipFrameLease.Transfer();
portalDepthLease.Transfer();
portalTunnelLease?.Transfer();
portalWaitNoticeLease?.Transfer();
skyLease.Transfer();
skyShaderLease?.Transfer();
particleLease.Transfer();
bindingsLease.Transfer();
Fault(LivePresentationCompositionPoint.ResultPublished);
scope.Complete();
return result;
}
private void Fault(LivePresentationCompositionPoint point) =>
_faultInjection?.Invoke(point);
private sealed class NullAnimationLoader : IAnimationLoader
{
public static NullAnimationLoader Instance { get; } = new();
public Animation? LoadAnimation(uint id) => null;
}
private sealed class DeferredSelectionInteractionSource
{
public SelectionInteractionController? Current { get; private set; }
public void Bind(SelectionInteractionController value)
{
ArgumentNullException.ThrowIfNull(value);
if (Current is not null)
{
throw new InvalidOperationException(
"Live motion selection interactions are already bound.");
}
Current = value;
}
}
}