• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DemoBytom / DemoEngine / 21911211831

11 Feb 2026 03:15PM UTC coverage: 29.787% (+3.5%) from 26.251%
21911211831

Pull #499

coveralls.net

DemoBytom
Unit test for `StaThreadService`

Unit test for `StaThreadService` that tests, that the requests send via `Channel<T>` to `StaThreadService`, are indeed executed on it's main thread.
Producer task sends 5 consecutive requests, ensuring that they are sent from a thread different to the STA main thread. Then, when invoked, an assertion is performed to validate that invocation occurs on STA main thread.
Test takes into account that STA thread apartment can only be enforced on Windows, so it validates that the thread name is `"Main STA thread"` on Windows, and `"Main thread"` on other platforms.
Pull Request #499: Feature/sta thread service refactor

630 of 2115 relevant lines covered (29.79%)

0.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.04
/src/Demo.Engine.Core/Services/MainLoopService.cs
1
// Copyright © Michał Dembski and contributors.
2
// Distributed under MIT license. See LICENSE file in the root for more information.
3

4
using System.Diagnostics;
5
using Demo.Engine.Core.Features.StaThread;
6
using Demo.Engine.Core.Interfaces;
7
using Demo.Engine.Core.Interfaces.Rendering;
8
using Demo.Engine.Core.Interfaces.Rendering.Shaders;
9
using Demo.Engine.Core.Requests.Keyboard;
10
using Demo.Engine.Core.ValueObjects;
11
using MediatR;
12
using Microsoft.Extensions.Logging;
13

14
namespace Demo.Engine.Core.Services;
15

16
internal sealed class MainLoopService
17
    : IMainLoopService,
18
      IAsyncDisposable
19
{
20
    private readonly ILogger<MainLoopService> _logger;
21
    private readonly IStaThreadWriter _staThreadWriter;
22
    private readonly IMediator _mediator;
23
    private readonly IShaderAsyncCompiler _shaderCompiler;
24
    private readonly IFpsTimer _fpsTimer;
25
    private readonly IMainLoopLifetime _mainLoopLifetime;
26
    private readonly ILoopJob _loopJob;
27
    private bool _disposedValue;
28

29
    public Task ExecutingTask { get; }
1✔
30

31
    public MainLoopService(
1✔
32
        ILogger<MainLoopService> logger,
1✔
33
        IStaThreadWriter staThreadWriter,
1✔
34
        IMediator mediator,
1✔
35
        IShaderAsyncCompiler shaderCompiler,
1✔
36
        IFpsTimer fpsTimer,
1✔
37
        IRenderingEngine renderingEngine,
1✔
38
        IMainLoopLifetime mainLoopLifetime,
1✔
39
        ILoopJob loopJob)
1✔
40
    {
41
        _logger = logger;
1✔
42
        _staThreadWriter = staThreadWriter;
1✔
43
        _mediator = mediator;
1✔
44
        _shaderCompiler = shaderCompiler;
1✔
45
        _fpsTimer = fpsTimer;
1✔
46
        _mainLoopLifetime = mainLoopLifetime;
1✔
47
        _loopJob = loopJob;
1✔
48
        ExecutingTask = Task.Run(async () =>
1✔
49
        {
1✔
50
            try
1✔
51
            {
1✔
52
                await DoAsync(renderingEngine);
1✔
53
            }
1✔
54
            catch (TaskCanceledException)
×
55
            {
1✔
56
                _mainLoopLifetime.Cancel();
×
57
            }
×
58
            catch (Exception ex)
×
59
            {
1✔
60
                _logger.LogCritical(ex, "Main loop failed with error! {errorMessage}", ex.Message);
×
61
                _mainLoopLifetime.Cancel();
×
62
                throw;
×
63
            }
1✔
64
        });
1✔
65
    }
1✔
66

67
    private async Task DoAsync(
68
        IRenderingEngine renderingEngine)
69
    {
70
        _ = await _shaderCompiler.CompileShaders(_mainLoopLifetime.Token);
1✔
71

72
        var keyboardHandle = await _mediator.Send(new KeyboardHandleRequest(), CancellationToken.None);
1✔
73
        var keyboardCharCache = await _mediator.Send(new KeyboardCharCacheRequest(), CancellationToken.None);
1✔
74

75
        var surfaces = new RenderingSurfaceId[]
1✔
76
        {
1✔
77
            await _staThreadWriter.CreateSurface(
1✔
78
                _mainLoopLifetime.Token),
1✔
79
            //await _channelWriter.CreateSurface(
1✔
80
            //    _mainLoopLifetime.Token),
1✔
81
        };
1✔
82

83
        var previous = Stopwatch.GetTimestamp();
1✔
84
        var lag = TimeSpan.Zero;
1✔
85

86
        var msPerUpdate = TimeSpan.FromSeconds(1) / 60;
1✔
87

88
        var doEventsOk = true;
1✔
89

90
        while (
1✔
91
            doEventsOk
1✔
92
            //&& IsRunning
1✔
93
            && !_disposedValue
1✔
94
            && !_mainLoopLifetime.Token.IsCancellationRequested)
1✔
95
        {
96
            var current = Stopwatch.GetTimestamp();
1✔
97
            var elapsed = Stopwatch.GetElapsedTime(previous, current);
1✔
98
            previous = current;
1✔
99
            lag += elapsed;
1✔
100

101
            //process input
102
            // TODO!
103

104
            while (lag >= msPerUpdate)
1✔
105
            {
106
                //Update
107
                // TODO - fix the UPS timer.. somehow :D
108
                _fpsTimer.StopUpdateTimer();
1✔
109
                foreach (var renderingSurfaceId in surfaces)
1✔
110
                {
111
                    if (!renderingEngine.TryGetRenderingSurface(
112
                        renderingSurfaceId,
1✔
113
                        out var renderingSurface))
1✔
114
                    {
115
                        _logger.LogCritical(
×
116
                            "Rendering surface {id} not found!",
×
117
                            renderingSurfaceId);
×
118
                        break;
×
119
                    }
120

121
                    await _loopJob.Update(
1✔
122
                          renderingSurface,
1✔
123
                          keyboardHandle,
1✔
124
                          keyboardCharCache);
1✔
125
                }
126
                lag -= msPerUpdate;
1✔
127
                _fpsTimer.StartUpdateTimer();
1✔
128
            }
129

130
            //Render
131
            foreach (var renderingSurfaceId in surfaces)
1✔
132
            {
133
                doEventsOk &= await _staThreadWriter.DoEventsOk(
1✔
134
                    renderingSurfaceId,
1✔
135
                    _mainLoopLifetime.Token);
1✔
136

137
                using var scope = _fpsTimer.StartRenderingTimerScope(
1✔
138
                    renderingSurfaceId);
1✔
139

140
                _loopJob.Render(
1✔
141
                    renderingEngine,
1✔
142
                    renderingSurfaceId);
1✔
143
            }
144
        }
145
        _mainLoopLifetime.Cancel();
1✔
146
    }
1✔
147

148
    private async ValueTask Dispose(bool disposing)
149
    {
150
        if (!_disposedValue)
1✔
151
        {
152
            if (disposing)
1✔
153
            {
154
            }
155

156
            _disposedValue = true;
1✔
157
            //Make sure the loop finishes
158
            await ExecutingTask;
1✔
159
        }
160
    }
1✔
161

162
    public async ValueTask DisposeAsync()
163
    {
164
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
165
        await Dispose(disposing: true);
1✔
166
        GC.SuppressFinalize(this);
1✔
167
    }
1✔
168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc