From 5d6c9ff56fbb81e793331979268bfab330856009 Mon Sep 17 00:00:00 2001 From: Highbyte Date: Fri, 18 Sep 2026 15:02:14 +0200 Subject: [PATCH] Add the LXA and ANE opcodes, count the harness exit write only with I/O visible LXA ($AB, LAX #imm) and ANE ($8B, XAA #imm) were not implemented in any compatibility profile, so the CPU ran them as one-byte instructions and executed their operand as the next opcode. Both now compute (A | $EE) & ...: the constant differs between chips, and $EE is the one every LXA and ANE vector of the SingleStepTests 6502 corpus shows, so the corpus now asserts them. They are in the StableUnofficial profile, the C64's default: a program using them derails otherwise. VICE's flibug test programs, whose FLI displayer runs LXA #0, now match their references. The VICE test harness took a write to $D7FF as the program's exit code in every memory configuration. The testbench register is in the I/O area, so a program that clears memory with I/O banked out was stopped early; the harness now counts the write only where I/O is visible (new C64.IsIOVisible) and runs the programs with the FullUnofficial profile. --- docs/home/development.md | 2 +- docs/libraries/core/dotnet6502.md | 2 +- .../CpuCompatibilityProfileOption.cs | 2 +- .../C64.cs | 9 +++++++ .../CpuModels/InstructionBindings.cs | 6 +++++ .../CpuModels/InstructionCores.cs | 25 +++++++++++++++++++ src/libraries/Highbyte.DotNet6502/OpCodeId.cs | 6 +++++ .../OpCodeInfoTests.cs | 6 +++++ .../SingleStepTests/SingleStepVectorTests.cs | 4 +-- .../Program.cs | 11 +++++--- 10 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/home/development.md b/docs/home/development.md index 59d8a393f..41029b439 100644 --- a/docs/home/development.md +++ b/docs/home/development.md @@ -100,7 +100,7 @@ The `Build & run tests` workflow sets both variables (with the ROM directory cac `tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs` runs the VIC-II test programs from the [VICE test programs](https://sourceforge.net/p/vice-emu/code/HEAD/tree/testprogs/) repository against the C64 emulation and compares the picture with the reference screenshots that ship with them. It is a console tool, not part of the test suite: the programs and their references are not in this repository (fetch a suite directory, for example `testprogs/VICII/border`, from the Subversion repository at `https://svn.code.sf.net/p/vice-emu/code/testprogs/VICII/`), and many of the pictures are not expected to match yet. -The tool boots the real ROMs to the `READY.` prompt, loads a `.prg`, runs it from its BASIC `SYS` line and stops at the first write to `$D7FF`, which is how the programs report their result (`$00` passed, `$FF` failed) and end. The frame completed before that write is compared with the reference, colour index by colour index, over the part of the frame both pictures cover. For each program it writes a picture with the reference, the emulator's frame and the differing pixels side by side, and a `results.md` table with the exit code and the number of differing pixels. +The tool boots the real ROMs to the `READY.` prompt, loads a `.prg`, runs it from its BASIC `SYS` line (with the CPU's `FullUnofficial` opcode profile, as the programs are written for the real chip) and stops at the first write to `$D7FF` made with the I/O area banked in, which is how the programs report their result (`$00` passed, `$FF` failed) and end. The frame completed before that write is compared with the reference, colour index by colour index, over the part of the frame both pictures cover. For each program it writes a picture with the reference, the emulator's frame and the differing pixels side by side, and a `results.md` table with the exit code and the number of differing pixels. ```sh dotnet run --project tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs -c Release -- \ diff --git a/docs/libraries/core/dotnet6502.md b/docs/libraries/core/dotnet6502.md index e949d4b3d..3d99d99ca 100644 --- a/docs/libraries/core/dotnet6502.md +++ b/docs/libraries/core/dotnet6502.md @@ -95,7 +95,7 @@ Higher profiles include everything from lower profiles. | Profile | Meaning | | ------- | ------- | | `OfficialOnly` | Only documented MOS 6502 opcodes are available. | -| `StableUnofficial` | Also enables the more predictable undocumented NMOS opcodes commonly used on real 6502/6510 hardware. | +| `StableUnofficial` | Also enables the more predictable undocumented NMOS opcodes commonly used on real 6502/6510 hardware, and `LXA` ($AB) and `ANE` ($8B), whose result depends on a chip-specific value ORed into A: the common `$EE` is used. | | `ExperimentalUnofficial` | Also enables the currently implemented but less reliable undocumented opcodes used for targeted compatibility testing. | | `FullUnofficial` | Also enables halt-style unofficial opcodes such as `JAM` / `KIL` that can intentionally jam the CPU until reset. | diff --git a/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/CpuCompatibilityProfileOption.cs b/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/CpuCompatibilityProfileOption.cs index be24e7df1..5d11e9c25 100644 --- a/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/CpuCompatibilityProfileOption.cs +++ b/src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/CpuCompatibilityProfileOption.cs @@ -15,7 +15,7 @@ public record CpuCompatibilityProfileOption(CpuCompatibilityProfile Profile, str new CpuCompatibilityProfileOption( CpuCompatibilityProfile.StableUnofficial, "Stable unofficial", - "Also enables the predictable NMOS unofficial opcodes commonly used on real 6502/6510 hardware."), + "Also enables the predictable NMOS unofficial opcodes commonly used on real 6502/6510 hardware, and LXA/ANE with the common chip-specific value."), new CpuCompatibilityProfileOption( CpuCompatibilityProfile.ExperimentalUnofficial, "Experimental unofficial", diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs index f1479ea37..720ca5a63 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs @@ -52,6 +52,14 @@ public class C64 : ISystem, ISystemMonitor, ISystemState, ISystemCleanup, ISyste public byte[] RAM { get; set; } = default!; public byte[] IO { get; set; } = default!; public byte CurrentBank { get; private set; } + + /// + /// Whether the I/O area (the VIC-II, SID, colour RAM, CIAs and cartridge I/O at + /// $D000-$DFFF) is visible to the CPU in the current memory configuration, as the processor + /// port and the cartridge lines select it. + /// + public bool IsIOVisible => _configurationMapsIO[Mem.CurrentConfiguration]; + private readonly bool[] _configurationMapsIO = new bool[32]; public Vic2 Vic2 { get; set; } = default!; public Cia1 Cia1 { get; set; } = default!; public Cia2 Cia2 { get; set; } = default!; @@ -487,6 +495,7 @@ private void SetCurrentAudioProvider(Type? audioProviderType) private void MapLocationsOnCurrentCPUBank(Memory mem, bool mapIO) { + _configurationMapsIO[mem.CurrentConfiguration] = mapIO; // Address 0x00: 6510 CPU data direction register. mem.MapReader(0x00, IoPortDirectionLoad); mem.MapWriter(0x00, IoPortDirectionStore); diff --git a/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionBindings.cs b/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionBindings.cs index 603218e49..ebd700d05 100644 --- a/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionBindings.cs +++ b/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionBindings.cs @@ -301,6 +301,12 @@ public static void ApplyNmosUndocumented(OpCodeDescriptor?[] table, CpuCompatibi Read(table, 0x4B, "ALR", AddrMode.I, 2, 2, InstructionCores.Alr, false, indexedDummyReads, documented: false); Read(table, 0xCB, "AXS", AddrMode.I, 2, 2, InstructionCores.Axs, false, indexedDummyReads, documented: false); + // The two immediate opcodes whose result depends on a chip-specific value, with the + // common one: closer to any real chip than running them as one-byte instructions, + // which derails programs that use them (FLI displayers use LXA #0). + Read(table, 0xAB, "LXA", AddrMode.I, 2, 2, InstructionCores.Lxa, false, indexedDummyReads, documented: false); + Read(table, 0x8B, "ANE", AddrMode.I, 2, 2, InstructionCores.Ane, false, indexedDummyReads, documented: false); + // $EB: undocumented alias of SBC #imm — same core as the official byte. Read(table, 0xEB, "SBC", AddrMode.I, 2, 2, InstructionCores.SbcNmos, false, indexedDummyReads, documented: false); } diff --git a/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionCores.cs b/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionCores.cs index 44838d634..8f2228007 100644 --- a/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionCores.cs +++ b/src/libraries/Highbyte.DotNet6502/CpuModels/InstructionCores.cs @@ -378,6 +378,31 @@ public static ulong Axs(CPU cpu, byte value) return 0; } + /// + /// The value the unstable immediate opcodes LXA and ANE OR into A before their AND. It + /// differs between chips (and with temperature); $EE is the common one, and the one every + /// LXA and ANE vector of the SingleStepTests 6502 corpus shows. + /// + public const byte UnstableImmediateMagic = 0xEE; + + /// LXA (LAX #imm): A = X = (A OR magic) AND value. + public static ulong Lxa(CPU cpu, byte value) + { + byte result = (byte)((cpu.A | UnstableImmediateMagic) & value); + cpu.A = result; + cpu.X = result; + BinaryArithmeticHelpers.SetFlagsAfterRegisterLoadIncDec(result, ref cpu.ProcessorStatus); + return 0; + } + + /// ANE (XAA #imm): A = (A OR magic) AND X AND value. + public static ulong Ane(CPU cpu, byte value) + { + cpu.A = (byte)((cpu.A | UnstableImmediateMagic) & cpu.X & value); + BinaryArithmeticHelpers.SetFlagsAfterRegisterLoadIncDec(cpu.A, ref cpu.ProcessorStatus); + return 0; + } + /// LAS: A = X = SP = value AND SP. public static ulong Las(CPU cpu, byte value) { diff --git a/src/libraries/Highbyte.DotNet6502/OpCodeId.cs b/src/libraries/Highbyte.DotNet6502/OpCodeId.cs index 859614bb4..685a694c1 100644 --- a/src/libraries/Highbyte.DotNet6502/OpCodeId.cs +++ b/src/libraries/Highbyte.DotNet6502/OpCodeId.cs @@ -420,6 +420,12 @@ public enum OpCodeId: byte // LAS — A = X = SP = memory & SP LAS_ABS_Y = 0xBB, + // LXA — A = X = (A | magic) & immediate + LXA_I = 0xAB, + + // ANE — A = (A | magic) & X & immediate + ANE_I = 0x8B, + // SBC duplicate (identical to SBC_I 0xE9) SBC_I_EB = 0xEB, } diff --git a/tests/Highbyte.DotNet6502.Tests/OpCodeInfoTests.cs b/tests/Highbyte.DotNet6502.Tests/OpCodeInfoTests.cs index 2f36eeb58..36b05d810 100644 --- a/tests/Highbyte.DotNet6502.Tests/OpCodeInfoTests.cs +++ b/tests/Highbyte.DotNet6502.Tests/OpCodeInfoTests.cs @@ -31,6 +31,8 @@ public void OfficialOnly_Profile_Excludes_Unofficial_OpCodes() Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.NOP)); Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.NOP_ILL_1A)); Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.ARR_I)); + Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.LXA_I)); + Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.ANE_I)); } [Fact] @@ -42,6 +44,8 @@ public void StableUnofficial_Profile_Includes_Stable_But_Not_Experimental_OpCode Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.SBC_I_EB)); Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.ARR_I)); Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.LAS_ABS_Y)); + Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.LXA_I)); + Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.ANE_I)); } [Fact] @@ -51,6 +55,8 @@ public void ExperimentalUnofficial_Profile_Includes_Experimental_OpCodes() Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.ARR_I)); Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.LAS_ABS_Y)); + Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.LXA_I)); + Assert.True(cpu.IsOpCodeDefined((byte)OpCodeId.ANE_I)); Assert.False(cpu.IsOpCodeDefined((byte)OpCodeId.JAM_02)); } diff --git a/tests/Highbyte.DotNet6502.Tests/SingleStepTests/SingleStepVectorTests.cs b/tests/Highbyte.DotNet6502.Tests/SingleStepTests/SingleStepVectorTests.cs index 38126846a..422a0e63e 100644 --- a/tests/Highbyte.DotNet6502.Tests/SingleStepTests/SingleStepVectorTests.cs +++ b/tests/Highbyte.DotNet6502.Tests/SingleStepTests/SingleStepVectorTests.cs @@ -34,7 +34,7 @@ public SingleStepVectorTests(Harness harness, ITestOutputHelper output) } private const string UnstableNmos = - "unstable undocumented opcode (result depends on a chip-specific 'magic' value and bus timing); not implemented in any profile, the corpus encodes one silicon's behavior"; + "unstable undocumented opcode (result depends on bus timing and the chip); not implemented in any profile, the corpus encodes one silicon's behavior"; private const string RockwellBitOps = "Rockwell/WDC bit instruction (RMB/SMB/BBR/BBS); the emulated NCR 65C02 executes these bytes as 1-cycle NOPs"; @@ -44,13 +44,11 @@ public SingleStepVectorTests(Harness harness, ITestOutputHelper output) { ["6502"] = new Dictionary { - [0x8B] = UnstableNmos, // ANE [0x93] = UnstableNmos, // SHA (zp),Y [0x9B] = UnstableNmos, // TAS [0x9C] = UnstableNmos, // SHY [0x9E] = UnstableNmos, // SHX [0x9F] = UnstableNmos, // SHA abs,Y - [0xAB] = UnstableNmos, // LXA }, ["wdc65c02"] = BuildWdcDeviations(), }; diff --git a/tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs/Program.cs b/tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs/Program.cs index e4aa7b0dd..ed3f1d9b1 100644 --- a/tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs/Program.cs +++ b/tools/vice-testprogs/Highbyte.DotNet6502.ViceTestprogs/Program.cs @@ -101,14 +101,14 @@ private static TestResult RunOne(string suite, string name, string prgPath, stri var frameHeight = rasterizer.NativeSize.Height; // The debug register: the test writes its exit code there when its picture is complete. - // Hooked in every bank configuration (the C64 has 32, for the processor port and the - // cartridge lines); the value also goes to RAM, as it would in a configuration without I/O. + // It is in the I/O area, so it is hooked in every bank configuration that shows I/O (the + // C64 has 32, for the processor port and the cartridge lines); in the others a write to + // $D7FF is a write to RAM, as a test that clears memory with I/O banked out makes. int? exitCode = null; var exitFrame = -1; var frame = 0; void OnDebugWrite(ushort address, byte value) { - c64.RAM[address] = value; if (exitCode == null) { exitCode = value; @@ -119,7 +119,8 @@ void OnDebugWrite(ushort address, byte value) for (var configuration = 0; configuration < c64.Mem.NumberOfConfigurations; configuration++) { c64.Mem.SetMemoryConfiguration(configuration); - c64.Mem.MapWriter(DebugRegister, OnDebugWrite); + if (c64.IsIOVisible) + c64.Mem.MapWriter(DebugRegister, OnDebugWrite); } c64.Mem.SetMemoryConfiguration(currentConfiguration); @@ -264,6 +265,8 @@ private static C64 BuildC64(string model, string romDir) AudioEnabled = false, RenderProviderType = typeof(Vic2Rasterizer), Vic2RasterizerPerLineSprites = true, + // The test programs are written for the real chip: every opcode it executes. + CpuCompatibilityProfile = CpuCompatibilityProfile.FullUnofficial, ROMs = [ new ROM { Name = "kernal", File = "kernal.901227-03.bin" },