diff --git a/pom.xml b/pom.xml index 70065b4..17f5b8c 100644 --- a/pom.xml +++ b/pom.xml @@ -189,6 +189,12 @@ 4.13.2 test + + com.google.code.gson + gson + test + 2.11.0 + org.xerial.thirdparty nestedvm diff --git a/src/main/java/jace/apple2e/MOS65C02.java b/src/main/java/jace/apple2e/MOS65C02.java index 3dbcf60..3abe82f 100644 --- a/src/main/java/jace/apple2e/MOS65C02.java +++ b/src/main/java/jace/apple2e/MOS65C02.java @@ -136,7 +136,7 @@ public class MOS65C02 extends CPU { BBS6(0x0ef, COMMAND.BBS6, MODE.ZP_REL, 5, true), BBS7(0x0ff, COMMAND.BBS7, MODE.ZP_REL, 5, true), BEQ_REL0(0x00F0, COMMAND.BEQ, MODE.RELATIVE, 2), - BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 2, true), + BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 3, true), BIT_ZP(0x0024, COMMAND.BIT, MODE.ZEROPAGE, 3), BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 4, true), BIT_AB(0x002C, COMMAND.BIT, MODE.ABSOLUTE, 4), @@ -193,7 +193,8 @@ public class MOS65C02 extends CPU { INX(0x00E8, COMMAND.INX, MODE.IMPLIED, 2), INY(0x00C8, COMMAND.INY, MODE.IMPLIED, 2), JMP_AB(0x004C, COMMAND.JMP, MODE.ABSOLUTE, 3, false, false), - JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 5), + // JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT_BUGGY, 6), + JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 6), JMP_IND_X(0x007C, COMMAND.JMP, MODE.INDIRECT_X, 6, true), JSR_AB(0x0020, COMMAND.JSR, MODE.ABSOLUTE, 6, false, false), LDA_IMM(0x00A9, COMMAND.LDA, MODE.IMMEDIATE, 2), @@ -386,6 +387,10 @@ public class MOS65C02 extends CPU { ZEROPAGE(2, "$~1", (cpu) -> cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) & 0x00FF), ZEROPAGE_X(2, "$~1,X", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X)), ZEROPAGE_Y(2, "$~1,Y", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.Y)), + INDIRECT_BUGGY(3, "$(~2~1)", (cpu) -> { + int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false); + return cpu.getMemory().readWordPageWraparound(address, TYPE.READ_DATA, true, false); + }), INDIRECT(3, "$(~2~1)", (cpu) -> { int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false); return cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false); @@ -396,15 +401,15 @@ public class MOS65C02 extends CPU { }), INDIRECT_ZP(2, "$(~1)", (cpu) -> { int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false); - return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false); + return cpu.getMemory().readWordPageWraparound(address & 0x0FF, TYPE.READ_DATA, true, false); }), INDIRECT_ZP_X(2, "$(~1,X)", (cpu) -> { int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X; - return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false); + return cpu.getMemory().readWordPageWraparound(address & 0x0FF, TYPE.READ_DATA, true, false); }), INDIRECT_ZP_Y(2, "$(~1),Y", (cpu) -> { int address = 0x00FF & cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false); - address = cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false); + address = cpu.getMemory().readWordPageWraparound(address, TYPE.READ_DATA, true, false); int address2 = address + cpu.Y; cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00)); cpu.setPageBoundaryApplied(true); @@ -526,7 +531,8 @@ public class MOS65C02 extends CPU { public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) { if ((value & (1 << bit)) == 0) { cpu.setProgramCounter(address); - cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + cpu.setPageBoundaryApplied(true); + cpu.addWaitCycles(1); } } } @@ -666,9 +672,9 @@ public class MOS65C02 extends CPU { BIT((address, value, addressMode, cpu) -> { int result = (cpu.A & value); cpu.Z = result == 0; - cpu.N = (value & 0x080) != 0; // As per http://www.6502.org/tutorials/vflag.html if (addressMode != MODE.IMMEDIATE) { + cpu.N = (value & 0x080) != 0; cpu.V = (value & 0x040) != 0; } }), @@ -831,7 +837,7 @@ public class MOS65C02 extends CPU { cpu.push((byte) cpu.A); }), PHP((address, value, addressMode, cpu) -> { - cpu.push((cpu.getStatus())); + cpu.push((byte) (cpu.getStatus() | 0x10)); }), PHX((address, value, addressMode, cpu) -> { cpu.push((byte) cpu.X); @@ -969,11 +975,11 @@ public class MOS65C02 extends CPU { cpu.setNZ(cpu.Y); }), TRB((address, value, addressMode, cpu) -> { - cpu.C = (value & cpu.A) != 0 ? 1 : 0; + cpu.Z = (value & cpu.A) == 0; cpu.getMemory().write(address, (byte) (value & ~cpu.A), true, false); }), TSB((address, value, addressMode, cpu) -> { - cpu.C = (value & cpu.A) != 0 ? 1 : 0; + cpu.Z = (value & cpu.A) == 0; cpu.getMemory().write(address, (byte) (value | cpu.A), true, false); }), TSX((address, value, addressMode, cpu) -> { @@ -1061,10 +1067,6 @@ public class MOS65C02 extends CPU { bytes = 2; wait = 2; } - case 3, 7, 0x0b, 0x0f -> { - wait = 1; - bytes = 1; - } case 4 -> { bytes = 2; if ((op & 0x0f0) == 0x040) { @@ -1081,8 +1083,12 @@ public class MOS65C02 extends CPU { wait = 4; } } - default -> bytes = 2; + default -> { + wait = 1; + bytes = 1; + } } + wait--; incrementProgramCounter(bytes); addWaitCycles(wait); @@ -1129,7 +1135,7 @@ public class MOS65C02 extends CPU { return getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false); } - private byte getStatus() { + public byte getStatus() { return (byte) ((N ? 0x080 : 0) | (V ? 0x040 : 0) | 0x020 @@ -1140,10 +1146,17 @@ public class MOS65C02 extends CPU { | ((C > 0) ? 0x01 : 0)); } - private void setStatus(byte b) { + public void setStatus(byte b) { + setStatus(b, false); + } + + public void setStatus(byte b, boolean setBreakFlag) { N = (b & 0x080) != 0; V = (b & 0x040) != 0; - // B flag is unaffected in this way. + // B flag is normally unaffected in this way, can be bypassed for unit testing + if (setBreakFlag) { + B = (b & 0x010) != 0; + } D = (b & 0x08) != 0; I = (b & 0x04) != 0; Z = (b & 0x02) != 0; @@ -1171,10 +1184,13 @@ public class MOS65C02 extends CPU { LOG.log(Level.WARNING, "BRK at ${0}", Integer.toString(getProgramCounter(), 16)); dumpTrace(); } - B = true; + programCounter++; + pushPC(); + push((byte) (getStatus() | 0x010)); // 65c02 clears D flag on BRK + I = true; D = false; - interruptSignalled = true; + setProgramCounter(getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false)); } // Hardware IRQ generated @@ -1191,10 +1207,10 @@ public class MOS65C02 extends CPU { } interruptSignalled = false; if (!I || B) { - I = false; + I = true; pushWord(getProgramCounter()); push(getStatus()); - I = true; + B = false; int newPC = getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false); // System.out.println("Interrupt generated, setting PC to (" + Integer.toString(INT_VECTOR, 16) + ") = " + Integer.toString(newPC, 16)); setProgramCounter(newPC); @@ -1206,6 +1222,7 @@ public class MOS65C02 extends CPU { public void reset() { pushWord(getProgramCounter()); push(getStatus()); + setWaitCycles(0); // STACK = 0x0ff; // B = false; B = true; @@ -1217,7 +1234,7 @@ public class MOS65C02 extends CPU { // Z = true; int resetVector = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false); int newPC = resetVector; - LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)}); + // LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)}); setProgramCounter(newPC); } diff --git a/src/main/java/jace/core/Device.java b/src/main/java/jace/core/Device.java index b325c77..d4de65e 100644 --- a/src/main/java/jace/core/Device.java +++ b/src/main/java/jace/core/Device.java @@ -65,6 +65,11 @@ public abstract class Device implements Reconfigurable { return _ram; } + // NOTE: This is for unit testing only, don't actually use this for anything else or expect things to be weird. + public void setMemory(RAM ram) { + _ram = ram; + } + Device parentDevice = null; public Device getParent() { return parentDevice; diff --git a/src/main/java/jace/core/RAM.java b/src/main/java/jace/core/RAM.java index e4e298e..7ab6c90 100644 --- a/src/main/java/jace/core/RAM.java +++ b/src/main/java/jace/core/RAM.java @@ -28,6 +28,7 @@ import java.util.function.Consumer; import jace.Emulator; import jace.apple2e.SoftSwitches; import jace.config.Reconfigurable; +import jace.core.RAMEvent.TYPE; /** * RAM is a 64K address space of paged memory. It also manages sets of memory @@ -164,6 +165,14 @@ public abstract class RAM implements Reconfigurable { return msb + lsb; } + // This is used by opcodes that wrap around page boundaries + public int readWordPageWraparound(int address, TYPE eventType, boolean triggerEvent, boolean requireSynchronization) { + int lsb = 0x00ff & read(address, eventType, triggerEvent, requireSynchronization); + int addr1 = ((address + 1) & 0x0ff) | (address & 0x0ff00); + int msb = (0x00ff & read(addr1, eventType, triggerEvent, requireSynchronization)) << 8; + return msb + lsb; + } + private synchronized void mapListener(RAMListener l, int address) { if ((address & 0x0FF00) == 0x0C000) { int index = address & 0x0FF; diff --git a/src/main/java/jace/hardware/mockingboard/Votrax.java b/src/main/java/jace/hardware/mockingboard/Votrax.java index 5fd047c..ade56e7 100644 --- a/src/main/java/jace/hardware/mockingboard/Votrax.java +++ b/src/main/java/jace/hardware/mockingboard/Votrax.java @@ -4,12 +4,17 @@ import static org.lwjgl.openal.AL10.AL_NO_ERROR; import static org.lwjgl.openal.AL10.alGetError; import java.io.InputStream; +import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; -import org.lwjgl.openal.EXTEfx; +import javax.sound.sampled.Mixer; + +import org.lwjgl.BufferUtils; +import static org.lwjgl.openal.AL11.*; +import static org.lwjgl.openal.EXTEfx.*; import jace.core.SoundMixer; import jace.core.SoundMixer.SoundBuffer; @@ -64,7 +69,7 @@ public class Votrax extends TimedDevice { // 1b: UH3 (nUt) // 1c: ER (bIRd) // 1d: R (Roof) - // 1e: R1 (Rug) + // 1e: R01 (Rug) // 1f: R2 (mutteR -- German) // 20: L (Lift) // 21: L1 (pLay) @@ -131,43 +136,17 @@ public class Votrax extends TimedDevice { this.sampleCounter = 0.0; this.samplesPerClock = clockFrequency / sampleRate; } - public Optional tick() { - sampleCounter += samplesPerClock; - if (sampleCounter >= 1.0) { - sampleCounter -= 1.0; + public abstract int getBufferDuration(); - return Optional.of(doGenerate()); - } else { - return Optional.empty(); + public void fillBuffer(ShortBuffer buffer) { + for (int i = 0; i < getBufferDuration(); i++) { + buffer.put((short) (doGenerate() * 32767)); } } public abstract double doGenerate(); } - public static class Mixer extends Generator { - public List inputs = new ArrayList<>(); - public List gains = new ArrayList<>(); - double volume=0.0; - - public void addInput(Generator input) { - inputs.add(input); - gains.add(1.0); - } - - public void setGain(int index, double gain) { - gains.set(index, gain); - } - - public double doGenerate() { - double sample = 0.0; - for (int i = 0; i < inputs.size(); i++) { - sample += inputs.get(i).doGenerate() * gains.get(i); - } - return sample; - } - } - public static class SawGenerator extends Generator { double pitch=440.0; double sample = 0.0; @@ -186,6 +165,12 @@ public class Votrax extends TimedDevice { changePerSample = direction * 2.0 * pitch / sampleRate; } + @Override + // Let's generate 10 loops of the sawtooth wave + public int getBufferDuration() { + return (int) (10.0 * sampleRate / pitch); + } + public double doGenerate() { sample += changePerSample; if (sample < -1.0) { @@ -202,29 +187,51 @@ public class Votrax extends TimedDevice { public double doGenerate() { return Math.random() * 2.0 - 1.0; } + @Override + // Let's generate 10 seconds of noise + public int getBufferDuration() { + return sampleRate * 10; + } } public float mixerGain = 32767.0f; public int[] filters = new int[5]; public SawGenerator formantGenerator = new SawGenerator(); public NoiseGenerator noiseGenerator = new NoiseGenerator(); - public Mixer mixer = new Mixer(); public static int FORMANT = 0; public static int NOISE = 1; - private Thread playbackThread = null; + // private Thread playbackThread = null; public Votrax() throws Exception { // loadPhonemeData(); formantGenerator.setSampleRate(44100); formantGenerator.setPitch(100); noiseGenerator.setSampleRate(44100); - mixer.addInput(formantGenerator); - mixer.addInput(noiseGenerator); - mixer.setGain(FORMANT, 0.5); - mixer.setGain(NOISE, 0.1); } - public void resume() { + int formantSource = -1; + int noiseSource = -1; + + public void resume() { + // Create a buffer for the sawtooth wave + ShortBuffer formantLoop = BufferUtils.createShortBuffer(formantGenerator.getBufferDuration()); + // Create a buffer for the noise + ShortBuffer noiseLoop = BufferUtils.createShortBuffer(noiseGenerator.getBufferDuration()); + // Fill the buffers + formantGenerator.fillBuffer(formantLoop); + noiseGenerator.fillBuffer(noiseLoop); + + // Create a source for the formant generator + formantSource = alGenSources(); + alSourcei(formantSource, AL_BUFFER, formantLoop.get(0)); + alSourcei(formantSource, AL_LOOPING, AL_TRUE); + alSourcePlay(formantSource); + // Create a source for the noise generator + noiseSource = alGenSources(); + alSourcei(noiseSource, AL_BUFFER, noiseLoop.get(0)); + alSourcei(noiseSource, AL_LOOPING, AL_TRUE); + alSourcePlay(noiseSource); + try { createFilters(); } catch (Exception e) { @@ -232,33 +239,33 @@ public class Votrax extends TimedDevice { suspend(); } super.resume(); - if (playbackThread != null && !playbackThread.isAlive()) { - return; - } - playbackThread = new Thread(() -> { - SoundBuffer soundBuffer = null; - try { - soundBuffer = SoundMixer.createBuffer(false); - while (isRunning()) { - try { - soundBuffer.playSample((short) (mixer.doGenerate() * mixerGain)); - } catch (Exception e) { - e.printStackTrace(); - suspend(); - } - } - } catch (InterruptedException | ExecutionException | SoundError e) { - e.printStackTrace(); - suspend(); - } finally { - try { - soundBuffer.shutdown(); - } catch (InterruptedException | ExecutionException | SoundError e) { - e.printStackTrace(); - } - } - }); - playbackThread.start(); + // if (playbackThread != null && !playbackThread.isAlive()) { + // return; + // } + // playbackThread = new Thread(() -> { + // SoundBuffer soundBuffer = null; + // try { + // soundBuffer = SoundMixer.createBuffer(false); + // while (isRunning()) { + // try { + // soundBuffer.playSample((short) (mixer.doGenerate() * mixerGain)); + // } catch (Exception e) { + // e.printStackTrace(); + // suspend(); + // } + // } + // } catch (InterruptedException | ExecutionException | SoundError e) { + // e.printStackTrace(); + // suspend(); + // } finally { + // try { + // soundBuffer.shutdown(); + // } catch (InterruptedException | ExecutionException | SoundError e) { + // e.printStackTrace(); + // } + // } + // }); + // playbackThread.start(); } private void createFilters() throws Exception { @@ -269,19 +276,22 @@ public class Votrax extends TimedDevice { for (int i = 0; i < 5; i++) { alGetError(); - filters[i] = EXTEfx.alGenFilters(); + filters[i] = alGenFilters(); if (alGetError() != AL_NO_ERROR) { throw new Exception("Failed to create filter " + i); } - if (EXTEfx.alIsFilter(filters[i])) { + if (alIsFilter(filters[i])) { // Set Filter type to Band-Pass and set parameters - EXTEfx.alFilteri(filters[i], EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_BANDPASS); + alFilteri(filters[i], AL_FILTER_TYPE, AL_FILTER_BANDPASS); if (alGetError() != AL_NO_ERROR) { System.out.println("Band pass filter not supported."); } else { - EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAIN, 0.5f); - EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAINHF, 0.5f); + alFilterf(filters[i], AL_BANDPASS_GAIN, 0.5f); + alFilterf(filters[i], AL_BANDPASS_GAINHF, 0.5f); System.out.println("Band pass filter "+i+" created."); + // Now add an aux send for the noise and formant sources to go to this filter + // Inspiration: https://github.com/LWJGL/lwjgl3/blob/master/modules/samples/src/test/java/org/lwjgl/demo/openal/EFXTest.java + alSource3i(formantSource, AL_AUXILIARY_SEND_FILTER, filters[i], 0, 0); } } } @@ -290,14 +300,14 @@ public class Votrax extends TimedDevice { public boolean suspend() { destroyFilters(); - playbackThread = null; + // playbackThread = null; return super.suspend(); } private void destroyFilters() { for (int i = 0; i < 5; i++) { - if (EXTEfx.alIsFilter(filters[i])) { - EXTEfx.alDeleteFilters(filters[i]); + if (alIsFilter(filters[i])) { + alDeleteFilters(filters[i]); } } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6d00fc5..b4a34b9 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -40,6 +40,7 @@ module jace { requires org.lwjgl.openal; requires org.lwjgl.stb; requires org.lwjgl.glfw; + requires org.lwjgl; // requires org.reflections; diff --git a/src/test/java/jace/TestUtils.java b/src/test/java/jace/TestUtils.java index 6313a07..e5abdcd 100644 --- a/src/test/java/jace/TestUtils.java +++ b/src/test/java/jace/TestUtils.java @@ -15,9 +15,14 @@ */ package jace; +import java.io.IOException; +import java.util.Arrays; + import jace.core.CPU; import jace.core.Computer; import jace.core.Device; +import jace.core.RAM; +import jace.core.RAMEvent.TYPE; import jace.core.Utility; import jace.ide.HeadlessProgram; import jace.ide.Program; @@ -36,6 +41,72 @@ public class TestUtils { Emulator.withComputer(Computer::reconfigure); } + public static class FakeRAM extends RAM { + byte[] memory = new byte[65536]; + public byte read(int address, TYPE eventType, boolean triggerEvent, boolean requireSyncronization) { + return memory[address & 0x0ffff]; + } + public byte readRaw(int address) { + return memory[address & 0x0ffff]; + } + + public void write(int address, byte value, boolean triggerEvent, boolean requireSyncronization) { + memory[address & 0x0ffff] = value; + } + + @Override + public String getName() { + return "Fake ram"; + } + + @Override + public String getShortName() { + return "ram"; + } + + @Override + public void reconfigure() { + } + + @Override + public void configureActiveMemory() { + } + + @Override + protected void loadRom(String path) throws IOException { + } + + @Override + public void attach() { + } + + @Override + public void performExtendedCommand(int i) { + } + + @Override + public String getState() { + return ""; + } + + @Override + public void resetState() { + } + } + + public static void clearFakeRam(RAM ram) { + Arrays.fill(((FakeRAM) ram).memory, (byte) 0); + } + + public static RAM initFakeRam() { + RAM ram = new FakeRAM(); + Emulator.withComputer(c -> { + c.setMemory(ram); + c.getCpu().setMemory(ram); + }); + return ram; + } + public static void assemble(String code, int addr) throws Exception { runAssemblyCode(code, addr, 0); } diff --git a/src/test/java/jace/apple2e/CpuUnitTest.java b/src/test/java/jace/apple2e/CpuUnitTest.java new file mode 100644 index 0000000..7a7b35f --- /dev/null +++ b/src/test/java/jace/apple2e/CpuUnitTest.java @@ -0,0 +1,263 @@ +package jace.apple2e; +import static jace.TestUtils.*; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import jace.Emulator; +import jace.core.Computer; +import jace.core.RAM; +import jace.core.SoundMixer; +import jace.core.RAMEvent.TYPE; + +public class CpuUnitTest { + // This will loop through each of the files in 65x02_unit_tests/wdc65c02 and run the tests in each file + // The goal is to produce an output report that shows the number of tests that passed and failed + // The output should be reported in a format compatible with junit but also capture multiple potential failures, not just the first faliure + + static Computer computer; + static MOS65C02 cpu; + static RAM ram; + + public static enum Operation { + read, write + } + TypeToken> testCollectionType = new TypeToken>(){}; + record TestResult(String source, String testName, boolean passed, String message) {} + // Note cycles are a mix of int and string so the parser doesn't like to serialize that into well-formed objects + record TestRecord(String name, @SerializedName("initial") MachineState initialState, @SerializedName("final") MachineState finalState, List> cycles) {} + record MachineState(int pc, int s, int a, int x, int y, byte p, List ram) {} + + public static boolean BREAK_ON_FAIL = false; + + @BeforeClass + public static void setUp() { + initComputer(); + SoundMixer.MUTE = true; + computer = Emulator.withComputer(c->c, null); + cpu = (MOS65C02) computer.getCpu(); + ram = initFakeRam(); + } + + @Before + public void resetState() { + // Reinit memory on each test to avoid weird side effects + cpu.reset(); + cpu.resume(); + } + + // Make a list of tests to skip + public static String[] SKIP_TESTS = new String[] { + "cb", "db" + }; + + public static String TEST_FOLDER = "/65x02_unit_tests/wdc65c02/v1"; + @Test + public void testAll() throws IOException, URISyntaxException { + // Read all the files in the directory + // For each file, read the contents and run the tests + + List results = new ArrayList<>(); + // Path testFolder = Paths.get(getClass().getResource("/65x02_unit_tests_wdc65c02/v1").toURI()); + for (String path : getSorted(getResourceListing(TEST_FOLDER))) { + boolean skip = false; + for (String skipPattern : SKIP_TESTS) { + if (path.contains(skipPattern)) { + skip = true; + } + } + if (skip) { + continue; + } + + String file = TEST_FOLDER + "/" + path; + results.addAll(runTest(file)); + if (BREAK_ON_FAIL && results.stream().anyMatch(r->!r.passed())) { + break; + } + } + // Report results + int passed = 0; + Set failedTests = new HashSet<>(); + for (TestResult result : results) { + if (result.passed()) { + passed++; + } else { + failedTests.add(result.testName()); + if (failedTests.size() < 20) { + System.err.println(result.source() + ";" + result.testName() + " " + "FAILED" + ": " + result.message()); + } + } + } + System.err.println("Passed: " + passed + " Failed: " + failedTests.size()); + if (failedTests.size() > 0) { + throw new RuntimeException("One or more tests failed, see log for details"); + } + } + + private String getStatusBits(int status) { + StringBuilder sb = new StringBuilder(); + sb.append((status & 0x80) != 0 ? "N" : "-"); + sb.append((status & 0x40) != 0 ? "V" : "-"); + sb.append((status & 0x20) != 0 ? "-" : "?"); + sb.append((status & 0x10) != 0 ? "B" : "-"); + sb.append((status & 0x08) != 0 ? "D" : "-"); + sb.append((status & 0x04) != 0 ? "I" : "-"); + sb.append((status & 0x02) != 0 ? "Z" : "-"); + sb.append((status & 0x01) != 0 ? "C" : "-"); + return sb.toString(); + } + + private Collection runTest(String file) { + Gson gson = new Gson(); + List results = new ArrayList<>(); + // Read the file which is a JSON file and parse it. + try { + // Given the JSON data in source, parse it to a usable list of tests + // For each test, run the test + Collection tests = gson.fromJson(new InputStreamReader(getClass().getResourceAsStream(file)), testCollectionType.getType()); + for (TestRecord t : tests) { + String name = t.name() + "_%d cycles_%04X->%04X".formatted(t.cycles().size(), t.initialState().pc(), t.finalState().pc()); + + // Set up the initial state by setting CPU registers and RAM + cpu.reset(); + cpu.setProgramCounter(t.initialState().pc()); + cpu.STACK = t.initialState().s(); + cpu.A = t.initialState().a(); + cpu.X = t.initialState().x(); + cpu.Y = t.initialState().y(); + cpu.setStatus(t.initialState().p(), true); + // Set up the memory values + for (int[] mem : t.initialState().ram()) { + ram.write(mem[0], (byte) mem[1], false, false); + } + // Step the CPU for each cycle + for (List c : t.cycles()) { + if (BREAK_ON_FAIL) { + cpu.traceLength = 100; + cpu.setTraceEnabled(true); + } + cpu.doTick(); + // TODO: Check the memory accesses + } + // Check the final state + boolean passed = true; + if (cpu.getProgramCounter() != t.finalState().pc()) { + results.add(new TestResult(file.toString(), name, false, "Program Counter mismatch, expected %04X but got %04X".formatted(t.finalState().pc(), cpu.getProgramCounter()))); + passed = false; + } + if (cpu.STACK != t.finalState().s()) { + results.add(new TestResult(file.toString(), name, false, "Stack Pointer mismatch, expected %02X but got %02X".formatted(t.finalState().s(), cpu.STACK))); + passed = false; + } + if (cpu.A != t.finalState().a()) { + results.add(new TestResult(file.toString(), name, false, "Accumulator mismatch, expected %02X but got %02X".formatted(t.finalState().a(), cpu.A))); + passed = false; + } + if (cpu.X != t.finalState().x()) { + results.add(new TestResult(file.toString(), name, false, "X Register mismatch, expected %02X but got %02X".formatted(t.finalState().x(), cpu.X))); + passed = false; + } + if (cpu.Y != t.finalState().y()) { + results.add(new TestResult(file.toString(), name, false, "Y Register mismatch, expected %02X but got %02X".formatted(t.finalState().y(), cpu.Y))); + passed = false; + } + if (cpu.getStatus() != t.finalState().p()) { + results.add(new TestResult(file.toString(), name, false, "Status Register mismatch, expected %s but got %s".formatted(getStatusBits(t.finalState().p()),getStatusBits(cpu.getStatus())))); + passed = false; + } + // Check the memory values + for (int[] mem : t.finalState().ram()) { + byte value = ram.read(mem[0], TYPE.EXECUTE, false, false); + if (value != (byte) mem[1]) { + results.add(new TestResult(file.toString(), name, false, "Memory mismatch at address %04X, expected %02X but got %02X".formatted(mem[0], mem[1], value))); + // results.add(new TestResult(file.toString(), name, false, "Memory mismatch, expected %s but got %s".formatted(getStatusBits(mem[1]),getStatusBits(value)))); + passed = false; + } + } + if (passed) { + results.add(new TestResult(file.toString(), t.name(), true, "All checks passed")); + } else if (BREAK_ON_FAIL) { + break; + } + // Clear out the memory for the next test + for (int[] mem : t.finalState().ram()) { + ram.write(mem[0], (byte) 0, false, false); + } + } + } catch (Exception e) { + results.add(new TestResult(file.toString(), "", false, "Unable to read file: " + e.getMessage())); + return results; + } + + return results; + } + +private String[] getSorted(String[] values) { + Set set = new TreeSet<>(); + for (String value : values) { + set.add(value); + } + return set.toArray(new String[0]); +} + + private String[] getResourceListing(String path) throws URISyntaxException, IOException { + URL dirURL = getClass().getResource(path); + if (dirURL != null && dirURL.getProtocol().equals("file")) { + /* A file path: easy enough */ + return new File(dirURL.toURI()).list(); + } + + if (dirURL == null) { + /* + * In case of a jar file, we can't actually find a directory. + * Have to assume the same jar as clazz. + */ + String me = getClass().getName().replace(".", "/")+".class"; + dirURL = getClass().getClassLoader().getResource(me); + } + + if (dirURL.getProtocol().equals("jar")) { + /* A JAR path */ + String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file + try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"))) { + Enumeration entries = jar.entries(); //gives ALL entries in jar + Set result = new HashSet(); //avoid duplicates in case it is a subdirectory + while(entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + if (name.startsWith(path)) { //filter according to the path + String entry = name.substring(path.length()); + int checkSubdir = entry.indexOf("/"); + if (checkSubdir >= 0) { + // if it is a subdirectory, we just return the directory name + entry = entry.substring(0, checkSubdir); + } + result.add(entry); + } + } + return result.toArray(new String[result.size()]); + } + } + throw new IOException("Unable to locate resource folder for path: " + path); + } +} + \ No newline at end of file