mirror of
https://github.com/badvision/jace.git
synced 2024-12-27 04:29:35 +00:00
Added single step tests and fixed a lot of CPU bugs
This commit is contained in:
parent
9c71dec304
commit
15e0133e4b
6
pom.xml
6
pom.xml
@ -189,6 +189,12 @@
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<scope>test</scope>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial.thirdparty</groupId>
|
||||
<artifactId>nestedvm</artifactId>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<Double> 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<Generator> inputs = new ArrayList<>();
|
||||
public List<Double> 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);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ module jace {
|
||||
requires org.lwjgl.openal;
|
||||
requires org.lwjgl.stb;
|
||||
requires org.lwjgl.glfw;
|
||||
requires org.lwjgl;
|
||||
|
||||
// requires org.reflections;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
263
src/test/java/jace/apple2e/CpuUnitTest.java
Normal file
263
src/test/java/jace/apple2e/CpuUnitTest.java
Normal file
@ -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<Collection<TestRecord>> testCollectionType = new TypeToken<Collection<TestRecord>>(){};
|
||||
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<List<String>> cycles) {}
|
||||
record MachineState(int pc, int s, int a, int x, int y, byte p, List<int[]> 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<TestResult> 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<String> 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<TestResult> 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<TestRecord> 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<String> 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(), "<INIT>", false, "Unable to read file: " + e.getMessage()));
|
||||
return results;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private String[] getSorted(String[] values) {
|
||||
Set<String> 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<JarEntry> entries = jar.entries(); //gives ALL entries in jar
|
||||
Set<String> result = new HashSet<String>(); //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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user