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

DemoBytom / DemoEngine / 21806835161

08 Feb 2026 09:00PM UTC coverage: 31.062% (-0.08%) from 31.138%
21806835161

push

coveralls.net

DemoBytom
Refactor STA Thread `SingleThreadedSynchronizationContext`

New implementation now uses `Channel<T>` instead `BlockingCollection<T>` as the queue for collecting and executing async `SendOrPostCallback`s.

1117 of 3596 relevant lines covered (31.06%)

0.32 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