15 Commits

Author SHA1 Message Date
Brendan Robert
93faf14815 Add missing JavaFX reflection configuration for native build
- Add javafx.scene.layout.GridPane and related methods
- Add javafx.scene.layout.ColumnConstraints
- Add javafx.scene.layout.RowConstraints
- Add javafx.geometry.HPos and VPos enums
- Add TilePane.setPrefColumns and setPrefRows methods

These reflection entries are required for FXML loading in native builds
to access JavaFX layout properties via reflection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 15:57:50 -05:00
Brendan Robert
c2cacbac17 Fix GluonFX configuration for Windows native executable build
- Add <target>host</target> to build executable instead of shared library
- Add <executable>Jace</executable> to specify output name
- Add <appIdentifier>jace</appIdentifier> for application ID
- Fix resourcesList XML syntax error (removed "ceAppl" typo)
- Add nativeImageArgs with --no-fallback flag
- Document Windows-specific PATH requirement for Visual Studio linker

The key issue was that Git's link command was found before Visual Studio's
link.exe in PATH, causing the native image linking to fail. Users must now
prepend the VS tools directory to PATH before building.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 15:42:16 -05:00
Badvision
8397dfcc36 Fix for bit unit tests, add hayes micromodem rom 2024-11-06 14:56:13 -06:00
Badvision
dad7632d62 Hide logs from git commits 2024-09-13 11:54:44 -05:00
Badvision
dfc2cbae4e Better handling of zero period values, still silencing if period <= 1 though 2024-08-27 23:46:42 -05:00
Badvision
14b0cfd967 Add mockingboard register logging and workaround for routines that use high frequencies instead of muting channels 2024-08-27 16:49:23 -05:00
Badvision
3701cd5457 Fixed broken interrupt handling 2024-08-25 23:14:47 -05:00
Badvision
15e0133e4b Added single step tests and fixed a lot of CPU bugs 2024-08-25 23:07:51 -05:00
Badvision
9c71dec304 Fixed a lot of weird CPU bugs and added the 65c02 singe step tests 2024-08-25 23:07:32 -05:00
Badvision
ba3a246f27 Make a better attempt to cancel the twinkle animation properly 2024-08-18 22:48:38 -05:00
Badvision
bef93772de Remove unused import 2024-08-18 22:35:23 -05:00
Badvision
4db3cca98d Inhibit fake reads from afecting any cards 2024-08-16 01:46:26 -05:00
Badvision
5444f40bd4 Fixed a big bug in softswitch handling of hires switch (was not triggering memory layout updates!); Got Aux memory tests working 2024-08-16 01:26:55 -05:00
Badvision
afebe4d56f Add Aux LC test 2024-08-15 08:58:38 -05:00
Badvision
2304eaab30 Fix fake reads so DiskII doesn't break 2024-08-14 22:51:53 -05:00
31 changed files with 806 additions and 168 deletions

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ hs_err_pid*
!/lib/nestedvm.jar
_acme_tmp*
.vscode/settings.json
*.log

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/test/resources/65x02_unit_tests"]
path = src/test/resources/65x02_unit_tests
url = https://github.com/SingleStepTests/65x02

View File

@@ -1 +1 @@
graalvm64-17.0.3
23

View File

@@ -48,6 +48,17 @@ The Gluon documentation provides a compatibility matrix for each OS platform and
All other native dependencies are automatically downloaded as needed by Maven for the various LWJGL libraries.
### Windows-specific build notes:
On Windows, the Visual Studio linker must be in your PATH before Git's linker. The easiest way to build is to use Git Bash with the Visual Studio tools directory prepended to PATH:
```bash
export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.41.34120/bin/Hostx64/x64:$PATH"
mvn clean gluonfx:build
```
The native executable will be created at `target\gluonfx\x86_64-windows\Jace.exe`. Note: You may need to adjust the MSVC version number (14.41.34120) to match your installed Visual Studio version.
### First time build note:
Because Jace provides an annotation processor for compilation, there is a chicken-and-egg problem when building the first time. Currently, this means the first time you compile, run `mvn install` twice. You don't have to do this step again as long as Maven is able to find a previously build version of Jace to provide this annotation processor. I tried to set up the profiles in the pom.xml so that it disables the annotation processor the first time you compile to avoid any issues. If running in a CICD environment, keep in mind you will likely always need to run the "mvn install" step twice, but only if your goal is to build the entire application including the annotations (should not be needed for just running unit tests.)

14
pom.xml
View File

@@ -31,14 +31,20 @@
<artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.23</version>
<configuration>
<target>host</target>
<mainClass>jace.JaceApplication</mainClass>
<resourcesList>ceAppl
<executable>Jace</executable>
<appIdentifier>jace</appIdentifier>
<resourcesList>
<resource>.*</resource>
</resourcesList>
<releaseConfiguration>
<vendor>org.badvision</vendor>
<skipSigning>true</skipSigning>
</releaseConfiguration>
<nativeImageArgs>
<arg>--no-fallback</arg>
</nativeImageArgs>
</configuration>
</plugin>
<plugin>
@@ -189,6 +195,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>

View File

@@ -413,8 +413,8 @@ public class Apple2e extends Computer {
animCycleNumber++;
} else {
getMemory().write(animAddr, animOldValue, true, true);
animationSchedule.cancel(false);
animAddr = 0;
animationSchedule.cancel(true);
}
};
@@ -422,6 +422,9 @@ public class Apple2e extends Computer {
if (hints.isEmpty()) {
hints.add(getMemory().observe("Helpful hints", RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{
animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS);
if (animationSchedule != null) {
animationSchedule.cancel(true);
}
animationSchedule =
animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS);
}));

View File

@@ -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);
@@ -415,7 +420,7 @@ public class MOS65C02 extends CPU {
int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
int address = 0x0FFFF & (address2 + cpu.X);
// False read
cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
cpu.getMemory().read(address, TYPE.READ_FAKE, true, false);
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
@@ -424,7 +429,7 @@ public class MOS65C02 extends CPU {
int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
int address = 0x0FFFF & (address2 + cpu.Y);
// False read
cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
cpu.getMemory().read(address, TYPE.READ_FAKE, true, false);
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
@@ -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
@@ -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);
}

View File

@@ -194,9 +194,9 @@ abstract public class RAM128k extends RAM {
public String getReadConfiguration() {
String rstate = "";
if (SoftSwitches.RAMRD.getState()) {
rstate += "Ra";
rstate += "Ra_";
} else {
rstate += "R0";
rstate += "R0_";
}
String LCR = "L0R";
if (SoftSwitches.LCRAM.isOn()) {
@@ -214,16 +214,16 @@ abstract public class RAM128k extends RAM {
}
rstate += LCR;
if (SoftSwitches.CXROM.getState()) {
rstate += "CXROM";
rstate += "_CXROM";
} else {
rstate += "!CX";
rstate += "_!CX";
if (SoftSwitches.SLOTC3ROM.isOff()) {
rstate += "C3";
rstate += "_C3";
}
if (SoftSwitches.INTC8ROM.isOn()) {
rstate += "C8";
rstate += "_C8";
} else {
rstate += "C8"+getActiveSlot();
rstate += "_C8"+getActiveSlot();
}
}
@@ -233,9 +233,9 @@ abstract public class RAM128k extends RAM {
public String getWriteConfiguration() {
String wstate = "";
if (SoftSwitches.RAMWRT.getState()) {
wstate += "Wa";
wstate += "Wa_";
} else {
wstate += "W0";
wstate += "W0_";
}
String LCW = "L0W";
if (SoftSwitches.LCWRITE.isOn()) {
@@ -256,22 +256,26 @@ abstract public class RAM128k extends RAM {
}
public String getAuxZPConfiguration() {
String astate = "";
String astate = "__";
if (SoftSwitches._80STORE.isOn()) {
astate += "80S";
astate += "80S_";
if (SoftSwitches.PAGE2.isOn()) {
astate += "2";
astate += "P2_";
} else {
astate += "P1_";
}
if (SoftSwitches.HIRES.isOn()) {
astate += "H";
astate += "H1_";
} else {
astate += "H0_";
}
}
// Handle zero-page bankswitching
if (SoftSwitches.AUXZP.getState()) {
astate += "Za";
astate += "Za_";
} else {
astate += "Z0";
astate += "Z0_";
}
return astate;
}
@@ -417,7 +421,8 @@ abstract public class RAM128k extends RAM {
state = newState;
log("MMU Switches");
// System.out.println("read: " + readConfiguration);
// System.out.println("write: " + writeConfiguration);
if (memoryConfigurations.containsKey(readConfiguration)) {
activeRead = memoryConfigurations.get(readConfiguration);
} else {

View File

@@ -74,11 +74,20 @@ public enum SoftSwitches {
if (_80STORE.isOn()) {
Emulator.withMemory(m->m.configureActiveMemory());
} else {
Emulator.withVideo(v->v.configureVideoMode());
super.stateChanged();
}
}
}),
HIRES(new VideoSoftSwitch("Hires", 0x0c056, 0x0c057, 0x0c01d, RAMEvent.TYPE.ANY, false)),
HIRES(new VideoSoftSwitch("Hires", 0x0c056, 0x0c057, 0x0c01d, RAMEvent.TYPE.ANY, false) {
@Override
public void stateChanged() {
// PAGE2 is a hybrid switch; 80STORE ? memory : video
if (_80STORE.isOn()) {
Emulator.withMemory(m->m.configureActiveMemory());
}
super.stateChanged();
}
}),
DHIRES(new VideoSoftSwitch("Double-hires", 0x0c05f, 0x0c05e, 0x0c07f, RAMEvent.TYPE.ANY, false)),
PB0(new MemorySoftSwitch("Pushbutton0", -1, -1, 0x0c061, RAMEvent.TYPE.ANY, null)),
PB1(new MemorySoftSwitch("Pushbutton1", -1, -1, 0x0c062, RAMEvent.TYPE.ANY, null)),

View File

@@ -38,7 +38,7 @@ public class MemorySoftSwitch extends SoftSwitch {
@Override
public void stateChanged() {
// System.out.println(getName()+ " was switched to "+getState());
// System.out.println(getName()+ " was switched to "+getState());
Emulator.withMemory(m->m.configureActiveMemory());
}

View File

@@ -17,6 +17,7 @@
package jace.core;
import jace.apple2e.SoftSwitches;
import jace.core.RAMEvent.TYPE;
/**
* Card is an abstraction of an Apple ][ hardware module which can carry its own
@@ -113,6 +114,9 @@ public abstract class Card extends TimedDevice {
int baseIO = 0x0c080 + slot * 16;
int baseRom = 0x0c000 + slot * 256;
ioListener = getMemory().observe("Slot " + getSlot() + " " + getDeviceName() + " IO access", RAMEvent.TYPE.ANY, baseIO, baseIO + 15, (e) -> {
if (e.getType() == TYPE.READ_FAKE) {
return;
}
int address = e.getAddress() & 0x0f;
handleIOAccess(address, e.getType(), e.getNewValue(), e);
});
@@ -121,7 +125,10 @@ public abstract class Card extends TimedDevice {
getMemory().setActiveCard(slot);
// Sather 6-4: Writes will still go through even when CXROM inhibits slot ROM
if (SoftSwitches.CXROM.isOff() || !e.getType().isRead()) {
handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e);
if (e.getType() == TYPE.READ_FAKE) {
return;
}
handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e);
}
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -40,6 +40,7 @@ public class RAMEvent {
READ(true),
READ_DATA(true),
READ_OPERAND(true),
READ_FAKE(true),
EXECUTE(true),
WRITE(false),
ANY(false);

View File

@@ -17,6 +17,8 @@
package jace.hardware;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -55,6 +57,19 @@ public class CardHayesMicromodem extends CardSSC {
TRANS_IRQ_ENABLED = false;
}
@Override
public void loadRom() throws IOException {
String path = "/jace/data/hayes-micromodem-8308a271.rom";
// Load rom file, first 0x0FF bytes are CX rom, next 0x0800 bytes are C8 rom
try (InputStream romFile = CardSSC.class.getResourceAsStream(path)) {
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0800;
byte[] rom8Data = new byte[c8RomLength];
getC8Rom().loadData(rom8Data);
getCxRom().loadData(Arrays.copyOf(rom8Data, cxRomLength));
}
}
@Override
public void clientConnected() {
setRingIndicator(true);
@@ -105,11 +120,6 @@ public class CardHayesMicromodem extends CardSSC {
}
}
@Override
public void loadRom(String path) throws IOException {
// Do nothing -- there is no rom for this card right now.
}
/**
* @return the ringIndicator
*/

View File

@@ -162,10 +162,10 @@ public class CardMockingboard extends Card {
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
e.setNewValue(val);
if (DEBUG) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
// if (DEBUG) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
if (DEBUG) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
// if (DEBUG) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
}
// Any firmware access will reset the idle counter and wake up the card, this allows the timers to start running again
// Games such as "Skyfox" use the timer to detect if the card is present.

View File

@@ -110,7 +110,7 @@ public class CardSSC extends Card {
@Override
public void setSlot(int slot) {
try {
loadRom("/jace/data/SSC.rom");
loadRom();
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -173,22 +173,25 @@ public class CardSSC extends Card {
System.out.println("Client disconnected");
}
public void loadRom(String path) throws IOException {
public void loadRom() throws IOException {
System.out.println("Loading SSC rom");
String path = "/jace/data/SSC.rom";
// Load rom file, first 0x0700 bytes are C8 rom, last 0x0100 bytes are CX rom
// CF00-CFFF are unused by the SSC
InputStream romFile = CardSSC.class.getResourceAsStream(path);
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0700;
byte[] romxData = new byte[cxRomLength];
byte[] rom8Data = new byte[c8RomLength];
if (romFile.read(rom8Data) != c8RomLength) {
throw new IOException("Bad SSC rom size");
try (InputStream romFile = CardSSC.class.getResourceAsStream(path)) {
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0700;
byte[] romxData = new byte[cxRomLength];
byte[] rom8Data = new byte[c8RomLength];
if (romFile.read(rom8Data) != c8RomLength) {
throw new IOException("Bad SSC rom size");
}
getC8Rom().loadData(rom8Data);
if (romFile.read(romxData) != cxRomLength) {
throw new IOException("Bad SSC rom size");
}
getCxRom().loadData(romxData);
}
getC8Rom().loadData(rom8Data);
if (romFile.read(romxData) != cxRomLength) {
throw new IOException("Bad SSC rom size");
}
getCxRom().loadData(romxData);
}
@Override
@@ -333,6 +336,8 @@ public class CardSSC extends Card {
}
}
break;
case READ_FAKE:
return;
}
if (newValue > -1) {
e.setNewValue(newValue);

View File

@@ -75,7 +75,9 @@ public class EnvelopeGenerator extends TimedGenerator {
}
}
int shape;
public void setShape(int shape) {
this.shape = shape & 15;
oddEven = false;
counter = 0;
cont = (shape & 8) != 0;

View File

@@ -23,6 +23,8 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import jace.hardware.CardMockingboard;
/**
* Implementation of the AY sound PSG chip. This class manages register values
* and mixes the channels together (in the update method) The work of
@@ -225,6 +227,32 @@ public class PSG {
case PortA, PortB -> {
}
}
if (CardMockingboard.DEBUG) {
debugStatus();
}
}
String lastStatus = "";
public void debugStatus() {
String status = String.format("b%02X: A %03X %s %01X | B %03X %s %01X | C %03X %s %01X | N %03X | E %01X %04X",
baseReg,
channels.get(0).period,
(channels.get(0).active ? "T" : "_") + (channels.get(0).noiseActive ? "N" : "_"),
channels.get(0).amplitude,
channels.get(1).period,
(channels.get(1).active ? "T" : "_") + (channels.get(1).noiseActive ? "N" : "_"),
channels.get(1).amplitude,
channels.get(2).period,
(channels.get(2).active ? "T" : "_") + (channels.get(2).noiseActive ? "N" : "_"),
channels.get(2).amplitude,
noiseGenerator.period,
envelopeGenerator.shape,
envelopeGenerator.period
);
if (!lastStatus.equals(status)) {
System.out.println(status);
lastStatus = status;
}
}
public void update(AtomicInteger bufA, boolean clearA, AtomicInteger bufB, boolean clearB, AtomicInteger bufC, boolean clearC) {

View File

@@ -51,7 +51,7 @@ public class TimedGenerator {
}
public void setPeriod(int _period) {
period = _period > 0 ? _period : 1;
period = _period;
clocksPerPeriod = (period * stepsPerCycle());
// set counter back... necessary?
// while (clocksPerPeriod > period) {
@@ -60,6 +60,10 @@ public class TimedGenerator {
}
protected int updateCounter() {
// Period == 0 means the generator is off
if (period <= 1 || clocksPerPeriod <= 1) {
return 0;
}
counter += cyclesPerSample;
int numStateChanges = 0;
while (counter >= clocksPerPeriod) {
@@ -71,6 +75,6 @@ public class TimedGenerator {
public void reset() {
counter = 0;
period = 1;
period = 0;
}
}

View File

@@ -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);
}
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]);
}
}
}

View File

@@ -40,6 +40,7 @@ module jace {
requires org.lwjgl.openal;
requires org.lwjgl.stb;
requires org.lwjgl.glfw;
requires org.lwjgl;
// requires org.reflections;

View File

@@ -571,10 +571,18 @@
"name":"javafx.geometry.NodeOrientation",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"javafx.geometry.HPos",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"javafx.geometry.Pos",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"javafx.geometry.VPos",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"javafx.scene.Camera"
},
@@ -852,15 +860,65 @@
],
"queriedMethods":[{"name":"getAlignment","parameterTypes":["javafx.scene.Node"] }]
},
{
"name":"javafx.scene.layout.ColumnConstraints",
"queryAllDeclaredMethods":true,
"queryAllPublicConstructors":true,
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"setHalignment","parameterTypes":["javafx.geometry.HPos"] },
{"name":"setHgrow","parameterTypes":["javafx.scene.layout.Priority"] },
{"name":"setMinWidth","parameterTypes":["double"] },
{"name":"setMaxWidth","parameterTypes":["double"] },
{"name":"setPrefWidth","parameterTypes":["double"] }
]
},
{
"name":"javafx.scene.layout.RowConstraints",
"queryAllDeclaredMethods":true,
"queryAllPublicConstructors":true,
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"setValignment","parameterTypes":["javafx.geometry.VPos"] },
{"name":"setVgrow","parameterTypes":["javafx.scene.layout.Priority"] },
{"name":"setMinHeight","parameterTypes":["double"] },
{"name":"setMaxHeight","parameterTypes":["double"] },
{"name":"setPrefHeight","parameterTypes":["double"] }
]
},
{
"name":"javafx.scene.layout.GridPane",
"queryAllDeclaredMethods":true,
"queryAllPublicConstructors":true,
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"getColumnConstraints","parameterTypes":[] },
{"name":"getRowConstraints","parameterTypes":[] },
{"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
{"name":"setAlignment","parameterTypes":["javafx.scene.Node","javafx.geometry.Pos"] },
{"name":"setColumnIndex","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
{"name":"setRowIndex","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
{"name":"setColumnSpan","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
{"name":"setRowSpan","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
{"name":"setHalignment","parameterTypes":["javafx.scene.Node","javafx.geometry.HPos"] },
{"name":"setValignment","parameterTypes":["javafx.scene.Node","javafx.geometry.VPos"] },
{"name":"setMargin","parameterTypes":["javafx.scene.Node","javafx.geometry.Insets"] }
],
"queriedMethods":[
{"name":"getAlignment","parameterTypes":["javafx.scene.Node"] },
{"name":"getColumnIndex","parameterTypes":["javafx.scene.Node"] },
{"name":"getRowIndex","parameterTypes":["javafx.scene.Node"] }
]
},
{
"name":"javafx.scene.layout.HBox",
"queryAllDeclaredMethods":true,
"queryAllPublicConstructors":true,
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
{"name":"setFillHeight","parameterTypes":["boolean"] },
{"name":"setHgrow","parameterTypes":["javafx.scene.Node","javafx.scene.layout.Priority"] },
{"name":"<init>","parameterTypes":[] },
{"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
{"name":"setFillHeight","parameterTypes":["boolean"] },
{"name":"setHgrow","parameterTypes":["javafx.scene.Node","javafx.scene.layout.Priority"] },
{"name":"setMargin","parameterTypes":["javafx.scene.Node","javafx.geometry.Insets"] }
],
"queriedMethods":[{"name":"getHgrow","parameterTypes":["javafx.scene.Node"] }]
@@ -905,11 +963,13 @@
"queryAllDeclaredMethods":true,
"queryAllPublicConstructors":true,
"methods":[
{"name":"<init>","parameterTypes":[] },
{"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
{"name":"setAlignment","parameterTypes":["javafx.scene.Node","javafx.geometry.Pos"] },
{"name":"setHgap","parameterTypes":["double"] },
{"name":"setVgap","parameterTypes":["double"] }
{"name":"<init>","parameterTypes":[] },
{"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
{"name":"setAlignment","parameterTypes":["javafx.scene.Node","javafx.geometry.Pos"] },
{"name":"setHgap","parameterTypes":["double"] },
{"name":"setVgap","parameterTypes":["double"] },
{"name":"setPrefColumns","parameterTypes":["int"] },
{"name":"setPrefRows","parameterTypes":["int"] }
],
"queriedMethods":[{"name":"getAlignment","parameterTypes":["javafx.scene.Node"] }]
},

View File

@@ -8,7 +8,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jace.apple2e.Full65C02Test;
import jace.apple2e.MOS65C02;
import jace.core.Computer;

View File

@@ -15,9 +15,16 @@
*/
package jace;
import java.io.IOException;
import java.util.Arrays;
import jace.apple2e.RAM128k;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.Device;
import jace.core.PagedMemory;
import jace.core.RAM;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.ide.HeadlessProgram;
import jace.ide.Program;
@@ -36,6 +43,89 @@ public class TestUtils {
Emulator.withComputer(Computer::reconfigure);
}
public static class FakeRAM extends RAM128k {
PagedMemory fakeMemory = new PagedMemory(0x0, PagedMemory.Type.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() {
}
@Override
public PagedMemory getAuxVideoMemory() {
return fakeMemory;
}
@Override
public PagedMemory getAuxMemory() {
return fakeMemory;
}
@Override
public PagedMemory getAuxLanguageCard() {
return fakeMemory;
}
@Override
public PagedMemory getAuxLanguageCard2() {
return fakeMemory;
}
}
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);
}

View File

@@ -0,0 +1,262 @@
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 (@SuppressWarnings("unused") List<String> c : t.cycles()) {
if (BREAK_ON_FAIL) {
cpu.traceLength = 100;
cpu.setTraceEnabled(true);
}
cpu.doTick();
}
// 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);
}
}

View File

@@ -400,18 +400,18 @@ public class Full65C02Test {
new TestProgram()
.add("LDA #$FF")
.add("STA $FF")
.assertTimed("BIT #0", 2)
.assertFlags(IS_ZERO, POSITIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$FF", 2)
.assertTimed("BIT #0", 3)
.assertFlags(IS_ZERO, NEGATIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$FF", 3)
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_CLEAR)
.assertTimed("BIT $FF", 3)
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_SET)
.add("CLV")
.add("LDA #$40")
.assertTimed("BIT #$40", 2)
.assertTimed("BIT #$40", 3)
.assertFlags(NOT_ZERO, POSITIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$80", 2)
.assertFlags(IS_ZERO, NEGATIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$80", 3)
.assertFlags(IS_ZERO, POSITIVE, OVERFLOW_CLEAR)
.assertTimed("BIT $1000", 4)
.assertTimed("BIT $1000,x", 4)
.assertTimed("BIT $00,X", 4)

View File

@@ -24,6 +24,7 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
@@ -36,6 +37,7 @@ import jace.TestProgram;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
import jace.core.RAMEvent.TYPE;
/**
* Test that memory listeners fire appropriately.
@@ -68,9 +70,13 @@ public class MemoryTest {
}
@Before
public void setup() {
public void resetEmulator() {
computer.pause();
cpu.clearState();
}
@Before
public void resetSoftSwitches() {
// Reset softswitches
for (SoftSwitches softswitch : SoftSwitches.values()) {
softswitch.getSwitch().reset();
@@ -194,7 +200,7 @@ public class MemoryTest {
* @throws ProgramException
*/
@Test
public void machineIdentificationTEst() throws ProgramException {
public void machineIdentificationTest() throws ProgramException {
TestProgram memoryDetectTestProgram = new TestProgram(MEMORY_TEST_COMMONS);
memoryDetectTestProgram.add(MACHINE_IDENTIFICATION);
// Assert this is an Apple //e
@@ -229,7 +235,6 @@ public class MemoryTest {
sta $FE1F ; FE1F is $60 in Apple II/plus/e/enhanced
cmp $FE1F
""")
//
.assertEquals("E0005: We tried to put the language card into read RAM, write RAM, but failed to write.")
.add("""
lda $C083 ; Read and write bank 2
@@ -500,6 +505,81 @@ public class MemoryTest {
!byte $23, $34, $11, $23, $34
!byte 0
""")
.runForTicks(10000000);
// .runForTicks(10000000);
.run();
}
@Test
public void auxLanguageCardTest() throws ProgramException {
// This is a repeat of the LC test but with AUX enabled
SoftSwitches.AUXZP.getSwitch().setState(true);
SoftSwitches.RAMRD.getSwitch().setState(true);
SoftSwitches.RAMWRT.getSwitch().setState(true);
SoftSwitches._80STORE.getSwitch().setState(true);
languageCardBankswitchTest();
}
public record MemoryTestCase(int[] softswitches, byte... expected) {}
int[] testLocations = {
0x0FF, 0x100, 0x200, 0x3FF, 0x427, 0x7FF, 0x800, 0x1FFF,
0x2000, 0x3FFF, 0x4000, 0x5FFF, 0xBFFF
};
private void assertMemoryTest(MemoryTestCase testCase) {
// Set the values in memory in main and aux banks
// This is done directly to ensure the values are exactly as expected
// The next tests will try to read these values using the softswitches
for (int location : testLocations) {
((RAM128k) ram).getMainMemory().writeByte(location, (byte) 1);
((RAM128k) ram).getAuxMemory().writeByte(location, (byte) 3);
}
resetSoftSwitches();
for (int softswitch : testCase.softswitches) {
System.out.println("Setting softswitch " + Integer.toHexString(softswitch));
ram.write(softswitch, (byte) 0, true, false);
}
for (int i=0; i < testLocations.length; i++) {
int address = testLocations[i];
byte current = ram.read(address, TYPE.READ_DATA, false, false);
ram.write(address, (byte) (current+1), false, false);
byte expected = testCase.expected[i];
try {
assertEquals("Unexpected value at " + Integer.toHexString(address), expected, ram.read(address, TYPE.READ_DATA, false, false));
} catch (AssertionError err) {
for (SoftSwitches softswitch : SoftSwitches.values()) {
System.out.println(MessageFormat.format("{0}\t{1}", softswitch.name(), (softswitch.isOn() ? "on" : "off")));
}
throw err;
}
}
}
@Test
public void auxBankSwitchTest() throws ProgramException {
byte M1 = (byte) 1; // Main + no change
byte M2 = (byte) 2; // Main + 1
byte A1 = (byte) 3; // Aux + no change
byte A2 = (byte) 4; // Aux + 1
// 80 STORE + RAMWRT + HIRES / Page 1 (Main mem)
assertMemoryTest(new MemoryTestCase(new int[] {0x0C005, 0x0C001, 0x0C057},
M2, M2, M1, M1, M2, M2, M1, M1, M2, M2, M1, M1, M1));
// RAMRD + AUXZP
assertMemoryTest(new MemoryTestCase(new int[] {0xC003, 0xC009},
A2, A2, A1, A1, A1, A1, A1, A1, A1, A1, A1, A1, A1));
// RAMRD + MAINZP
assertMemoryTest(new MemoryTestCase(new int[] {0xC003, 0xC008},
M2, M2, A1, A1, A1, A1, A1, A1, A1, A1, A1, A1, A1));
// 80 STORE + HIRES' + Page 2
assertMemoryTest(new MemoryTestCase(new int[] {0x0C001, 0x0C056, 0x0C055},
M2, M2, M2, M2, A2, A2, M2, M2, M2, M2, M2, M2, M2));
// 80 STORE + HIRES + Page 2
assertMemoryTest(new MemoryTestCase(new int[] {0x0C001, 0x0C057, 0x0C055},
M2, M2, M2, M2, A2, A2, M2, M2, A2, A2, M2, M2, M2));
}
}

View File

@@ -188,15 +188,18 @@ RESETALL
sta CSW
lda #>COUT1
sta CSW+1
sta RESET_RAMRD
sta RESET_RAMWRT
; Zelly's original test resets flags, but we might want to test with flags set.
; So only reset softswitches that don't affect memory state.
; Anyway, our @before setup function does this part already.
;sta RESET_RAMRD
;sta RESET_RAMWRT
;; Save return address in X and A, in case we switch zero-page memory.
sta RESET_80STORE
;sta RESET_80STORE
sta RESET_INTCXROM
sta RESET_ALTZP
;sta RESET_ALTZP
sta RESET_SLOTC3ROM
sta RESET_INTC8ROM
sta RESET_80COL
;sta RESET_80COL
sta RESET_ALTCHRSET
sta SET_TEXT
sta RESET_MIXED