< Summary

Class:Imagini.AppBase
Assembly:Imagini.Core
File(s):/home/razer/vscode-projects/project-grove/imagini/Imagini.Core/App.cs
Covered lines:141
Uncovered lines:14
Coverable lines:155
Total lines:484
Line coverage:90.9% (141 of 155)
Covered branches:85
Total branches:97
Branch coverage:87.6% (85 of 97)

Metrics

MethodCyclomatic complexity NPath complexity Sequence coverage Branch coverage
.ctor(...)10100%100%
.ctor(...)20100%50%
.ctor()100%100%
SetupEvents()10100%100%
Initialize()10100%100%
ProcessEvents()10100%100%
CheckIfInitialized()20100%100%
OnWindowStateChange(...)21088.88%90.47%
RequestExit()10100%100%
CancelExitRequest()10100%100%
Terminate()10100%100%
SuppressDraw()10100%100%
ResetElapsedTime()10100%100%
Run()200%0%
Tick()40093.75%92.5%
DoUpdate(...)10100%100%
DoDraw(...)10100%100%
Dispose()40100%100%

File(s)

/home/razer/vscode-projects/project-grove/imagini/Imagini.Core/App.cs

#LineLine coverage
 1using System;
 2
 3using static SDL2.SDL_events;
 4using static SDL2.SDL_mouse;
 5using static SDL2.SDL_timer;
 6using static SDL2.SDL_render;
 7using static Imagini.ErrorHandler;
 8using System.Diagnostics;
 9using System.Threading;
 10using System.Runtime.CompilerServices;
 11using System.Diagnostics.CodeAnalysis;
 12using System.Drawing;
 13using System.Runtime.InteropServices;
 14using static System.Runtime.InteropServices.RuntimeInformation;
 15using Imagini.Core.Internal;
 16
 17/// <summary>
 18/// The core namespace.
 19/// </summary>
 20namespace Imagini
 21{
 22  /// <summary>
 23  /// Base app class which instantiates a window and event loop.
 24  /// Derive from this if you want to provide your own renderer for the
 25  /// game loop.
 26  /// </summary>
 27  public abstract class AppBase : IDisposable
 28  {
 29    /// <summary>
 30    /// Returns the app's window.
 31    /// </summary>
 39332    public Window Window { get; protected set; }
 33    /// <summary>
 34    /// Provides access to the events sent to app's window.
 35    /// </summary>
 35336    public Events Events { get; private set; }
 37    private EventManager.EventQueue _eventQueue;
 38
 39    /// <summary>
 40    /// Returns total number of milliseconds since when the library was initialized.
 41    /// </summary>
 42    public static long TotalTime
 43    {
 2744      get => SDL_GetTicks();
 45    }
 46
 47    /// <summary>
 48    /// Gets the total app running time.
 49    /// </summary>
 20450    public TimeSpan ElapsedAppTime { get; private set; }
 51
 52    /// <summary>
 53    /// Gets or sets if the system mouse cursor should be visible.
 54    /// </summary>
 55    public bool IsMouseVisible
 56    {
 457      get => TryGet(() => SDL_ShowCursor(SDL_QUERY), "SDL_ShowCursor") > 0;
 458      set => TryGet(() => SDL_ShowCursor(value ? 1 : 0), "SDL_ShowCursor");
 59    }
 60
 61    /// <summary>
 62    /// Gets or sets whether the mouse should be captured by the window.
 63    /// If enabled, mouse events will report only relative movement and
 64    /// the cursor should stay fixed (useful for 3D cameras).
 65    /// </summary>
 66    /// <seealso cref="MousePosition" />
 67    public bool CaptureMouse
 68    {
 069      get => TryGet(() => SDL_GetRelativeMouseMode(), "SDL_GetRelativeMouseMode") > 0;
 070      set => Try(() => SDL_SetRelativeMouseMode(value ? 1 : 0), "SDL_SetRelativeMouseMode");
 71    }
 72
 73    /// <summary>
 74    /// Returns the last recorded mouse position.
 75    /// Can be used with <see cref="CaptureMouse" /> to query relative mouse movement.
 76    /// </summary>
 77    /// <seealso cref="CaptureMouse" />
 78    public Point MousePosition
 79    {
 80      get
 81      {
 082        int x = 0, y = 0;
 083        SDL_GetRelativeMouseState(ref x, ref y);
 084        return new Point(x, y);
 85      }
 86    }
 87
 88    /// <summary>
 89    /// Indicates if this app is visible and have input focus.
 90    /// </summary>
 91    public bool IsActive
 92    {
 193      get => Window.Current == Window;
 94    }
 95
 96    /// <summary>
 97    /// Gets or sets the target time between each frame if
 98    /// <see cref="IsFixedTimeStep" /> is set to true.
 99    /// </summary>
 100    /// <remarks>Default is ~16 ms (60 FPS).</remarks>
 101    public TimeSpan TargetElapsedTime
 102    {
 897617103      get => _targetElapsedTime;
 104      set
 105      {
 4106        if (value > MaxElapsedTime)
 1107          throw new ArgumentOutOfRangeException("Time must be less or equal to MaxElapsedTime");
 3108        if (value <= TimeSpan.Zero)
 1109          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 2110        if (value > InactiveSleepTime)
 1111          throw new ArgumentOutOfRangeException("Time must be less or equal to InactiveSleepTime");
 1112        _targetElapsedTime = value;
 1113      }
 114    }
 64115    private TimeSpan _targetElapsedTime = TimeSpan.FromMilliseconds(16.6667);
 64116    private Stopwatch _appStopwatch = new Stopwatch();
 117
 118    /// <summary>
 119    /// Gets or sets the target time between each frame if the window is
 120    /// inactive and <see cref="IsFixedTimeStep" /> is set to true.
 121    /// </summary>
 122    /// <remarks>Default is ~33 ms (30 FPS).</remarks>
 123    public TimeSpan InactiveSleepTime
 124    {
 7125      get => _inactiveSleepTime;
 126      set
 127      {
 4128        if (value > MaxElapsedTime)
 1129          throw new ArgumentOutOfRangeException("Time should be less or equal to MaxElapsedTime");
 3130        if (value <= TimeSpan.Zero)
 1131          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 2132        if (value < TargetElapsedTime)
 1133          throw new ArgumentOutOfRangeException("Time should be greater or equal to TargetElapsedTime");
 1134        _inactiveSleepTime = value;
 1135      }
 136    }
 64137    private TimeSpan _inactiveSleepTime = TimeSpan.FromMilliseconds(33.3334);
 138
 139    /// <summary>
 140    /// Gets or sets the maximum amout of time the app will frameskip.
 141    /// </summary>
 142    /// <remarks>Defaults to 500 milliseconds.</remarks>
 143    public TimeSpan MaxElapsedTime
 144    {
 11145      get => _maxElapsedTime;
 146      set
 147      {
 4148        if (value <= TimeSpan.Zero)
 1149          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 3150        if (value < TargetElapsedTime)
 1151          throw new ArgumentOutOfRangeException("Time must be greater or equal to TargetElapsedTime");
 2152        if (value < InactiveSleepTime)
 1153          throw new ArgumentOutOfRangeException("Time must be greater or equal to InactiveSleepTime");
 1154      }
 155    }
 64156    private TimeSpan _maxElapsedTime = TimeSpan.FromMilliseconds(500);
 157    /// <summary>
 158    /// Gets or sets if the time between each frame should be fixed.
 159    /// </summary>
 448792160    public bool IsFixedTimeStep { get; set; } = true;
 161    /// <summary>
 162    /// Indicates if the last app frame took longer than <see cref="TargetElapsedTime" />.
 163    /// </summary>
 72164    public bool IsRunningSlowly { get; private set; }
 165    private int _slowFrameCount = 0;
 166    private const int c_MaxSlowFrameCount = 5;
 167    private long _previousTicks = 0;
 168    private long _accumulatedTicks = 0;
 169
 170
 171    /* -------------------------- Initialization ------------------------ */
 172    /// <summary>
 173    /// Creates a new app with the specified window settings.
 174    /// </summary>
 175    /// <remarks>
 176    /// If you have your own constructor, make sure to call this
 177    /// one because it initializes the window and the event queue.
 178    /// </remarks>
 179    public AppBase(WindowSettings windowSettings)
 64180    : this(new Window(windowSettings ?? new WindowSettings()))
 181    {
 64182    }
 183
 64184    public AppBase(Window window)
 185    {
 64186      Window = window;
 64187      SetupEvents();
 64188    }
 189
 0190    protected AppBase()
 0191    { }
 192
 193    public void SetupEvents()
 194    {
 64195      _eventQueue = EventManager.CreateQueueFor(Window);
 64196      Events = new Events(this);
 64197      Events.Window.StateChanged += OnWindowStateChange;
 64198    }
 199
 200    /// <summary>
 201    /// Called when the app starts. Use it for the initialization logic.
 202    /// </summary>
 13203    protected virtual void Initialize() { }
 204
 205
 206    /* --------------------------- App events --------------------------- */
 207    private bool _suppressDraw = false;
 208    private bool _isExiting = false;
 4209    internal bool IsExiting => _isExiting;
 210    private bool _isExited = false;
 3211    internal bool IsExited => _isExited;
 212    private bool _isInitialized = false;
 213
 214    /// <summary>
 215    /// Fires when the app's window gets activated.
 216    /// </summary>
 217    public event EventHandler<EventArgs> Activated;
 218    /// <summary>
 219    /// Fires when the app's window gets deactivated.
 220    /// </summary>
 221    public event EventHandler<EventArgs> Deactivated;
 222    /// <summary>
 223    /// Fires when the app is disposed.
 224    /// </summary>
 225    public event EventHandler<EventArgs> Disposed;
 226    /// <summary>
 227    /// Fires when the user requests to exit the app. Can be cancelled
 228    /// through <see cref="AppExitEventArgs.Cancel" />.
 229    /// </summary>
 230    public event EventHandler<AppExitEventArgs> Exiting;
 231    /// <summary>
 232    /// Fires when the app's window gets resized.
 233    /// </summary>
 234    public event EventHandler<EventArgs> Resized;
 235
 236    private void ProcessEvents()
 237    {
 97238      EventManager.Poll();
 97239      _eventQueue.ProcessAll(Events);
 97240    }
 241
 242    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 243    private void CheckIfInitialized()
 244    {
 84245      if (!_isInitialized)
 246      {
 13247        Initialize();
 13248        _isInitialized = true;
 13249        _appStopwatch.Start();
 250      }
 84251    }
 252
 253    private void OnWindowStateChange(object sender, WindowStateChangeEventArgs args)
 254    {
 44255      switch (args.State)
 256      {
 257        case WindowStateChange.Shown:
 258        case WindowStateChange.Exposed:
 259        case WindowStateChange.MouseEnter:
 260        case WindowStateChange.FocusGained:
 20261          Activated?.Invoke(this, new EventArgs());
 4262          break;
 263        case WindowStateChange.Hidden:
 264        case WindowStateChange.MouseLeave:
 265        case WindowStateChange.FocusLost:
 10266          Deactivated?.Invoke(this, new EventArgs());
 3267          break;
 268        case WindowStateChange.Maximized:
 269        case WindowStateChange.Restored:
 270        case WindowStateChange.SizeChanged:
 13271          Resized?.Invoke(this, new EventArgs());
 2272          break;
 273        case WindowStateChange.Closed:
 0274          RequestExit();
 275          break;
 276      }
 1277    }
 278
 279    /// <summary>
 280    /// Requests the app to exit. Can be cancelled by user's code.
 281    /// </summary>
 282    /// <seealso cref="Exiting" />
 3283    public void RequestExit() => _isExiting = true;
 284
 285    /// <summary>
 286    /// Cancels the app exit request, if it exists.
 287    /// </summary>
 1288    public void CancelExitRequest() => _isExiting = false;
 289
 290    /// <summary>
 291    /// Terminates the app loop and disposes the app.
 292    /// </summary>
 1293    public void Terminate() => _isExited = true;
 294
 295    /// <summary>
 296    /// Suppresses the <see cref="Draw" /> call for the next frame.
 297    /// </summary>
 1298    public void SuppressDraw() => _suppressDraw = true;
 299
 300    /* ---------------------------- App loop ---------------------------- */
 301    /// <summary>
 302    /// Resets the total elapsed app time.
 303    /// </summary>
 304    public void ResetElapsedTime()
 305    {
 1306      ElapsedAppTime = TimeSpan.Zero;
 1307      _previousTicks = 0;
 1308      _accumulatedTicks = 0;
 1309      _appStopwatch.Reset();
 1310    }
 311    /// <summary>
 312    /// Runs the app loop.
 313    /// </summary>
 314    public void Run()
 315    {
 0316      while (!_isExited)
 0317        Tick();
 0318    }
 319
 320    /// <summary>
 321    /// Performs a single app loop tick.
 322    /// </summary>
 323    public void Tick()
 324    {
 84325      if (_isExited) return;
 84326      CheckIfNotDisposed();
 84327      CheckIfInitialized();
 328      TimeSpan elapsedFrameTime;
 329
 330    RetryTick:
 331      // Advance the current app time
 448578332      var currentTicks = _appStopwatch.Elapsed.Ticks;
 448578333      _appStopwatch.Start();
 448578334      _accumulatedTicks += (currentTicks - _previousTicks);
 448578335      _previousTicks = currentTicks;
 336      // If the frame took less time than specified, sleep for the
 337      // remaining time and try again
 448578338      if (IsFixedTimeStep && _accumulatedTicks < TargetElapsedTime.Ticks)
 339      {
 448494340        var sleepTime = TimeSpan.FromTicks(TargetElapsedTime.Ticks - _accumulatedTicks).TotalMilliseconds;
 448494341        if (!IsOSPlatform(OSPlatform.Windows))
 342        {
 449296343          if (sleepTime >= 2.0) Thread.Sleep(1);
 344        }
 345        else
 346        {
 0347          Native.Windows.SleepAtMost(sleepTime);
 348        }
 0349        goto RetryTick;
 350      }
 351      // Limit the maximum frameskip time
 84352      if (_accumulatedTicks > _maxElapsedTime.Ticks)
 0353        _accumulatedTicks = _maxElapsedTime.Ticks;
 354
 84355      if (IsFixedTimeStep)
 356      {
 68357        elapsedFrameTime = TargetElapsedTime;
 68358        var stepCount = 0;
 359        // Perform as many fixed timestep updates as we can
 149360        while (_accumulatedTicks >= TargetElapsedTime.Ticks && !_isExited)
 361        {
 81362          ElapsedAppTime += TargetElapsedTime;
 81363          _accumulatedTicks -= TargetElapsedTime.Ticks;
 81364          ++stepCount;
 81365          DoUpdate(TargetElapsedTime);
 366        }
 367        // If the frame took more time than specified, then we got
 368        // more than one update step
 68369        _slowFrameCount += Math.Max(0, stepCount - 1);
 370        // Check if we should reset the IsRunningSlowly flag
 68371        if (IsRunningSlowly)
 372        {
 16373          if (_slowFrameCount == 0)
 1374            IsRunningSlowly = false;
 375        } // If the flag is not set, check if we should set it
 52376        else if (_slowFrameCount > c_MaxSlowFrameCount)
 1377          IsRunningSlowly = true;
 378
 379        // If this frame is not too late, decrease the lag counter
 68380        if (stepCount == 1 && _slowFrameCount > 0)
 13381          _slowFrameCount--;
 382
 383        // Set the target time for the Draw method
 68384        elapsedFrameTime = TimeSpan.FromTicks(TargetElapsedTime.Ticks * stepCount);
 385      }
 386      else
 387      {
 388        // Perform a non-fixed timestep update
 16389        var accumulated = TimeSpan.FromTicks(_accumulatedTicks);
 16390        ElapsedAppTime += accumulated;
 16391        elapsedFrameTime = accumulated;
 16392        accumulated = TimeSpan.Zero;
 16393        DoUpdate(elapsedFrameTime);
 394      }
 395
 84396      if (_suppressDraw)
 1397        _suppressDraw = false;
 398      else
 83399        DoDraw(elapsedFrameTime);
 400
 401      // The user requests us to exit
 84402      if (_isExiting)
 403      {
 3404        var args = new AppExitEventArgs();
 3405        Exiting?.Invoke(this, args);
 4406        if (!args.Cancel) Terminate();
 407      }
 84408      if (_isExited && !IsDisposed)
 1409        Dispose();
 84410      _appStopwatch.Stop();
 84411    }
 412
 413    private void DoUpdate(TimeSpan frameTime)
 414    {
 97415      ProcessEvents();
 97416      Update(frameTime);
 97417    }
 418
 419    private void DoDraw(TimeSpan frameTime)
 420    {
 83421      BeforeDraw(frameTime);
 83422      Draw(frameTime);
 83423      AfterDraw(frameTime);
 83424    }
 425
 426    [ExcludeFromCodeCoverage]
 427    protected virtual void BeforeDraw(TimeSpan frameTime) { }
 428    [ExcludeFromCodeCoverage]
 429    protected virtual void AfterDraw(TimeSpan frameTime) { }
 430
 431    [ExcludeFromCodeCoverage]
 432    protected virtual void Update(TimeSpan frameTime) { }
 433    [ExcludeFromCodeCoverage]
 434    protected virtual void Draw(TimeSpan frameTime) { }
 435
 436    /* ------------------------- Disposing logic ------------------------ */
 437    /// <summary>
 438    /// Called when the app is being disposed (when exiting or by external code).
 439    /// </summary>
 440    [ExcludeFromCodeCoverage]
 441    protected virtual void OnDispose() { }
 442
 443    private bool _isDisposed;
 444    /// <summary>
 445    /// Indicates if this app is disposed.
 446    /// </summary>
 85447    public bool IsDisposed => _isDisposed;
 448    /// <summary>
 449    /// Disposes the app object and its dependencies.
 450    /// </summary>
 451    public void Dispose()
 452    {
 68453      if (_isDisposed) return;
 64454      OnDispose();
 64455      Events.Window.StateChanged -= OnWindowStateChange;
 64456      EventManager.DeleteQueueFor(Window);
 64457      Window.Destroy();
 458
 64459      Events = null;
 64460      _eventQueue = null;
 64461      Window = null;
 64462      _isDisposed = true;
 64463      Disposed?.Invoke(this, new EventArgs());
 1464    }
 465
 466    [ExcludeFromCodeCoverage]
 467    private void CheckIfNotDisposed()
 468    {
 469      if (IsDisposed)
 470        throw new ObjectDisposedException("This app is disposed");
 471    }
 472  }
 473
 474  /// <summary>
 475  /// App exit event args.
 476  /// </summary>
 477  public class AppExitEventArgs : EventArgs
 478  {
 479    /// <summary>
 480    /// Set to true if app should not exit if requested by user.
 481    /// </summary>
 482    public bool Cancel { get; set; }
 483  }
 484}