First stab at a logging framwork for the EightBit/Gaming libraries

This commit is contained in:
Adrian Conlon committed 2026-09-06 16:32:12 +01:00
1 parent 0e41855b84
commit 65f2f960ea
10 files changed
+191 -78

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
namespace EightBit
{
public abstract class AbstractLogger(string context)
{
private string _context = context;
private ILogger.LogLevel _verbosity = ILogger.LogLevel.Critical;
public ILogger.LogLevel Verbosity
{
get => this._verbosity;
set => this._verbosity = value;
}
public string Context
{
get => this._context;
set => this._context = value;
}
public bool Strict
{
get => this.Verbosity < ILogger.LogLevel.Information;
set => throw new NotImplementedException("Strictness is derived from logging verbosity");
}
public abstract void Log(string context, string message, ILogger.LogLevel level);
}
}
+1 -2
View File
@@ -78,8 +78,7 @@ namespace EightBit
protected void LoadHexFile(string path)
{
var file = new IntelHexFile(path);
//file.Verbosity = IntelHexFile.LogLevel.Debugging;
//file.Strict = true;
//file.Logger.Verbosity = ILogger.LogLevel.Debugging;
foreach (var (address, content) in file.Parse())
{
if (file.ExtendedSegmentAddress != 0)
+10
View File
@@ -0,0 +1,10 @@
namespace EightBit
{
public sealed class ConsoleLogger(string context) : AbstractLogger(context), ILogger
{
public override void Log(string context, string message, ILogger.LogLevel level)
{
Console.WriteLine($"{context}: {level}: {message}");
}
}
}
+45
View File
@@ -0,0 +1,45 @@
namespace EightBit
{
public interface ILogger
{
enum LogLevel { Debugging, Information, Warning, Critical, }
abstract string Context { get; set; }
abstract LogLevel Verbosity { get; set; }
abstract bool Strict { get; set; }
abstract void Log(string context, string message, LogLevel level);
void Log(string message, LogLevel level) => this.Log(this.Context, message, level);
void MaybeLog(string message, LogLevel level)
{
if (level >= this.Verbosity)
this.Log(message, level);
}
void Debug(string message) => this.MaybeLog(message, level: LogLevel.Debugging);
void Inform(string message) => this.MaybeLog(message, level: LogLevel.Information);
void Warn(string message) => this.MaybeLog(message, level: LogLevel.Warning);
void Scream(string message) => this.MaybeLog(message, level: LogLevel.Critical);
void MaybeThrow(string message)
{
if (this.Strict)
throw new InvalidDataException(message);
Warn(message);
}
bool Check(bool failure, string message)
{
if (failure)
this.MaybeThrow(message);
return failure;
}
}
}
+25 -56
View File
@@ -4,7 +4,7 @@
namespace EightBit
{
public class IntelHexFile(string path)
public sealed class IntelHexFile(string path)
{
private readonly string _path = path;
private bool _eof;
@@ -13,12 +13,9 @@ namespace EightBit
private ushort _programCounter;
private ushort _extendedLinearAddressHigh;
private uint _startLinearAddress;
private readonly ILogger _logger = new ConsoleLogger("Intel Hex File");
public enum LogLevel { Debugging, Information, Warning, Critical, }
public LogLevel Verbosity { get; set; } = LogLevel.Critical;
public bool Strict { get; set; }
public ILogger Logger => this._logger;
private bool EOF => this._eof;
@@ -32,42 +29,14 @@ namespace EightBit
public uint StartLinearAddress => this._startLinearAddress;
private void Log(string message, LogLevel level)
{
if (level >= this.Verbosity)
Console.WriteLine($"Intel Hex File: {level}: {message}");
}
private void Debug(string message) => this.Log(message, level: LogLevel.Debugging);
private void Inform(string message) => this.Log(message, level: LogLevel.Information);
private void Warn(string message) => this.Log(message, level: LogLevel.Warning);
private void Scream(string message) => this.Log(message, level: LogLevel.Critical);
private void MaybeThrow(string message)
{
if (this.Strict)
throw new InvalidDataException(message);
Warn(message);
}
private bool Check(bool failure, string message)
{
if (failure)
this.MaybeThrow(message);
return failure;
}
public IEnumerable<Tuple<ushort, byte[]>> Parse()
{
this._eof = false;
this.Debug($"Opening: {this._path}");
this._logger.Debug($"Opening: {this._path}");
using var reader = File.OpenText(this._path);
this.Debug("Reading");
this._logger.Debug("Reading");
while (!reader.EndOfStream)
{
var line = reader.ReadLine() ?? throw new InvalidDataException("Early EOF detected");
@@ -75,8 +44,8 @@ namespace EightBit
if (parsed is not null)
yield return parsed;
}
this.Check(!this.EOF, "File is missing an EOF record");
this.Debug("Finished");
this._logger.Check(!this.EOF, "File is missing an EOF record");
this._logger.Debug("Finished");
}
private static byte CalculateChecksum(byte[] data)
@@ -90,23 +59,23 @@ namespace EightBit
private bool VerifyChecksum(byte desired, byte[] data)
{
var calculated = CalculateChecksum(data);
return !this.Check(calculated != desired, "Checksum failure"); ;
return !this._logger.Check(calculated != desired, "Checksum failure"); ;
}
private Tuple<ushort, byte[]>? Parse(string line)
{
ArgumentNullException.ThrowIfNullOrEmpty(line);
if (this.Check(this.EOF, "Reading past EOF marker"))
if (this._logger.Check(this.EOF, "Reading past EOF marker"))
return null;
var colon = line[..1];
if (this.Check(colon != ":", "Record does not begin with a colon"))
if (this._logger.Check(colon != ":", "Record does not begin with a colon"))
return null;
var useful = line[1..];
var characterCount = useful.Length;
if (this.Check(characterCount % 2 != 0, "Record data cannot be parsed as two character hex numbers"))
if (this._logger.Check(characterCount % 2 != 0, "Record data cannot be parsed as two character hex numbers"))
return null;
var byteCount = characterCount / 2;
@@ -119,7 +88,7 @@ namespace EightBit
bytes[index] = datum;
}
if (this.Check(byteCount < 5, "Not enough bytes in record"))
if (this._logger.Check(byteCount < 5, "Not enough bytes in record"))
return null;
var count = bytes[0];
@@ -128,7 +97,7 @@ namespace EightBit
var lastIndex = byteCount - 1;
var checksum = bytes[lastIndex];
var data = bytes[4..lastIndex];
if (this.Check(count != data.Length, "Record count does not match data length"))
if (this._logger.Check(count != data.Length, "Record count does not match data length"))
return null;
// All data, except the desired checksum (obviously!)
@@ -139,55 +108,55 @@ namespace EightBit
{
// Data
case 0x00:
this.Inform($"Type: {type:x2}, Address: {address:x4}, Count: {count}");
this._logger.Inform($"Type: {type:x2}, Address: {address:x4}, Count: {count}");
return new Tuple<ushort, byte[]>(address, data);
// End-of-file
case 0x01:
this.Check(this.EOF, "HEX file contains multiple EOF markers");
this._logger.Check(this.EOF, "HEX file contains multiple EOF markers");
this._eof = true;
this.Inform($"Type: {type:x2}, EOF");
this._logger.Inform($"Type: {type:x2}, EOF");
return null;
// Extended segment address
case 0x02:
if (this.Check(count != 2, "Byte count is invalid for field type 2 (Extended Segment Address)"))
if (this._logger.Check(count != 2, "Byte count is invalid for field type 2 (Extended Segment Address)"))
return null;
this._extendedSegmentAddress = Chip.MakeShort(data[1], bytes[0]);
this.Inform($"Type: {type:x2}, Extended segment address: {this.ExtendedSegmentAddress:x4}");
this._logger.Inform($"Type: {type:x2}, Extended segment address: {this.ExtendedSegmentAddress:x4}");
return null;
// Start segment address
case 0x03:
if (this.Check(count != 4, "Byte count is invalid for field type 3 (Start Segment Address)"))
if (this._logger.Check(count != 4, "Byte count is invalid for field type 3 (Start Segment Address)"))
return null;
this._codeSegment = Chip.MakeShort(data[1], bytes[0]);
this._programCounter = Chip.MakeShort(data[2], bytes[3]);
this.Inform($"Type: {type:x2}, CS: {this.CodeSegment:x4}, PC: {this.ProgramCounter:x4}");
this._logger.Inform($"Type: {type:x2}, CS: {this.CodeSegment:x4}, PC: {this.ProgramCounter:x4}");
return null;
// Extended linear address
case 0x04:
if (this.Check(count != 2, "Byte count is invalid for field type 4 (Extended Linear Address)"))
if (this._logger.Check(count != 2, "Byte count is invalid for field type 4 (Extended Linear Address)"))
return null;
this._extendedLinearAddressHigh = Chip.MakeShort(data[1], bytes[0]);
this.Inform($"Type: {type:x2}, Extended linear address high: {this.ExtendedLinearAddressHigh:x4}");
this._logger.Inform($"Type: {type:x2}, Extended linear address high: {this.ExtendedLinearAddressHigh:x4}");
return null;
// Start linear address
case 0x05:
if (this.Check(count != 4, "Byte count is invalid for field type 5 (Start Linear Address)"))
if (this._logger.Check(count != 4, "Byte count is invalid for field type 5 (Start Linear Address)"))
return null;
{
var high = Chip.MakeShort(data[1], bytes[0]);
var low = Chip.MakeShort(data[2], bytes[3]);
this._startLinearAddress = Chip.MakeInteger(low, high);
}
this.Inform($"Type: {type:x2}, Start linear address: {this.StartLinearAddress:x8}");
this._logger.Inform($"Type: {type:x2}, Start linear address: {this.StartLinearAddress:x8}");
return null;
default:
this.MaybeThrow("Unhandled HEX file record.");
this._logger.MaybeThrow("Unhandled HEX file record.");
return null;
}
}
+5 -6
View File
@@ -8,6 +8,7 @@ namespace EightBit
public class TestHarness<TBus, TProcessor>(TBus board, TProcessor cpu, int targetClocksPerSecond = 2_000_000) where TBus : Bus where TProcessor : Processor
{
private readonly ILogger _logger = new ConsoleLogger("Test harness");
private readonly Stopwatch _timer = new();
private readonly TBus _board = board;
private readonly TProcessor _cpu = cpu;
@@ -51,12 +52,10 @@ namespace EightBit
}
this._timer.Stop();
Console.Out.WriteLine();
Console.Out.WriteLine($"Guest cycles = {this.TotalCycles:N0}");
Console.Out.WriteLine($"Instructions executed = {this.TotalInstructions:N0} ({this.InstructionsPerSecond:N0} instructions per second)");
Console.Out.WriteLine($"Elapsed time (at {this.ActualClockSpeed:g3}MHz actual) = {this.ActualElapsed}");
Console.Out.WriteLine($"Elapsed time (at {this.TargetClockSpeed:g3}MHz target) = {this.TargetElapsed}");
this._logger.Inform($"Guest cycles = {this.TotalCycles:N0}");
this._logger.Inform($"Instructions executed = {this.TotalInstructions:N0} ({this.InstructionsPerSecond:N0} instructions per second)");
this._logger.Inform($"Elapsed time (at {this.ActualClockSpeed:g3}MHz actual) = {this.ActualElapsed}");
this._logger.Inform($"Elapsed time (at {this.TargetClockSpeed:g3}MHz target) = {this.TargetElapsed}");
}
}
}
+13 -8
View File
@@ -9,13 +9,15 @@
using System.Linq;
using System.Runtime.InteropServices;
public abstract class Game(SDL.LogPriority logging) : Device
public abstract class Game(ILogger.LogLevel logging) : Device
{
private readonly Wrapper _wrapper = new(logging);
private bool _vsync;
private readonly SortedDictionary<uint, GameController> _gameControllers = [];
public ILogger Logger => this._wrapper.Logger;
protected ScopedHandle Window { get; } = new(SDL.DestroyWindow);
protected ScopedHandle Renderer { get; } = new(SDL.DestroyRenderer);
@@ -60,6 +62,9 @@
public virtual void Initialise()
{
this.Logger.Context = this.Title;
this.Logger.Verbosity = this._wrapper.Verbosity;
this._wrapper.RaisePOWER();
this.Window.Handle = SDL.CreateWindow(this.Title, this.WindowWidth, this.WindowHeight, 0L);
@@ -80,20 +85,20 @@
this._vsync = Math.Abs(framesPerSecond - refreshRate.Value) < 0.001;
if (this._vsync)
{
SDL.LogInfo(SDL.LogCategory.Render, "Attempting to configure renderer VSYNC");
this.Logger.Inform("Attempting to configure renderer VSYNC");
this._vsync = SDL.SetRenderVSync(this.Renderer, 1);
if (!this._vsync)
SDL.LogWarn(SDL.LogCategory.Render, $"Unable to set render VSYNC ({SDL.GetError()})");
this.Logger.Warn($"Unable to set render VSYNC ({SDL.GetError()})");
}
else
{
SDL.LogWarn(SDL.LogCategory.Render, $"Display refresh rate is incompatible with required rate ({this.FramesPerSecond})");
this.Logger.Warn($"Display refresh rate is incompatible with required rate ({this.FramesPerSecond})");
}
}
if (!this._vsync)
{
SDL.LogInfo(SDL.LogCategory.Render, "Setting callback rate hint");
this.Logger.Inform("Setting callback rate hint");
var success = SDL.SetHint("SDL_MAIN_CALLBACK_RATE", this.FramesPerSecond.ToString(CultureInfo.InvariantCulture));
Wrapper.MaybeThrowException(success, "Unable to set event loop callback rate hint");
}
@@ -191,7 +196,7 @@
Debug.Assert(found);
Debug.Assert(gameController != null, "controller is not null");
_ = this._gameControllers.Remove(which);
SDL.LogInfo(SDL.LogCategory.Input, $"Joystick device {which} removed ({this._gameControllers.Count} controllers available)");
this.Logger.Inform($"Joystick device {which} removed ({this._gameControllers.Count} controllers available)");
}
protected virtual void AddGampad(SDL.Event e)
@@ -199,9 +204,9 @@
var which = e.JDevice.Which;
var found = this._gameControllers.ContainsKey(which);
Debug.Assert(!found);
GameController gameController = new(which);
GameController gameController = new(this.Logger, which);
this._gameControllers[which] = gameController;
SDL.LogInfo(SDL.LogCategory.Input, $"Joystick device {which} address ({this._gameControllers.Count} controllers available)");
this.Logger.Inform($"Joystick device {which} address ({this._gameControllers.Count} controllers available)");
}
public GameController Gamepad(uint which)
+4 -2
View File
@@ -6,12 +6,14 @@
public class GameController : IDisposable
{
private readonly EightBit.ILogger _logger;
private readonly uint _index;
private readonly ScopedHandle _gamepad = new(SDL.CloseGamepad);
private bool _disposed;
public GameController(uint index)
public GameController(EightBit.ILogger logger, uint index)
{
this._logger = logger;
this._index = index;
this.Open();
}
@@ -23,7 +25,7 @@
Wrapper.MaybeThrowException(this._gamepad, "Unable to open gamepad");
var gamepadName = SDL.GetGamepadName(this._gamepad);
Wrapper.MaybeThrowException(gamepadName != null, "Unable to obtain gamepad name");
SDL.LogInfo(SDL.LogCategory.Input, "Game controller name: " + gamepadName);
this._logger.Inform($"Game controller name: {gamepadName}");
}
public void StartRumble()
+48
View File
@@ -0,0 +1,48 @@
namespace Gaming
{
using SDL3;
using System;
using EightBit;
internal sealed class SdlLogger(string context) : AbstractLogger(context), ILogger
{
public static ILogger.LogLevel Translate(SDL.LogPriority priority)
{
switch (priority)
{
case SDL.LogPriority.Debug:
return ILogger.LogLevel.Debugging;
case SDL.LogPriority.Info:
return ILogger.LogLevel.Information;
case SDL.LogPriority.Warn:
return ILogger.LogLevel.Warning;
case SDL.LogPriority.Critical:
return ILogger.LogLevel.Critical;
default:
throw new InvalidOperationException($"Unhandled logging priority ({priority})");
}
}
public static SDL.LogPriority Translate(ILogger.LogLevel priority)
{
switch (priority)
{
case ILogger.LogLevel.Debugging:
return SDL.LogPriority.Debug;
case ILogger.LogLevel.Information:
return SDL.LogPriority.Info;
case ILogger.LogLevel.Warning:
return SDL.LogPriority.Warn;
case ILogger.LogLevel.Critical:
return SDL.LogPriority.Critical;
default:
throw new InvalidOperationException($"Unhandled logging priority ({priority})");
}
}
public override void Log(string context, string message, ILogger.LogLevel level)
{
SDL.LogMessage(SDL.LogCategory.Application, Translate(level), $"{context}: {message}");
}
}
}
+12 -4
View File
@@ -4,8 +4,16 @@
using EightBit;
using System;
public class Wrapper(SDL.LogPriority logging = SDL.LogPriority.Warn) : Device
public class Wrapper(ILogger.LogLevel logging = ILogger.LogLevel.Warning) : Device
{
private readonly ILogger _logger = new SdlLogger("Unnamed Game");
private ILogger.LogLevel _verbosity = logging;
public ILogger Logger => this._logger;
public ILogger.LogLevel Verbosity => this._verbosity;
public override void RaisePOWER()
{
base.RaisePOWER();
@@ -22,13 +30,13 @@
{
var success = SDL.Init(SDL.InitFlags.Video | SDL.InitFlags.Audio | SDL.InitFlags.Gamepad | SDL.InitFlags.Haptic);
Wrapper.MaybeThrowException(success, "Unable to initialise SDL library");
SDL.SetLogPriorities(logging);
SDL.LogInfo(SDL.LogCategory.Application, "SDL library initialised");
SDL.SetLogPriorities(SdlLogger.Translate(this.Verbosity));
this.Logger.Inform("SDL library initialised");
}
public virtual void Terminate()
{
SDL.LogInfo(SDL.LogCategory.Application, "SDL library terminating");
this.Logger.Inform("SDL library terminating");
SDL.Quit();
}