35public sealed class Engine : IDisposable
37 #region static members
39 private static readonly Lazy<Engine> _instance =
new(() =>
new Engine());
49 public static Engine
Instance => _instance.Value;
59 public static ILogger<Engine>
Logger => EngineLogger.GetLogger<Engine>();
63 #region private fields
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;
76 private long _grossCyclesThisMeasure = 0;
77 private long _netCyclesThisMeasure = 0;
78 private double _grossCPS = 0;
79 private double _netFPS = 0;
81 private Task? _cycleTask;
83 private bool _deferredDisposeScheduled;
85 #endregion private fields
198 "Legacy CPS/FPS notifications are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
199 DiagnosticId =
"GOND0001")]
232 _engineTelemetry =
Profiler.RegisterSource(
"Engine",
"Core")!;
233 _engineTelemetry.Define(
"cycle.cpu.ms");
234 _engineTelemetry.Define(
"background.cpu.ms");
235 _engineTelemetry.Define(
"foreground.cpu.ms");
238 private volatile bool _isInitialized =
false;
239 private volatile bool _isInitializing =
false;
240 private readonly ManualResetEventSlim _initDone =
new(
false);
298 string? configFileName =
null,
299 bool? autoSaveConfig =
null,
303 IGamepadManager<IGamepadAdapter>? gamepadManager =
null)
311 configurationStore:
null);
326 bool? autoSaveConfig =
null,
330 IGamepadManager<IGamepadAdapter>? gamepadManager =
null)
332 ArgumentNullException.ThrowIfNull(configurationStore);
335 configFileName:
null,
344 private void InitializeCore(
345 string? configFileName,
346 bool? autoSaveConfig,
350 IGamepadManager<IGamepadAdapter>? gamepadManager,
353 if (_isInitialized || _isInitializing)
359 _isInitializing =
true;
369 _configurationStore?.Dispose();
373 Logger.LogError(ex,
"Error saving the engine configuration during initialization.");
376 _configurationStore = configurationStore ?? EngineConfigurationFile.Load(configFileName, autoSaveConfig);
377 if (configurationStore is not
null && autoSaveConfig.HasValue)
378 _configurationStore.AutoSave = autoSaveConfig.Value;
388 EngineState.MergeFromFile(stateFile.File, stateFile.IsCompressed, stateFile.OverwriteExisting, stateFile.EngineStateParts);
392 if (keyboardAdapter !=
null)
393 KeyboardEventPoller.Initialize(keyboardAdapter);
395 if (mouseAdapter !=
null)
396 MouseEventPoller.Initialize(mouseAdapter);
398 if (touchAdapter !=
null)
399 Input.TouchAdapter = touchAdapter;
401 if (gamepadManager is not
null)
402 Input.GamepadManager = gamepadManager;
409 EnginePluginRegistry.InvokeInitialize(
this);
411 _isInitialized =
true;
420 _isInitializing =
false;
433 var store = _configurationStore
434 ??
throw new InvalidOperationException(
"The engine has no initialized configuration store.");
462 if (SynchronizationContext.Current ==
null)
463 throw new InvalidOperationException(
"SynchronizationContext cannot be null.");
465 Start(SynchronizationContext.Current);
510 public void Start(SynchronizationContext uiContext)
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.");
528 throw new InvalidOperationException(
529 "Engine initialization failed on another thread. Call Initialize() again and resolve the initialization error.");
537 throw new InvalidOperationException(
"Engine failed to initialize.");
540 _isTimerDriven =
false;
541 EngineSimulationClock.UseWallClock();
544 _startTick = HighResTimer.GetCurrentTick();
545 _lastCPSSamplingTick = _startTick;
546 _lastCycleTick = _startTick;
548 _cycleTask = Task.Run(() =>
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.");
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;
622 EngineSimulationClock.BeginTimerDriven(_startTick);
624 _isTimerDriven =
true;
660 lock (RenderStateSynchronization.SyncRoot)
664 long driverTick = HighResTimer.GetCurrentTick();
665 var batch = _timerDrivenSteps.Advance(
670 bool render = IsTimerDrivenForegroundDue(driverTick);
671 double frameDelta = HighResTimer.GetDuration(_lastForegroundTick, driverTick);
673 if (batch.StepCount == 0)
676 RenderFrame(driverTick, frameDelta);
679 CalculateCPS(driverTick);
684 double fixedDelta = batch.StepTicks / (double)HighResTimer.TicksPerSecond;
686 for (
int step = 0; step < batch.StepCount &&
IsRunning; step++)
688 long simulationTick = batch.GetStepTick(step);
689 EngineSimulationClock.SetTimerDrivenTick(simulationTick);
691 bool isLastStep = step == batch.StepCount - 1;
695 render && isLastStep,
698 sampleCps: isLastStep);
735 _isTimerDriven =
false;
736 EngineSimulationClock.UseWallClock();
737 InvokeShutdownAfterCycleStops();
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.");
758 cycleTask?.GetAwaiter().GetResult();
761 private void InvokeShutdownAfterCycleStops()
763 var cycleTask = _cycleTask;
765 if (cycleTask is not
null && !cycleTask.IsCompleted)
767 cycleTask.ContinueWith(
768 _ => EnginePluginRegistry.InvokeShutdown(
this),
769 System.Threading.CancellationToken.None,
770 System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously,
771 System.Threading.Tasks.TaskScheduler.Default);
775 EnginePluginRegistry.InvokeShutdown(
this);
778 #region public properties
870 "Legacy CPS/FPS properties are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
871 DiagnosticId =
"GOND0001")]
889 "Legacy CPS/FPS properties are deprecated. Use Engine.Profiler for runtime telemetry; ProfilerWidget provides an optional in-game display.",
890 DiagnosticId =
"GOND0001")]
929 get => Volatile.Read(ref _config!);
930 private set => Volatile.Write(ref _config, value);
990 #endregion public properties
992 #region private methods
999 RenderStateSynchronization.WaitForPendingGpuRenderers();
1001 lock (RenderStateSynchronization.SyncRoot)
1005 long tick = HighResTimer.GetCurrentTick();
1006 var deltaSeconds = HighResTimer.GetDuration(_lastCycleTick, tick);
1007 _lastCycleTick = tick;
1012 IsForegroundDue(tick),
1019 private void RunSimulationCycle(
1020 long simulationTick,
1021 double simulationDelta,
1032 long telemetryGeneration = _engineTelemetry.BeginSample();
1033 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1034 EnginePluginRegistry.InvokePreCycle(
this, simulationDelta);
1036 DoBackgroundTasks(simulationTick);
1039 RenderFrame(renderTick, frameDelta);
1041 _grossCyclesThisMeasure++;
1044 CalculateCPS(renderTick);
1046 EnginePluginRegistry.InvokePostCycle(
this, simulationDelta);
1047 if (telemetryGeneration != 0)
1048 _engineTelemetry.Record(telemetryGeneration,
"cycle.cpu.ms",
1049 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1052 private bool IsForegroundDue(
long tick) =>
1053 Configuration.TargetFPS <= 0
1054 || (tick - _lastForegroundTick) >= HighResTimer.TicksPerSecond /
Configuration.TargetFPS;
1056 private bool IsTimerDrivenForegroundDue(
long tick)
1059 long elapsedTicks = Math.Max(0, tick - _lastTimerDrivenForegroundDriverTick);
1060 _lastTimerDrivenForegroundDriverTick = tick;
1064 _timerDrivenForegroundAccumulatedTicks = 0;
1065 _timerDrivenForegroundTargetFps = targetFps;
1069 if (_timerDrivenForegroundTargetFps != targetFps)
1071 _timerDrivenForegroundTargetFps = targetFps;
1072 _timerDrivenForegroundAccumulatedTicks = 0;
1075 _timerDrivenForegroundAccumulatedTicks += elapsedTicks;
1077 long frameIntervalTicks = Math.Max(1, HighResTimer.TicksPerSecond / targetFps);
1078 if (_timerDrivenForegroundAccumulatedTicks < frameIntervalTicks)
1085 _timerDrivenForegroundAccumulatedTicks %= frameIntervalTicks;
1089 private void RenderFrame(
long tick,
double delta)
1091 long telemetryGeneration = _engineTelemetry.BeginSample();
1092 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1093 EnginePluginRegistry.InvokePreFrameRender(
this, delta);
1095 DoForegroundTasks(tick);
1097 EnginePluginRegistry.InvokePostFrameRender(
this, delta);
1099 _lastForegroundTick = tick;
1100 _netCyclesThisMeasure++;
1101 if (telemetryGeneration != 0)
1102 _engineTelemetry.Record(telemetryGeneration,
"foreground.cpu.ms",
1103 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1106 private void DoBackgroundTasks(
long tick)
1108 long telemetryGeneration = _engineTelemetry.BeginSample();
1109 long telemetryStarted = telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
1111 var deltaSeconds = HighResTimer.GetDuration(_lastBackgroundTick, tick);
1116 Timer.RaiseTimerEvents(
TimerType.PreCycle, tick);
1119 KeyboardEventPoller.Instance?.PollForEvents(tick);
1122 MouseEventPoller.Instance?.PollForEvents(tick);
1125 TouchEventPoller.Instance?.PollForEvents(tick);
1128 GamepadEventPoller.Instance?.PollForEvents(tick);
1131 for (
int i = 0; i < Tile.TilesAnimating.Count; i++)
1132 Tile.TilesAnimating[i].TileAnimator.CycleAnimation(tick);
1135 SpriteManager.Instance.MoveSprites(tick);
1138 foreach (var scene
in Scenes.Scene.GetAllScenes())
1140 foreach (var layer
in scene.SceneLayers)
1141 layer.CollisionResolver.Resolve();
1145 foreach (var surface
in RenderSurfaceHostRegistry.All)
1146 surface.ViewManager.UpdateCameras(deltaSeconds);
1150 _lastBackgroundTick = tick;
1151 if (telemetryGeneration != 0)
1152 _engineTelemetry.Record(telemetryGeneration,
"background.cpu.ms",
1153 HighResTimer.GetDuration(telemetryStarted, HighResTimer.GetCurrentTick()) * 1000d);
1156 private void DoForegroundTasks(
long tick)
1162 foreach (var surface
in RenderSurfaceHostRegistry.All)
1163 surface.Effects.Update(tick);
1166 DirectDrawingManager.Instance.UpdateAll(tick);
1169 foreach (var surface
in RenderSurfaceHostRegistry.All)
1170 surface.ProduceRenderFrameSnapshot(tick);
1173 foreach (var surface
in RenderSurfaceHostRegistry.All)
1174 if (!surface.Backbuffer.IsGlThreadRendered)
1175 surface.RenderToBackbuffer(tick);
1178 foreach (var surface
in RenderSurfaceHostRegistry.All)
1179 if (!surface.Backbuffer.IsGlThreadRendered)
1180 surface.PresentBackbufferToAdapter();
1187 Timer.RaiseTimerEvents(
TimerType.PostCycle, tick);
1190 private void CalculateCPS(
long tick)
1193 long elapsedTicks = tick - _lastCPSSamplingTick;
1198 long grossCycles = _grossCyclesThisMeasure;
1199 long netCycles = _netCyclesThisMeasure;
1202 double elapsedSec = elapsedTicks / (double)HighResTimer.TicksPerSecond;
1203 double grossCps = grossCycles * HighResTimer.TicksPerSecond / (double)elapsedTicks;
1204 double netCps = netCycles * HighResTimer.TicksPerSecond / (double)elapsedTicks;
1207 long totalGpuFrames = 0;
1208 int gpuSurfaceCount = 0;
1209 foreach (var surface
in RenderSurfaceHostRegistry.All.ToArray())
1211 if (surface.Backbuffer is GpuBackbuffer gpuBb)
1213 totalGpuFrames += gpuBb.ConsumeFrameCount();
1217 double? gpuFps = gpuSurfaceCount > 0
1218 ? totalGpuFrames * HighResTimer.TicksPerSecond / (double)elapsedTicks
1222 var args =
new CyclesPerSecondCalculatedEventArgs(
1234 _grossCPS = grossCps;
1238 _lastCPSSamplingTick = tick;
1239 _grossCyclesThisMeasure = 0;
1240 _netCyclesThisMeasure = 0;
1243 private static void ConfigureLogging(EngineConfiguration configuration)
1248 EngineLogger.StopAsyncLogging(flush:
true);
1259 #endregion private methods
1261 #region IDisposable support
1263 private void Dispose(
bool disposing)
1279 catch (Exception ex)
1281 Logger.LogError(ex,
"Unhandled exception calling Stop()");
1287 var cycleTask = _cycleTask;
1288 if (cycleTask is not
null &&
EngineDispatcher.IsOnEngineThread && !cycleTask.IsCompleted)
1290 if (!_deferredDisposeScheduled)
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);
1306 catch (Exception ex)
1308 Logger.LogError(ex,
"Error waiting for engine loop to exit.");
1311 CompleteManagedDisposal();
1321 private void CompleteManagedDisposal()
1335 Input.KeyboardEventPoller?.StopMonitoringAllKeys();
1336 MouseEventPoller.Reset();
1337 TouchEventPoller.Reset();
1339 if (
Input.GamepadManager is not
null)
1340 foreach (var gamepadAdapter
in Input.GamepadManager.ConnectedAdapters)
1341 Input.GamepadEventPoller?.StopMonitoringAllButtons(gamepadAdapter.GamepadId);
1345 _configurationStore?.Dispose();
1347 catch (Exception ex)
1349 Logger.LogError(ex,
"Error saving the engine configuration during disposal.");
1353 _configurationStore =
null;
1357 EngineLogger.StopAsyncLogging(flush:
Configuration.FlushAsyncLogsOnShutdown);
1372 private static void SafeInvoke(Action? evnt)
1374 try { evnt?.Invoke(); }
1375 catch (Exception ex)
1378 Logger.LogError(ex,
"Unhandled exception in disposal event handler.");
1407 GC.SuppressFinalize(
this);
1416 #endregion IDisposable support