Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
EngineLogger.cs
1using System.Collections.Concurrent;
2using System.Diagnostics;
3using System.Threading.Channels;
4using Microsoft.Extensions.Logging;
5
7
38public static partial class EngineLogger
39{
40 private static ILoggerFactory? _loggerFactory;
41 private static bool _usingExternalLoggerFactory;
42 private static readonly object _factoryLock = new();
43 private static readonly Dictionary<string, Func<ILoggerProvider>> _persistentProviderFactories =
44 new(StringComparer.Ordinal);
45
46 // Cache wrappers (not raw loggers)
47 private static readonly ConcurrentDictionary<Type, ILogger> _loggerCache = new();
48
49 // Mode (default async)
50 private static volatile EngineLoggingMode _mode = EngineLoggingMode.Asynchronous;
51
56 public static event EventHandler<LoggingErrorEventArgs>? LoggingError;
57
67 public static EngineLoggingMode Mode
68 {
69 get => _mode;
70 set
71 {
72 _mode = value;
73
74 // If switching to async, ensure worker exists.
75 // EnsureAsyncStarted is idempotent and safely handles repeated calls.
76 if (_mode == EngineLoggingMode.Asynchronous)
77 EnsureAsyncStarted(forceRestart: false);
78 }
79 }
80
81 // Async pipeline
82 private static readonly object _asyncGate = new();
83 private static Channel<LogEvent>? _channel;
84 private static Task? _worker;
85 private static CancellationTokenSource? _cts;
86
87 // Defaults: bounded + drop on full (fire-and-forget)
88 private const int DefaultCapacity = 8192;
89 private static int _capacity = DefaultCapacity;
90
97 private static ILoggerFactory GetOrCreateLoggerFactory()
98 {
99 if (_loggerFactory != null)
100 return _loggerFactory;
101
102 lock (_factoryLock)
103 {
104 if (_loggerFactory != null)
105 return _loggerFactory;
106
107 // Check if we're running in a browser/WASM environment
108 bool isBrowser = OperatingSystem.IsBrowser();
109
110 _loggerFactory = LoggerFactory.Create(builder =>
111 {
112 builder.AddDebug();
113
114 // Only add Console logger on non-browser platforms
115 // Console logging creates background threads which are not supported in WASM
116 if (!isBrowser)
117 builder.AddConsole();
118
119 AddPersistentProviders(builder);
120 });
121
122 return _loggerFactory;
123 }
124 }
125
130 public static void StartAsyncLogging(int capacity = DefaultCapacity)
131 {
132 if (capacity <= 0)
133 throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be > 0.");
134
135 lock (_asyncGate)
136 {
137 _capacity = capacity;
138 }
139
140 EnsureAsyncStarted(forceRestart: false);
141 }
142
148 public static void StopAsyncLogging(bool flush = true, TimeSpan? flushTimeout = null)
149 {
150 Channel<LogEvent>? ch;
151 Task? worker;
152 CancellationTokenSource? cts;
153
154 lock (_asyncGate)
155 {
156 ch = _channel;
157 worker = _worker;
158 cts = _cts;
159
160 if (ch == null || worker == null)
161 return;
162
163 // Completing the writer lets the worker drain then exit.
164 try { ch.Writer.TryComplete(); } catch { /* ignore */ }
165
166 // Clear shared references so other threads can't enqueue on this instance.
167 _channel = null;
168 _worker = null;
169 _cts = null;
170 }
171
172 try { cts?.Cancel(); } catch { /* never crash */ }
173
174 if (flush)
175 {
176 var timeout = flushTimeout ?? TimeSpan.FromSeconds(2);
177 try { worker!.Wait(timeout); } catch { /* never crash */ }
178 }
179
180 try { cts?.Dispose(); } catch { /* ignore */ }
181 }
182
187 public static void SwitchToSyncAndFlush(TimeSpan? flushTimeout = null)
188 {
189 if (_mode == EngineLoggingMode.Asynchronous)
190 StopAsyncLogging(flush: true, flushTimeout: flushTimeout);
191
192 _mode = EngineLoggingMode.Synchronous;
193 }
194
199 public static void SwitchToAsync(int? capacity = null)
200 {
201 lock (_asyncGate)
202 {
203 if (capacity is not null)
204 {
205 if (capacity.Value <= 0)
206 throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be > 0.");
207 _capacity = capacity.Value;
208 }
209
210 _mode = EngineLoggingMode.Asynchronous;
211 }
212
213 EnsureAsyncStarted(forceRestart: false);
214 }
215
216 internal static void Initialize(ILoggerFactory loggerFactory)
217 {
218 lock (_factoryLock)
219 {
220 _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
221
222 foreach (Func<ILoggerProvider> providerFactory in _persistentProviderFactories.Values)
223 _loggerFactory.AddProvider(providerFactory());
224
225 _usingExternalLoggerFactory = true;
226 _loggerCache.Clear(); // refresh wrappers
227 }
228 }
229
236 internal static void RegisterPersistentProvider(
237 string key,
238 Func<ILoggerProvider> providerFactory)
239 {
240 ArgumentException.ThrowIfNullOrWhiteSpace(key);
241 ArgumentNullException.ThrowIfNull(providerFactory);
242
243 lock (_factoryLock)
244 {
245 if (_persistentProviderFactories.ContainsKey(key))
246 return;
247
248 _persistentProviderFactories.Add(key, providerFactory);
249 _loggerFactory?.AddProvider(providerFactory());
250 }
251 }
252
261 public static ILoggerFactory EngineLoggerFactory => GetOrCreateLoggerFactory();
262
276 public static ILogger<T> GetLogger<T>() =>
277 (ILogger<T>)_loggerCache.GetOrAdd(typeof(T), static _ => new ModeLogger<T>());
278
291 public static void SetLogLevel(LogLevel level)
292 {
293 lock (_factoryLock)
294 {
295 if (_usingExternalLoggerFactory)
296 return;
297
298 // Check if we're running in a browser/WASM environment
299 bool isBrowser = OperatingSystem.IsBrowser();
300
301 _loggerFactory = LoggerFactory.Create(builder =>
302 {
303 builder.AddDebug()
304 .SetMinimumLevel(level);
305
306 // Only add Console logger on non-browser platforms
307 if (!isBrowser)
308 builder.AddConsole();
309
310 AddPersistentProviders(builder);
311 });
312
313 _loggerCache.Clear(); // refresh wrappers
314 }
315 }
316
317 private static void AddPersistentProviders(ILoggingBuilder builder)
318 {
319 foreach (Func<ILoggerProvider> providerFactory in _persistentProviderFactories.Values)
320 builder.AddProvider(providerFactory());
321 }
322
323 private static void EnsureAsyncStarted(bool forceRestart)
324 {
325 lock (_asyncGate)
326 {
327 EnsureAsyncStarted_NoLock(forceRestart);
328 }
329 }
330
331 // Must be called under _asyncGate.
332 private static void EnsureAsyncStarted_NoLock(bool forceRestart)
333 {
334 if (_mode != EngineLoggingMode.Asynchronous)
335 return;
336
337 if (!forceRestart && _worker != null)
338 return;
339
340 if (forceRestart && _channel != null)
341 {
342 // Best-effort stop without flush; caller controls flush via StopAsyncLogging.
343 try { _channel.Writer.TryComplete(); } catch { /* ignore */ }
344 }
345
346 int capacityLocal = _capacity;
347
348 var channel = Channel.CreateBounded<LogEvent>(new BoundedChannelOptions(capacityLocal)
349 {
350 SingleReader = true,
351 SingleWriter = false,
352 FullMode = BoundedChannelFullMode.DropWrite // fire-and-forget; drop if full
353 });
354
355 _channel = channel;
356
357 _cts?.Dispose();
358 _cts = new CancellationTokenSource();
359
360 // Pass reader so WorkerLoop never dereferences static _channel (avoids races with Stop).
361 var reader = channel.Reader;
362 var token = _cts.Token;
363 _worker = Task.Run(() => WorkerLoop(reader, token));
364 }
365
366 private static bool TryEnqueue(in LogEvent ev)
367 {
368 lock (_asyncGate)
369 {
370 if (_mode != EngineLoggingMode.Asynchronous)
371 return false;
372
373 EnsureAsyncStarted_NoLock(forceRestart: false);
374
375 var ch = _channel;
376 if (ch == null)
377 return false;
378
379 // Fire-and-forget: never block, drop if full
380 return ch.Writer.TryWrite(ev);
381 }
382 }
383
384 private static async Task WorkerLoop(ChannelReader<LogEvent> reader, CancellationToken ct)
385 {
386 try
387 {
388 while (await reader.WaitToReadAsync(ct).ConfigureAwait(false))
389 {
390 while (reader.TryRead(out var ev))
391 {
392 try
393 {
394 // Executes formatting + sinks (console) off the engine thread.
395 var logger = GetOrCreateLoggerFactory().CreateLogger(ev.CategoryName);
396 logger.Log(ev.LogLevel, ev.EventId, ev.State, ev.Exception, ev.Formatter);
397 }
398 catch (Exception ex)
399 {
400 // Fallback: write to debug output to aid diagnostics without crashing.
401 Debug.WriteLine($"[EngineLogger] Logging failed: {ex.GetType().Name}: {ex.Message}");
402
403 // Raise event for applications that want to monitor logging failures.
404 try
405 {
406 LoggingError?.Invoke(null, new LoggingErrorEventArgs(ex, ev.CategoryName, ev.LogLevel));
407 }
408 catch
409 {
410 // Never let event handlers crash the logging infrastructure.
411 }
412 }
413 }
414 }
415 }
416 catch (System.OperationCanceledException) when (ct.IsCancellationRequested)
417 {
418 // Normal cancellation: no action required.
419 }
420 catch
421 {
422 // Swallow any channel or unexpected exceptions.
423 }
424 }
425}
426
430public sealed class LoggingErrorEventArgs : EventArgs
431{
435 public Exception Exception { get; }
436
440 public string CategoryName { get; }
441
445 public LogLevel LogLevel { get; }
446
447 internal LoggingErrorEventArgs(Exception exception, string categoryName, LogLevel logLevel)
448 {
449 Exception = exception ?? throw new ArgumentNullException(nameof(exception));
450 CategoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
451 LogLevel = logLevel;
452 }
453}
Provides data for the LoggingError event.
Exception Exception
Gets the exception that occurred during logging.
string CategoryName
Gets the category name of the logger that failed.
LogLevel LogLevel
Gets the log level of the message that failed to log.
EngineLoggingMode
Specifies the logging mode for the engine.