diff --git a/EightBitNet.sln b/EightBitNet.sln index cff62dc..d5c9d50 100644 --- a/EightBitNet.sln +++ b/EightBitNet.sln @@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Z80.FuseTest", "Z80\Z80.Fus EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Z80.Test", "Z80\Z80.Test\Z80.Test.csproj", "{E6AE640E-4A42-4D8F-8ECE-2FBEBC29741B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "M6502.HarteTest", "M6502\M6502.HarteTest\M6502.HarteTest.csproj", "{8686B0DC-A431-4239-836E-ABC90BD69920}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,6 +107,10 @@ Global {E6AE640E-4A42-4D8F-8ECE-2FBEBC29741B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6AE640E-4A42-4D8F-8ECE-2FBEBC29741B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6AE640E-4A42-4D8F-8ECE-2FBEBC29741B}.Release|Any CPU.Build.0 = Release|Any CPU + {8686B0DC-A431-4239-836E-ABC90BD69920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8686B0DC-A431-4239-836E-ABC90BD69920}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8686B0DC-A431-4239-836E-ABC90BD69920}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8686B0DC-A431-4239-836E-ABC90BD69920}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/M6502/M6502.HarteTest/Checker.cs b/M6502/M6502.HarteTest/Checker.cs new file mode 100644 index 0000000..2546161 --- /dev/null +++ b/M6502/M6502.HarteTest/Checker.cs @@ -0,0 +1,259 @@ +namespace M6502.HarteTest +{ + internal class Checker + { + private TestRunner Runner { get; } + + private EightBit.Symbols Symbols { get; } = new(); + + private EightBit.Disassembler Disassembler { get; } + + private bool CycleCountMismatch { get; set; } + + public int Cycles { get; private set; } + + public bool Valid { get; private set; } + + public bool Invalid => !this.Valid; + + public bool Unimplemented => this.Invalid && this.CycleCountMismatch && (this.Cycles == 1); + + public bool Implemented => !this.Unimplemented; + + public List Messages { get; } = []; + + private List ActualCycles { get; } = []; + + public Checker(TestRunner runner) + { + this.Runner = runner; + this.Disassembler = new EightBit.Disassembler(this.Runner, this.Runner.CPU, this.Symbols); + } + + public void Check(Test test) + { + var cpu = this.Runner.CPU; + + this.Messages.Clear(); + this.ActualCycles.Clear(); + + this.Runner.RaisePOWER(); + this.InitialiseState(test); + var pc = cpu.PC.Word; + + this.Cycles = cpu.Step(); + this.Runner.LowerPOWER(); + + this.Valid = this.CheckState(test); + + if (this.Unimplemented) + { + this.Messages.Add("Unimplemented"); + return; + } + + if (this.Invalid && this.Implemented) + { + this.AddDisassembly(pc); + + var final = test.Final ?? throw new InvalidOperationException("Final test state cannot be null"); + + this.Raise("PC", final.PC, cpu.PC.Word); + this.Raise("S", final.S, cpu.S); + this.Raise("A", final.A, cpu.A); + this.Raise("X", final.X, cpu.X); + this.Raise("Y", final.Y, cpu.Y); + this.Raise("P", final.P, cpu.P); + + if (test.Cycles == null) + { + throw new InvalidOperationException("test cycles cannot be null"); + } + + this.Messages.Add($"Stepped cycles: {this.Cycles}, expected events: {test.Cycles.Count}, actual events: {this.ActualCycles.Count}"); + + this.DumpCycles("-- Expected cycles", test.AvailableCycles()); + this.DumpCycles("-- Actual cycles", this.ActualCycles); + } + } + + private bool Check(string what, T expected, T actual) + { + ArgumentNullException.ThrowIfNull(expected); + ArgumentNullException.ThrowIfNull(actual); + var success = actual.Equals(expected); + if (!success) + { + this.Raise(what, expected, actual); + } + return success; + } + + private bool Check(string what, ushort address, byte expected, byte actual) + { + var success = actual == expected; + if (!success) + { + this.Raise($"{what}: {address}", expected, actual); + } + return success; + } + + private void AddDisassembly(ushort address) => this.Messages.Add(this.Disassemble(address)); + + private string Disassemble(ushort address) => this.Disassembler.Disassemble(address); + + private bool CheckState(Test test) + { + var cpu = this.Runner.CPU; + var ram = this.Runner.RAM; + + var expected_cycles = test.AvailableCycles() ?? throw new InvalidOperationException("Expected cycles cannot be null"); + var actual_cycles = this.ActualCycles; + + var actual_idx = 0; + foreach (var expected_cycle in expected_cycles) { + + if (actual_idx >= actual_cycles.Count) + { + this.CycleCountMismatch = true; + return false; // more expected cycles than actual + } + + var actual_cycle = actual_cycles[actual_idx++]; + + var expected_address = expected_cycle.Address; + var actual_address = actual_cycle.Address; + this.Check("Cycle address", expected_address, actual_address); + + var expected_value = expected_cycle.Value; + var actual_value = actual_cycle.Value; + this.Check("Cycle value", expected_value, actual_value); + + var expected_action = expected_cycle.Type; + var actual_action = actual_cycle.Type; + this.Check("Cycle action", expected_action, actual_action); + } + + if (actual_idx < actual_cycles.Count) + { + this.CycleCountMismatch = true; + return false; // less expected cycles than actual + } + + if (this.Messages.Count > 0) + { + return false; + } + + var final = test.Final ?? throw new InvalidOperationException("Final state cannot be null"); + var pc_good = this.Check("PC", final.PC, cpu.PC.Word); + var s_good = this.Check("S", final.S, cpu.S); + var a_good = this.Check("A", final.A, cpu.A); + var x_good = this.Check("X", final.X, cpu.X); + var y_good = this.Check("Y", final.Y, cpu.Y); + var p_good = this.Check("P", final.P, cpu.P); + + if (final.RAM == null) + { + throw new InvalidOperationException("Expected RAM cannot be null"); + } + + var ram_problem = false; + foreach (var entry in final.RAM) + { + + var count = entry.Length; + if (count != 2) + { + throw new InvalidOperationException("RAM entry length must be 2"); + } + + var address = (ushort)entry[0]; + var value = (byte)entry[1]; + + var ram_good = this.Check("RAM", address, value, ram.Peek(address)); + if (!ram_good && !ram_problem) + { + ram_problem = true; + } + } + + return + pc_good && s_good + && a_good && x_good && y_good && p_good + && !ram_problem; + } + + private void Raise(string what, T expected, T actual) => this.Messages.Add($"{what}: expected: {expected}, actual: {actual}"); + + public void Initialise() + { + this.Runner.ReadByte += this.Runner_ReadByte; + this.Runner.WrittenByte += this.Runner_WrittenByte; + } + + private void InitialiseState(Test test) + { + var cpu = this.Runner.CPU; + var ram = this.Runner.RAM; + + var initial = test.Initial ?? throw new InvalidOperationException("Test cannot have an invalid initial state"); + cpu.PC.Word = initial.PC; + cpu.S = initial.S; + cpu.A = initial.A; + cpu.X = initial.X; + cpu.Y = initial.Y; + cpu.P = initial.P; + + var initialRAM = initial.RAM ?? throw new InvalidOperationException("Initial test state cannot have invalid RAM"); + foreach (var entry in initialRAM) + { + var count = entry.Length; + if (count != 2) + { + throw new InvalidOperationException("RAM entry length must be 2"); + } + + var address = (ushort)entry[0]; + var value = (byte)entry[1]; + ram.Poke(address, value); + } + } + + private void Runner_ReadByte(object? sender, EventArgs e) => this.AddActualReadCycle(this.Runner.Address, this.Runner.Data); + + private void Runner_WrittenByte(object? sender, EventArgs e) => this.AddActualWriteCycle(this.Runner.Address, this.Runner.Data); + + private void AddActualReadCycle(EightBit.Register16 address, byte value) => this.AddActualCycle(address, value, "read"); + + private void AddActualWriteCycle(EightBit.Register16 address, byte value) => this.AddActualCycle(address, value, "write"); + + private void AddActualCycle(EightBit.Register16 address, byte value, string action) => this.AddActualCycle(address.Word, value, action); + + private void AddActualCycle(ushort address, byte value, string action) => this.ActualCycles.Add(new Cycle(address, value, action)); + + private void DumpCycle(ushort address, byte value, string? action) + { + ArgumentNullException.ThrowIfNull(action); + this.Messages.Add($"Address: {address}, value: {value}, action: {action}"); + } + + private void DumpCycle(Cycle cycle) => this.DumpCycle(cycle.Address, cycle.Value, cycle.Type); + + private void DumpCycles(IEnumerable? cycles) + { + ArgumentNullException.ThrowIfNull(cycles); + foreach (var cycle in cycles) + { + this.DumpCycle(cycle); + } + } + + private void DumpCycles(string which, IEnumerable? events) + { + this.Messages.Add(which); + this.DumpCycles(events); + } + } +} diff --git a/M6502/M6502.HarteTest/Cycle.cs b/M6502/M6502.HarteTest/Cycle.cs new file mode 100644 index 0000000..8d08bc3 --- /dev/null +++ b/M6502/M6502.HarteTest/Cycle.cs @@ -0,0 +1,30 @@ +namespace M6502.HarteTest +{ + public class Cycle + { + public ushort Address { get; set; } + + public byte Value { get; set; } + + public string? Type { get; set; } + + public Cycle(ushort address, byte value, string type) + { + this.Address = address; + this.Value = value; + this.Type = type; + } + + public Cycle(List input) + { + if (input.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(input), input, "Cycles can only have three elements"); + } + + this.Address = ((System.Text.Json.JsonElement)input[0]).GetUInt16(); + this.Value = ((System.Text.Json.JsonElement)input[1]).GetByte(); ; + this.Type = ((System.Text.Json.JsonElement)input[2]).GetString(); ; + } + } +} diff --git a/M6502/M6502.HarteTest/M6502.HarteTest.csproj b/M6502/M6502.HarteTest/M6502.HarteTest.csproj new file mode 100644 index 0000000..5fc2cb3 --- /dev/null +++ b/M6502/M6502.HarteTest/M6502.HarteTest.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/M6502/M6502.HarteTest/OpcodeTestSuite.cs b/M6502/M6502.HarteTest/OpcodeTestSuite.cs new file mode 100644 index 0000000..122b2eb --- /dev/null +++ b/M6502/M6502.HarteTest/OpcodeTestSuite.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace M6502.HarteTest +{ + internal class OpcodeTestSuite(string path) : IDisposable + { + private static readonly JsonSerializerOptions SerializerOptions = new() + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + private bool disposed; + + public string Path { get; set; } = path; + + private readonly FileStream stream = File.Open(path, FileMode.Open); + + public IAsyncEnumerable TestsAsync => JsonSerializer.DeserializeAsyncEnumerable(this.stream, SerializerOptions); + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.stream.Dispose(); + } + + this.disposed = true; + } + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/M6502/M6502.HarteTest/ProcessorTestSuite.cs b/M6502/M6502.HarteTest/ProcessorTestSuite.cs new file mode 100644 index 0000000..7a2681b --- /dev/null +++ b/M6502/M6502.HarteTest/ProcessorTestSuite.cs @@ -0,0 +1,15 @@ +namespace M6502.HarteTest +{ + internal class ProcessorTestSuite(string location) + { + public string Location { get; set; } = location; + + public IEnumerable OpcodeTests() + { + foreach (var filename in Directory.EnumerateFiles(this.Location, "*.json")) + { + yield return new OpcodeTestSuite(filename); + } + } + } +} diff --git a/M6502/M6502.HarteTest/Program.cs b/M6502/M6502.HarteTest/Program.cs new file mode 100644 index 0000000..2372db7 --- /dev/null +++ b/M6502/M6502.HarteTest/Program.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// + +using System.IO; + +namespace M6502.HarteTest +{ + public static class Program + { + public static async Task Main(string[] args) + { + var directory = @"C:\github\spectrum\libraries\EightBit\modules\65x02\6502\v1"; + + await ProcessTestSuiteAsync(directory); + } + + private static async Task ProcessTestSuiteAsync(string directory) + { + var startTime = DateTime.Now; + + var unimplemented_opcode_count = 0; + var invalid_opcode_count = 0; + + var runner = new TestRunner(); + runner.Initialize(); + + var checker = new Checker(runner); + checker.Initialise(); + + var testSuite = new ProcessorTestSuite(directory); + foreach (var opcode in testSuite.OpcodeTests()) + { + Console.WriteLine($"Processing: {Path.GetFileName(opcode.Path)}"); + + List testNames = []; + var tests = opcode.TestsAsync ?? throw new InvalidOperationException("No tests are available"); + await foreach (var test in tests) + { + if (test == null) + { + throw new InvalidOperationException("Test cannot be null"); + } + + checker.Check(test); + if (checker.Invalid) + { + ++invalid_opcode_count; + + // Was it just unimplemented? + if (checker.Unimplemented) + { + ++unimplemented_opcode_count; + } + + // Let's see if we had any successes! + if (testNames.Count > 0) + { + Console.WriteLine("**** The follow test variations succeeeded"); + foreach (var testName in testNames) + { + Console.WriteLine($"****** {testName}"); + } + } + + // OK, we've attempted an implementation, how did it fail? + foreach (var message in checker.Messages) + { + Console.WriteLine($"**** {message}"); + } + + // I'm not really interested in the remaining tests for this opcode + break; + } + + testNames.Add(test.Name); + } + } + + var finishTime = DateTime.Now; + var elapsedTime = finishTime - startTime; + + Console.Write($"Elapsed time: {elapsedTime.TotalSeconds} seconds"); + Console.Write($", unimplemented opcode count: {unimplemented_opcode_count}"); + Console.Write($", invalid opcode count: {invalid_opcode_count - unimplemented_opcode_count}"); + Console.WriteLine(); + } + } +} \ No newline at end of file diff --git a/M6502/M6502.HarteTest/State.cs b/M6502/M6502.HarteTest/State.cs new file mode 100644 index 0000000..9a39bf4 --- /dev/null +++ b/M6502/M6502.HarteTest/State.cs @@ -0,0 +1,19 @@ +namespace M6502.HarteTest +{ + public class State + { + public ushort PC { get; set; } + + public byte S { get; set; } + + public byte A { get; set; } + + public byte X { get; set; } + + public byte Y { get; set; } + + public byte P { get; set; } + + public int[][]? RAM { get; set; } + } +} diff --git a/M6502/M6502.HarteTest/Test.cs b/M6502/M6502.HarteTest/Test.cs new file mode 100644 index 0000000..effd292 --- /dev/null +++ b/M6502/M6502.HarteTest/Test.cs @@ -0,0 +1,36 @@ +namespace M6502.HarteTest +{ + public class Test + { + public string? Name { get; set; } + + public State? Initial { get; set; } + + public State? Final { get; set; } + + public List>? Cycles { get; set; } + + public IEnumerable AvailableCycles() + { + if (this.Cycles == null) + { + throw new InvalidOperationException("Cycles have not been initialised"); + } + + foreach (var cycle in this.Cycles) + { + yield return new Cycle(cycle); + } + } + + public Cycle CycleAt(int index) + { + if (this.Cycles == null) + { + throw new InvalidOperationException("Cycles have not been initialised"); + } + + return new Cycle(this.Cycles[index]); + } + } +} diff --git a/M6502/M6502.HarteTest/TestRunner.cs b/M6502/M6502.HarteTest/TestRunner.cs new file mode 100644 index 0000000..3f6c806 --- /dev/null +++ b/M6502/M6502.HarteTest/TestRunner.cs @@ -0,0 +1,42 @@ +using EightBit; + +namespace M6502.HarteTest +{ + internal class TestRunner : EightBit.Bus + { + public EightBit.Ram RAM { get; } = new(0x10000); + + public EightBit.M6502 CPU { get; } + + private readonly MemoryMapping mapping; + + public TestRunner() + { + this.CPU = new(this); + this.mapping = new(this.RAM, 0x0000, (ushort)Mask.Sixteen, AccessLevel.ReadWrite); + } + + public override void Initialize() + { + } + + public override void LowerPOWER() + { + this.CPU.LowerPOWER(); + base.LowerPOWER(); + } + + public override MemoryMapping Mapping(ushort absolute) => this.mapping; + + public override void RaisePOWER() + { + base.RaisePOWER(); + this.CPU.RaisePOWER(); + this.CPU.RaiseRESET(); + this.CPU.RaiseINT(); + this.CPU.RaiseNMI(); + this.CPU.RaiseSO(); + this.CPU.RaiseRDY(); + } + } +} diff --git a/M6502/M6502.csproj b/M6502/M6502.csproj index 4bba741..816ad4a 100644 --- a/M6502/M6502.csproj +++ b/M6502/M6502.csproj @@ -19,8 +19,11 @@ + + +