acdream/src/AcDream.App/Rendering/GameWindow.cs
Erik 31e6e192b3 refactor(app): extract focused window lifetime
Move the exact retryable shutdown manifest, typed root snapshot, terminal reporting, and native-window-last release out of GameWindow. Keep session and GPU convergence as hard barriers while reporting persistent physical callback cleanup without stranding dependent owners.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 19:43:52 +02:00

1625 lines
77 KiB
C#

using AcDream.Core.Plugins;
using AcDream.App.Composition;
using AcDream.App.Physics;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.World;
using AcDream.Content;
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 AcDream.Core.Selection.SelectionState _selection;
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 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 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 AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private FrameRootRuntimeBindings? _frameRootBindings;
private IDisposable? _frameGraphPublication;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
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 =
new(Console.WriteLine);
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 readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
// 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 readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache = new();
// #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 AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
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 = new();
// 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;
// Phase F.1-H.1 — client-side state classes fed by GameEventWiring.
// Exposed publicly so plugins + UI panels can bind directly.
public readonly AcDream.Core.Chat.ChatLog Chat = new();
// Phase I.6 — runtime state for retail's TurbineChat (0xF7DE) global
// chat rooms. Empty/disabled until the server fires
// SetTurbineChatChannels (0x0295) shortly after EnterWorld.
public readonly AcDream.Core.Chat.TurbineChatState TurbineChat = new();
public readonly AcDream.Core.Combat.CombatState Combat = new();
public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
public readonly AcDream.Core.Social.FriendsState Friends = new();
public readonly AcDream.Core.Social.SquelchState Squelch = new();
private readonly DesiredComponentSnapshotState _desiredComponents = new();
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
_desiredComponents.Items;
// 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 readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
public readonly AcDream.Core.Items.ClientObjectTable Objects = new();
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
private readonly ShortcutSnapshotState _shortcutSnapshots = new();
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts
{
get => _shortcutSnapshots.Items;
private set => _shortcutSnapshots.Items = value;
}
// 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 readonly AcDream.Core.Player.LocalPlayerState LocalPlayer = null!;
// 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.CombatFeedbackSlot
_combatFeedback = new();
private AcDream.App.Combat.CombatAttackController? _combatAttackController;
private AcDream.App.Combat.CombatTargetController? _combatTargetController;
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
private readonly AcDream.Core.Items.ExternalContainerState _externalContainers = new();
private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle;
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
private AcDream.App.Spells.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;
// 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 readonly AcDream.App.Input.LocalPlayerControllerSlot _playerControllerSlot = new();
private AcDream.App.Input.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 = new();
private uint _playerServerGuid
{
get => _localPlayerIdentity.ServerGuid;
set => _localPlayerIdentity.ServerGuid = value;
}
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
// replaces this before any in-world drag can normally occur.
private readonly PlayerCharacterOptionsState _characterOptions = new();
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = 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: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json,
// 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 = LoadStartupKeyBindings();
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings()
{
var path = AcDream.UI.Abstractions.Input.KeyBindings.DefaultPath();
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.
// Slice 3: the controller is the sole App owner. Outbound feature
// callbacks resolve this borrowed handle at call time and never cache it.
private AcDream.App.Net.LiveSessionController? _liveSessionController;
private AcDream.App.Net.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.World.WorldEntity> EmptyLiveEntityMap =
new Dictionary<uint, AcDream.Core.World.WorldEntity>();
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid =>
_liveEntities?.MaterializedWorldEntities ?? EmptyLiveEntityMap;
/// <summary>Visible-only view for radar, picking, status, and targets.</summary>
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _visibleEntitiesByServerGuid =>
_liveEntities?.WorldEntities ?? EmptyLiveEntityMap;
/// <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.Core.Selection.SelectionState selection,
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
{
_options = options ?? throw new System.ArgumentNullException(nameof(options));
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(
_inputCapture);
_framebufferResize = new FramebufferResizeController(_viewportAspect);
_datDir = options.DatDir;
_worldGameState = worldGameState;
_worldEvents = worldEvents;
_displayFramePacing = new DisplayFramePacingController(
options.UncappedRendering,
_frameProfiler);
_runtimeSettings = new RuntimeSettingsController(
new JsonRuntimeSettingsStorage(
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath()),
log: Console.WriteLine);
_selection = selection ?? throw new System.ArgumentNullException(nameof(selection));
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
_uiRegistry = uiRegistry;
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
_liveEntityRuntimeSlot);
SpellBook = new AcDream.Core.Spells.Spellbook();
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
// #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(
_physicsEngine,
_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 AcDream.App.Input.LocalPlayerOutboundController(
_movementTruthDiagnostics);
}
public void Run()
{
// 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.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.PublishMagicCatalog(
AcDream.App.Spells.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
|| _combatTargetController 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;
_combatTargetController = result.CombatTarget;
_externalContainerLifecycle = result.ExternalContainerLifecycle;
_itemInteractionController = result.ItemInteraction;
_interactionUiLateBindings = result.LateBindings;
if (result.RetainedUi is { } retained)
{
_uiHost = retained.Host;
_retailUiRuntime = retained.Runtime;
_vitalsVm ??= retained.Vitals;
_retailChatVm = retained.Chat;
_characterSheetProvider = retained.CharacterSheet;
_magicRuntime = retained.Magic;
_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
|| _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
|| _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;
_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;
_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
|| _liveSessionController 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;
_liveSessionController = result.LiveSession;
_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 (_worldLifecycleAutomation is not null
|| _frameRootBindings is not null
|| _frameGraphPublication is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns frame roots.");
}
_worldLifecycleAutomation = result.LifecycleAutomation;
_frameRootBindings = result.RuntimeBindings;
_frameGraphPublication = result.FrameGraphPublication;
}
private static void PublishCompositionOwner<T>(
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.
_physicsEngine.DataCache = _physicsDataCache;
GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();
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,
_physicsDataCache,
_animationDiagnostics.DumpMotionEnabled,
SpellBook,
_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)
: 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,
Chat,
Combat,
LocalPlayer,
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!,
initialCenterLandblockId,
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,
Combat,
_combatAttackOperations,
_selection,
_externalContainers,
Objects,
contentEffectsAudio.MagicCatalog,
SpellBook,
Chat,
LocalPlayer,
ItemMana,
_stackSplitQuantity,
_uiRegistry,
_liveCombatModeCommands,
_localPlayerIdentity,
_playerControllerSlot,
_localPlayerMode,
_characterOptions,
_shortcutSnapshots,
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,
_selection,
Objects,
_liveEntityRuntimeSlot,
_liveEntityMotionBindings,
_entityEffectAdvance,
_effectPoses,
_remotePhysicsUpdater,
_localPlayerShadow,
_animatedEntities,
_animationDiagnostics,
_classificationCache,
_translucencyFades,
_retailAlphaQueue,
_cellVisibility,
_liveWorldOrigin,
_localPlayerIdentity,
_playerControllerSlot,
_pointerPosition,
_playerApproachCompletions,
_renderResourceLifetime,
_portalTunnelFallback,
_hookRouter,
_renderDiagnosticLog,
WorldTime,
settingsDevTools.DevTools?.LateBindings.WorldEntities,
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
_uiFrameDiagnostics,
compositionToast),
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi);
},
(hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation) =>
new SessionPlayerCompositionPhase(
new SessionPlayerDependencies(
_options,
_window!,
_datLock,
_runtimeSettings,
settingsDevTools,
_worldEnvironment,
_worldSceneDebugState,
_runtimeDiagnosticCommands,
_liveCombatModeCommands,
_hostQuiescence,
_physicsEngine,
_physicsDataCache,
_worldGameState,
_worldEvents,
_selection,
Objects,
_classificationCache,
_liveEntityRuntimeSlot,
_animatedEntities,
_remoteMovementObservations,
_remotePhysicsUpdater,
_remoteInboundMotion,
_inboundEntityEvents,
_liveEntityMotionBindings,
_localPlayerIdentity,
_playerControllerSlot,
_playerHostSlot,
_localPlayerMode,
_chaseCameraInput,
_localPlayerOutbound,
_localPlayerTeleportSink,
_liveWorldOrigin,
_renderRange,
_localPlayerShadow,
_localPlayerSkills,
_viewportAspect,
_playerApproachCompletions,
_characterOptions,
_shortcutSnapshots,
_desiredComponents,
_pointerPosition,
_movementInput,
_inputCapture,
_particleVisibility,
_translucencyFades,
_effectPoses,
_updateFrameClock,
_movementTruthDiagnostics,
_combatAttackOperations,
Combat,
_combatFeedback,
Chat,
LocalPlayer,
SpellBook,
ItemMana,
Friends,
Squelch,
TurbineChat,
_externalContainers,
_portalTunnelFallback,
Console.WriteLine),
this).Compose(
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation),
(platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation,
sessionPlayer) => new FrameRootCompositionPhase(
new FrameRootDependencies(
_options,
platformResult.Graphics,
_window!,
platformResult.Input,
WorldTime,
Weather,
Lighting,
_worldEnvironment,
_physicsEngine,
_cellVisibility,
_localPlayerMode,
_localPlayerIdentity,
_chaseCameraInput,
_playerControllerSlot,
_liveWorldOrigin,
_particleVisibility,
_effectPoses,
_renderRange,
_runtimeSettings,
_displayFramePacing,
_worldSceneDebugState,
_retailAlphaQueue,
_frameProfiler,
_frameDiag,
_renderDiagnosticLog,
_debugVmRenderFacts,
_inputCapture,
_cameraInput,
_selection,
_animatedEntities,
_updateFrameClock,
Combat,
_frameGraphs,
Console.WriteLine),
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation,
sessionPlayer),
frameRoots => new SessionStartCompositionPhase(
new SessionStartDependencies(_options, 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,
_liveSessionController,
_runtimeSettings,
_movementInput,
_cameraInput,
_windowCallbacks),
new FrameShutdownRoots(
_frameGraphPublication,
_frameRootBindings,
_sessionPlayerBindings,
_interactionUiLateBindings),
new LiveShutdownRoots(
_cameraPointerInput,
_retailUiLease,
_combatTargetController,
_combatAttackController,
_itemInteractionController,
_externalContainerLifecycle,
_streamer,
_equippedChildRenderer,
_liveEntities,
_livePresentationBindings,
_entityEffectAdvance,
_entityEffects,
_hookRegistrations,
_liveEntityLights,
_liveEntityPresentation,
_remoteTeleportController,
_animationHookFrames,
_effectPoses,
_audioEngine),
new RenderShutdownRoots(
_gpuFrameFlights,
_devToolsComposition,
_localPlayerTeleport,
_portalTunnelFallback,
_paperdollViewportRenderer,
_wbDrawDispatcher,
_envCellRenderer,
_portalDepthMask,
_clipFrame,
_skyRenderer,
_particleRenderer,
_samplerCache,
_textureCache,
_wbMeshAdapter,
_meshShader,
_terrain,
_terrainModernShader,
_sceneLightingUbo,
_debugLines,
_textRenderer,
_debugFont,
_displayFramePacing,
_frameProfiler,
_renderResourceLifetime,
_glConstructionCleanup),
new PlatformShutdownRoots(
_dats,
_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;
}
}