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 extends TestResult> 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