mirror of
https://github.com/MoleskiCoder/EightBitNet.git
synced 2026-10-23 08:02:16 +00:00
First stab at a logging framwork for the EightBit/Gaming libraries
This commit is contained in:
1 parent
0e41855b84
commit
65f2f960ea
10 files changed
+191
-78
No files matched your search
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user