Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
Engine.cs
13using Gondwana.Timers;
14using Microsoft.Extensions.Logging;
16
17namespace Gondwana;
18
35public sealed class Engine : IDisposable
36{
37 #region static members
38
39 private static readonly Lazy<Engine> _instance = new(() => new Engine());
40
49 public static Engine Instance => _instance.Value;
50
59 public static ILogger<Engine> Logger => EngineLogger.GetLogger<Engine>();
60
61 #endregion
62
63 #region private fields
64
65 private long _startTick;
66 private long _lastCPSSamplingTick;
67 private long _lastBackgroundTick = HighResTimer.GetCurrentTick();
68 private long _lastForegroundTick = HighResTimer.GetCurrentTick();
69 private long _lastCycleTick = HighResTimer.GetCurrentTick();
70 private readonly FixedStepAccumulator _timerDrivenSteps = new();
71 private long _timerDrivenForegroundAccumulatedTicks;
72 private long _lastTimerDrivenForegroundDriverTick;
73 private int _timerDrivenForegroundTargetFps;
74 private bool _isTimerDriven;
75
76 private long _grossCyclesThisMeasure = 0;
77 private long _netCyclesThisMeasure = 0;
78 private double _grossCPS = 0;
79 private double _netFPS = 0;
80
81 private Task? _cycleTask;
82 private IEngineConfigurationStore? _configurationStore;
83 private bool _deferredDisposeScheduled;
84
85 #endregion private fields
86
87 #region events
88
103 public event Action? PreInitialization;
104
123 public event Action? PostInitialization;
124
138 public event Action? InitializationComplete;
139
149 public event Action? BeforeBackgroundTasksExecute;
150
160 public event Action? AfterBackgroundTasksExecute;
161
171 public event Action? BeforeFrameRender;
172
182 public event Action? AfterFrameRender;
183
197 [Obsolete(
198 "Legacy CPS/FPS notifications are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
199 DiagnosticId = "GOND0001")]
200 public event Action<CyclesPerSecondCalculatedEventArgs>? CPSCalculated;
201
210 public event Action? Disposing;
211
220 public event Action? Disposed;
221
222 #endregion events
223
224 private readonly Diagnostics.TelemetrySource _engineTelemetry;
225
228 public Diagnostics.RuntimeProfiler Profiler { get; } = new();
229
230 private Engine()
231 {
232 _engineTelemetry = Profiler.RegisterSource("Engine", "Core")!;
233 _engineTelemetry.Define("cycle.cpu.ms");
234 _engineTelemetry.Define("background.cpu.ms");
235 _engineTelemetry.Define("foreground.cpu.ms");
236 }
237
238 private volatile bool _isInitialized = false;
239 private volatile bool _isInitializing = false;
240 private readonly ManualResetEventSlim _initDone = new(false);
241
297 public void Initialize(
298 string? configFileName = null,
299 bool? autoSaveConfig = null,
300 IKeyboardAdapter? keyboardAdapter = null,
301 IMouseAdapter? mouseAdapter = null,
302 ITouchAdapter? touchAdapter = null,
303 IGamepadManager<IGamepadAdapter>? gamepadManager = null)
304 => InitializeCore(
305 configFileName,
306 autoSaveConfig,
307 keyboardAdapter,
308 mouseAdapter,
309 touchAdapter,
310 gamepadManager,
311 configurationStore: null);
312
325 IEngineConfigurationStore configurationStore,
326 bool? autoSaveConfig = null,
327 IKeyboardAdapter? keyboardAdapter = null,
328 IMouseAdapter? mouseAdapter = null,
329 ITouchAdapter? touchAdapter = null,
330 IGamepadManager<IGamepadAdapter>? gamepadManager = null)
331 {
332 ArgumentNullException.ThrowIfNull(configurationStore);
333
334 InitializeCore(
335 configFileName: null,
336 autoSaveConfig,
337 keyboardAdapter,
338 mouseAdapter,
339 touchAdapter,
340 gamepadManager,
341 configurationStore);
342 }
343
344 private void InitializeCore(
345 string? configFileName,
346 bool? autoSaveConfig,
347 IKeyboardAdapter? keyboardAdapter,
348 IMouseAdapter? mouseAdapter,
349 ITouchAdapter? touchAdapter,
350 IGamepadManager<IGamepadAdapter>? gamepadManager,
351 IEngineConfigurationStore? configurationStore)
352 {
353 if (_isInitialized || _isInitializing)
354 return;
355
356 // reset in case this instance has been initialized before
357 _initDone.Reset();
358
359 _isInitializing = true;
360 try
361 {
362 if (UiDispatcher == null)
363 PreInitialization?.Invoke();
364 else
365 UiDispatcher!.Post(() => PreInitialization?.Invoke());
366
367 try
368 {
369 _configurationStore?.Dispose();
370 }
371 catch (Exception ex)
372 {
373 Logger.LogError(ex, "Error saving the engine configuration during initialization.");
374 }
375
376 _configurationStore = configurationStore ?? EngineConfigurationFile.Load(configFileName, autoSaveConfig);
377 if (configurationStore is not null && autoSaveConfig.HasValue)
378 _configurationStore.AutoSave = autoSaveConfig.Value;
379
380 Configuration = _configurationStore.Configuration;
381
382 ConfigureLogging(Configuration);
383
384 if (Configuration.StateFiles?.Any() ?? false)
385 {
386 foreach (var stateFile in Configuration.StateFiles)
387 {
388 EngineState.MergeFromFile(stateFile.File, stateFile.IsCompressed, stateFile.OverwriteExisting, stateFile.EngineStateParts);
389 }
390 }
391
392 if (keyboardAdapter != null)
393 KeyboardEventPoller.Initialize(keyboardAdapter);
394
395 if (mouseAdapter != null)
396 MouseEventPoller.Initialize(mouseAdapter);
397
398 if (touchAdapter != null)
399 Input.TouchAdapter = touchAdapter;
400
401 if (gamepadManager is not null)
402 Input.GamepadManager = gamepadManager;
403
404 if (UiDispatcher == null)
405 PostInitialization?.Invoke();
406 else
407 UiDispatcher!.Post(() => PostInitialization?.Invoke());
408
409 EnginePluginRegistry.InvokeInitialize(this);
410
411 _isInitialized = true;
412
413 if (UiDispatcher == null)
414 InitializationComplete?.Invoke();
415 else
416 UiDispatcher!.Post(() => InitializationComplete?.Invoke());
417 }
418 finally
419 {
420 _isInitializing = false;
421 _initDone.Set();
422 }
423 }
424
431 public void SaveConfiguration()
432 {
433 var store = _configurationStore
434 ?? throw new InvalidOperationException("The engine has no initialized configuration store.");
435
436 store.Save();
437 }
438
460 public void Start()
461 {
462 if (SynchronizationContext.Current == null)
463 throw new InvalidOperationException("SynchronizationContext cannot be null.");
464
465 Start(SynchronizationContext.Current);
466 }
467
510 public void Start(SynchronizationContext uiContext)
511 {
512 if (IsRunning)
513 return;
514
515 UiDispatcher = new UiDispatcher(uiContext);
516
517 if (!IsInitialized)
518 {
519 if (IsInitializing)
520 {
521 var initializationWaitTimeoutSeconds = Configuration.StartInitializationWaitTimeout;
522 var initializationWaitTimeout = TimeSpan.FromSeconds(initializationWaitTimeoutSeconds);
523 if (!_initDone.Wait(initializationWaitTimeout))
524 throw new InvalidOperationException(
525 $"Engine initialization did not complete within {initializationWaitTimeoutSeconds:0.###} seconds.");
526
527 if (!IsInitialized)
528 throw new InvalidOperationException(
529 "Engine initialization failed on another thread. Call Initialize() again and resolve the initialization error.");
530 }
531 else
532 {
533 Initialize(); // we're the initializer—do it now
534 }
535
536 if (!IsInitialized)
537 throw new InvalidOperationException("Engine failed to initialize.");
538 }
539
540 _isTimerDriven = false;
541 EngineSimulationClock.UseWallClock();
542 IsRunning = true;
543
544 _startTick = HighResTimer.GetCurrentTick();
545 _lastCPSSamplingTick = _startTick;
546 _lastCycleTick = _startTick;
547
548 _cycleTask = Task.Run(() =>
549 {
550 EngineDispatcher.BindToCurrentThread();
551
552 while (Instance.IsRunning)
553 {
554 Instance.Cycle();
555 Thread.Yield(); // optional
556 }
557 });
558 }
559
593 public void StartTimerDriven(SynchronizationContext uiContext)
594 {
595 if (IsRunning)
596 return;
597
598 UiDispatcher = new UiDispatcher(uiContext);
599
600 if (!IsInitialized)
601 {
602 if (IsInitializing)
603 throw new InvalidOperationException(
604 "Engine initialization is already in progress on another thread. " +
605 "Blocking waits are not supported in timer-driven (single-threaded) mode.");
606
607 Initialize();
608 }
609
610 // Bind the dispatcher to the calling (UI) thread — Tick() will be called from here.
611 EngineDispatcher.BindToCurrentThread();
612
613 _startTick = HighResTimer.GetCurrentTick();
614 _lastCPSSamplingTick = _startTick;
615 _lastCycleTick = _startTick;
616 _lastBackgroundTick = _startTick;
617 _lastForegroundTick = _startTick;
618 _timerDrivenSteps.Reset(_startTick);
619 _timerDrivenForegroundAccumulatedTicks = 0;
620 _lastTimerDrivenForegroundDriverTick = _startTick;
621 _timerDrivenForegroundTargetFps = Configuration.TargetFPS;
622 EngineSimulationClock.BeginTimerDriven(_startTick);
623
624 _isTimerDriven = true;
625 IsRunning = true;
626
627 // _cycleTask is intentionally left null; the caller drives the loop via Tick().
628 }
629
649 public void Tick()
650 {
651 if (!IsRunning)
652 return;
653
654 if (!_isTimerDriven)
655 {
656 Cycle();
657 return;
658 }
659
660 lock (RenderStateSynchronization.SyncRoot)
661 {
662 EngineDispatcher.Drain();
663
664 long driverTick = HighResTimer.GetCurrentTick();
665 var batch = _timerDrivenSteps.Advance(
666 driverTick,
667 Configuration.TimerDrivenSimulationRate,
668 Configuration.MaxTimerDrivenSimulationSteps);
669
670 bool render = IsTimerDrivenForegroundDue(driverTick);
671 double frameDelta = HighResTimer.GetDuration(_lastForegroundTick, driverTick);
672
673 if (batch.StepCount == 0)
674 {
675 if (render)
676 RenderFrame(driverTick, frameDelta);
677
678 if (Configuration.LegacyCpsSamplingTime > 0)
679 CalculateCPS(driverTick);
680
681 return;
682 }
683
684 double fixedDelta = batch.StepTicks / (double)HighResTimer.TicksPerSecond;
685
686 for (int step = 0; step < batch.StepCount && IsRunning; step++)
687 {
688 long simulationTick = batch.GetStepTick(step);
689 EngineSimulationClock.SetTimerDrivenTick(simulationTick);
690
691 bool isLastStep = step == batch.StepCount - 1;
692 RunSimulationCycle(
693 simulationTick,
694 fixedDelta,
695 render && isLastStep,
696 driverTick,
697 frameDelta,
698 sampleCps: isLastStep);
699 }
700 }
701 }
702
729 public void Stop()
730 {
731 if (!IsRunning)
732 return;
733
734 IsRunning = false;
735 _isTimerDriven = false;
736 EngineSimulationClock.UseWallClock();
737 InvokeShutdownAfterCycleStops();
738 }
739
751 public void StopAndWait()
752 {
753 var cycleTask = _cycleTask;
754 if (cycleTask is not null && !cycleTask.IsCompleted && EngineDispatcher.IsOnEngineThread)
755 throw new InvalidOperationException("StopAndWait must be called outside the background engine thread.");
756
757 Stop();
758 cycleTask?.GetAwaiter().GetResult();
759 }
760
761 private void InvokeShutdownAfterCycleStops()
762 {
763 var cycleTask = _cycleTask;
764
765 if (cycleTask is not null && !cycleTask.IsCompleted)
766 {
767 cycleTask.ContinueWith(
768 _ => EnginePluginRegistry.InvokeShutdown(this),
769 System.Threading.CancellationToken.None,
770 System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously,
771 System.Threading.Tasks.TaskScheduler.Default);
772 return;
773 }
774
775 EnginePluginRegistry.InvokeShutdown(this);
776 }
777
778 #region public properties
779
791 public IUiDispatcher? UiDispatcher { get; private set; }
792
803
812 public bool IsInitialized => _isInitialized;
813
822 public bool IsInitializing => _isInitializing;
823
832 public bool IsRunning { get; private set; }
833
842 public long TotalTicksEngineRunning => HighResTimer.GetCurrentTick() - _startTick;
843
852 public double TotalSecondsEngineRunning => TotalTicksEngineRunning / (double)HighResTimer.TicksPerSecond;
853
869 [Obsolete(
870 "Legacy CPS/FPS properties are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
871 DiagnosticId = "GOND0001")]
872 public double CyclesPerSecond => _grossCPS;
873
888 [Obsolete(
889 "Legacy CPS/FPS properties are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
890 DiagnosticId = "GOND0001")]
891 public double FramesPerSecond => _netFPS;
892
908 public EngineState State { get; } = new EngineState();
909
910 private EngineConfiguration? _config = new();
911
928 {
929 get => Volatile.Read(ref _config!);
930 private set => Volatile.Write(ref _config, value);
931 }
932
947 public EngineManagers Managers { get; } = new();
948
968 public EngineInputSystems Input { get; } = new();
969
978 public bool IsDisposed { get; private set; } = false;
979
988 public bool IsDisposing { get; private set; }
989
990 #endregion public properties
991
992 #region private methods
993
994 private void Cycle()
995 {
996 // Give a GPU paint callback that is already waiting for live render state
997 // a chance to acquire the shared gate before this unconstrained loop takes
998 // another simulation cycle.
999 RenderStateSynchronization.WaitForPendingGpuRenderers();
1000
1001 lock (RenderStateSynchronization.SyncRoot)
1002 {
1004
1005 long tick = HighResTimer.GetCurrentTick();
1006 var deltaSeconds = HighResTimer.GetDuration(_lastCycleTick, tick);
1007 _lastCycleTick = tick;
1008
1009 RunSimulationCycle(
1010 tick,
1011 deltaSeconds,
1012 IsForegroundDue(tick),
1013 tick,
1014 deltaSeconds,
1015 sampleCps: true);
1016 }
1017 }
1018
1019 private void RunSimulationCycle(
1020 long simulationTick,
1021 double simulationDelta,
1022 bool render,
1023 long renderTick,
1024 double frameDelta,
1025 bool sampleCps)
1026 {
1027 // Refresh native gamepad state before any simulation callbacks consume it.
1028 // Use the real driver/render tick rather than the fixed-step simulation clock so
1029 // timer-driven catch-up steps cannot compress or delay hardware polling cadence.
1030 Input.RefreshGamepads(renderTick, Configuration);
1031
1032 long telemetryGeneration = _engineTelemetry.BeginSample();
1033 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1034 EnginePluginRegistry.InvokePreCycle(this, simulationDelta);
1035
1036 DoBackgroundTasks(simulationTick);
1037
1038 if (render)
1039 RenderFrame(renderTick, frameDelta);
1040
1041 _grossCyclesThisMeasure++;
1042
1043 if (sampleCps && Configuration.LegacyCpsSamplingTime > 0)
1044 CalculateCPS(renderTick);
1045
1046 EnginePluginRegistry.InvokePostCycle(this, simulationDelta);
1047 if (telemetryGeneration != 0)
1048 _engineTelemetry.Record(telemetryGeneration, "cycle.cpu.ms",
1049 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1050 }
1051
1052 private bool IsForegroundDue(long tick) =>
1053 Configuration.TargetFPS <= 0
1054 || (tick - _lastForegroundTick) >= HighResTimer.TicksPerSecond / Configuration.TargetFPS;
1055
1056 private bool IsTimerDrivenForegroundDue(long tick)
1057 {
1058 int targetFps = Configuration.TargetFPS;
1059 long elapsedTicks = Math.Max(0, tick - _lastTimerDrivenForegroundDriverTick);
1060 _lastTimerDrivenForegroundDriverTick = tick;
1061
1062 if (targetFps <= 0)
1063 {
1064 _timerDrivenForegroundAccumulatedTicks = 0;
1065 _timerDrivenForegroundTargetFps = targetFps;
1066 return true;
1067 }
1068
1069 if (_timerDrivenForegroundTargetFps != targetFps)
1070 {
1071 _timerDrivenForegroundTargetFps = targetFps;
1072 _timerDrivenForegroundAccumulatedTicks = 0;
1073 }
1074
1075 _timerDrivenForegroundAccumulatedTicks += elapsedTicks;
1076
1077 long frameIntervalTicks = Math.Max(1, HighResTimer.TicksPerSecond / targetFps);
1078 if (_timerDrivenForegroundAccumulatedTicks < frameIntervalTicks)
1079 return false;
1080
1081 // Render at most once per externally driven Tick(), but preserve the fractional
1082 // remainder so small requestAnimationFrame timing jitter cannot permanently lower
1083 // the effective render rate. Whole missed intervals are discarded rather than
1084 // causing an unbounded render catch-up sequence after a long browser stall.
1085 _timerDrivenForegroundAccumulatedTicks %= frameIntervalTicks;
1086 return true;
1087 }
1088
1089 private void RenderFrame(long tick, double delta)
1090 {
1091 long telemetryGeneration = _engineTelemetry.BeginSample();
1092 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1093 EnginePluginRegistry.InvokePreFrameRender(this, delta);
1094
1095 DoForegroundTasks(tick);
1096
1097 EnginePluginRegistry.InvokePostFrameRender(this, delta);
1098
1099 _lastForegroundTick = tick;
1100 _netCyclesThisMeasure++;
1101 if (telemetryGeneration != 0)
1102 _engineTelemetry.Record(telemetryGeneration, "foreground.cpu.ms",
1103 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1104 }
1105
1106 private void DoBackgroundTasks(long tick)
1107 {
1108 long telemetryGeneration = _engineTelemetry.BeginSample();
1109 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1110 // find total real seconds passed since last background loop
1111 var deltaSeconds = HighResTimer.GetDuration(_lastBackgroundTick, tick);
1112
1114
1115 // raise pre-cycle timer events
1116 Timer.RaiseTimerEvents(TimerType.PreCycle, tick);
1117
1118 // check for keyboard events
1119 KeyboardEventPoller.Instance?.PollForEvents(tick);
1120
1121 // check for mouse events
1122 MouseEventPoller.Instance?.PollForEvents(tick);
1123
1124 // check for touch events
1125 TouchEventPoller.Instance?.PollForEvents(tick);
1126
1127 // check for gamepad events
1128 GamepadEventPoller.Instance?.PollForEvents(tick);
1129
1130 // cycle Animator frames
1131 for (int i = 0; i < Tile.TilesAnimating.Count; i++)
1132 Tile.TilesAnimating[i].TileAnimator.CycleAnimation(tick);
1133
1134 // advance Sprite Movement paths
1135 SpriteManager.Instance.MoveSprites(tick);
1136
1137 // resolve collisions after movement
1138 foreach (var scene in Scenes.Scene.GetAllScenes())
1139 {
1140 foreach (var layer in scene.SceneLayers)
1141 layer.CollisionResolver.Resolve();
1142 }
1143
1144 // update cameras so any movement can mark RefreshNeeded = All.
1145 foreach (var surface in RenderSurfaceHostRegistry.All)
1146 surface.ViewManager.UpdateCameras(deltaSeconds);
1147
1149
1150 _lastBackgroundTick = tick;
1151 if (telemetryGeneration != 0)
1152 _engineTelemetry.Record(telemetryGeneration, "background.cpu.ms",
1153 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1154 }
1155
1156 private void DoForegroundTasks(long tick)
1157 {
1158 // raise event
1159 BeforeFrameRender?.Invoke();
1160
1161 // Effects are presentation state, so advance them on the foreground/render cadence.
1162 foreach (var surface in RenderSurfaceHostRegistry.All)
1163 surface.Effects.Update(tick);
1164
1165 // update the DirectDrawing instances' states
1166 DirectDrawingManager.Instance.UpdateAll(tick);
1167
1168 // Resolve desktop GPU visual state at foreground cadence before requesting repaint.
1169 foreach (var surface in RenderSurfaceHostRegistry.All)
1170 surface.ProduceRenderFrameSnapshot(tick);
1171
1172 // Bitmap rendering remains on the engine thread.
1173 foreach (var surface in RenderSurfaceHostRegistry.All)
1174 if (!surface.Backbuffer.IsGlThreadRendered)
1175 surface.RenderToBackbuffer(tick);
1176
1177 // render each Backbuffer to RenderSurfaceHost adapter (skip GL-thread surfaces)
1178 foreach (var surface in RenderSurfaceHostRegistry.All)
1179 if (!surface.Backbuffer.IsGlThreadRendered)
1180 surface.PresentBackbufferToAdapter();
1181
1182 // raise event
1183 // GL thread rendering is done as part of this invocation
1184 AfterFrameRender?.Invoke();
1185
1186 // raise post-cycle timer events
1187 Timer.RaiseTimerEvents(TimerType.PostCycle, tick);
1188 }
1189
1190 private void CalculateCPS(long tick)
1191 {
1192 // Has the sampling interval elapsed?
1193 long elapsedTicks = tick - _lastCPSSamplingTick;
1194 if (elapsedTicks < Configuration.LegacyCpsSamplingTicks)
1195 return;
1196
1197 // SNAPSHOT the counters BEFORE resetting or posting
1198 long grossCycles = _grossCyclesThisMeasure;
1199 long netCycles = _netCyclesThisMeasure;
1200
1201 // Compute using the snapshot
1202 double elapsedSec = elapsedTicks / (double)HighResTimer.TicksPerSecond;
1203 double grossCps = grossCycles * HighResTimer.TicksPerSecond / (double)elapsedTicks;
1204 double netCps = netCycles * HighResTimer.TicksPerSecond / (double)elapsedTicks;
1205
1206 // Collect actual GPU FPS from all registered GPU-rendered backbuffers.
1207 long totalGpuFrames = 0;
1208 int gpuSurfaceCount = 0;
1209 foreach (var surface in RenderSurfaceHostRegistry.All.ToArray())
1210 {
1211 if (surface.Backbuffer is GpuBackbuffer gpuBb)
1212 {
1213 totalGpuFrames += gpuBb.ConsumeFrameCount();
1214 gpuSurfaceCount++;
1215 }
1216 }
1217 double? gpuFps = gpuSurfaceCount > 0
1218 ? totalGpuFrames * HighResTimer.TicksPerSecond / (double)elapsedTicks
1219 : null;
1220
1221 // Build immutable args NOW (so lambda doesn't read changing fields later)
1222 var args = new CyclesPerSecondCalculatedEventArgs(
1223 grossCycles,
1224 netCycles,
1225 grossCps,
1226 netCps,
1227 elapsedSec,
1228 gpuFps
1229 );
1230
1231 // Post the snapshot
1232 UiDispatcher!.Post(() => CPSCalculated?.Invoke(args));
1233
1234 _grossCPS = grossCps;
1235 _netFPS = netCps;
1236
1237 // Reset for next window
1238 _lastCPSSamplingTick = tick;
1239 _grossCyclesThisMeasure = 0;
1240 _netCyclesThisMeasure = 0;
1241 }
1242
1243 private static void ConfigureLogging(EngineConfiguration configuration)
1244 {
1245 // Logging may already be active because hosts can configure it before
1246 // Engine.Initialize. Recreate the async pipeline so its channel uses
1247 // the capacity loaded from this configuration.
1248 EngineLogger.StopAsyncLogging(flush: true);
1249
1250 if (configuration.LoggingMode == EngineLoggingMode.Asynchronous)
1251 {
1252 EngineLogger.SwitchToAsync(configuration.LoggingQueueCapacity);
1253 return;
1254 }
1255
1256 EngineLogger.Mode = EngineLoggingMode.Synchronous;
1257 }
1258
1259 #endregion private methods
1260
1261 #region IDisposable support
1262
1263 private void Dispose(bool disposing)
1264 {
1265 if (!IsDisposed)
1266 {
1267 if (disposing)
1268 {
1269 if (IsDisposing)
1270 return;
1271
1272 IsDisposing = true;
1273
1274 // stop the loop first so handlers don't race the cycle thread
1275 try
1276 {
1277 Stop();
1278 }
1279 catch (Exception ex)
1280 {
1281 Logger.LogError(ex, "Unhandled exception calling Stop()");
1282 }
1283
1284 // wait for the background loop to actually exit unless we're already on it
1285 try
1286 {
1287 var cycleTask = _cycleTask;
1288 if (cycleTask is not null && EngineDispatcher.IsOnEngineThread && !cycleTask.IsCompleted)
1289 {
1290 if (!_deferredDisposeScheduled)
1291 {
1292 _deferredDisposeScheduled = true;
1293 cycleTask.ContinueWith(
1294 _ => CompleteManagedDisposal(),
1295 System.Threading.CancellationToken.None,
1296 System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously,
1297 System.Threading.Tasks.TaskScheduler.Default);
1298 }
1299
1300 return;
1301 }
1302
1303 if (cycleTask is not null && !EngineDispatcher.IsOnEngineThread)
1304 cycleTask.Wait();
1305 }
1306 catch (Exception ex)
1307 {
1308 Logger.LogError(ex, "Error waiting for engine loop to exit.");
1309 }
1310
1311 CompleteManagedDisposal();
1312 }
1313 else
1314 {
1315 // unmanaged cleanup...
1316 IsDisposed = true;
1317 }
1318 }
1319 }
1320
1321 private void CompleteManagedDisposal()
1322 {
1323 if (IsDisposed)
1324 return;
1325
1326 // raise Disposing on UI thread if possible; otherwise inline
1327 if (UiDispatcher is not null)
1328 UiDispatcher.Post(() => SafeInvoke(Disposing));
1329 else
1330 SafeInvoke(Disposing);
1331
1332 Profiler.Dispose();
1333
1334 // managed cleanup...
1335 Input.KeyboardEventPoller?.StopMonitoringAllKeys();
1336 MouseEventPoller.Reset();
1337 TouchEventPoller.Reset();
1338
1339 if (Input.GamepadManager is not null)
1340 foreach (var gamepadAdapter in Input.GamepadManager.ConnectedAdapters)
1341 Input.GamepadEventPoller?.StopMonitoringAllButtons(gamepadAdapter.GamepadId);
1342
1343 try
1344 {
1345 _configurationStore?.Dispose();
1346 }
1347 catch (Exception ex)
1348 {
1349 Logger.LogError(ex, "Error saving the engine configuration during disposal.");
1350 }
1351 finally
1352 {
1353 _configurationStore = null;
1354 }
1355
1356 if (EngineLogger.Mode == EngineLoggingMode.Asynchronous)
1357 EngineLogger.StopAsyncLogging(flush: Configuration.FlushAsyncLogsOnShutdown);
1358
1359 Timer.ClearAll();
1360 State.Clear();
1361
1362 // unmanaged cleanup...
1363 IsDisposed = true;
1364
1365 // now signal we're fully torn down
1366 if (UiDispatcher is not null)
1367 UiDispatcher.Post(() => SafeInvoke(Disposed));
1368 else
1369 SafeInvoke(Disposed);
1370 }
1371
1372 private static void SafeInvoke(Action? evnt)
1373 {
1374 try { evnt?.Invoke(); }
1375 catch (Exception ex)
1376 {
1377 // Keep disposal robust; log and continue
1378 Logger.LogError(ex, "Unhandled exception in disposal event handler.");
1379 }
1380 }
1381
1404 public void Dispose()
1405 {
1406 Dispose(disposing: true);
1407 GC.SuppressFinalize(this);
1408 }
1409
1410 ~Engine()
1411 {
1412 // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
1413 Dispose(disposing: false);
1414 }
1415
1416 #endregion IDisposable support
1417}
Settings used by the engine when cycling.
int LoggingQueueCapacity
Maximum number of queued log events when LoggingMode is EngineLoggingMode.Asynchronous....
EngineLoggingMode LoggingMode
Controls whether engine logging operations are performed synchronously or asynchronously.
Opt-in, bounded runtime measurements independent of legacy CPS notifications.
Bounded extension source. Define measurements once, capture a generation before work,...
Provides thread-safe dispatching of actions to the engine's background thread. This dispatcher allows...
void Drain()
Executes all queued actions that have been posted to this dispatcher since the last call to Drain....
Provides centralized access to all the input systems of the engine, including gamepad,...
Provides centralized access to all engine resource managers.
Represents the complete serializable state of the game engine, including assets, scenes,...
Represents the core game engine singleton responsible for managing the main update loop,...
Definition Engine.cs:36
void Tick()
Advances the timer-driven engine by one externally presented frame.
Definition Engine.cs:649
Action? Disposed
Raised after the engine has completed explicit disposal.
Definition Engine.cs:220
Action? BeforeFrameRender
Occurs immediately before DoForegroundTasks(long) executes within each engine cycle.
Definition Engine.cs:171
void SaveConfiguration()
Persists the current engine configuration through the store supplied during initialization.
Definition Engine.cs:431
void Initialize(string? configFileName=null, bool? autoSaveConfig=null, IKeyboardAdapter? keyboardAdapter=null, IMouseAdapter? mouseAdapter=null, ITouchAdapter? touchAdapter=null, IGamepadManager< IGamepadAdapter >? gamepadManager=null)
Performs one-time or on-demand initialization of the Engine instance, loading configuration,...
double FramesPerSecond
Gets the current net frames per second rate.
Definition Engine.cs:891
static Engine Instance
Gets the singleton instance of the Engine.
Definition Engine.cs:49
bool IsInitialized
Gets a value indicating whether the engine has completed its initialization sequence.
Definition Engine.cs:812
void Start(SynchronizationContext uiContext)
Starts the Engine main loop using the provided SynchronizationContext, initializing the engine if it ...
Definition Engine.cs:510
long TotalTicksEngineRunning
Gets the total number of high-resolution timer ticks that have elapsed since the engine started.
Definition Engine.cs:842
void InitializeWithConfigurationStore(IEngineConfigurationStore configurationStore, bool? autoSaveConfig=null, IKeyboardAdapter? keyboardAdapter=null, IMouseAdapter? mouseAdapter=null, ITouchAdapter? touchAdapter=null, IGamepadManager< IGamepadAdapter >? gamepadManager=null)
Initializes the engine using a caller-supplied configuration persistence store.
Definition Engine.cs:324
EngineState State
Gets the engine's persistent state container for storing arbitrary key-value data.
Definition Engine.cs:908
Action? AfterFrameRender
Occurs immediately after DoForegroundTasks(long) completes within each engine cycle.
Definition Engine.cs:182
Action? PostInitialization
Occurs after all internal initialization routines have completed, but before InitializationComplete i...
Definition Engine.cs:123
void StartTimerDriven(SynchronizationContext uiContext)
Starts the Engine in timer-driven mode using the provided SynchronizationContext, without spawning a ...
Definition Engine.cs:593
Diagnostics.RuntimeProfiler Profiler
Opt-in runtime CPU measurements and bounded history, independent of CPS notifications.
Definition Engine.cs:228
Action< CyclesPerSecondCalculatedEventArgs >? CPSCalculated
Occurs whenever cycles-per-second (CPS) and frames-per-second (FPS) metrics are calculated.
Definition Engine.cs:200
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Definition Engine.cs:59
void StopAndWait()
Stops the engine and waits for its background cycle to finish before returning. Resources used by ren...
Definition Engine.cs:751
IEngineDispatcher EngineDispatcher
Gets the engine dispatcher used for marshalling operations to the engine's background thread.
Definition Engine.cs:802
void Dispose()
Releases all resources used by the Engine and stops all subsystems.
Definition Engine.cs:1404
Action? InitializationComplete
Occurs after all initialization steps and post-initialization logic have completed.
Definition Engine.cs:138
bool IsRunning
Gets a value indicating whether the engine's main loop is currently executing.
Definition Engine.cs:832
EngineInputSystems Input
Gets the collection of input subsystems for keyboard, mouse, touch, and gamepad input.
Definition Engine.cs:968
Action? AfterBackgroundTasksExecute
Occurs immediately after DoBackgroundTasks(long) has completed.
Definition Engine.cs:160
double CyclesPerSecond
Gets the current gross cycles per second rate.
Definition Engine.cs:872
EngineManagers Managers
Gets the collection of resource managers for audio, SVG, sprites, fonts, tilesheets,...
Definition Engine.cs:947
bool IsDisposed
Gets a value indicating whether the engine has been disposed.
Definition Engine.cs:978
Action? PreInitialization
Occurs immediately before the engine begins its internal initialization sequence.
Definition Engine.cs:103
IUiDispatcher? UiDispatcher
Gets the UI dispatcher used for marshalling operations to the UI thread.
Definition Engine.cs:791
Action? Disposing
Raised when Dispose() begins the explicit disposal sequence.
Definition Engine.cs:210
Action? BeforeBackgroundTasksExecute
Occurs immediately before DoBackgroundTasks(long) executes within each engine cycle.
Definition Engine.cs:149
void Stop()
Stops the Engine main loop and halts all ongoing processing.
Definition Engine.cs:729
bool IsInitializing
Gets a value indicating whether the engine is currently in the process of initializing.
Definition Engine.cs:822
EngineConfiguration Configuration
Gets the current engine configuration settings.
Definition Engine.cs:928
bool IsDisposing
Gets a value indicating whether the engine is currently executing its disposal sequence.
Definition Engine.cs:988
void Start()
Starts the Engine using the current thread's SynchronizationContext.
Definition Engine.cs:460
double TotalSecondsEngineRunning
Gets the total number of seconds that have elapsed since the engine started.
Definition Engine.cs:852
Represents a high-resolution timer that can trigger events at specified intervals and supports variou...
Definition Timer.cs:16
static void ClearAll()
Removes and disposes all active timers, clearing the entire timer collection.
Definition Timer.cs:210
Provides a concrete implementation of IUiDispatcher for dispatching actions to the UI thread using a ...
void Post(Action action)
Asynchronously posts an action to be executed on the UI thread. This method queues the action and ret...
Provides a persistence boundary for EngineConfiguration.
Defines a contract for dispatching actions to the engine's background thread in a thread-safe manner....
Defines a contract for dispatching actions to the UI thread in a thread-safe manner....
Represents an adapter for keyboard input, providing access to the currently pressed keys and active m...
Defines an adapter interface for accessing mouse device state, including cursor position,...
Defines a passive adapter interface for accessing touch device state. Implementations update their in...
EngineLoggingMode
Specifies the logging mode for the engine.
TimerType
Specifies when a Timer should have its Timer.Tick event raised relative to the engine's main update a...
Definition TimerType.cs:22
@ Scenes
Represents the scenes state, including scene definitions, layer configurations, entity placements,...