acdream/src/AcDream.App/Rendering/GameWindow.cs
Erik 4f94ad7ddd feat(render): Campaign V slice V1 - OpenGL RHI backend (dark)
Implements GlGpuDevice and the rest of AcDream.App.Rendering.Gpu.Gl,
filling the V0-pinned IGpuDevice contract on OpenGL 4.3. This is the
first of the port slices described in
docs/plans/2026-07-27-vulkan-campaign.md: every later renderer port
(V2 onward) needs a real, driver-proven GL implementation of the RHI
to port onto, and the GL backend is deliberately built to be
behaviour-preserving rather than optimal, because that is what turns
each subsequent slice's pixel gate into a strict identity check
instead of a moving target. The Vulkan backend (V5+) is where the
actual efficiency gains land.

GlGpuDevice is a fresh root, not derived from Chorizite's
BaseGraphicsDevice/OpenGLGraphicsDevice - shedding that inheritance is
one of the things this campaign explicitly does. It owns its own
BindlessSupport instance rather than sharing the legacy WB render
path's, which is what lets it be constructed the moment a GL context
and a GpuFrameFlightController exist, with no dependency on when
WorldRenderCompositionPhase happens to detect bindless support later
in startup. The ring buffer keeps a managed staging array plus a real
GL buffer per flight slot and flushes with one BufferSubData
immediately before each Draw/DrawIndexed/MultiDrawIndexedIndirect
(never at bind time, since a renderer may still write after binding);
V1 throws on an over-capacity ring request rather than growing it,
since nothing consumes the device yet and a silent grow would hide a
future renderer's real working set. The texture table is a bump/free-
list allocator over a managed uvec2 handle array, gated through the
frame-flight retirement queue so a released slot cannot be reused
while a submitted frame might still read it. Push constants are
applied by uniform name on the currently-bound program, cached per
program, and explicitly re-applied whenever BindPipeline switches
programs - GL uniforms are per-program state, so the "survives
pipeline changes within a pass" guarantee the interface documents (a
freebie on Vulkan's shared pipeline layout) has to be emulated here.

BindlessSupport gained one additive method,
GetResidentHandle(texture, sampler), calling the same
ArbBindlessTexture.GetTextureSamplerHandle entry point
ManagedGLTextureArray already uses through a different path. The
existing GetResidentHandle(texture) cannot express
IGpuDevice.RegisterTexture's documented pair semantics ("the same
texture registered with two samplers occupies two slots"), so this
was the minimal change needed rather than a workaround.

The pure bookkeeping - ring watermark/alignment arithmetic, the
texture-slot allocator, render-state diffing, the push-constant field-
to-uniform-name table, and GL format mapping - lives in small GL-free
classes so it is unit-testable without a live context, following the
same seam pattern GpuFrameFlightController already uses for its fence
API. GlGpuTimerPool follows suit with an injectable timer-query API.

The device is constructed in HostInputCameraCompositionPhase
immediately after the frame-flight controller (the same phase that
already builds GpuFrameFlightController), rather than in
WorldRenderCompositionPhase as first considered: GlGpuDevice's self-
contained bindless detection means it has no ordering dependency on
the legacy WB path's BindlessSupport, so it can be proven against the
real driver as early as possible while keeping the composition change
to one phase. Composition, publication, and shutdown wiring follow
the existing acquire/publish/fault-injection pattern exactly, and GPU
device disposal is scheduled through the frame-flight retirement queue
before that queue itself is torn down. Nothing consumes the device
yet - that starts at V4a - so this slice's pixel gate is trivially a
tripwire.

App tests: 3834 passed / 3 skipped (V0 baseline 3785 + 49 new: ring,
texture-slot, render-state, push-constant, format-mapping, enum-
mapping, and timer-pool tests, plus one new fault-injection point in
the existing composition theory).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:11:04 +02:00

1703 lines
80 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.Runtime;
using AcDream.Runtime.Platform;
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.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
public sealed class GameWindow :
IDisposable,
IGameWindowPlatformPublication<GL, IInputContext>,
IGameWindowHostInputCameraPublication,
IGameWindowContentEffectsAudioPublication,
IGameWindowSettingsDevToolsPublication,
IGameWindowWorldRenderPublication,
IGameWindowInteractionRetainedUiPublication,
IGameWindowLivePresentationPublication,
IGameWindowSessionPlayerPublication,
IGameWindowFrameRootPublication
{
private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp()
/ (double)System.Diagnostics.Stopwatch.Frequency;
private readonly AcDream.App.RuntimeOptions _options;
private readonly AnimationPresentationDiagnostics _animationDiagnostics;
private readonly string _datDir;
private readonly WorldGameState _worldGameState;
private readonly WorldEvents _worldEvents;
private readonly HostQuiescenceGate _hostQuiescence = new();
private IWindow? _window;
private SilkWindowCallbackBinding? _windowCallbacks;
private GL? _gl;
private IInputContext? _input;
private TerrainModernRenderer? _terrain;
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
/// <see cref="_terrain"/> at draw time but allocated + disposed here.</summary>
private Shader? _terrainModernShader;
private CameraController? _cameraController;
private IDatReaderWriter? _dats;
private IPreparedAssetSource? _preparedAssets;
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput;
private Shader? _meshShader;
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;
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
/// support. Required at startup — missing bindless throws
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
private SamplerCache? _samplerCache;
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 (see _debugVm below). 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 can'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 readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
_renderResourceLifetime = new();
private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
_glConstructionCleanup = new();
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;
private readonly GameWindowLifetime _lifetime = new();
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;
// #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;
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
// 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.App.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;
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. Null unless ACDREAM_DEVTOOLS=1.
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter;
private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus;
private DevToolsCompositionOwner? _devToolsComposition;
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
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;
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
// its panel; the VM remains here because runtime feedback producers bind
// directly to it during composition.
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
// DevToolsEnabled reads through typed RuntimeOptions.
private bool DevToolsEnabled => _options.DevTools;
// 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 GraphicalCapabilityRecord? _graphicalCapabilities;
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));
_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);
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;
_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);
_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,
AcDream.App.Physics.RemoteServerControlledVelocityCycle.Apply);
_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);
}
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;
FramePacingPolicy startupPacing =
_displayFramePacing.InitializeStartup(startup.Display.VSync);
var options = WindowOptions.Default with
{
Size = new Vector2D<int>(1280, 720),
Title = "acdream — phase 1",
API = new GraphicsAPI(
ContextAPI.OpenGL,
ContextProfile.Core,
ContextFlags.ForwardCompatible,
new APIVersion(4, 3)),
VSync = startupPacing.UseVSync,
// A.5 T22.5: MSAA from quality preset (0 = disabled, 2/4/8 = multisample).
// Silk.NET passes this to SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES).
// Cannot be changed at runtime; Quality changes mid-session that would
// alter MsaaSamples are logged as a restart-required warning.
Samples = startup.Quality.MsaaSamples,
// #117 (2026-06-11): the aperture punch's depth gate needs a
// stencil buffer (PortalDepthMaskRenderer two-pass mark+punch).
// GLFW defaults to 8 stencil bits, but make the requirement
// explicit rather than platform-implicit.
PreferredStencilBufferBits = 8,
};
_window = Window.Create(options);
_lifetime.PublishNativeWindow(_window);
_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)
{
_glConstructionCleanup.RetainFrom(failure);
throw;
}
}
void IGameWindowPlatformPublication<GL, IInputContext>.PublishGraphics(
GL graphics) =>
PublishCompositionOwner(ref _gl, graphics, "graphics API");
void IGameWindowPlatformPublication<GL, IInputContext>.PublishInput(
IInputContext input) =>
PublishCompositionOwner(ref _input, input, "input context");
void IGameWindowHostInputCameraPublication.PublishGpuFrameFlights(
GpuFrameFlightController value) =>
PublishCompositionOwner(ref _gpuFrameFlights, value, "GPU frame flights");
void IGameWindowHostInputCameraPublication.PublishGpuDevice(
IGpuDevice value) =>
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
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.App.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 IGameWindowSettingsDevToolsPublication.PublishDevTools(
DevToolsCompositionOwner value)
{
ArgumentNullException.ThrowIfNull(value);
if (_devToolsComposition is not null
|| _devToolsFramePresenter is not null
|| _devToolsCommandBus is not null
|| _debugVm is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns developer tools.");
}
_devToolsComposition = value;
_devToolsFramePresenter = value.Presenter;
_devToolsCommandBus = value.CommandBus;
_vitalsVm = value.Vitals;
_debugVm = value.Debug;
}
void IGameWindowWorldRenderPublication.PublishBindlessSupport(
BindlessSupport value) =>
PublishCompositionOwner(
ref _bindlessSupport,
value,
"bindless support");
void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) =>
PublishCompositionOwner(
ref _terrainModernShader,
value,
"terrain shader");
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.PublishMeshShader(Shader value) =>
PublishCompositionOwner(ref _meshShader, value, "mesh shader");
void IGameWindowWorldRenderPublication.PublishWbMeshAdapter(
WbMeshAdapter value) =>
PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter");
void IGameWindowWorldRenderPublication.PublishTextureCache(
TextureCache value) =>
PublishCompositionOwner(ref _textureCache, value, "texture cache");
void IGameWindowWorldRenderPublication.PublishSamplerCache(
SamplerCache value) =>
PublishCompositionOwner(ref _samplerCache, value, "sampler 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;
_vitalsVm ??= retained.Vitals;
_retailChatVm = retained.Chat;
_characterSheetProvider = retained.CharacterSheet;
_frameScreenshots = retained.Screenshots;
}
}
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
|| _remoteTeleportController 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
|| _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;
_remoteTeleportController = result.RemoteTeleport;
_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;
_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(_gl), nameof(_input))]
private GameWindowPlatformResult<GL, IInputContext> AcquirePlatform()
{
GameWindowPlatformResult<GL, IInputContext> platform =
GameWindowPlatformAcquisition.Acquire(
() => GL.GetApi(_window!),
static gl => gl.Dispose(),
() => _window!.CreateInput(),
static input => input.Dispose(),
this);
if (_gl is null || _input is null)
throw new InvalidOperationException(
"Platform acquisition returned without publishing both owners.");
return platform;
}
private void OnLoad()
{
// Task 7: wire the physics data cache into the engine so Transition can
// run narrow-phase BSP tests during FindObjCollisions.
GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();
string capabilityReportPath = Path.Combine(
_applicationPaths.DiagnosticsDirectory,
"graphical-capabilities.json");
_graphicalCapabilities =
GraphicalCapabilityGuard.CaptureVerifyAndWrite(
platform.Graphics,
_window!,
platform.Input,
_platformServices,
capabilityReportPath);
GraphicalCapabilityGuard.ThrowIfUnsupported(
_graphicalCapabilities,
capabilityReportPath);
Console.WriteLine(
"graphics: capability gate passed " +
$"({_graphicalCapabilities.ActiveDisplayProtocol}, " +
$"{_graphicalCapabilities.GlVendor}, " +
$"{_graphicalCapabilities.GlRenderer}, " +
$"{_graphicalCapabilities.GlVersion}); " +
$"report={capabilityReportPath}");
GameWindowCompositionPipeline.Run<
GameWindowPlatformResult<GL, 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) =>
{
SettingsDevToolsOptionalDependencies? optionalDevTools = null;
if (DevToolsEnabled)
{
var devToolsWorldEntities =
new DeferredCanonicalWorldEntityCountSource();
var devToolsFrameDiagnostics =
new DeferredRenderFrameDiagnosticsSource();
var devToolsPlayerModeCommands =
new DeferredDevToolsPlayerModeCommands();
var devToolsFacts = new DevToolsRuntimeFacts(
_localPlayerMode,
_playerControllerSlot,
hostInputCamera.CameraController,
devToolsWorldEntities,
_animatedEntities,
_debugVmRenderFacts,
_physicsEngine,
_worldSceneDebugState,
_renderRange,
hostInputCamera.CameraPointerInput,
_worldEnvironment,
Lighting,
contentEffectsAudio.ParticleSystem,
devToolsFrameDiagnostics);
IRuntimeKeyBindingTarget? keyBindingTarget =
hostInputCamera.InputDispatcher is { } settingsDispatcher
? new RuntimeKeyBindingTarget(
settingsDispatcher,
_applicationPaths.KeyBindingsFile)
: null;
optionalDevTools = new SettingsDevToolsOptionalDependencies(
devToolsFacts,
keyBindingTarget,
devToolsWorldEntities,
devToolsFrameDiagnostics,
devToolsPlayerModeCommands);
}
return new SettingsDevToolsCompositionPhase(
new SettingsDevToolsDependencies(
_window!,
_runtimeSettings,
new RuntimeSettingsStartupTargets(
new SilkRuntimeDisplayWindowTarget(_window!),
_displayFramePacing,
hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine),
_hostQuiescence,
_runtime,
optionalDevTools,
_runtimeDiagnosticCommands,
_combatFeedback,
_keyBindings,
_frameProfiler,
_framebufferResize,
Console.WriteLine),
this).Compose(platformResult, hostInputCamera, contentEffectsAudio);
},
(platformResult, contentEffectsAudio, settingsDevTools) =>
{
const uint initialCenterLandblockId = 0xA9B4FFFFu;
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
new WorldRenderDependencies(
_worldEnvironment,
_renderResourceLifetime,
_gpuFrameFlights!,
_options.ResidencyBudgets,
initialCenterLandblockId,
_applicationPaths.DiagnosticsDirectory,
Console.WriteLine),
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
Console.WriteLine(
$"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
return worldRender;
},
(platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) =>
{
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
? text => devTools.Debug.AddToast(text)
: null;
return new InteractionRetainedUiCompositionPhase(
new InteractionRetainedUiDependencies(
_options,
platformResult.Graphics,
_window!,
platformResult.Input,
worldRender.Foundation.ShadersDirectory,
contentEffectsAudio.Dats,
_datLock,
worldRender.Foundation.TextureCache,
worldRender.Foundation.DebugFont,
_hostQuiescence,
_retainedInputCapture,
hostInputCamera.InputDispatcher,
_runtimeSettings,
_runtime,
_combatAttackOperations,
_combatTargetOperations,
_spellCastOperations,
contentEffectsAudio.MagicCatalog,
_stackSplitQuantity,
_uiRegistry,
_liveCombatModeCommands,
_localPlayerIdentity,
_localPlayerMode,
viewPlane => new SelectionCameraSource(
hostInputCamera.CameraController,
_window!,
viewPlane),
_uiFrameDiagnostics,
settingsDevTools.DevTools?.Vitals,
compositionToast,
ClientTimerNow,
Console.WriteLine),
_retailUiLease,
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender);
},
(platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi) =>
{
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
? text => devTools.Debug.AddToast(text)
: 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,
settingsDevTools.DevTools?.LateBindings.WorldEntities,
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
_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),
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)
{
using var _updStage = _frameProfiler.BeginStage(
AcDream.App.Diagnostics.FrameStage.Update);
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
}
// 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)
{
Vector2D<int> size = _window!.Size;
_frameGraphs.Render(
new AcDream.App.Rendering.RenderFrameInput(
deltaSeconds,
size.X,
size.Y),
out _);
}
// 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)
_lifetime.PublishShutdownRoots(CaptureShutdownRoots());
GameWindowLifetimeReport report = releaseNativeWindow
? _lifetime.CompleteAndReleaseNativeWindow()
: _lifetime.TryComplete();
if (report.Status == GameWindowLifetimeStatus.Complete)
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}");
}
private GameWindowShutdownRoots CaptureShutdownRoots() => new(
new IngressShutdownRoots(
_hostQuiescence,
_liveCombatModeCommands,
_runtimeDiagnosticCommands,
_retainedUiGameplayBinding,
_gameplayInputActions,
_cameraPointerInput,
_inputDispatcher,
_mouseSource,
_kbSource,
_retailUiLease,
_uiHost,
_devToolsComposition,
_runtime,
_runtimeSettings,
_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,
_remoteTeleportController,
_animationHookFrames,
_effectPoses,
_audioEngine),
new RenderShutdownRoots(
_gpuFrameFlights,
_gpuDevice,
_devToolsComposition,
_localPlayerTeleport,
_portalTunnelFallback,
_paperdollViewportRenderer,
_creatureAppraisalViewportRenderer,
_wbDrawDispatcher,
_envCellRenderer,
_portalDepthMask,
_clipFrame,
_skyRenderer,
_particleRenderer,
_samplerCache,
_textureCache,
_wbMeshAdapter,
_meshShader,
_terrain,
_terrainModernShader,
_sceneLightingUbo,
_debugLines,
_textRenderer,
_debugFont,
_displayFramePacing,
_frameProfiler,
_renderResourceLifetime,
_glConstructionCleanup),
new PlatformShutdownRoots(
_dats,
_preparedAssets,
_input,
_gl));
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;
}
}