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

DemoBytom / DemoEngine / 21651055502

03 Feb 2026 10:55PM UTC coverage: 31.138% (+0.06%) from 31.077%
21651055502

push

coveralls.net

DemoBytom
Fix endless hang if an exception is thrown within `MainLoopService`

Previously if an exception was thrown within `DoAsync` it would not propagate outwards in `EngineService` - the task was not marked as faulted within `Task.WhenAll`? Not sure. `Task.Run` with async lambda works.

1116 of 3584 relevant lines covered (31.14%)

0.32 hits per line

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

90.91
/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 (Exception ex)
×
55
            {
1✔
56
                _logger.LogCritical(ex, "Main loop failed with error! {errorMessage}", ex.Message);
×
57
                _mainLoopLifetime.Cancel();
×
58
                throw;
×
59
            }
1✔
60
        });
1✔
61
    }
1✔
62

63
    private async Task DoAsync(
64
        IRenderingEngine renderingEngine)
65
    {
66
        _ = await _shaderCompiler.CompileShaders(_mainLoopLifetime.Token);
1✔
67

68
        var keyboardHandle = await _mediator.Send(new KeyboardHandleRequest(), CancellationToken.None);
1✔
69
        var keyboardCharCache = await _mediator.Send(new KeyboardCharCacheRequest(), CancellationToken.None);
1✔
70

71
        var surfaces = new RenderingSurfaceId[]
1✔
72
        {
1✔
73
            await _staThreadWriter.CreateSurface(
1✔
74
                _mainLoopLifetime.Token),
1✔
75
            //await _channelWriter.CreateSurface(
1✔
76
            //    _mainLoopLifetime.Token),
1✔
77
        };
1✔
78

79
        var previous = Stopwatch.GetTimestamp();
1✔
80
        var lag = TimeSpan.Zero;
1✔
81

82
        var msPerUpdate = TimeSpan.FromSeconds(1) / 60;
1✔
83

84
        var doEventsOk = true;
1✔
85

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

97
            //process input
98
            // TODO!
99

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

117
                    await _loopJob.Update(
1✔
118
                          renderingSurface,
1✔
119
                          keyboardHandle,
1✔
120
                          keyboardCharCache);
1✔
121
                }
122
                lag -= msPerUpdate;
1✔
123
                _fpsTimer.StartUpdateTimer();
1✔
124
            }
125

126
            //Render
127
            foreach (var renderingSurfaceId in surfaces)
1✔
128
            {
129
                doEventsOk &= await _staThreadWriter.DoEventsOk(
1✔
130
                    renderingSurfaceId,
1✔
131
                    _mainLoopLifetime.Token);
1✔
132

133
                using var scope = _fpsTimer.StartRenderingTimerScope(
1✔
134
                    renderingSurfaceId);
1✔
135

136
                _loopJob.Render(
1✔
137
                    renderingEngine,
1✔
138
                    renderingSurfaceId);
1✔
139
            }
140
        }
141
        _mainLoopLifetime.Cancel();
1✔
142
    }
1✔
143

144
    private async ValueTask Dispose(bool disposing)
145
    {
146
        if (!_disposedValue)
1✔
147
        {
148
            if (disposing)
1✔
149
            {
150
            }
151

152
            _disposedValue = true;
1✔
153
            //Make sure the loop finishes
154
            await ExecutingTask;
1✔
155
        }
156
    }
1✔
157

158
    public async ValueTask DisposeAsync()
159
    {
160
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
161
        await Dispose(disposing: true);
1✔
162
        GC.SuppressFinalize(this);
1✔
163
    }
1✔
164
}
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