diff --git a/EightBit/AbstractLogger.cs b/EightBit/AbstractLogger.cs new file mode 100644 index 0000000..3a29855 --- /dev/null +++ b/EightBit/AbstractLogger.cs @@ -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); + } +} diff --git a/EightBit/Bus.cs b/EightBit/Bus.cs index 2323cfe..c38ec72 100644 --- a/EightBit/Bus.cs +++ b/EightBit/Bus.cs @@ -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) diff --git a/EightBit/ConsoleLogger.cs b/EightBit/ConsoleLogger.cs new file mode 100644 index 0000000..f289593 --- /dev/null +++ b/EightBit/ConsoleLogger.cs @@ -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}"); + } + } +} diff --git a/EightBit/ILogger.cs b/EightBit/ILogger.cs new file mode 100644 index 0000000..aecd7db --- /dev/null +++ b/EightBit/ILogger.cs @@ -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; + } + } +} diff --git a/EightBit/IntelHexFile.cs b/EightBit/IntelHexFile.cs index 303a213..860fafd 100644 --- a/EightBit/IntelHexFile.cs +++ b/EightBit/IntelHexFile.cs @@ -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> 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? 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(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; } } diff --git a/EightBit/TestHarness.cs b/EightBit/TestHarness.cs index 0455560..9a53c37 100644 --- a/EightBit/TestHarness.cs +++ b/EightBit/TestHarness.cs @@ -8,6 +8,7 @@ namespace EightBit public class TestHarness(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}"); } } } diff --git a/Gaming/Game.cs b/Gaming/Game.cs index 991f067..eb17b79 100644 --- a/Gaming/Game.cs +++ b/Gaming/Game.cs @@ -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 _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) diff --git a/Gaming/GameController.cs b/Gaming/GameController.cs index bb46d58..597fd7e 100644 --- a/Gaming/GameController.cs +++ b/Gaming/GameController.cs @@ -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() diff --git a/Gaming/SdlLogger.cs b/Gaming/SdlLogger.cs new file mode 100644 index 0000000..d0f9e98 --- /dev/null +++ b/Gaming/SdlLogger.cs @@ -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}"); + } + } +} diff --git a/Gaming/Wrapper.cs b/Gaming/Wrapper.cs index c98ba96..a4e31e7 100644 --- a/Gaming/Wrapper.cs +++ b/Gaming/Wrapper.cs @@ -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(); }