GameWindow.Dispose() (via Program.cs's `using var window = ...`) runs
unconditionally even when invoked mid-unwind of an exception that escaped
Run()'s Silk.NET frame loop. Resource teardown itself can converge
cleanly regardless, so CompleteShutdown had no way to tell "normal Run()
return" from "a crash is propagating through me right now" and always
wrote the hardcoded exited{code:0,reason:"graceful"} — exactly the
symptom #406 observed against a real 0xE0434352 crash. Fixed by latching
_runFailure in Run()'s existing catch block (before the pre-existing
throw) and consulting it from a new ReportExited method, the one call
site for the terminal status write: crashed(1)/graceful(0)/
shutdown-incomplete(1) as appropriate. No wire-contract amendment needed
— §LA1 pins the exited event NAME, and reason is already free text that
StatusEventParser round-trips unchanged.
Sibling gap fixed in the same commit: the launcher discarded the child's
stdout/stderr entirely, which is why diagnosing this exact crash required
a manual console re-run. Added BoundedProcessOutputCapture, a 2 MiB-capped
sink mirroring SessionStatusWriter's open-append-flush-close-per-write
posture (a long-lived write handle is not actually concurrently readable
on Windows even with FileShare.Read — confirmed by isolated repro), wired
into both SystemChildProcess (ProcessStartInfo.RedirectStandardError;
Linux + Windows graphical children, i.e. this bug's own scenario) and
WindowsSystemChildProcess (a real native pipe via CreateChildOutputPipe,
mirroring the existing stdin pipe; Windows console-capable/Headless
children). Opt-in via LauncherProcessSpec.StderrLogPath (null = unchanged
behavior), threaded through SessionConfigComposer -> client.err.log
beside status.jsonl -> LauncherExecutableSet -> LauncherOrchestrator.
Tests: GameWindowCrashStatusTests (source-shape, matching the existing
GameWindow test pattern — the class cannot be constructed without a live
GPU/window), BoundedProcessOutputCaptureTests (10 unit tests), and three
new LauncherProcessSupervisorTests spawning real child processes through
both capture code paths.
Launcher.Core.Tests: 337/0 (was 324/0). Launcher.Tests: 67/0 (unchanged).
Full solution build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1858 lines
89 KiB
C#
1858 lines
89 KiB
C#
using AcDream.Core.Plugins;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Settings;
|
|
using AcDream.App.Platform;
|
|
using AcDream.App.World;
|
|
using AcDream.Content;
|
|
using AcDream.Platform;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Session;
|
|
using DatReaderWriter;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.Maths;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
public sealed class GameWindow :
|
|
IDisposable,
|
|
IGameWindowPlatformPublication<GameWindowGraphics, IInputContext>,
|
|
IGameWindowHostInputCameraPublication,
|
|
IGameWindowContentEffectsAudioPublication,
|
|
IGameWindowWorldRenderPublication,
|
|
IGameWindowInteractionRetainedUiPublication,
|
|
IGameWindowLivePresentationPublication,
|
|
IGameWindowSessionPlayerPublication,
|
|
IGameWindowFrameRootPublication
|
|
{
|
|
private static double ClientTimerNow() =>
|
|
System.Diagnostics.Stopwatch.GetTimestamp()
|
|
/ (double)System.Diagnostics.Stopwatch.Frequency;
|
|
|
|
private readonly AcDream.App.RuntimeOptions _options;
|
|
// Campaign LA slice LA1: no-op instance when --session-config didn't
|
|
// configure a statusFile (or the env-var launch path was used at all).
|
|
private readonly SessionStatusWriter _statusWriter;
|
|
private readonly AnimationPresentationDiagnostics _animationDiagnostics;
|
|
private readonly string _datDir;
|
|
private readonly WorldGameState _worldGameState;
|
|
private readonly WorldEvents _worldEvents;
|
|
private readonly HostQuiescenceGate _hostQuiescence = new();
|
|
private IWindow? _window;
|
|
// #343: mirrors Silk's own ViewImplementationBase._inRenderLoop guard,
|
|
// which we cannot read directly (it is a private field on Silk's
|
|
// internal type). Set true before OnUpdate/OnRender run and cleared
|
|
// only on their normal (non-throwing) return — exactly like Silk's own
|
|
// bracket around DoUpdate/DoRender — so a frame callback that throws
|
|
// leaves this stuck true, same as Silk's real guard. GameWindowLifetime
|
|
// reads it before disposing the native window, so a wounded loop defers
|
|
// the release instead of calling Reset() while Silk still thinks it is
|
|
// mid-frame (which throws "You cannot call `Reset` inside of the render
|
|
// loop!" and would otherwise bury whatever exception actually wounded
|
|
// the loop). See docs/ISSUES.md #343.
|
|
private bool _renderLoopArmed;
|
|
private SilkWindowCallbackBinding? _windowCallbacks;
|
|
private GameWindowGraphics? _graphics;
|
|
// Campaign V slice V6h: borrowed, not owned — _graphics owns the context and
|
|
// disposes it. Retained because the render callback has to bring the
|
|
// swapchain up to date before a frame opens, which is the one piece of
|
|
// presentation the RHI contract deliberately does not cover.
|
|
private AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext? _vulkanGraphics;
|
|
private FramePacingPolicy _startupPacing;
|
|
private AcDream.UI.Abstractions.Settings.QualitySettings _startupQuality =
|
|
AcDream.UI.Abstractions.Settings.QualitySettings.From(
|
|
AcDream.UI.Abstractions.Settings.QualityPreset.High);
|
|
private IInputContext? _input;
|
|
private TerrainModernRenderer? _terrain;
|
|
private CameraController? _cameraController;
|
|
private IDatReaderWriter? _dats;
|
|
private IPreparedAssetSource? _preparedAssets;
|
|
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
|
|
private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput;
|
|
private TextureCache? _textureCache;
|
|
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
|
/// after <c>OnLoad</c> completes (modern path is mandatory as of N.5).</summary>
|
|
private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter;
|
|
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
|
private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator;
|
|
private RetailStaticAnimatingObjectScheduler? _staticAnimationScheduler;
|
|
private RenderSceneShadowRuntime? _renderSceneShadow;
|
|
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
|
private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene;
|
|
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
|
|
private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions;
|
|
private DebugLineRenderer? _debugLines;
|
|
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
|
// wireframes are noisy outdoors and confuse first-time users into
|
|
// thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel
|
|
// → Diagnostics → "Toggle collision wires" button toggles too.
|
|
private readonly AcDream.App.Rendering.WorldSceneDebugState
|
|
_worldSceneDebugState = new();
|
|
|
|
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in favor of
|
|
// the ImGui-backed DebugPanel, which Campaign V slice V11 removed in turn.
|
|
// The TextRenderer + BitmapFont fields stay alive because they're shared
|
|
// with UiHost and reserved for the future world-space HUD (D.6 —
|
|
// damage floaters, name plates) where ImGui couldn't reach into the 3D
|
|
// scene. They are no longer used for any debug overlay.
|
|
private TextRenderer? _textRenderer;
|
|
private BitmapFont? _debugFont;
|
|
// [FRAME-DIAG]: retain only the render-thread entity upload distribution.
|
|
// Landblock publication timing/counts live with the focused publishers and
|
|
// are read from their typed snapshots when diagnostics flush.
|
|
private readonly bool _frameDiag = string.Equals(
|
|
System.Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
|
|
"1",
|
|
System.StringComparison.Ordinal);
|
|
|
|
// MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call
|
|
// per OnRender + three stage scopes. All logic lives in
|
|
// AcDream.App.Diagnostics.FrameProfiler (structure rule 1).
|
|
private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new();
|
|
private readonly AcDream.App.Rendering.IRenderFrameDiagnosticLog
|
|
_renderDiagnosticLog =
|
|
new AcDream.App.Rendering.ConsoleRenderFrameDiagnosticLog();
|
|
private readonly AcDream.App.Rendering.DebugVmRenderFactsPublisher
|
|
_debugVmRenderFacts = new();
|
|
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
|
|
_renderFrameDiagnostics;
|
|
private LivePresentationRuntimeBindings? _livePresentationBindings;
|
|
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
|
|
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
|
private FrameRootRuntimeBindings? _frameRootBindings;
|
|
private IDisposable? _frameGraphPublication;
|
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
|
private IGpuDevice? _gpuDevice;
|
|
private GpuDeviceFrameLifetime? _gpuFrameLifetime;
|
|
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
|
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
|
_renderResourceLifetime = new();
|
|
private readonly AcDream.App.Rendering.ResourceConstructionCleanupLedger
|
|
_constructionCleanup = new();
|
|
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;
|
|
private readonly GameWindowLifetime _lifetime = new();
|
|
// fix #406: set by Run()'s own catch the instant an exception escapes
|
|
// the Silk.NET frame loop, BEFORE it is rethrown and unwinds through
|
|
// Program.cs's `using var window = ...` (which calls Dispose() —
|
|
// therefore CompleteShutdown() — while that exception is still in
|
|
// flight). CompleteShutdown consults this so a crash is never reported
|
|
// as the hardcoded "exited{code:0,reason:graceful}" the resource
|
|
// teardown transaction's own convergence would otherwise imply.
|
|
private Exception? _runFailure;
|
|
private readonly DisplayFramePacingController _displayFramePacing;
|
|
private readonly RuntimeSettingsController _runtimeSettings;
|
|
|
|
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
|
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
|
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
|
private AcDream.App.Streaming.LandblockPresentationPipeline?
|
|
_landblockPresentationPipeline;
|
|
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
|
private AcDream.App.Streaming.StreamingController? _streamingController;
|
|
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
|
_streamingOriginRecenter;
|
|
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
|
private AcDream.App.Streaming.DatSpawnClaimHydrationClassifier?
|
|
_spawnClaimHydration;
|
|
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
|
_localPlayerTeleportSink = new();
|
|
private AcDream.App.Streaming.LocalPlayerTeleportController?
|
|
_localPlayerTeleport;
|
|
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
|
new(nearRadius: 4, farRadius: 12);
|
|
// Phase B.3: physics engine — populated from the streaming pipeline.
|
|
private AcDream.Core.Physics.PhysicsEngine _physicsEngine =>
|
|
_runtimeEntityObjects.Physics.Engine;
|
|
|
|
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
|
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
|
// ConcurrentDictionary inside makes cross-thread access safe.
|
|
private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =>
|
|
_runtimeEntityObjects.Physics.DataCache;
|
|
|
|
/// <summary>
|
|
/// TS-23 (Campaign P Slice P3, 2026-07-30): resolves a mover's own
|
|
/// PK/PKLite/Impenetrable <see cref="AcDream.Core.Physics.ObjectInfoState"/>
|
|
/// bits from its <c>ClientObjectTable</c> row — the same table CreateObject
|
|
/// already populates <c>PublicWeenieBitfield</c> on (per-GUID, including
|
|
/// every remote's own row, not just collision targets).
|
|
/// </summary>
|
|
private AcDream.Core.Physics.ObjectInfoState GetMoverPvpState(uint serverGuid) =>
|
|
AcDream.Core.Physics.EntityCollisionFlagsExt.ResolveMoverPvpState(
|
|
_runtimeEntityObjects.Objects,
|
|
serverGuid);
|
|
|
|
// #184 Slice 2a: the per-remote dead-reckoning tick, extracted out of the
|
|
// >10k-line TickAnimations (Code Structure Rule 1). Reusable setup/motion
|
|
// policy is supplied through the once-bound live motion runtime, while the
|
|
// stateless server-velocity cycle remains a focused Physics helper.
|
|
private readonly AcDream.App.Physics.RemotePhysicsUpdater _remotePhysicsUpdater;
|
|
private readonly AcDream.App.Physics.RemoteInboundMotionDispatcher
|
|
_remoteInboundMotion;
|
|
private readonly AcDream.App.World.RetailInboundEventDispatcher
|
|
_inboundEntityEvents = new();
|
|
private LiveEntityAnimationScheduler _liveAnimationScheduler = null!;
|
|
private LiveEntityAnimationPresenter _animationPresenter = null!;
|
|
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
|
_movementTruthDiagnostics;
|
|
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
|
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock;
|
|
// Step 7 projectile presentation. The controller owns no identity map;
|
|
// each runtime component is stored on the canonical LiveEntityRecord.
|
|
private AcDream.App.Physics.ProjectileController? _projectileController;
|
|
private AcDream.App.World.LiveEntityProjectionWithdrawalController?
|
|
_liveEntityProjectionWithdrawal;
|
|
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
|
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
|
private AcDream.App.Net.LiveEntitySessionController? _liveEntitySessionEvents;
|
|
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
|
_liveEntityMotionBindings = new();
|
|
|
|
// Step 4: portal-based interior cell visibility.
|
|
private readonly CellVisibility _cellVisibility = new();
|
|
|
|
// Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe.
|
|
// DatReaderWriter's DatBinReader uses a shared buffer position internally —
|
|
// concurrent _dats.Get<T> calls from the streaming worker thread (T11+) and
|
|
// the render thread (LandblockBuildFactory on the worker; live-spawn
|
|
// handlers + animation ticks on the render
|
|
// thread) corrupt that state and produce half-populated LandBlock.Height[]
|
|
// arrays, rendering as "a giant ball with spikes". All _dats.Get<T> call
|
|
// sites that can race with the streaming worker MUST hold this lock.
|
|
//
|
|
// Worker-thread DAT reads enter through LandblockBuildFactory.Build, which
|
|
// holds _datLock for the complete CPU transaction. The terrain-mesh closure
|
|
// consumes that completed heightmap and performs no DAT read. Render-thread
|
|
// live hydration holds this lock via its outer wrapper; all remaining
|
|
// render-thread
|
|
// _dats.Get calls run only when no worker dat read can be in flight (during
|
|
// initialization or within the same lock scope).
|
|
private readonly object _datLock = new();
|
|
|
|
// Terrain build context shared by the off-thread mesh-build closure across
|
|
// all streamed landblocks.
|
|
private float[]? _heightTable;
|
|
private AcDream.Core.Terrain.TerrainBlendingContext? _blendCtx;
|
|
// Was: Dictionary<uint, SurfaceInfo>. ConcurrentDictionary so the off-thread
|
|
// mesh builder (A.5 T11+) can call LandblockMesh.Build without a lock.
|
|
private System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.Core.Terrain.SurfaceInfo>? _surfaceCache;
|
|
|
|
// Phase A8 (2026-05-28): WB EnvCellRenderManager port. Cells render
|
|
// through this dedicated pipeline now, NOT through WbDrawDispatcher.
|
|
// The dispatcher's IndoorPass still walks cell-static stabs (WorldEntity
|
|
// records with real GfxObj MeshRefs); only the cell GEOMETRY (walls /
|
|
// floors / ceilings) flows through here.
|
|
private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer;
|
|
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
|
|
|
|
// R1 (render redesign): portal-view draw owners are retained transitively
|
|
// by the frame orchestrator rather than duplicated as GameWindow roots.
|
|
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
|
private readonly AcDream.App.Rendering.TransferableResourceSlot<
|
|
AcDream.App.Rendering.PortalTunnelPresentation> _portalTunnelFallback = new();
|
|
|
|
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
|
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
|
|
// REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip
|
|
// state is re-uploaded each frame before terrain/entities draw, so the whole
|
|
// scene renders ungated (identical to pre-U.3). The buffer ids are handed to the
|
|
// three renderers so each re-binds binding=2 immediately before its own draw.
|
|
// U.4 replaces the NoClip() frame with one built from the portal-visibility result.
|
|
private ClipFrame? _clipFrame;
|
|
|
|
/// <summary>
|
|
/// Phase 6.4: per-entity animation playback state for entities whose
|
|
/// MotionTable resolved to a real cycle. The render loop ticks each
|
|
/// of these every frame, advances the current frame number, then
|
|
/// rebuilds the entity's MeshRefs by re-flattening the Setup against
|
|
/// the new <see cref="DatReaderWriter.Types.AnimationFrame"/>.
|
|
/// Static decorations and entities with no motion table never
|
|
/// appear in this map.
|
|
/// </summary>
|
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
|
|
private readonly AcDream.App.World.LiveEntityRuntimeSlot _liveEntityRuntimeSlot = new();
|
|
|
|
/// <summary>
|
|
/// Tier 1 cache (#53): per-entity classification results for static
|
|
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
|
|
/// paired with <see cref="_animatedEntities"/> — that dictionary is the
|
|
/// gating predicate, this cache is the lookup that depends on it.
|
|
/// Passed to <see cref="AcDream.App.Rendering.Wb.WbDrawDispatcher"/> at
|
|
/// construction time. Tasks 9-10 of the cache plan wire the per-entity
|
|
/// miss-populate / hit-fast-path through the dispatcher's loop.
|
|
/// </summary>
|
|
private readonly AcDream.App.Rendering.Wb.EntityClassificationCache _classificationCache = new();
|
|
|
|
private AcDream.Core.Physics.IAnimationLoader? _animLoader;
|
|
private AcDream.Runtime.Physics.LiveEntityCollisionBuilder? _liveEntityCollisionBuilder;
|
|
|
|
// Phase E.1: central fan-out for animation hooks. Audio (E.2),
|
|
// particles (E.3), combat (E.4), and renderer state mutators all
|
|
// register sinks at startup. The router is always non-null so the
|
|
// per-entity tick loop can just call it unconditionally.
|
|
private readonly AcDream.Core.Physics.AnimationHookRouter _hookRouter = new();
|
|
private AcDream.App.Composition.AnimationHookRegistrationSet?
|
|
_hookRegistrations;
|
|
private readonly AcDream.App.Rendering.Vfx.DeferredEntityEffectAdvanceSource
|
|
_entityEffectAdvance = new();
|
|
|
|
// Phase E.2 audio. Null when ACDREAM_NO_AUDIO=1 or the OpenAL driver
|
|
// failed to init; all three are set together.
|
|
private AcDream.App.Audio.OpenAlAudioEngine? _audioEngine;
|
|
private AcDream.Core.Audio.DatSoundCache? _soundCache;
|
|
private AcDream.App.Audio.DictionaryEntitySoundTable? _entitySoundTables;
|
|
private AcDream.App.Audio.AudioHookSink? _audioSink;
|
|
|
|
// Phase E.3 particles.
|
|
private AcDream.Core.Vfx.EmitterDescRegistry? _emitterRegistry;
|
|
private AcDream.Core.Vfx.ParticleSystem? _particleSystem;
|
|
private AcDream.Core.Vfx.ParticleHookSink? _particleSink;
|
|
private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new();
|
|
private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new();
|
|
private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames;
|
|
// Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754)
|
|
// and typed PlayScriptType (0xF755) events through one effect owner, then
|
|
// fans every dat-defined hook to particles, audio, lights, translucency,
|
|
// and nested/default-script routing at its StartTime offset.
|
|
private AcDream.Core.Vfx.PhysicsScriptRunner? _scriptRunner;
|
|
private AcDream.Content.Vfx.RetailPhysicsScriptLoader? _physicsScriptLoader;
|
|
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
|
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
|
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue;
|
|
// Remote-entity motion inference: tracks when each remote entity last
|
|
// moved meaningfully. Used in TickAnimations to swap to Ready when
|
|
// position has stalled for >StopIdleMs — retail observer pattern per
|
|
// ACE Player_Tick.cs line 368: the client never sends "released forward"
|
|
// MoveToState, so the server never broadcasts an explicit stop. Observer
|
|
// must infer it from position deltas.
|
|
private readonly AcDream.App.Physics.RemoteMovementObservationTracker
|
|
_remoteMovementObservations = new();
|
|
|
|
/// <summary>
|
|
/// Per-remote-entity movement state for smoothing between server
|
|
/// UpdatePosition broadcasts. Retail queues each received position and
|
|
/// advances the body toward the queue head through
|
|
/// <c>InterpolationManager::adjust_offset</c> every object tick.
|
|
///
|
|
/// <para>
|
|
/// Each entry owns the retail interpolation queue, MovementManager,
|
|
/// PhysicsBody, latest authoritative position, and the velocity estimate
|
|
/// used only to select animation cycles for NPCs that receive no motion
|
|
/// messages. Body translation consumes queue catch-up plus the literal
|
|
/// root-motion Frame emitted by CSequence; it never guesses movement from
|
|
/// a Walk/Run command or from packet cadence.
|
|
/// </para>
|
|
/// </summary>
|
|
|
|
/// <summary>
|
|
/// L.2g S1 (DEV-6): per-entity inbound movement-event staleness gates,
|
|
/// keyed by server guid. Retail keeps these stamps on CPhysicsObj
|
|
/// (update_times); seeded from CreateObject's PhysicsDesc timestamp
|
|
/// block during <see cref="LiveEntityHydrationController.OnCreate"/>, consulted at the
|
|
/// top of <see cref="LiveEntityNetworkUpdateController.OnMotion"/>, and dropped by the live
|
|
/// entity teardown owner.
|
|
/// </summary>
|
|
private AcDream.App.World.LiveEntityRuntime? _liveEntities;
|
|
private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness;
|
|
|
|
// J4/J5.1: Runtime owns communication, selection, combat, and target-mode
|
|
// state. App, UI, plugins, and live-session routing borrow exact children.
|
|
private readonly GameRuntime _runtime;
|
|
private readonly IDisposable _runtimeHostLease;
|
|
private RuntimeCommunicationState _runtimeCommunication =>
|
|
_runtime.CommunicationOwner;
|
|
private RuntimeActionState _runtimeActions => _runtime.ActionOwner;
|
|
public AcDream.Core.Selection.SelectionState Selection =>
|
|
_runtimeActions.Selection;
|
|
internal SessionStatusWriter StatusWriter => _statusWriter;
|
|
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
|
|
public AcDream.Core.Chat.TurbineChatState TurbineChat =>
|
|
_runtimeCommunication.TurbineChat;
|
|
public AcDream.Core.Social.FriendsState Friends =>
|
|
_runtimeCommunication.Friends;
|
|
public AcDream.Core.Social.SquelchState Squelch =>
|
|
_runtimeCommunication.Squelch;
|
|
public AcDream.Core.Combat.CombatState Combat =>
|
|
_runtimeActions.Combat;
|
|
private RuntimeEntityObjectLifetime _runtimeEntityObjects =>
|
|
_runtime.EntityObjects;
|
|
private RuntimeInventoryState _runtimeInventory =>
|
|
_runtime.InventoryOwner;
|
|
private RuntimeCharacterState _runtimeCharacter =>
|
|
_runtime.CharacterOwner;
|
|
public AcDream.Core.Items.ItemManaState ItemMana =>
|
|
_runtimeInventory.ItemMana;
|
|
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
|
// installs that immutable table once DatCollection opens in OnLoad.
|
|
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
|
public AcDream.Core.Spells.Spellbook SpellBook =>
|
|
_runtimeCharacter.Spellbook;
|
|
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
|
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts =>
|
|
_runtimeInventory.Shortcuts.Items;
|
|
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
|
// PlayerDescription so the Vitals HUD can render those bars.
|
|
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
|
// buffs into the max formula via Spellbook.GetVitalMod.
|
|
public AcDream.Core.Player.LocalPlayerState LocalPlayer =>
|
|
_runtimeCharacter.LocalPlayer;
|
|
|
|
// Phase D.2a — ImGui devtools UI overlay. Removed at Campaign V slice V11;
|
|
// see docs/plans/2026-04-24-ui-framework.md for the staged UI strategy and
|
|
// docs/plans/2026-07-27-vulkan-campaign.md for the removal.
|
|
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
|
|
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
|
private AcDream.App.UI.UiHost? _uiHost;
|
|
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
|
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
|
private InteractionUiLateBindings? _interactionUiLateBindings;
|
|
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
|
|
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
|
_combatAttackOperations = new();
|
|
private readonly AcDream.App.Combat.RuntimeCombatTargetOperationsSlot
|
|
_combatTargetOperations = new();
|
|
private readonly AcDream.App.Combat.RuntimeCombatModeOperationsSlot
|
|
_combatModeOperations = new();
|
|
private readonly AcDream.App.Spells.RuntimeSpellCastOperationsSlot
|
|
_spellCastOperations = new();
|
|
private readonly AcDream.App.Combat.CombatFeedbackSlot
|
|
_combatFeedback = new();
|
|
private RuntimeCombatAttackState? _combatAttackController;
|
|
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
|
private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle;
|
|
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
|
private MagicCatalog? _magicCatalog;
|
|
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
|
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
|
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
|
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
|
private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter;
|
|
private AcDream.App.Rendering.CreatureAppraisalViewportRenderer?
|
|
_creatureAppraisalViewportRenderer;
|
|
private AcDream.App.Rendering.CreatureAppraisalFramePresenter?
|
|
_creatureAppraisalFramePresenter;
|
|
// Campaign CC slice CC6b-MOUNT — the chargen Appearance-page preview,
|
|
// same guard/shutdown shape as the paperdoll/creature-appraisal
|
|
// viewports above.
|
|
private AcDream.App.Rendering.ChargenPreviewRenderer? _chargenPreviewRenderer;
|
|
private AcDream.App.Rendering.ChargenPreviewController? _chargenPreviewController;
|
|
// Campaign CC slice CC5: the Summary page's own gmCG3DView instance —
|
|
// a SEPARATE renderer/controller pair from the Appearance preview above.
|
|
private AcDream.App.Rendering.ChargenPreviewRenderer? _summaryPreviewRenderer;
|
|
private AcDream.App.Rendering.ChargenPreviewController? _summaryPreviewController;
|
|
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
|
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
|
private AcDream.App.Plugins.GraphicalPluginSession? _pluginSession;
|
|
// Campaign V slice V11 deleted the ImGui developer-tools frontend along
|
|
// with the OpenGL backend it required, so no host ever composes a
|
|
// developer UI regardless of ACDREAM_DEVTOOLS. The flag still reaches
|
|
// VulkanGraphicsContext, where it selects the optional debug-utils
|
|
// instance/device extensions — see the ACDREAM_DEVTOOLS log line in
|
|
// Run() below, which is the one remaining observable effect of the flag.
|
|
private const bool DevToolsEnabled = false;
|
|
|
|
// Phase G.1-G.2 world lighting/time state. The environment owner keeps
|
|
// the clock, selected day group, and weather transitions coherent.
|
|
public readonly AcDream.Core.World.WorldTimeService WorldTime;
|
|
public readonly AcDream.Core.Lighting.LightManager Lighting = new();
|
|
|
|
public readonly AcDream.Core.World.WeatherSystem Weather;
|
|
// Wired into the hook router in OnLoad so SetLightHook fires
|
|
// from the animation pipeline flip the matching LightSource.IsLit.
|
|
private AcDream.Core.Lighting.LightingHookSink? _lightingSink;
|
|
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
|
|
private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation;
|
|
|
|
// #188 — TransparentPartHook fires from the animation pipeline drive
|
|
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
|
// per frame. Wired into the hook router in OnLoad, advanced once per
|
|
// frame in the main loop regardless of _animatedEntities membership
|
|
// (a fade must keep running even if its entity's one-shot cycle ends).
|
|
private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades = new();
|
|
private AcDream.Core.Rendering.TranslucencyHookSink? _translucencySink;
|
|
|
|
// Phase G.1 sky renderer + shared UBO. Created once the GL context
|
|
// exists in OnLoad; shared across every other renderer via
|
|
// binding = 1 so terrain/mesh/instanced/sky all read the same
|
|
// sun / ambient / fog / flash data per frame.
|
|
private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo;
|
|
private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer;
|
|
// Phase B.2: player movement mode.
|
|
private RuntimeLocalPlayerMovementState _playerControllerSlot =>
|
|
_runtime.MovementOwner;
|
|
private PlayerMovementController? _playerController
|
|
=> _playerControllerSlot.Controller;
|
|
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new();
|
|
private AcDream.App.Rendering.ChaseCamera? _chaseCamera
|
|
=> _chaseCameraInput.Legacy;
|
|
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera
|
|
=> _chaseCameraInput.Retail;
|
|
private readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new();
|
|
private bool _playerMode
|
|
=> _localPlayerMode.IsPlayerMode;
|
|
private readonly AcDream.App.Input.LocalPlayerIdentityState
|
|
_localPlayerIdentity;
|
|
private uint _playerServerGuid
|
|
{
|
|
get => _localPlayerIdentity.ServerGuid;
|
|
set => _localPlayerIdentity.ServerGuid = value;
|
|
}
|
|
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
|
|
|
|
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
|
|
private readonly FramebufferResizeController _framebufferResize;
|
|
private AcDream.App.Input.PlayerModeController? _playerModeController;
|
|
private readonly AcDream.App.Interaction.PlayerApproachCompletionState
|
|
_playerApproachCompletions = new();
|
|
private AcDream.App.Input.LocalPlayerAnimationController?
|
|
_localPlayerAnimation;
|
|
private AcDream.App.Physics.LocalPlayerShadowSynchronizer?
|
|
_localPlayerShadowSynchronizer;
|
|
// Phase D.2b-C — live character-sheet assembly + raise flow (extracted
|
|
// feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1.
|
|
private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider;
|
|
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
|
// by LiveSessionHost's entered-world transition; ticked from
|
|
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
|
// other path that pre-empts the chase camera). Skipped entirely
|
|
// offline (orbit camera stays the foreground). The class internally
|
|
// tracks IsArmed; we read it via the guard rather than mirroring
|
|
// the bool here.
|
|
private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry;
|
|
|
|
// Phase K.1b / Slice 8 F — one semantic input path. Transitional actions
|
|
// flow through GameplayInputActionRouter; held movement is polled through
|
|
// InputDispatcher. Raw axis motion belongs to CameraPointerInputController.
|
|
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
|
|
private AcDream.App.Input.SilkMouseSource? _mouseSource;
|
|
private AcDream.UI.Abstractions.Input.InputDispatcher? _inputDispatcher;
|
|
private readonly AcDream.App.Input.RetainedUiInputCaptureSlot _retainedInputCapture;
|
|
private readonly AcDream.App.Input.CompositeInputCaptureSource _inputCapture;
|
|
private readonly AcDream.App.Input.DispatcherMovementInputSource _movementInput;
|
|
private readonly AcDream.App.Input.DispatcherCameraInputSource _cameraInput = new();
|
|
private AcDream.App.Input.IMouseLookCursor? _mouseLookCursor;
|
|
private AcDream.App.Input.GameplayInputFrameController? _gameplayInputFrame;
|
|
private AcDream.App.Input.GameplayInputActionRouter? _gameplayInputActions;
|
|
private AcDream.App.Input.RetainedUiGameplayBinding? _retainedUiGameplayBinding;
|
|
private readonly AcDream.App.Diagnostics.RuntimeDiagnosticCommandSlot
|
|
_runtimeDiagnosticCommands = new();
|
|
private readonly AcDream.App.Combat.LiveCombatModeCommandSlot
|
|
_liveCombatModeCommands = new();
|
|
// K.1c/L0: load user-customized bindings from the canonical per-platform
|
|
// configuration path,
|
|
// falling back to the retail-faithful defaults if the file is missing
|
|
// or corrupt. This is THE single source of truth for the keymap at
|
|
// startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
|
|
// should land in the GameWindow construction path.
|
|
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
|
|
private readonly GraphicalHostPlatformServices _platformServices;
|
|
private readonly ApplicationPathSet _applicationPaths;
|
|
|
|
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings(
|
|
string path)
|
|
{
|
|
var bindings = AcDream.UI.Abstractions.Input.KeyBindings.LoadOrDefault(path);
|
|
Console.WriteLine($"keybinds: loaded {bindings.All.Count} bindings from {path}");
|
|
return bindings;
|
|
}
|
|
|
|
// Phase 4.7: optional live connection to an ACE server. Enabled only when
|
|
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
|
// the offline rendering pipeline.
|
|
// Runtime owns the canonical session generation and transport lifetime.
|
|
// The window retains only the composition handles needed for startup and
|
|
// shutdown; graphical feature callbacks borrow state through App adapters.
|
|
private LiveSessionHost? _liveSessionHost;
|
|
private AcDream.Core.Net.WorldSession? LiveSession =>
|
|
_liveSessionHost?.CurrentSession;
|
|
private readonly AcDream.App.World.LiveWorldOriginState _liveWorldOrigin = new();
|
|
private int _liveCenterX => _liveWorldOrigin.CenterX;
|
|
private int _liveCenterY => _liveWorldOrigin.CenterY;
|
|
// K-fix1 (2026-04-26): cached at startup so per-frame branches are
|
|
// single-flag reads instead of env-var lookups. True iff
|
|
// ACDREAM_LIVE=1 was set when the window came up.
|
|
// Backed by RuntimeOptions.LiveMode via the _options field.
|
|
/// <summary>
|
|
/// Phase 6.6/6.7: server-guid → local WorldEntity lookup so
|
|
/// UpdateMotion and UpdatePosition handlers can find the entity the
|
|
/// server is talking about. This is the canonical materialized top-level
|
|
/// projection, including live objects parked in pending landblocks;
|
|
/// visibility must never suppress authoritative mutation.
|
|
/// </summary>
|
|
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
|
|
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
|
|
/// <summary>
|
|
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> for each
|
|
/// guid. Captured before the renderability gate so no-position inventory /
|
|
/// parented children retain Setup and parent metadata; hydration rereads
|
|
/// this canonical snapshot after every relationship callback.
|
|
/// <see cref="LiveEntityHydrationController.OnAppearance"/> reuses the cached
|
|
/// position/setup/motion fields when a 0xF625 ObjDescEvent carries only
|
|
/// updated visuals.
|
|
/// </summary>
|
|
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> LastSpawns =>
|
|
_liveEntities?.Snapshots ?? EmptyLiveSpawnMap;
|
|
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
|
|
// TargetManager voyeur system, stored on the exact live record so remote
|
|
// entities chasing the player resolve it. Replaces the AP-79
|
|
// _playerMoveToTarget* poll fields.
|
|
private readonly AcDream.App.Input.LocalPlayerPhysicsHostSlot
|
|
_playerHostSlot = new();
|
|
private EntityPhysicsHost? _playerHost
|
|
=> _playerHostSlot.Host;
|
|
|
|
// R5-V2: guid → per-entity IPhysicsObjHost registry (retail's
|
|
// CObjectMaint::GetObjectA lookup). Backs every host's GetObjectA seam,
|
|
// giving the TargetManager voyeur round-trip its cross-entity delivery
|
|
// path. Populated for remotes plus the PlayerModeController local entry
|
|
// (player); pruned only by logical LiveEntityRuntime teardown.
|
|
|
|
// ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184
|
|
// Slice 2a — the DR tick's stale-velocity anim-stop was its only user).
|
|
public GameWindow(
|
|
AcDream.App.RuntimeOptions options,
|
|
WorldGameState worldGameState,
|
|
WorldEvents worldEvents,
|
|
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
|
|
: this(
|
|
options,
|
|
worldGameState,
|
|
worldEvents,
|
|
uiRegistry,
|
|
GraphicalHostPlatformServices.Resolve())
|
|
{
|
|
}
|
|
|
|
internal GameWindow(
|
|
AcDream.App.RuntimeOptions options,
|
|
WorldGameState worldGameState,
|
|
WorldEvents worldEvents,
|
|
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry,
|
|
GraphicalHostPlatformServices platformServices)
|
|
{
|
|
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
|
_statusWriter = new SessionStatusWriter(options.StatusFilePath);
|
|
_platformServices = platformServices
|
|
?? throw new ArgumentNullException(nameof(platformServices));
|
|
_applicationPaths = _platformServices.Paths;
|
|
_keyBindings = LoadStartupKeyBindings(
|
|
_applicationPaths.KeyBindingsFile);
|
|
_runtime = new GameRuntime(new GameRuntimeDependencies(
|
|
_combatAttackOperations,
|
|
_combatTargetOperations,
|
|
_combatModeOperations,
|
|
_spellCastOperations,
|
|
Log: Console.WriteLine,
|
|
TimeSyncDiagnostic:
|
|
options.DumpSky ? Console.WriteLine : null));
|
|
_runtimeHostLease = _runtime.AcquireHostLease(
|
|
"graphical GameWindow");
|
|
_localPlayerIdentity = new AcDream.App.Input.LocalPlayerIdentityState(
|
|
_runtime.PlayerIdentity);
|
|
_updateFrameClock = new AcDream.App.Update.UpdateFrameClock(
|
|
_runtime.Clock);
|
|
_worldEnvironment = new AcDream.App.World.WorldEnvironmentController(
|
|
_runtime.EnvironmentOwner,
|
|
options.ForcedDayGroupIndex,
|
|
Console.WriteLine,
|
|
options.PinnedWorldDayFraction);
|
|
var alphaScratchBudgets =
|
|
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
|
_options.ResidencyBudgets.AlphaScratchBytes);
|
|
_retailAlphaQueue = new AcDream.App.Rendering.RetailAlphaQueue(
|
|
alphaScratchBudgets.QueueBytes);
|
|
WorldTime = _worldEnvironment.WorldTime;
|
|
Weather = _worldEnvironment.Weather;
|
|
// Campaign OP slice OP4 (2026-08-11): PlayerOption
|
|
// DisableDistanceFog, polled — see WeatherSystem.
|
|
// DisableDistanceFogSource's doc comment. Bound once here (not
|
|
// per-session): _runtime.CharacterOwner is the ONE canonical
|
|
// RuntimeCharacterOptionsState instance for this GameRuntime's
|
|
// whole lifetime, reused (not replaced) across reconnects.
|
|
Weather.DisableDistanceFogSource = () =>
|
|
_runtime.CharacterOwner.Options.GetOptionBit(
|
|
AcDream.Core.Net.Messages.CharacterOptionId.DisableDistanceFog);
|
|
// Campaign OP slice OP4 (2026-08-11): PlayerOption
|
|
// DisplayTimeStamps, polled — see RuntimeCommunicationState.
|
|
// DisplayTimestampsSource's doc comment. Same one-time,
|
|
// whole-lifetime bind as the two above.
|
|
_runtime.CommunicationOwner.DisplayTimestampsSource = () =>
|
|
_runtime.CharacterOwner.Options.GetOptionBit(
|
|
AcDream.Core.Net.Messages.CharacterOptionId.DisplayTimeStamps);
|
|
_retainedInputCapture = new AcDream.App.Input.RetainedUiInputCaptureSlot();
|
|
_inputCapture = new AcDream.App.Input.CompositeInputCaptureSource(
|
|
new AcDream.App.Input.DevToolsInputCaptureSource(options.DevTools),
|
|
_retainedInputCapture);
|
|
_movementInput = new AcDream.App.Input.DispatcherMovementInputSource(
|
|
_playerControllerSlot,
|
|
_inputCapture);
|
|
// Campaign OP slice OP4 (2026-08-11): retail PlayerOption id 0xA
|
|
// (acclient.h's ToggleRun_PlayerOption — ACE's name for the same
|
|
// bit is RunAsDefaultMovement; N7, OP4 review-fix round
|
|
// 2026-08-11), polled — see RuntimeLocalPlayerMovementState.
|
|
// RunAsDefaultMovementSource's doc comment. Same one-time,
|
|
// whole-lifetime bind as DisableDistanceFogSource above.
|
|
_playerControllerSlot.RunAsDefaultMovementSource = () =>
|
|
_runtime.CharacterOwner.Options.GetOptionBit(
|
|
AcDream.Core.Net.Messages.CharacterOptionId.ToggleRun);
|
|
_framebufferResize = new FramebufferResizeController(_viewportAspect);
|
|
_datDir = options.DatDir;
|
|
_worldGameState = worldGameState;
|
|
_worldEvents = worldEvents;
|
|
_displayFramePacing = new DisplayFramePacingController(
|
|
options.UncappedRendering,
|
|
_frameProfiler,
|
|
_platformServices.FramePacingWaiters);
|
|
_runtimeSettings = new RuntimeSettingsController(
|
|
new JsonRuntimeSettingsStorage(
|
|
_applicationPaths.SettingsFile),
|
|
log: Console.WriteLine);
|
|
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
|
_uiRegistry = uiRegistry;
|
|
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
|
_liveEntityRuntimeSlot);
|
|
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
|
// setup/motion dependency crosses the fail-fast construction bridge;
|
|
// the server-velocity animation cycle is stateless shared policy.
|
|
_remotePhysicsUpdater = new AcDream.App.Physics.RemotePhysicsUpdater(
|
|
_runtimeEntityObjects.Physics,
|
|
_liveEntityMotionBindings.GetSetupCylinder,
|
|
_liveEntityMotionBindings.GetSetupMoverShape,
|
|
AcDream.App.Physics.RemoteServerControlledVelocityCycle.Apply,
|
|
GetMoverPvpState);
|
|
_remoteInboundMotion = new AcDream.App.Physics.RemoteInboundMotionDispatcher(
|
|
(movement, cellId, update) =>
|
|
_liveEntityMotionBindings.RouteServerMoveTo(
|
|
movement, cellId, update),
|
|
(host, targetGuid) =>
|
|
_liveEntityMotionBindings.StickToObjectFromWire(host, targetGuid));
|
|
_movementTruthDiagnostics =
|
|
new AcDream.App.Input.MovementTruthDiagnosticController(
|
|
options.DumpMoveTruth,
|
|
_playerControllerSlot,
|
|
_localPlayerIdentity);
|
|
_localPlayerOutbound = new LocalPlayerOutboundController(
|
|
_movementTruthDiagnostics);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transfers the graphical plugin lifetime into the window shutdown graph
|
|
/// and starts it before retained UI construction drains registrations.
|
|
/// </summary>
|
|
internal void StartPluginHosting(
|
|
AcDream.App.Plugins.GraphicalPluginSession pluginSession)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pluginSession);
|
|
if (_pluginSession is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The graphical plugin session is already attached.");
|
|
}
|
|
|
|
_pluginSession = pluginSession;
|
|
pluginSession.Start();
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
_platformServices.ConfigureWindowBackend();
|
|
// The runtime-settings owner was constructed before Window.Create and
|
|
// exposes the one immutable startup snapshot. MSAA is a context
|
|
// attribute, so it must come from this same snapshot rather than a
|
|
// second settings load during OnLoad.
|
|
RuntimeSettingsSnapshot startup = _runtimeSettings.Startup;
|
|
if (_options.VulkanCapabilityProbe)
|
|
{
|
|
// Campaign V slice V6h reduced the V5 bring-up host to what its name
|
|
// says: a capability probe. It opens its own window, runs the gate,
|
|
// presents the synthetic V6c/V6d verification scenes, and exits —
|
|
// useful for answering "does this machine pass the Vulkan gate, and
|
|
// does the backend draw?" without the client. The production Vulkan
|
|
// path is the composition host below.
|
|
using var probe = new AcDream.App.Rendering.Gpu.Vk.VulkanBringUpHost(
|
|
_options,
|
|
_platformServices,
|
|
startup.Display.VSync);
|
|
probe.Run();
|
|
return;
|
|
}
|
|
|
|
FramePacingPolicy startupPacing =
|
|
_displayFramePacing.InitializeStartup(startup.Display.VSync);
|
|
// Vulkan needs a client-API-less window (the surface comes from
|
|
// VK_KHR_surface); neither MSAA nor the stencil bit count is a window
|
|
// attribute there — both are attachment properties the RHI device
|
|
// configures instead. The raw-GL window options this used to fork to
|
|
// were deleted at Campaign V slice V11.
|
|
var options = WindowOptions.DefaultVulkan with
|
|
{
|
|
Size = new Vector2D<int>(1280, 720),
|
|
Title = "acdream — Vulkan",
|
|
VSync = startupPacing.UseVSync,
|
|
};
|
|
_startupPacing = startupPacing;
|
|
_startupQuality = startup.Quality;
|
|
|
|
_window = Window.Create(options);
|
|
IWindow window = _window;
|
|
_lifetime.PublishNativeWindow(
|
|
window,
|
|
isRenderLoopArmed: () => _renderLoopArmed,
|
|
requestClose: window.Close);
|
|
_displayFramePacing.BindSurface(
|
|
new SilkDisplayFramePacingSurface(_window));
|
|
// The fixed binding preserves main Render before post-render pacing,
|
|
// owns rollback/reverse-detach, and gates every native entry point.
|
|
_windowCallbacks = SilkWindowCallbackBinding.Create(
|
|
_window,
|
|
new WindowCallbackTargets(
|
|
OnLoad,
|
|
OnUpdate,
|
|
OnRender,
|
|
OnClosing,
|
|
OnFocusChanged,
|
|
OnFramebufferResize),
|
|
_displayFramePacing,
|
|
_hostQuiescence);
|
|
_windowCallbacks.Attach();
|
|
try
|
|
{
|
|
_window.Run();
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
_constructionCleanup.RetainFrom(failure);
|
|
// fix #406: latch BEFORE rethrowing — Dispose() (and therefore
|
|
// CompleteShutdown) can run mid-unwind of this exact exception,
|
|
// via Program.cs's `using var window = ...`.
|
|
_runFailure = failure;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void IGameWindowPlatformPublication<GameWindowGraphics, IInputContext>.PublishGraphics(
|
|
GameWindowGraphics graphics) =>
|
|
PublishCompositionOwner(ref _graphics, graphics, "graphics API");
|
|
|
|
void IGameWindowPlatformPublication<GameWindowGraphics, IInputContext>.PublishInput(
|
|
IInputContext input) =>
|
|
PublishCompositionOwner(ref _input, input, "input context");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishGpuFrameFlights(
|
|
GpuFrameFlightController? value)
|
|
{
|
|
// Null on a backend whose RHI device owns its own frame flights.
|
|
if (value is not null)
|
|
PublishCompositionOwner(ref _gpuFrameFlights, value, "GPU frame flights");
|
|
}
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishGpuDevice(
|
|
IGpuDevice value) =>
|
|
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime(
|
|
GpuDeviceFrameLifetime value) =>
|
|
PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
|
AcDream.App.Input.SilkKeyboardSource value) =>
|
|
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishMouseSource(
|
|
AcDream.App.Input.SilkMouseSource value) =>
|
|
PublishCompositionOwner(ref _mouseSource, value, "mouse source");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishMouseLookCursor(
|
|
AcDream.App.Input.IMouseLookCursor value) =>
|
|
PublishCompositionOwner(ref _mouseLookCursor, value, "mouse-look cursor");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishInputDispatcher(
|
|
AcDream.UI.Abstractions.Input.InputDispatcher value) =>
|
|
PublishCompositionOwner(ref _inputDispatcher, value, "input dispatcher");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishCameraController(
|
|
CameraController value) =>
|
|
PublishCompositionOwner(ref _cameraController, value, "camera controller");
|
|
|
|
void IGameWindowHostInputCameraPublication.PublishCameraPointerInput(
|
|
AcDream.App.Input.CameraPointerInputController value) =>
|
|
PublishCompositionOwner(ref _cameraPointerInput, value, "camera pointer input");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishDatCollection(
|
|
IDatReaderWriter value) =>
|
|
PublishCompositionOwner(ref _dats, value, "DAT collection");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishPreparedAssetSource(
|
|
IPreparedAssetSource value) =>
|
|
PublishCompositionOwner(
|
|
ref _preparedAssets,
|
|
value,
|
|
"prepared asset source");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog(
|
|
MagicCatalog value) =>
|
|
PublishCompositionOwner(ref _magicCatalog, value, "magic catalog");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishAnimationLoader(
|
|
AcDream.Core.Physics.IAnimationLoader value) =>
|
|
PublishCompositionOwner(ref _animLoader, value, "animation loader");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishLiveEntityCollisionBuilder(
|
|
AcDream.Runtime.Physics.LiveEntityCollisionBuilder value) =>
|
|
PublishCompositionOwner(
|
|
ref _liveEntityCollisionBuilder,
|
|
value,
|
|
"live-entity collision builder");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishEmitterRegistry(
|
|
AcDream.Core.Vfx.EmitterDescRegistry value) =>
|
|
PublishCompositionOwner(ref _emitterRegistry, value, "emitter registry");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishParticleSystem(
|
|
AcDream.Core.Vfx.ParticleSystem value) =>
|
|
PublishCompositionOwner(ref _particleSystem, value, "particle system");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishParticleSink(
|
|
AcDream.Core.Vfx.ParticleHookSink value) =>
|
|
PublishCompositionOwner(ref _particleSink, value, "particle hook sink");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishAnimationHookFrames(
|
|
AcDream.App.Rendering.Vfx.AnimationHookFrameQueue value) =>
|
|
PublishCompositionOwner(
|
|
ref _animationHookFrames,
|
|
value,
|
|
"animation-hook frame queue");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptLoader(
|
|
AcDream.Content.Vfx.RetailPhysicsScriptLoader value) =>
|
|
PublishCompositionOwner(
|
|
ref _physicsScriptLoader,
|
|
value,
|
|
"physics-script loader");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptRunner(
|
|
AcDream.Core.Vfx.PhysicsScriptRunner value) =>
|
|
PublishCompositionOwner(ref _scriptRunner, value, "physics-script runner");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishLightingSink(
|
|
AcDream.Core.Lighting.LightingHookSink value) =>
|
|
PublishCompositionOwner(ref _lightingSink, value, "lighting hook sink");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishTranslucencySink(
|
|
AcDream.Core.Rendering.TranslucencyHookSink value) =>
|
|
PublishCompositionOwner(
|
|
ref _translucencySink,
|
|
value,
|
|
"translucency hook sink");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishHookRegistrations(
|
|
AcDream.App.Composition.AnimationHookRegistrationSet value) =>
|
|
PublishCompositionOwner(
|
|
ref _hookRegistrations,
|
|
value,
|
|
"animation-hook registrations");
|
|
|
|
void IGameWindowContentEffectsAudioPublication.PublishAudio(
|
|
ContentAudioGraph value)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
if (_soundCache is not null
|
|
|| _audioEngine is not null
|
|
|| _entitySoundTables is not null
|
|
|| _audioSink is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns audio state.");
|
|
}
|
|
|
|
_soundCache = value.SoundCache;
|
|
_audioEngine = value.Engine;
|
|
_entitySoundTables = value.EntitySoundTables;
|
|
_audioSink = value.HookSink;
|
|
}
|
|
|
|
void IGameWindowWorldRenderPublication.PublishSceneLighting(
|
|
SceneLightingUboBinding value) =>
|
|
PublishCompositionOwner(
|
|
ref _sceneLightingUbo,
|
|
value,
|
|
"scene lighting");
|
|
|
|
void IGameWindowWorldRenderPublication.PublishDebugLines(
|
|
DebugLineRenderer value) =>
|
|
PublishCompositionOwner(ref _debugLines, value, "debug lines");
|
|
|
|
void IGameWindowWorldRenderPublication.PublishHudResources(
|
|
BitmapFont font,
|
|
TextRenderer text)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(font);
|
|
ArgumentNullException.ThrowIfNull(text);
|
|
if (_debugFont is not null || _textRenderer is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns world HUD resources.");
|
|
}
|
|
_debugFont = font;
|
|
_textRenderer = text;
|
|
}
|
|
|
|
void IGameWindowWorldRenderPublication.PublishTerrain(
|
|
TerrainModernRenderer value) =>
|
|
PublishCompositionOwner(ref _terrain, value, "terrain renderer");
|
|
|
|
void IGameWindowWorldRenderPublication.PublishTerrainBuildState(
|
|
float[] heightTable,
|
|
AcDream.Core.Terrain.TerrainBlendingContext blending,
|
|
System.Collections.Concurrent.ConcurrentDictionary<
|
|
uint,
|
|
AcDream.Core.Terrain.SurfaceInfo> surfaceCache)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(heightTable);
|
|
ArgumentNullException.ThrowIfNull(blending);
|
|
ArgumentNullException.ThrowIfNull(surfaceCache);
|
|
if (_heightTable is not null || _blendCtx is not null || _surfaceCache is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns terrain build state.");
|
|
}
|
|
_heightTable = heightTable;
|
|
_blendCtx = blending;
|
|
_surfaceCache = surfaceCache;
|
|
}
|
|
|
|
void IGameWindowWorldRenderPublication.PublishWbMeshAdapter(
|
|
WbMeshAdapter value) =>
|
|
PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter");
|
|
|
|
void IGameWindowWorldRenderPublication.PublishTextureCache(
|
|
TextureCache value) =>
|
|
PublishCompositionOwner(ref _textureCache, value, "texture cache");
|
|
|
|
void IGameWindowInteractionRetainedUiPublication.PublishInteractionRetainedUi(
|
|
InteractionRetainedUiResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
if (_combatAttackController is not null
|
|
|| _externalContainerLifecycle is not null
|
|
|| _itemInteractionController is not null
|
|
|| _interactionUiLateBindings is not null
|
|
|| _uiHost is not null
|
|
|| _retailUiRuntime is not null
|
|
|| _retailChatVm is not null
|
|
|| _characterSheetProvider is not null
|
|
|| _magicRuntime is not null
|
|
|| _frameScreenshots is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns interaction/UI state.");
|
|
}
|
|
|
|
_combatAttackController = result.CombatAttack;
|
|
_externalContainerLifecycle = result.ExternalContainerLifecycle;
|
|
_itemInteractionController = result.ItemInteraction;
|
|
_interactionUiLateBindings = result.LateBindings;
|
|
_magicRuntime = result.Magic;
|
|
if (result.RetainedUi is { } retained)
|
|
{
|
|
_uiHost = retained.Host;
|
|
_retailUiRuntime = retained.Runtime;
|
|
_retailChatVm = retained.Chat;
|
|
_characterSheetProvider = retained.CharacterSheet;
|
|
_frameScreenshots = retained.Screenshots;
|
|
// #348: cursor switches ride a process-lifetime native cache
|
|
// instead of Silk's recreate-per-assignment path.
|
|
retained.Runtime.AttachNativeCursorWindow(_window?.Native?.Glfw ?? 0);
|
|
}
|
|
}
|
|
|
|
void IGameWindowLivePresentationPublication.PublishLivePresentation(
|
|
LivePresentationResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
if (_liveEntities is not null
|
|
|| _wbEntitySpawnAdapter is not null
|
|
|| _entityScriptActivator is not null
|
|
|| _staticAnimationScheduler is not null
|
|
|| _renderSceneShadow is not null
|
|
|| _wbDrawDispatcher is not null
|
|
|| _retailSelectionScene is not null
|
|
|| _worldSelectionQuery is not null
|
|
|| _selectionInteractions is not null
|
|
|| _retainedUiGameplayBinding is not null
|
|
|| _equippedChildRenderer is not null
|
|
|| _entityEffects is not null
|
|
|| _liveEntityPresentation is not null
|
|
|| _projectileController is not null
|
|
|| _liveEntityProjectionWithdrawal is not null
|
|
|| _liveEntityLights is not null
|
|
|| _liveAnimationScheduler is not null
|
|
|| _animationPresenter is not null
|
|
|| _paperdollViewportRenderer is not null
|
|
|| _paperdollFramePresenter is not null
|
|
|| _creatureAppraisalViewportRenderer is not null
|
|
|| _creatureAppraisalFramePresenter is not null
|
|
|| _chargenPreviewRenderer is not null
|
|
|| _chargenPreviewController is not null
|
|
|| _envCellRenderer is not null
|
|
|| _envCellFrustum is not null
|
|
|| _landblockPresentationPipeline is not null
|
|
|| _portalDepthMask is not null
|
|
|| _clipFrame is not null
|
|
|| _skyRenderer is not null
|
|
|| _particleRenderer is not null
|
|
|| _renderFrameDiagnostics is not null
|
|
|| _livePresentationBindings is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns live presentation state.");
|
|
}
|
|
|
|
_wbEntitySpawnAdapter = result.EntitySpawnAdapter;
|
|
_entityScriptActivator = result.EntityScriptActivator;
|
|
_staticAnimationScheduler = result.StaticAnimationScheduler;
|
|
_worldState = result.WorldState;
|
|
_renderSceneShadow = result.RenderSceneShadow;
|
|
_liveEntities = result.LiveEntities;
|
|
_projectileController = result.ProjectileController;
|
|
_liveEntityProjectionWithdrawal = result.ProjectionWithdrawal;
|
|
_liveEntityLights = result.Lights;
|
|
_liveAnimationScheduler = result.AnimationScheduler;
|
|
_animationPresenter = result.AnimationPresenter;
|
|
_equippedChildRenderer = result.EquippedChildren;
|
|
_entityEffects = result.EntityEffects;
|
|
_liveEntityPresentation = result.Presentation;
|
|
_wbDrawDispatcher = result.DrawDispatcher;
|
|
_retailSelectionScene = result.SelectionScene;
|
|
_worldSelectionQuery = result.SelectionQuery;
|
|
_selectionInteractions = result.SelectionInteractions;
|
|
_retainedUiGameplayBinding = result.RetainedGameplay;
|
|
_paperdollViewportRenderer = result.PaperdollRenderer;
|
|
_paperdollFramePresenter = result.PaperdollPresenter;
|
|
_creatureAppraisalViewportRenderer = result.CreatureAppraisalRenderer;
|
|
_creatureAppraisalFramePresenter = result.CreatureAppraisalPresenter;
|
|
_chargenPreviewRenderer = result.ChargenPreviewRenderer;
|
|
_chargenPreviewController = result.ChargenPreviewController;
|
|
_summaryPreviewRenderer = result.SummaryPreviewRenderer;
|
|
_summaryPreviewController = result.SummaryPreviewController;
|
|
_envCellFrustum = result.EnvCellFrustum;
|
|
_envCellRenderer = result.EnvCellRenderer;
|
|
_landblockPresentationPipeline = result.LandblockPipeline;
|
|
_clipFrame = result.ClipFrame;
|
|
_portalDepthMask = result.PortalDepthMask;
|
|
_skyRenderer = result.SkyRenderer;
|
|
_particleRenderer = result.ParticleRenderer;
|
|
_renderFrameDiagnostics = result.FrameDiagnostics;
|
|
_livePresentationBindings = result.RuntimeBindings;
|
|
}
|
|
|
|
void IGameWindowSessionPlayerPublication.PublishSessionPlayer(
|
|
SessionPlayerResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
if (_streamer is not null
|
|
|| _streamingController is not null
|
|
|| _streamingOriginRecenter is not null
|
|
|| _worldReveal is not null
|
|
|| _spawnClaimHydration is not null
|
|
|| _liveEntityHydration is not null
|
|
|| _liveEntityNetworkUpdates is not null
|
|
|| _liveEntityLiveness is not null
|
|
|| _liveEntitySessionEvents is not null
|
|
|| _gameplayInputFrame is not null
|
|
|| _localPlayerAnimation is not null
|
|
|| _localPlayerShadowSynchronizer is not null
|
|
|| _playerModeController is not null
|
|
|| _playerModeAutoEntry is not null
|
|
|| _localPlayerTeleport is not null
|
|
|| _liveSessionHost is not null
|
|
|| _gameplayInputActions is not null
|
|
|| _sessionPlayerBindings is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns session/player state.");
|
|
}
|
|
|
|
_streamer = result.Streamer;
|
|
_streamingController = result.Streaming;
|
|
_streamingOriginRecenter = result.StreamingOriginRecenter;
|
|
_worldReveal = result.WorldReveal;
|
|
_spawnClaimHydration = result.SpawnClaimHydration;
|
|
_liveEntityHydration = result.Hydration;
|
|
_liveEntityNetworkUpdates = result.NetworkUpdates;
|
|
_liveEntityLiveness = result.Liveness;
|
|
_liveEntitySessionEvents = result.SessionEvents;
|
|
_gameplayInputFrame = result.GameplayInput;
|
|
_localPlayerAnimation = result.LocalPlayerAnimation;
|
|
_localPlayerShadowSynchronizer = result.LocalPlayerShadow;
|
|
_playerModeController = result.PlayerMode;
|
|
_playerModeAutoEntry = result.PlayerModeAutoEntry;
|
|
_localPlayerTeleport = result.LocalTeleport;
|
|
_liveSessionHost = result.SessionHost;
|
|
_gameplayInputActions = result.GameplayActions;
|
|
_sessionPlayerBindings = result.RuntimeBindings;
|
|
}
|
|
|
|
void IGameWindowFrameRootPublication.PublishFrameRoots(
|
|
FrameRootResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
if (_frameRootBindings is not null
|
|
|| _frameGraphPublication is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The GameWindow composition shell already owns frame roots.");
|
|
}
|
|
|
|
_frameRootBindings = result.RuntimeBindings;
|
|
_frameGraphPublication = result.FrameGraphPublication;
|
|
}
|
|
|
|
private static void PublishCompositionOwner<T>(
|
|
ref T? destination,
|
|
T value,
|
|
string name)
|
|
where T : class
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
if (destination is not null)
|
|
throw new InvalidOperationException(
|
|
$"The GameWindow composition shell already owns {name}.");
|
|
destination = value;
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_graphics), nameof(_input))]
|
|
private GameWindowPlatformResult<GameWindowGraphics, IInputContext> AcquirePlatform()
|
|
{
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform =
|
|
GameWindowPlatformAcquisition.Acquire(
|
|
CreateGraphics,
|
|
static graphics => graphics.Dispose(),
|
|
() => _window!.CreateInput(),
|
|
static input => input.Dispose(),
|
|
this);
|
|
if (_graphics is null || _input is null)
|
|
throw new InvalidOperationException(
|
|
"Platform acquisition returned without publishing both owners.");
|
|
return platform;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How the UI probe reads a completed frame.
|
|
/// <see cref="IGpuDevice.CaptureBackbuffer"/> is documented top-left-origin,
|
|
/// so <see cref="FrameScreenshotController"/> flips it to match the PNG's
|
|
/// row order. The raw-GL read this used to fork from — whose rows already
|
|
/// come out bottom-up, needing no flip — was deleted at Campaign V slice
|
|
/// V11.
|
|
/// </summary>
|
|
private static Func<int, int, byte[]> CreateBackbufferReader(
|
|
GameWindowGraphics graphics,
|
|
IGpuDevice device) =>
|
|
(width, height) =>
|
|
AcDream.App.Diagnostics.FrameScreenshotController.FlipRows(
|
|
device.CaptureBackbuffer(width, height),
|
|
width,
|
|
height);
|
|
|
|
/// <summary>
|
|
/// Vulkan's context acquisition runs its own capability gate inside
|
|
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext.Acquire"/>,
|
|
/// throwing <see cref="NotSupportedException"/> into the same exit-code-4
|
|
/// contract <c>Program.cs</c> publishes. The raw-GL fork this used to make
|
|
/// was deleted at Campaign V slice V11.
|
|
/// </summary>
|
|
private GameWindowGraphics CreateGraphics()
|
|
{
|
|
AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext vulkan =
|
|
AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext.Acquire(
|
|
_window!,
|
|
_options,
|
|
_platformServices,
|
|
_startupPacing,
|
|
_startupQuality.MsaaSamples,
|
|
Console.WriteLine);
|
|
_vulkanGraphics = vulkan;
|
|
return new VulkanGameWindowGraphics(vulkan);
|
|
}
|
|
|
|
private void OnLoad()
|
|
{
|
|
// Task 7: wire the physics data cache into the engine so Transition can
|
|
// run narrow-phase BSP tests during FindObjCollisions.
|
|
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform = AcquirePlatform();
|
|
// The raw-GL capability gate that used to run here (reading GL
|
|
// extension strings) was deleted at Campaign V slice V11. Vulkan's
|
|
// equivalent gate already ran inside VulkanGraphicsContext.Acquire and
|
|
// wrote its own report.
|
|
|
|
// #391: capture the monitor's curated resolution catalog once, on the
|
|
// windowing thread, before any Options-panel mount reads it.
|
|
DisplayModeCatalog.InstallFromWindow(_window!);
|
|
|
|
GameWindowCompositionPipeline.Run<
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult,
|
|
FrameRootResult>(
|
|
platform,
|
|
platformResult => new HostInputCameraCompositionPhase(
|
|
new HostInputCameraDependencies(
|
|
_framebufferResize,
|
|
_window!.FramebufferSize,
|
|
_hostQuiescence,
|
|
_inputCapture,
|
|
_keyBindings,
|
|
_movementInput,
|
|
_cameraInput,
|
|
_localPlayerMode,
|
|
_chaseCameraInput,
|
|
_pointerPosition,
|
|
_renderDiagnosticLog),
|
|
this).Compose(platformResult),
|
|
(platformResult, hostInputCamera) =>
|
|
new ContentEffectsAudioCompositionPhase(
|
|
new ContentEffectsAudioDependencies(
|
|
_datDir,
|
|
_options.PreparedAssetPath,
|
|
_options.ResidencyBudgets,
|
|
_physicsDataCache,
|
|
_animationDiagnostics.DumpMotionEnabled,
|
|
_runtime,
|
|
_hookRouter,
|
|
_effectPoses,
|
|
_entityEffectAdvance,
|
|
Lighting,
|
|
_translucencyFades,
|
|
_options.NoAudio,
|
|
Console.WriteLine,
|
|
Console.Error.WriteLine),
|
|
this).Compose(platformResult, hostInputCamera),
|
|
(platformResult, hostInputCamera, contentEffectsAudio) =>
|
|
new SettingsDevToolsCompositionPhase(
|
|
new SettingsDevToolsDependencies(
|
|
_runtimeSettings,
|
|
new RuntimeSettingsStartupTargets(
|
|
new SilkRuntimeDisplayWindowTarget(_window!),
|
|
_displayFramePacing,
|
|
hostInputCamera.CameraController,
|
|
contentEffectsAudio.Audio?.Engine)))
|
|
.Compose(platformResult, hostInputCamera, contentEffectsAudio),
|
|
(platformResult, contentEffectsAudio, settingsDevTools) =>
|
|
{
|
|
const uint initialCenterLandblockId = 0xA9B4FFFFu;
|
|
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
|
|
new WorldRenderDependencies(
|
|
_worldEnvironment,
|
|
_renderResourceLifetime,
|
|
// Campaign V slice V6h: the device's own retirement queue
|
|
// rather than the GL frame-flight controller directly. On
|
|
// GL these are the same object (GlGpuDevice.Retirement
|
|
// returns the controller it was constructed with); on
|
|
// Vulkan the device owns its timeline-backed queue.
|
|
_gpuDevice!.Retirement,
|
|
_options.ResidencyBudgets,
|
|
initialCenterLandblockId,
|
|
_applicationPaths.DiagnosticsDirectory,
|
|
Console.WriteLine,
|
|
_gpuDevice!,
|
|
_gpuFrameLifetime!),
|
|
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
|
Console.WriteLine(
|
|
$"loading world view centered on " +
|
|
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
|
return worldRender;
|
|
},
|
|
(platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) =>
|
|
{
|
|
// The ImGui developer-tools debug toast sink was removed at
|
|
// Campaign V slice V11 along with the rest of the ImGui
|
|
// frontend; there is no replacement toast surface yet.
|
|
Action<string>? compositionToast = null;
|
|
return new InteractionRetainedUiCompositionPhase(
|
|
new InteractionRetainedUiDependencies(
|
|
_options,
|
|
platformResult.Graphics,
|
|
CreateBackbufferReader(
|
|
platformResult.Graphics,
|
|
hostInputCamera.GpuDevice),
|
|
_window!,
|
|
platformResult.Input,
|
|
worldRender.Foundation.ShadersDirectory,
|
|
contentEffectsAudio.Dats,
|
|
_datLock,
|
|
worldRender.Foundation.TextureCache,
|
|
worldRender.Foundation.DebugFont,
|
|
_hostQuiescence,
|
|
_retainedInputCapture,
|
|
hostInputCamera.InputDispatcher,
|
|
_applicationPaths.KeyBindingsFile,
|
|
_runtimeSettings,
|
|
_runtime,
|
|
_combatAttackOperations,
|
|
_combatTargetOperations,
|
|
_spellCastOperations,
|
|
contentEffectsAudio.MagicCatalog,
|
|
_stackSplitQuantity,
|
|
_uiRegistry,
|
|
_liveCombatModeCommands,
|
|
_localPlayerIdentity,
|
|
_localPlayerMode,
|
|
viewPlane => new SelectionCameraSource(
|
|
hostInputCamera.CameraController,
|
|
_window!,
|
|
viewPlane),
|
|
_uiFrameDiagnostics,
|
|
ExistingVitals: null,
|
|
compositionToast,
|
|
ClientTimerNow,
|
|
Console.WriteLine,
|
|
hostInputCamera.GpuDevice,
|
|
hostInputCamera.GpuFrameLifetime),
|
|
_retailUiLease,
|
|
this).Compose(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender);
|
|
},
|
|
(platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi) =>
|
|
{
|
|
// The ImGui developer-tools debug toast sink was removed at
|
|
// Campaign V slice V11 along with the rest of the ImGui
|
|
// frontend; there is no replacement toast surface yet.
|
|
Action<string>? compositionToast = null;
|
|
return new LivePresentationCompositionPhase(
|
|
new LivePresentationDependencies(
|
|
_options,
|
|
platformResult.Graphics,
|
|
_window!,
|
|
_datLock,
|
|
_runtimeSettings,
|
|
_hostQuiescence,
|
|
_physicsEngine,
|
|
_physicsDataCache,
|
|
_worldGameState,
|
|
_worldEvents,
|
|
_runtime,
|
|
_liveEntityRuntimeSlot,
|
|
_liveEntityMotionBindings,
|
|
_entityEffectAdvance,
|
|
_effectPoses,
|
|
_remotePhysicsUpdater,
|
|
_localPlayerShadow,
|
|
_animatedEntities,
|
|
_animationDiagnostics,
|
|
_classificationCache,
|
|
_translucencyFades,
|
|
_retailAlphaQueue,
|
|
_cellVisibility,
|
|
_liveWorldOrigin,
|
|
_localPlayerIdentity,
|
|
_pointerPosition,
|
|
_playerApproachCompletions,
|
|
_renderResourceLifetime,
|
|
_portalTunnelFallback,
|
|
_hookRouter,
|
|
_renderDiagnosticLog,
|
|
WorldTime,
|
|
DevWorldEntities: null,
|
|
DevFrameDiagnostics: null,
|
|
_uiFrameDiagnostics,
|
|
Console.WriteLine,
|
|
compositionToast),
|
|
this).Compose(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi);
|
|
},
|
|
(hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation) =>
|
|
new SessionPlayerCompositionPhase(
|
|
new SessionPlayerDependencies(
|
|
_options,
|
|
_runtime,
|
|
_window!,
|
|
_datLock,
|
|
_runtimeSettings,
|
|
settingsDevTools,
|
|
_worldEnvironment,
|
|
_worldSceneDebugState,
|
|
_runtimeDiagnosticCommands,
|
|
_liveCombatModeCommands,
|
|
_combatModeOperations,
|
|
_hostQuiescence,
|
|
_physicsEngine,
|
|
_physicsDataCache,
|
|
_worldGameState,
|
|
_worldEvents,
|
|
_classificationCache,
|
|
_liveEntityRuntimeSlot,
|
|
_animatedEntities,
|
|
_remoteMovementObservations,
|
|
_remotePhysicsUpdater,
|
|
_remoteInboundMotion,
|
|
_inboundEntityEvents,
|
|
_liveEntityMotionBindings,
|
|
_localPlayerIdentity,
|
|
_playerHostSlot,
|
|
_localPlayerMode,
|
|
_chaseCameraInput,
|
|
_localPlayerOutbound,
|
|
_localPlayerTeleportSink,
|
|
_liveWorldOrigin,
|
|
_renderRange,
|
|
_localPlayerShadow,
|
|
_viewportAspect,
|
|
_playerApproachCompletions,
|
|
_pointerPosition,
|
|
_movementInput,
|
|
_inputCapture,
|
|
_particleVisibility,
|
|
_translucencyFades,
|
|
_effectPoses,
|
|
_updateFrameClock,
|
|
_movementTruthDiagnostics,
|
|
_combatAttackOperations,
|
|
_combatFeedback,
|
|
_portalTunnelFallback,
|
|
Console.WriteLine,
|
|
_statusWriter),
|
|
this).Compose(
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation),
|
|
(platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation,
|
|
sessionPlayer) => new FrameRootCompositionPhase(
|
|
new FrameRootDependencies(
|
|
_options,
|
|
_runtime,
|
|
platformResult.Graphics,
|
|
_window!,
|
|
platformResult.Input,
|
|
WorldTime,
|
|
Weather,
|
|
Lighting,
|
|
_worldEnvironment,
|
|
_physicsEngine,
|
|
_cellVisibility,
|
|
_localPlayerMode,
|
|
_localPlayerIdentity,
|
|
_chaseCameraInput,
|
|
_liveWorldOrigin,
|
|
_particleVisibility,
|
|
_effectPoses,
|
|
_renderRange,
|
|
_runtimeSettings,
|
|
_displayFramePacing,
|
|
_worldSceneDebugState,
|
|
_retailAlphaQueue,
|
|
_frameProfiler,
|
|
_frameDiag,
|
|
_renderDiagnosticLog,
|
|
_debugVmRenderFacts,
|
|
_inputCapture,
|
|
_cameraInput,
|
|
_animatedEntities,
|
|
_updateFrameClock,
|
|
_frameGraphs,
|
|
Console.WriteLine),
|
|
this).Compose(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation,
|
|
sessionPlayer),
|
|
frameRoots => new SessionStartCompositionPhase(
|
|
new SessionStartDependencies(
|
|
Console.WriteLine))
|
|
.Start(frameRoots));
|
|
}
|
|
|
|
private void OnUpdate(double dt)
|
|
{
|
|
// #343: armed before the callback body runs, cleared only on normal
|
|
// return. Deliberately NOT a try/finally — if Tick throws, the flag
|
|
// must stay true (mirroring Silk's own stuck _inRenderLoop guard),
|
|
// not get cleared on the way out.
|
|
_renderLoopArmed = true;
|
|
using var _updStage = _frameProfiler.BeginStage(
|
|
AcDream.App.Diagnostics.FrameStage.Update);
|
|
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
|
|
_renderLoopArmed = false;
|
|
}
|
|
|
|
// Performance overlay state — updated every ~0.5s and written to the
|
|
// window title so there's zero rendering cost (no font/overlay needed).
|
|
private void OnRender(double deltaSeconds)
|
|
{
|
|
// #343: see OnUpdate above — armed on entry, cleared on every normal
|
|
// exit path below, left stuck true if anything here throws.
|
|
_renderLoopArmed = true;
|
|
Vector2D<int> size = _window!.Size;
|
|
// Campaign V slice V6h: swapchain currency is the one piece of
|
|
// presentation the RHI contract deliberately leaves to the host (plan
|
|
// §4.9). A minimised window has no swapchain to render into; an
|
|
// out-of-date one is recreated at a frame boundary, the only safe point.
|
|
// The handoff below stays exactly one call on both backends.
|
|
if (_vulkanGraphics is { } vulkan && !vulkan.PrepareFrame())
|
|
{
|
|
_renderLoopArmed = false;
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
_frameGraphs.Render(
|
|
new AcDream.App.Rendering.RenderFrameInput(
|
|
deltaSeconds,
|
|
size.X,
|
|
size.Y),
|
|
out _);
|
|
}
|
|
catch (AcDream.App.Rendering.Gpu.Vk.VulkanSwapchainOutOfDateException)
|
|
when (_vulkanGraphics is not null)
|
|
{
|
|
_vulkanGraphics.RequestRecreate();
|
|
_renderLoopArmed = false;
|
|
return;
|
|
}
|
|
|
|
_vulkanGraphics?.NoteFrameClosed();
|
|
_renderLoopArmed = false;
|
|
}
|
|
|
|
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
|
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
|
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
|
// animatedIds build site — every Sequencer entity is now added
|
|
// unconditionally, which is the known-good pre-optimization behavior.
|
|
|
|
// R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is
|
|
// driven through the SAME MotionTableDispatchSink/DefaultSink funnel
|
|
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
|
|
// PlayerMovementController; airborne-Falling falls out of
|
|
// contact_allows_move + apply_current_movement). The #45 sidestep
|
|
// 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it —
|
|
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
|
// always played (w6-cutover-map.md R3).
|
|
|
|
/// <summary>
|
|
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
|
/// + camera aspect when the window is resized (by the user dragging
|
|
/// the corner OR by the runtime display target applying a saved
|
|
/// Resolution). Without this, the viewport stays pinned at the
|
|
/// startup size, producing a small render inside a big window.
|
|
/// Also force-resets ImGui panel layout so panels that were
|
|
/// previously off the new viewport snap back to default positions.
|
|
/// </summary>
|
|
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
|
=> _framebufferResize.Resize(newSize);
|
|
|
|
private void OnClosing() => CompleteShutdown(releaseNativeWindow: false);
|
|
|
|
private void CompleteShutdown(bool releaseNativeWindow)
|
|
{
|
|
if (!_lifetime.HasShutdownRoots)
|
|
{
|
|
// Campaign LA slice LA1: capture BEFORE the shutdown roots run —
|
|
// by the time teardown completes, IsInWorld is always false
|
|
// regardless of whether a real session was ever connected.
|
|
// OnClosing() and Dispose() both funnel through this method;
|
|
// HasShutdownRoots's own guard means this fires exactly once,
|
|
// from whichever of the two reaches it first.
|
|
if (_runtime.Session.IsInWorld)
|
|
_statusWriter.Disconnected(_options.SessionId ?? "app", "stopped");
|
|
_lifetime.PublishShutdownRoots(CaptureShutdownRoots());
|
|
}
|
|
|
|
GameWindowLifetimeReport report = releaseNativeWindow
|
|
? _lifetime.CompleteAndReleaseNativeWindow()
|
|
: _lifetime.TryComplete();
|
|
if (report.Status == GameWindowLifetimeStatus.Complete)
|
|
{
|
|
// "exited" = terminal — only the true Dispose() call (not the
|
|
// OnClosing() native-window-close-request pass) represents the
|
|
// process actually being done.
|
|
if (releaseNativeWindow)
|
|
ReportExited(report);
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[shutdown] status={report.Status}, blocked={report.BlockedStage ?? "none"}");
|
|
foreach (ResourceShutdownCleanupFailure cleanup in report.CleanupFailures)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[shutdown] cleanup '{cleanup.Operation}' in '{cleanup.Stage}': " +
|
|
cleanup.Error);
|
|
}
|
|
|
|
if (report.Error is not null)
|
|
Console.Error.WriteLine($"[shutdown] {report.Error}");
|
|
|
|
if (releaseNativeWindow)
|
|
ReportExited(report);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the ONE terminal "exited" status event for this session
|
|
/// (fix #406). A resource-shutdown transaction can converge cleanly
|
|
/// (<paramref name="report"/>'s own <see cref="GameWindowLifetimeReport.Status"/>
|
|
/// says nothing about this) even though this <see cref="Dispose"/> call
|
|
/// is running mid-unwind of an exception that escaped
|
|
/// <see cref="Run"/>'s frame loop and is about to terminate the process
|
|
/// via the CLR's unhandled-exception path — <see cref="_runFailure"/>
|
|
/// is the one signal that actually distinguishes those two cases.
|
|
/// Before this fix every such crash wrote the exact same
|
|
/// "exited{code:0,reason:graceful}" as a real graceful shutdown,
|
|
/// sending any launcher-side diagnosis in the wrong direction (#406).
|
|
/// </summary>
|
|
private void ReportExited(GameWindowLifetimeReport report)
|
|
{
|
|
string sessionId = _options.SessionId ?? "app";
|
|
if (_runFailure is not null)
|
|
{
|
|
// The real OS-level exit code (e.g. 0xE0434352 on Windows for
|
|
// an unhandled .NET exception) is produced by the runtime AFTER
|
|
// this method returns and the exception keeps propagating — it
|
|
// cannot be predicted from here. "crashed" is the truthful,
|
|
// platform-independent classification; the launcher's own
|
|
// process supervisor observes the real OS exit code separately.
|
|
_statusWriter.Exited(sessionId, 1, "crashed");
|
|
return;
|
|
}
|
|
|
|
if (report.Status == GameWindowLifetimeStatus.Complete)
|
|
_statusWriter.Exited(sessionId, 0, "graceful");
|
|
else
|
|
_statusWriter.Exited(sessionId, 1, "shutdown-incomplete");
|
|
}
|
|
|
|
private GameWindowShutdownRoots CaptureShutdownRoots() => new(
|
|
new IngressShutdownRoots(
|
|
_hostQuiescence,
|
|
_liveCombatModeCommands,
|
|
_runtimeDiagnosticCommands,
|
|
_retainedUiGameplayBinding,
|
|
_gameplayInputActions,
|
|
_cameraPointerInput,
|
|
_inputDispatcher,
|
|
_mouseSource,
|
|
_kbSource,
|
|
_retailUiLease,
|
|
_uiHost,
|
|
_pluginSession,
|
|
_runtime,
|
|
_movementInput,
|
|
_cameraInput,
|
|
_windowCallbacks),
|
|
new FrameShutdownRoots(
|
|
_frameGraphPublication,
|
|
_frameRootBindings,
|
|
_sessionPlayerBindings,
|
|
_interactionUiLateBindings),
|
|
new LiveShutdownRoots(
|
|
_cameraPointerInput,
|
|
_retailUiLease,
|
|
_magicRuntime,
|
|
_itemInteractionController,
|
|
_externalContainerLifecycle,
|
|
_streamer,
|
|
_equippedChildRenderer,
|
|
_liveEntities,
|
|
_runtime,
|
|
_runtimeHostLease,
|
|
_renderSceneShadow,
|
|
_livePresentationBindings,
|
|
_entityEffectAdvance,
|
|
_entityEffects,
|
|
_hookRegistrations,
|
|
_liveEntityLights,
|
|
_liveEntityPresentation,
|
|
_animationHookFrames,
|
|
_effectPoses,
|
|
_audioEngine),
|
|
new RenderShutdownRoots(
|
|
_gpuFrameFlights,
|
|
_gpuDevice,
|
|
_localPlayerTeleport,
|
|
_portalTunnelFallback,
|
|
_paperdollViewportRenderer,
|
|
_creatureAppraisalViewportRenderer,
|
|
_chargenPreviewRenderer,
|
|
_chargenPreviewController,
|
|
_summaryPreviewRenderer,
|
|
_summaryPreviewController,
|
|
_wbDrawDispatcher,
|
|
_envCellRenderer,
|
|
_portalDepthMask,
|
|
_clipFrame,
|
|
_skyRenderer,
|
|
_particleRenderer,
|
|
_textureCache,
|
|
_wbMeshAdapter,
|
|
_terrain,
|
|
_sceneLightingUbo,
|
|
_debugLines,
|
|
_textRenderer,
|
|
_debugFont,
|
|
_displayFramePacing,
|
|
_frameProfiler,
|
|
_renderResourceLifetime,
|
|
_constructionCleanup),
|
|
new PlatformShutdownRoots(
|
|
_dats,
|
|
_preparedAssets,
|
|
_input,
|
|
_graphics));
|
|
private void OnFocusChanged(bool focused)
|
|
=> _cameraPointerInput?.HandleFocusChanged(focused);
|
|
|
|
public void Dispose()
|
|
{
|
|
// Closing is the normal path and runs while the render context is
|
|
// current. This direct call also covers a constructed-but-never-run
|
|
// window and exceptions during Window.Create/Run, so owned kernel
|
|
// handles (including the frame timer) never depend on a native window
|
|
// event for disposal.
|
|
CompleteShutdown(releaseNativeWindow: true);
|
|
_window = null;
|
|
}
|
|
|
|
}
|