mirror of
https://github.com/badvision/jace.git
synced 2026-03-11 08:42:12 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93faf14815 | ||
|
|
c2cacbac17 | ||
|
|
8397dfcc36 | ||
|
|
dad7632d62 | ||
|
|
dfc2cbae4e | ||
|
|
14b0cfd967 | ||
|
|
3701cd5457 | ||
|
|
15e0133e4b | ||
|
|
9c71dec304 | ||
|
|
ba3a246f27 | ||
|
|
bef93772de | ||
|
|
4db3cca98d | ||
|
|
5444f40bd4 | ||
|
|
afebe4d56f | ||
|
|
2304eaab30 | ||
|
|
519c561537 | ||
|
|
1c26ecad3d | ||
|
|
751c66c53f | ||
|
|
f34ba40ff0 | ||
|
|
45dfabfe14 | ||
|
|
d6252e5c8b | ||
|
|
9e04a05726 | ||
|
|
7f598bcdc1 | ||
|
|
dafd4453eb | ||
|
|
3a8e55d9dd | ||
|
|
1fb9f925fc | ||
|
|
2dfe146e54 | ||
|
|
bb90292ab5 | ||
|
|
39dd9d81b5 | ||
|
|
88cd03a9e6 | ||
|
|
455151abf7 | ||
|
|
9769f3282a | ||
|
|
4c6a8f976c | ||
|
|
6d1a5e7edd | ||
|
|
7e638dbf05 | ||
|
|
41310b43e7 | ||
|
|
2d2753ed99 | ||
|
|
0f475ba186 | ||
|
|
f413c1baad | ||
|
|
55d444477c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,3 +20,5 @@ hs_err_pid*
|
||||
*.DS_Store
|
||||
!/lib/nestedvm.jar
|
||||
_acme_tmp*
|
||||
.vscode/settings.json
|
||||
*.log
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal 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
|
||||
@@ -1 +1 @@
|
||||
graalvm64-17.0.3
|
||||
23
|
||||
|
||||
13
README.md
13
README.md
@@ -48,12 +48,23 @@ 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.)
|
||||
|
||||
## Support JACE:
|
||||
|
||||
JACE will always be free, but it does take considerable time to refine and add new features. If you would like to show your support and encourage the author to keep maintaining this emulator, why not throw him some change to buy him a drink? (The emulator was named for the Jack and Cokes consumed during its inception.)
|
||||
JACE will always be free, and remain Apache-licensed, but it does take considerable time to refine and add new features. If you would like to show your support and encourage the author to keep maintaining this emulator, why not throw him some change to buy him a drink? (The emulator was named for the Jack and Cokes consumed during its inception.) Also, should you want to use Jace under the terms of the Apache-license for commercial works, you are under no obligation to contribute any source code modifications or royalties to me, but I would appreciate you credit and mention my project and let me know about it.
|
||||
|
||||
Donate here to support Jace developement:
|
||||
<a href="https://www.paypal.me/BrendanRobert"><img src="images/donate.png" width="64"></a>
|
||||
|
||||
52
pom.xml
52
pom.xml
@@ -6,16 +6,16 @@
|
||||
|
||||
<groupId>org.badvision</groupId>
|
||||
<artifactId>jace</artifactId>
|
||||
<version>3.0</version>
|
||||
<version>3.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>JaceApplication</name>
|
||||
<name>Jace</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<mainClass>jace.JaceApplication</mainClass>
|
||||
<netbeans.hint.license>apache20</netbeans.hint.license>
|
||||
<lwjgl.version>3.3.3</lwjgl.version>
|
||||
<lwjgl.version>3.3.4</lwjgl.version>
|
||||
</properties>
|
||||
|
||||
<organization>
|
||||
@@ -24,14 +24,17 @@
|
||||
</organization>
|
||||
|
||||
<build>
|
||||
<finalName>JaceApplication</finalName>
|
||||
<finalName>Jace</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.gluonhq</groupId>
|
||||
<artifactId>gluonfx-maven-plugin</artifactId>
|
||||
<version>1.0.22</version>
|
||||
<version>1.0.23</version>
|
||||
<configuration>
|
||||
<target>host</target>
|
||||
<mainClass>jace.JaceApplication</mainClass>
|
||||
<executable>Jace</executable>
|
||||
<appIdentifier>jace</appIdentifier>
|
||||
<resourcesList>
|
||||
<resource>.*</resource>
|
||||
</resourcesList>
|
||||
@@ -39,6 +42,9 @@
|
||||
<vendor>org.badvision</vendor>
|
||||
<skipSigning>true</skipSigning>
|
||||
</releaseConfiguration>
|
||||
<nativeImageArgs>
|
||||
<arg>--no-fallback</arg>
|
||||
</nativeImageArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -93,7 +99,7 @@
|
||||
<plugin>
|
||||
<groupId>org.moditect</groupId>
|
||||
<artifactId>moditect-maven-plugin</artifactId>
|
||||
<version>1.0.0.Final</version>
|
||||
<version>1.2.2.Final</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<?m2e execute onConfiguration,onIncremental?>
|
||||
@@ -158,7 +164,7 @@
|
||||
<limit>
|
||||
<counter>COMPLEXITY</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.60</minimum>
|
||||
<minimum>0.35</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
@@ -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>
|
||||
@@ -197,31 +209,31 @@
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>21.0.2</version>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>21.0.2</version>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
<version>21.0.2</version>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
<version>21.0.2</version>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
<version>21.0.2</version>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -281,7 +293,7 @@
|
||||
<path>
|
||||
<groupId>org.badvision</groupId>
|
||||
<artifactId>jace</artifactId>
|
||||
<version>3.0</version>
|
||||
<version>3.1</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>jace.config.InvokableActionAnnotationProcessor</annotationProcessors>
|
||||
@@ -307,7 +319,7 @@
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
<version>3.11.0</version>
|
||||
<version>3.13.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
@@ -336,6 +348,18 @@
|
||||
<lwjgl.natives>natives-macos</lwjgl.natives>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>lwjgl-natives-macos-arm64</id>
|
||||
<activation>
|
||||
<os>
|
||||
<family>mac</family>
|
||||
<arch>aarch64</arch>
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<lwjgl.natives>natives-macos-arm64</lwjgl.natives>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>lwjgl-natives-windows-amd64</id>
|
||||
<activation>
|
||||
|
||||
@@ -61,6 +61,10 @@ public class Emulator {
|
||||
if (instance.computer != null) {
|
||||
instance.computer.getMotherboard().suspend();
|
||||
instance.computer.getMotherboard().detach();
|
||||
if (instance.computer.getVideo() != null) {
|
||||
instance.computer.getVideo().suspend();
|
||||
instance.computer.getVideo().detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
instance = null;
|
||||
|
||||
@@ -365,28 +365,28 @@ public class Apple2e extends Computer {
|
||||
return;
|
||||
}
|
||||
int row = 2;
|
||||
for (String s : new String[]{
|
||||
" Welcome to",
|
||||
" _ __ ___ ____ ",
|
||||
" | | / /\\ / / ` | |_ ",
|
||||
" \\_|_| /_/--\\ \\_\\_, |_|__ ",
|
||||
"",
|
||||
" Java Apple Computer Emulator",
|
||||
"",
|
||||
" Presented by BLuRry",
|
||||
" https://goo.gl/SnzqG",
|
||||
"",
|
||||
"To insert a disk, please drag it over",
|
||||
"this window and drop on the desired",
|
||||
"drive icon.",
|
||||
"",
|
||||
"Press CTRL+SHIFT+C for configuration.",
|
||||
"Press CTRL+SHIFT+I for IDE window.",
|
||||
"",
|
||||
"O-A: Alt/Option",
|
||||
"C-A: Shortcut/Command",
|
||||
"Reset: Delete/Backspace"
|
||||
}) {
|
||||
for (String s : """
|
||||
Welcome to
|
||||
_ __ ___ ____
|
||||
| | / /\\ / / ` | |_
|
||||
\\_|_| /_/--\\ \\_\\_, |_|__
|
||||
|
||||
Java Apple Computer Emulator
|
||||
|
||||
Presented by Brendan Robert
|
||||
https://github.com/badvision/jace
|
||||
|
||||
To insert a disk, please drag it over
|
||||
this window and drop on the desired
|
||||
drive icon.
|
||||
|
||||
Press CTRL+SHIFT+C for configuration.
|
||||
Press CTRL+SHIFT+I for IDE window.
|
||||
|
||||
O-A: Alt/Option
|
||||
C-A: Shortcut/Command
|
||||
Reset: Delete/Backspace"""
|
||||
.split("\n")) {
|
||||
int addr = 0x0401 + VideoDHGR.calculateTextOffset(row++);
|
||||
for (char c : s.toCharArray()) {
|
||||
getMemory().write(addr++, (byte) (c | 0x080), false, true);
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.config.ConfigurableField;
|
||||
import jace.core.CPU;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
@@ -137,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),
|
||||
@@ -194,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),
|
||||
@@ -387,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);
|
||||
@@ -397,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,6 +419,8 @@ public class MOS65C02 extends CPU {
|
||||
ABSOLUTE_X(3, "$~2~1,X", (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_FAKE, true, false);
|
||||
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
|
||||
cpu.setPageBoundaryApplied(true);
|
||||
return address;
|
||||
@@ -422,6 +428,8 @@ public class MOS65C02 extends CPU {
|
||||
ABSOLUTE_Y(3, "$~2~1,Y", (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_FAKE, true, false);
|
||||
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
|
||||
cpu.setPageBoundaryApplied(true);
|
||||
return address;
|
||||
@@ -523,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,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;
|
||||
}
|
||||
}),
|
||||
@@ -828,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);
|
||||
@@ -966,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) -> {
|
||||
@@ -1058,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) {
|
||||
@@ -1078,8 +1083,12 @@ public class MOS65C02 extends CPU {
|
||||
wait = 4;
|
||||
}
|
||||
}
|
||||
default -> bytes = 2;
|
||||
default -> {
|
||||
wait = 1;
|
||||
bytes = 1;
|
||||
}
|
||||
}
|
||||
wait--;
|
||||
incrementProgramCounter(bytes);
|
||||
addWaitCycles(wait);
|
||||
|
||||
@@ -1126,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
|
||||
@@ -1137,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;
|
||||
@@ -1168,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
|
||||
@@ -1203,6 +1222,7 @@ public class MOS65C02 extends CPU {
|
||||
public void reset() {
|
||||
pushWord(getProgramCounter());
|
||||
push(getStatus());
|
||||
setWaitCycles(0);
|
||||
// STACK = 0x0ff;
|
||||
// B = false;
|
||||
B = true;
|
||||
@@ -1214,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package jace.apple2e.softswitch;
|
||||
|
||||
import jace.core.RAMEvent;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
|
||||
/**
|
||||
@@ -23,32 +24,30 @@ import jace.core.RAMEvent.TYPE;
|
||||
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
|
||||
*/
|
||||
public class Memory2SoftSwitch extends MemorySoftSwitch {
|
||||
public Memory2SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, TYPE changeType, Boolean initalState) {
|
||||
super(name, offAddress, onAddress, queryAddress, changeType, initalState);
|
||||
}
|
||||
|
||||
public Memory2SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, TYPE changeType, Boolean initalState) {
|
||||
super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState);
|
||||
}
|
||||
|
||||
// The switch must be set true two times in a row before it will actually be set.
|
||||
int count = 0;
|
||||
int readCount = 0;
|
||||
@Override
|
||||
public void setState(boolean newState) {
|
||||
public void setState(boolean newState, RAMEvent e) {
|
||||
if (!newState) {
|
||||
count = 0;
|
||||
super.setState(newState);
|
||||
super.setState(false);
|
||||
readCount = 0;
|
||||
} else {
|
||||
count++;
|
||||
if (count >= 2) {
|
||||
super.setState(newState);
|
||||
count = 0;
|
||||
if (e.getType().isRead()) {
|
||||
readCount++;
|
||||
} else {
|
||||
readCount = 0;
|
||||
}
|
||||
if (readCount >= 2) {
|
||||
super.setState(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName()+(getState()?":1":":0")+"~~"+count;
|
||||
return getName()+(getState()?":1":":0")+"~~"+readCount;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ public class ApplesoftProgram {
|
||||
* Move variables around to accommodate bigger program
|
||||
* @param programEnd Program ending address
|
||||
*/
|
||||
private void relocateVariables(int programEnd) {
|
||||
public void relocateVariables(int programEnd) {
|
||||
Emulator.withMemory(memory->{
|
||||
int currentEnd = memory.readWordRaw(END_OF_PROG_POINTER);
|
||||
memory.writeWord(END_OF_PROG_POINTER, programEnd, false, true);
|
||||
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class ConfigurationUIController {
|
||||
@@ -99,6 +100,16 @@ public class ConfigurationUIController {
|
||||
String current = getCurrentNodePath();
|
||||
getExpandedNodes("", deviceTree.getRoot(), expanded);
|
||||
deviceTree.setRoot(Configuration.BASE);
|
||||
for (ConfigNode node : Configuration.BASE.getChildren()) {
|
||||
String prefix = node.name;
|
||||
expanded.add(prefix);
|
||||
for (ConfigNode child : node.getChildren()) {
|
||||
expanded.add(prefix + DELIMITER + child.toString());
|
||||
for (ConfigNode grandchild : node.getChildren()) {
|
||||
expanded.add(prefix + DELIMITER + child.toString() + DELIMITER + grandchild.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
setExpandedNodes("", deviceTree.getRoot(), expanded);
|
||||
setCurrentNodePath(current);
|
||||
}
|
||||
@@ -204,16 +215,16 @@ public class ConfigurationUIController {
|
||||
Text widget = new Text(value);
|
||||
widget.setWrappingWidth(180.0);
|
||||
widget.getStyleClass().add("setting-keyboard-value");
|
||||
widget.setOnMouseClicked((event) -> editKeyboardShortcut(node, actionName, widget));
|
||||
// widget.setOnMouseClicked((event) -> editKeyboardShortcut(node, actionName, widget));
|
||||
label.setLabelFor(widget);
|
||||
row.getChildren().add(label);
|
||||
row.getChildren().add(widget);
|
||||
return Optional.of(row);
|
||||
}
|
||||
|
||||
private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
// private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
|
||||
// throw new UnsupportedOperationException("Not supported yet.");
|
||||
// }
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
|
||||
@@ -237,13 +248,40 @@ public class ConfigurationUIController {
|
||||
return buildTextField(node, settingName, value, null);
|
||||
}
|
||||
} else if (type.equals(File.class)) {
|
||||
// TODO: Add file support!
|
||||
return buildFileField(node, settingName, value);
|
||||
} else if (ISelection.class.isAssignableFrom(type)) {
|
||||
return buildDynamicSelectComponent(node, settingName, value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: This was written but not tested/used currently. Test before using!
|
||||
private Node buildFileField(ConfigNode node, String settingName, Serializable value) {
|
||||
// Create a label that shows the name of the file and lets you select a file when the label is clicked.
|
||||
HBox hbox = new HBox();
|
||||
Label label = new Label(value == null ? "" : ((File) value).getName());
|
||||
label.setMinWidth(150.0);
|
||||
label.getStyleClass().add("setting-file-label");
|
||||
label.setOnMouseClicked((e) -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
File file = fileChooser.showOpenDialog(label.getScene().getWindow());
|
||||
if (file != null) {
|
||||
node.setFieldValue(settingName, file);
|
||||
label.setText(file.getName());
|
||||
}
|
||||
});
|
||||
hbox.getChildren().add(label);
|
||||
// Add a button that lets you clear the file selection.
|
||||
Label clearButton = new Label("Clear");
|
||||
clearButton.getStyleClass().add("setting-file-clear");
|
||||
clearButton.setOnMouseClicked((e) -> {
|
||||
node.setFieldValue(settingName, null);
|
||||
label.setText("");
|
||||
});
|
||||
return hbox;
|
||||
|
||||
}
|
||||
|
||||
private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) {
|
||||
TextField widget = new TextField(String.valueOf(value));
|
||||
widget.textProperty().addListener((e) -> node.setFieldValue(settingName, widget.getText()));
|
||||
|
||||
@@ -197,11 +197,11 @@ public class InvokableActionRegistryImpl extends InvokableActionRegistry {
|
||||
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.pause", ex);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
annotation = createInvokableAction("Reset", "general", "Process user-initatiated reboot (ctrl+apple+reset)", "reboot;reset;three-finger-salute;restart", true, false, new String[]{"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"});
|
||||
putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> {
|
||||
putStaticAction(annotation.name(), jace.core.Computer.class, annotation, (b) -> {
|
||||
try {
|
||||
((jace.core.Computer) o).invokeReset();
|
||||
jace.core.Computer.invokeReset();
|
||||
} catch (Exception ex) {
|
||||
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeWarmStart", ex);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.JaceApplication;
|
||||
import jace.apple2e.SoftSwitches;
|
||||
import jace.config.ConfigurableField;
|
||||
import jace.config.Configuration;
|
||||
import jace.config.InvokableAction;
|
||||
@@ -182,12 +182,9 @@ public abstract class Computer implements Reconfigurable {
|
||||
category = "general",
|
||||
alternatives = "reboot;reset;three-finger-salute;restart",
|
||||
defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
|
||||
public void invokeReset() {
|
||||
if (SoftSwitches.PDL0.isOn()) {
|
||||
coldStart();
|
||||
} else {
|
||||
warmStart();
|
||||
}
|
||||
public static void invokeReset() {
|
||||
System.out.println("Resetting computer");
|
||||
Emulator.withComputer(Computer::coldStart);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class RAMEvent {
|
||||
READ(true),
|
||||
READ_DATA(true),
|
||||
READ_OPERAND(true),
|
||||
READ_FAKE(true),
|
||||
EXECUTE(true),
|
||||
WRITE(false),
|
||||
ANY(false);
|
||||
|
||||
@@ -114,8 +114,7 @@ public abstract class SoftSwitch {
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
if (!exclusionActivate.contains(e.getAddress())) {
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
|
||||
setState(!getState());
|
||||
setState(!getState(), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -148,7 +147,7 @@ public abstract class SoftSwitch {
|
||||
}
|
||||
if (!exclusionActivate.contains(e.getAddress())) {
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
|
||||
setState(true);
|
||||
setState(true, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -177,7 +176,7 @@ public abstract class SoftSwitch {
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
if (!exclusionDeactivate.contains(e.getAddress())) {
|
||||
setState(false);
|
||||
setState(false, e);
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
|
||||
}
|
||||
}
|
||||
@@ -250,6 +249,12 @@ public abstract class SoftSwitch {
|
||||
});
|
||||
}
|
||||
|
||||
// Most softswitches act the same regardless of the ram event triggering them
|
||||
// But some softswitches are a little tricky (such as language card write) and need to assert extra conditions
|
||||
public void setState(boolean newState, RAMEvent e) {
|
||||
setState(newState);
|
||||
}
|
||||
|
||||
public void setState(boolean newState) {
|
||||
if (inhibit()) {
|
||||
return;
|
||||
|
||||
@@ -135,7 +135,7 @@ public class SoundMixer extends Device {
|
||||
soundThreadExecutor.submit(operation, action);
|
||||
}
|
||||
|
||||
protected static void initSound() {
|
||||
public static void initSound() {
|
||||
if (Utility.isHeadlessMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import jace.config.ConfigurableField;
|
||||
public abstract class TimedDevice extends Device {
|
||||
// From the holy word of Sather 3:5 (Table 3.1) :-)
|
||||
// This average speed averages in the "long" cycles
|
||||
public static final long NTSC_1MHZ = 1020484L;
|
||||
public static final long PAL_1MHZ = 1015625L;
|
||||
public static final int NTSC_1MHZ = 1020484;
|
||||
public static final int PAL_1MHZ = 1015625;
|
||||
public static final long SYNC_FREQ_HZ = 60;
|
||||
public static final double NANOS_PER_SECOND = 1000000000.0;
|
||||
public static final long NANOS_PER_MILLISECOND = 1000000L;
|
||||
@@ -81,6 +81,7 @@ public abstract class TimedDevice extends Device {
|
||||
|
||||
public final void resetSyncTimer() {
|
||||
nextSync = System.nanoTime() + nanosPerInterval;
|
||||
waitUntil = null;
|
||||
cycleTimer = 0;
|
||||
}
|
||||
|
||||
@@ -119,9 +120,7 @@ public abstract class TimedDevice extends Device {
|
||||
|
||||
public final void setMaxSpeed(boolean enabled) {
|
||||
maxspeed = enabled;
|
||||
if (!enabled) {
|
||||
resetSyncTimer();
|
||||
}
|
||||
resetSyncTimer();
|
||||
}
|
||||
|
||||
public final boolean isMaxSpeedEnabled() {
|
||||
@@ -129,7 +128,7 @@ public abstract class TimedDevice extends Device {
|
||||
}
|
||||
|
||||
public final boolean isMaxSpeed() {
|
||||
return forceMaxspeed || maxspeed;
|
||||
return forceMaxspeed || maxspeed || tempSpeedDuration > 0;
|
||||
}
|
||||
|
||||
public final long getSpeedInHz() {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -103,6 +103,14 @@ public class FloppyDisk {
|
||||
// This constructor is only used for disk conversion...
|
||||
}
|
||||
|
||||
public static enum SectorOrder {
|
||||
DOS(DOS_33_SECTOR_ORDER), PRODOS(PRODOS_SECTOR_ORDER), UNKNOWN(DOS_33_SECTOR_ORDER);
|
||||
public final int[] sectors;
|
||||
SectorOrder(int[] sectors) {
|
||||
this.sectors = sectors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param diskFile
|
||||
@@ -110,17 +118,17 @@ public class FloppyDisk {
|
||||
*/
|
||||
public FloppyDisk(File diskFile) throws IOException {
|
||||
FileInputStream input = new FileInputStream(diskFile);
|
||||
String name = diskFile.getName().toUpperCase();
|
||||
readDisk(input, name.endsWith(".PO"));
|
||||
readDisk(input, diskFile.getName().toUpperCase().endsWith(".PO") ? SectorOrder.PRODOS : SectorOrder.DOS);
|
||||
writeProtected = !diskFile.canWrite();
|
||||
diskPath = diskFile;
|
||||
}
|
||||
|
||||
// brendanr: refactored to use input stream
|
||||
public void readDisk(InputStream diskFile, boolean prodosOrder) throws IOException {
|
||||
public void readDisk(InputStream diskFile, SectorOrder assumedOrder) throws IOException {
|
||||
isNibblizedImage = true;
|
||||
volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER;
|
||||
headerLength = 0;
|
||||
SectorOrder sectorOrder = SectorOrder.UNKNOWN;
|
||||
try {
|
||||
int bytesRead = diskFile.read(nibbles);
|
||||
if (bytesRead == DISK_2MG_NIB_LENGTH) {
|
||||
@@ -134,13 +142,38 @@ public class FloppyDisk {
|
||||
if (bytesRead == DISK_2MG_NON_NIB_LENGTH) {
|
||||
bytesRead -= 0x040;
|
||||
// Try to pick up correct sector ordering and volume from 2MG header.
|
||||
prodosOrder = (nibbles[12] == 01);
|
||||
if (nibbles[12] == 01) {
|
||||
sectorOrder = SectorOrder.PRODOS;
|
||||
} else {
|
||||
sectorOrder = SectorOrder.DOS;
|
||||
}
|
||||
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
|
||||
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
|
||||
|
||||
headerLength = 0x040;
|
||||
}
|
||||
currentSectorOrder = prodosOrder ? PRODOS_SECTOR_ORDER : DOS_33_SECTOR_ORDER;
|
||||
|
||||
if (sectorOrder == SectorOrder.UNKNOWN) {
|
||||
if (isProdosVolumeBlock(nibbles, 0x0400)) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Prodos volume block found at 0x0400");
|
||||
}
|
||||
sectorOrder = SectorOrder.PRODOS;
|
||||
} else if (isProdosVolumeBlock(nibbles, 0x0B00)) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Prodos volume block found at 0x0B00");
|
||||
}
|
||||
sectorOrder = SectorOrder.DOS;
|
||||
}
|
||||
}
|
||||
if (sectorOrder == SectorOrder.UNKNOWN) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Assuming sector order based on file extension");
|
||||
}
|
||||
sectorOrder = assumedOrder;
|
||||
}
|
||||
System.out.println(null == sectorOrder ? "Sector order is null" : "Sector order is " + sectorOrder.name());
|
||||
currentSectorOrder = sectorOrder.sectors;
|
||||
if (bytesRead == DISK_PLAIN_LENGTH) {
|
||||
isNibblizedImage = false;
|
||||
nibbles = nibblize(nibbles);
|
||||
@@ -157,6 +190,23 @@ public class FloppyDisk {
|
||||
StateManager.markDirtyValue(currentSectorOrder);
|
||||
}
|
||||
|
||||
private boolean isProdosVolumeBlock(byte[] nibbles, int offset) {
|
||||
// First two bytes are zero (no previous block)
|
||||
if (nibbles[offset] != 0 || nibbles[offset+1] != 0) {
|
||||
return false;
|
||||
}
|
||||
// Next two bytes are either both zero or at least in the range of 3...280
|
||||
int nextBlock = (nibbles[offset+2] & 0x0ff) | (nibbles[offset+3] << 8);
|
||||
if (nextBlock == 1 || nextBlock == 2 || nextBlock > 280) {
|
||||
return false;
|
||||
}
|
||||
// Now check total blocks at offset 0x29
|
||||
int totalBlocks = (nibbles[offset+0x29] & 0x0ff) | (nibbles[offset+0x2a] << 8);
|
||||
if (totalBlocks != 280) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
* Convert a block-format disk to a 6-by-2 nibblized encoding scheme (raw NIB disk format)
|
||||
*/
|
||||
|
||||
@@ -41,8 +41,9 @@ import jace.core.Utility;
|
||||
import jace.core.Utility.OS;
|
||||
import jace.state.Stateful;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
|
||||
/**
|
||||
@@ -200,6 +201,7 @@ public class Joystick extends Device {
|
||||
selections.put("", "***Empty***");
|
||||
for (int i = GLFW.GLFW_JOYSTICK_1; i <= GLFW.GLFW_JOYSTICK_LAST; i++) {
|
||||
if (GLFW.glfwJoystickPresent(i)) {
|
||||
// System.out.println("Detected " + GLFW.glfwGetJoystickName(i) + ": " + GLFW.glfwGetJoystickGUID(i));
|
||||
selections.put(GLFW.glfwGetJoystickName(i), GLFW.glfwGetJoystickName(i));
|
||||
}
|
||||
}
|
||||
@@ -271,15 +273,47 @@ public class Joystick extends Device {
|
||||
if (JaceApplication.getApplication() == null) {
|
||||
return;
|
||||
}
|
||||
Stage stage = JaceApplication.getApplication().primaryStage;
|
||||
Scene scene = JaceApplication.getApplication().primaryStage.getScene();
|
||||
// Register a mouse handler on the primary stage that tracks the
|
||||
// mouse x/y position as a percentage of window width and height
|
||||
stage.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
|
||||
scene.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
|
||||
if (!useKeyboard && !selectedPhysicalController()) {
|
||||
joyX = (int) (event.getX() / stage.getWidth() * 255);
|
||||
joyY = (int) (event.getY() / stage.getHeight() * 255);
|
||||
joyX = (int) (event.getX() / scene.getWidth() * 255);
|
||||
joyY = (int) (event.getY() / scene.getHeight() * 255);
|
||||
}
|
||||
});
|
||||
scene.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (!useKeyboard && !selectedPhysicalController()) {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (port == 0) {
|
||||
SoftSwitches.PB0.getSwitch().setState(true);
|
||||
Keyboard.isOpenApplePressed = true;
|
||||
} else {
|
||||
SoftSwitches.PB2.getSwitch().setState(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
Keyboard.isClosedApplePressed = true;
|
||||
SoftSwitches.PB1.getSwitch().setState(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
scene.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (port == 0) {
|
||||
SoftSwitches.PB0.getSwitch().setState(false);
|
||||
Keyboard.isOpenApplePressed = false;
|
||||
} else {
|
||||
SoftSwitches.PB2.getSwitch().setState(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
Keyboard.isClosedApplePressed = false;
|
||||
SoftSwitches.PB1.getSwitch().setState(false);
|
||||
}
|
||||
});
|
||||
this.port = port;
|
||||
if (port == 0) {
|
||||
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL0.getSwitch();
|
||||
@@ -288,6 +322,25 @@ public class Joystick extends Device {
|
||||
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL2.getSwitch();
|
||||
ySwitch = (MemorySoftSwitch) SoftSwitches.PDL3.getSwitch();
|
||||
}
|
||||
new Thread(()->{
|
||||
try {
|
||||
// We have to wait for the the library to be loaded and the UI to be active
|
||||
// Otherwise this will fail
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (port == 0 && glfwController.getSelections().keySet().size() == 2) {
|
||||
// Get the entry that is not null
|
||||
glfwController.setValue(glfwController.getSelections().keySet().stream().filter(s->s != null && !s.isBlank()).findFirst().get());
|
||||
System.out.println("Using device for joystick: " + glfwController.getValue());
|
||||
useKeyboard = false;
|
||||
useDPad = true;
|
||||
reconfigure();
|
||||
} else {
|
||||
System.out.println("Using device for joystick: " + (useKeyboard ? "keyboard" : "mouse"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
public boolean leftPressed = false;
|
||||
public boolean rightPressed = false;
|
||||
@@ -546,7 +599,7 @@ public class Joystick extends Device {
|
||||
|
||||
@InvokableAction(name = "Left", category = "joystick", defaultKeyMapping = "left", notifyOnRelease = true)
|
||||
public boolean joystickLeft(boolean pressed) {
|
||||
if (!isAttached || !useKeyboard) {
|
||||
if (!useKeyboard) {
|
||||
return false;
|
||||
}
|
||||
leftPressed = pressed;
|
||||
@@ -558,7 +611,7 @@ public class Joystick extends Device {
|
||||
|
||||
@InvokableAction(name = "Right", category = "joystick", defaultKeyMapping = "right", notifyOnRelease = true)
|
||||
public boolean joystickRight(boolean pressed) {
|
||||
if (!isAttached || !useKeyboard) {
|
||||
if (!useKeyboard) {
|
||||
return false;
|
||||
}
|
||||
rightPressed = pressed;
|
||||
@@ -570,7 +623,7 @@ public class Joystick extends Device {
|
||||
|
||||
@InvokableAction(name = "Up", category = "joystick", defaultKeyMapping = "up", notifyOnRelease = true)
|
||||
public boolean joystickUp(boolean pressed) {
|
||||
if (!isAttached || !useKeyboard) {
|
||||
if (!useKeyboard) {
|
||||
return false;
|
||||
}
|
||||
upPressed = pressed;
|
||||
@@ -582,7 +635,7 @@ public class Joystick extends Device {
|
||||
|
||||
@InvokableAction(name = "Down", category = "joystick", defaultKeyMapping = "down", notifyOnRelease = true)
|
||||
public boolean joystickDown(boolean pressed) {
|
||||
if (!isAttached || !useKeyboard) {
|
||||
if (!useKeyboard) {
|
||||
return false;
|
||||
}
|
||||
downPressed = pressed;
|
||||
|
||||
103
src/main/java/jace/hardware/Votrax.java
Normal file
103
src/main/java/jace/hardware/Votrax.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package jace.hardware;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Votrax {
|
||||
// This is a speech synthesizer based on the Votrax SC-02
|
||||
// There are 2 sound generators, one for the voice and one for the noise
|
||||
// The voice generator is a saw-tooth wave generator at a frequency determined by the voice frequency register
|
||||
// The noise generator is a pseudo-random noise generator
|
||||
|
||||
// The Votrax has 5 filters that can be applied to the voice generator, controlled by a filter frequency register
|
||||
// The fifth filter takes both the voice and noise generators as input, but other filters only take the voice generator
|
||||
// There is also a final high-pass filter that can be applied to the output of the voice and noise generators
|
||||
|
||||
// There is a phoneme register which controls the phoneme to be spoken (0-63)
|
||||
// There is a duration register which controls the duration of the phoneme (0-3)
|
||||
// There is a rate register which controls the rate of speech (0-15)
|
||||
// There is an inflection register which controls the inflection of the voice (0-15)
|
||||
|
||||
// For each phoneme there are 8 bytes that control the filters and sound generator levels
|
||||
byte[] phonemeData = new byte[64 * 8];
|
||||
// Phoneme chart:
|
||||
// 00: PA (pause)
|
||||
// 01: E (mEEt)
|
||||
// 02: E1 (bEnt)
|
||||
// 03: Y (bEfore)
|
||||
// 04: Y1 (Year)
|
||||
// 05: AY (plEAse)
|
||||
// 06: IE (anY)
|
||||
// 07: I (sIx)
|
||||
// 08: A (mAde)
|
||||
// 09: A1 (cAre)
|
||||
// 0a: EH (nEst)
|
||||
// 0b: EH1 (bElt)
|
||||
// 0c: AE (dAd)
|
||||
// 0d: AE1 (After)
|
||||
// 0e: AH (gOt)
|
||||
// 0f: AH1 (fAther)
|
||||
// 10: AW (Office)
|
||||
// 11: O (stOre)
|
||||
// 12: OU (bOAt)
|
||||
// 13: OO (lOOk)
|
||||
// 14: IU (yOU)
|
||||
// 15: IU1 (cOUld)
|
||||
// 16: U (tUne)
|
||||
// 17: U1 (cartOOn)
|
||||
// 18: UH (wOnder)
|
||||
// 19: UH1 (lOve)
|
||||
// 1a: UH2 (whAt)
|
||||
// 1b: UH3 (nUt)
|
||||
// 1c: ER (bIRd)
|
||||
// 1d: R (Roof)
|
||||
// 1e: R1 (Rug)
|
||||
// 1f: R2 (mutteR -- German)
|
||||
// 20: L (Lift)
|
||||
// 21: L1 (pLay)
|
||||
// 22: LF (faLL)
|
||||
// 23: W (Water)
|
||||
// 24: B (Bag)
|
||||
// 25: D (paiD)
|
||||
// 26: KV (taG)
|
||||
// 27: P (Pen)
|
||||
// 28: T (Tart)
|
||||
// 29: K (Kit)
|
||||
// 2a: HV - Hold Vocal
|
||||
// 2b: HVC - Hold Vocal Closure
|
||||
// 2c: HF - (Heart)
|
||||
// 2d: HFC - Hold Frictave Closure
|
||||
// 2e: HN - Hold Nasal
|
||||
// 2f: Z (Zero)
|
||||
// 30: S (Same)
|
||||
// 31: J (meaSure)
|
||||
// 32: SCH (SHip)
|
||||
// 33: V (Very)
|
||||
// 34: F (Four)
|
||||
// 35: THV (THere)
|
||||
// 36: TH (wiTH)
|
||||
// 37: M (More)
|
||||
// 38: N (NiNe)
|
||||
// 39: NG (raNG)
|
||||
// 3a: :A (mAErchen -- German)
|
||||
// 3b: :OH (lOwe - French)
|
||||
// 3c: :U (fUEnf -- German)
|
||||
// 3d: :UH (menU -- French)
|
||||
// 3e: E2 (bittE -- German)
|
||||
// 3f: LB (Lube)
|
||||
|
||||
public void loadPhonemeData() {
|
||||
InputStream romFile = Votrax.class.getResourceAsStream("/jace/data/sc01a.bin");
|
||||
if (romFile == null) {
|
||||
throw new RuntimeException("Cannot find Votrax SC-01A ROM");
|
||||
}
|
||||
// Load into phonemeData
|
||||
try {
|
||||
if (romFile.read(phonemeData) != phonemeData.length) {
|
||||
throw new RuntimeException("Bad Votrax SC-01A ROM size");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Error loading Votrax SC-01A ROM", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -152,7 +152,6 @@ public class DirectoryNode extends DiskNode implements FileFilter {
|
||||
end = start + ENTRIES_PER_BLOCK;
|
||||
}
|
||||
for (int i = start; i < end && i < directoryEntries.size(); i++, offset += FILE_ENTRY_SIZE) {
|
||||
// TODO: Add any parts that are not file entries.
|
||||
// System.out.println("Entry "+i+": "+children.get(i).getName()+"; offset "+offset);
|
||||
generateFileEntry(buffer, offset, i);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
328
src/main/java/jace/hardware/mockingboard/Votrax.java
Normal file
328
src/main/java/jace/hardware/mockingboard/Votrax.java
Normal file
@@ -0,0 +1,328 @@
|
||||
package jace.hardware.mockingboard;
|
||||
|
||||
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 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;
|
||||
import jace.core.SoundMixer.SoundError;
|
||||
import jace.core.TimedDevice;
|
||||
|
||||
public class Votrax extends TimedDevice {
|
||||
// This is a speech synthesizer based on the Votrax SC-02
|
||||
// There are 2 sound generators, one for the voice and one for the noise
|
||||
// The voice generator is a saw-tooth wave generator at a frequency determined by the voice frequency register
|
||||
// The noise generator is a pseudo-random noise generator
|
||||
|
||||
// The Votrax has 5 filters that can be applied to the voice generator, controlled by a filter frequency register
|
||||
// The fifth filter takes both the voice and noise generators as input, but other filters only take the voice generator
|
||||
// There is also a final high-pass filter that can be applied to the output of the voice and noise generators
|
||||
|
||||
// There is a phoneme register which controls the phoneme to be spoken (0-63)
|
||||
// There is a duration register which controls the duration of the phoneme (0-3)
|
||||
// There is a rate register which controls the rate of speech (0-15)
|
||||
// There is an inflection register which controls the inflection of the voice (0-15)
|
||||
|
||||
// For each phoneme there are 8 bytes that control the filters and sound generator levels
|
||||
byte[] phonemeData = new byte[64 * 8];
|
||||
// Phoneme chart:
|
||||
// 00: PA (pause)
|
||||
// 01: E (mEEt)
|
||||
// 02: E1 (bEnt)
|
||||
// 03: Y (bEfore)
|
||||
// 04: Y1 (Year)
|
||||
// 05: AY (plEAse)
|
||||
// 06: IE (anY)
|
||||
// 07: I (sIx)
|
||||
// 08: A (mAde)
|
||||
// 09: A1 (cAre)
|
||||
// 0a: EH (nEst)
|
||||
// 0b: EH1 (bElt)
|
||||
// 0c: AE (dAd)
|
||||
// 0d: AE1 (After)
|
||||
// 0e: AH (gOt)
|
||||
// 0f: AH1 (fAther)
|
||||
// 10: AW (Office)
|
||||
// 11: O (stOre)
|
||||
// 12: OU (bOAt)
|
||||
// 13: OO (lOOk)
|
||||
// 14: IU (yOU)
|
||||
// 15: IU1 (cOUld)
|
||||
// 16: U (tUne)
|
||||
// 17: U1 (cartOOn)
|
||||
// 18: UH (wOnder)
|
||||
// 19: UH1 (lOve)
|
||||
// 1a: UH2 (whAt)
|
||||
// 1b: UH3 (nUt)
|
||||
// 1c: ER (bIRd)
|
||||
// 1d: R (Roof)
|
||||
// 1e: R01 (Rug)
|
||||
// 1f: R2 (mutteR -- German)
|
||||
// 20: L (Lift)
|
||||
// 21: L1 (pLay)
|
||||
// 22: LF (faLL)
|
||||
// 23: W (Water)
|
||||
// 24: B (Bag)
|
||||
// 25: D (paiD)
|
||||
// 26: KV (taG)
|
||||
// 27: P (Pen)
|
||||
// 28: T (Tart)
|
||||
// 29: K (Kit)
|
||||
// 2a: HV - Hold Vocal
|
||||
// 2b: HVC - Hold Vocal Closure
|
||||
// 2c: HF - (Heart)
|
||||
// 2d: HFC - Hold Frictave Closure
|
||||
// 2e: HN - Hold Nasal
|
||||
// 2f: Z (Zero)
|
||||
// 30: S (Same)
|
||||
// 31: J (meaSure)
|
||||
// 32: SCH (SHip)
|
||||
// 33: V (Very)
|
||||
// 34: F (Four)
|
||||
// 35: THV (THere)
|
||||
// 36: TH (wiTH)
|
||||
// 37: M (More)
|
||||
// 38: N (NiNe)
|
||||
// 39: NG (raNG)
|
||||
// 3a: :A (mAErchen -- German)
|
||||
// 3b: :OH (lOwe - French)
|
||||
// 3c: :U (fUEnf -- German)
|
||||
// 3d: :UH (menU -- French)
|
||||
// 3e: E2 (bittE -- German)
|
||||
// 3f: LB (Lube)
|
||||
|
||||
public void loadPhonemeData() {
|
||||
InputStream romFile = Votrax.class.getResourceAsStream("jace/data/sc01a.bin");
|
||||
if (romFile == null) {
|
||||
throw new RuntimeException("Cannot find Votrax SC-01A ROM");
|
||||
}
|
||||
// Load into phonemeData
|
||||
try {
|
||||
if (romFile.read(phonemeData) != phonemeData.length) {
|
||||
throw new RuntimeException("Bad Votrax SC-01A ROM size");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Error loading Votrax SC-01A ROM", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Generator {
|
||||
public int clockFrequency = 1;
|
||||
public int sampleRate = 1;
|
||||
public double samplesPerClock;
|
||||
public double sampleCounter = 0.0;
|
||||
public void setClockFrequency(int clockFrequency) {
|
||||
this.clockFrequency = clockFrequency;
|
||||
this.updateFrequency();
|
||||
}
|
||||
public void setSampleRate(int sampleRate) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.updateFrequency();
|
||||
}
|
||||
public void updateFrequency() {
|
||||
this.sampleCounter = 0.0;
|
||||
this.samplesPerClock = clockFrequency / sampleRate;
|
||||
}
|
||||
public abstract int getBufferDuration();
|
||||
|
||||
public void fillBuffer(ShortBuffer buffer) {
|
||||
for (int i = 0; i < getBufferDuration(); i++) {
|
||||
buffer.put((short) (doGenerate() * 32767));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract double doGenerate();
|
||||
}
|
||||
|
||||
public static class SawGenerator extends Generator {
|
||||
double pitch=440.0;
|
||||
double sample = 0.0;
|
||||
double direction = 1.0;
|
||||
double changePerSample = 0.0;
|
||||
public void setPitch(double frequency) {
|
||||
this.pitch = frequency;
|
||||
this.updateFrequency();
|
||||
}
|
||||
public void setDirection(double direction) {
|
||||
this.direction = direction;
|
||||
this.updateFrequency();
|
||||
}
|
||||
public void updateFrequency() {
|
||||
super.updateFrequency();
|
||||
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) {
|
||||
sample += 2.0;
|
||||
} else if (sample > 1.0) {
|
||||
sample -= 2.0;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NoiseGenerator extends Generator {
|
||||
double sample = 0.0;
|
||||
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 static int FORMANT = 0;
|
||||
public static int NOISE = 1;
|
||||
// private Thread playbackThread = null;
|
||||
|
||||
public Votrax() throws Exception {
|
||||
// loadPhonemeData();
|
||||
formantGenerator.setSampleRate(44100);
|
||||
formantGenerator.setPitch(100);
|
||||
noiseGenerator.setSampleRate(44100);
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
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();
|
||||
}
|
||||
|
||||
private void createFilters() throws Exception {
|
||||
// TODO: Consider filter values from here: https://modwiggler.com/forum/viewtopic.php?t=234128
|
||||
// Bark scale: 60, 150, 250, 350, 450, 570, 700, 840, 1000, 1170, 1370, 1600, 1850, 2150, 2500, 2900
|
||||
// Roland SVC-350: 150, 220, 350, 500, 760, 1100, 1600, 2200, 3600, 5200. 6000 highpass filter
|
||||
// EMS Vocoder System 3000: 125, 185, 270, 350, 430, 530, 630, 780, 950, 1150, 1380, 2070, 2780, 3800, 6400
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
alGetError();
|
||||
filters[i] = alGenFilters();
|
||||
if (alGetError() != AL_NO_ERROR) {
|
||||
throw new Exception("Failed to create filter " + i);
|
||||
}
|
||||
if (alIsFilter(filters[i])) {
|
||||
// Set Filter type to Band-Pass and set parameters
|
||||
alFilteri(filters[i], AL_FILTER_TYPE, AL_FILTER_BANDPASS);
|
||||
if (alGetError() != AL_NO_ERROR) {
|
||||
System.out.println("Band pass filter not supported.");
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean suspend() {
|
||||
destroyFilters();
|
||||
|
||||
// playbackThread = null;
|
||||
return super.suspend();
|
||||
}
|
||||
|
||||
private void destroyFilters() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (alIsFilter(filters[i])) {
|
||||
alDeleteFilters(filters[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return "Votrax";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeviceName() {
|
||||
return "Votrax SC-02 / SSI-263";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ module jace {
|
||||
requires org.lwjgl.openal;
|
||||
requires org.lwjgl.stb;
|
||||
requires org.lwjgl.glfw;
|
||||
requires org.lwjgl;
|
||||
|
||||
// requires org.reflections;
|
||||
|
||||
|
||||
@@ -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"] }]
|
||||
},
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<AnchorPane id="AnchorPane" prefHeight="426.0" prefWidth="600.0" styleClass="mainFxmlClass" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jace.config.ConfigurationUIController">
|
||||
<AnchorPane id="AnchorPane" prefHeight="600" prefWidth="800.0" styleClass="mainFxmlClass" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jace.config.ConfigurationUIController">
|
||||
<stylesheets>
|
||||
<URL value="@../styles/style.css" />
|
||||
</stylesheets>
|
||||
<children>
|
||||
<ToolBar prefHeight="40.0" prefWidth="600.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<ToolBar prefHeight="40.0" prefWidth="800.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<items>
|
||||
<Button mnemonicParsing="false" onMouseClicked="#reloadConfig" text="Reload" />
|
||||
<Button mnemonicParsing="false" onMouseClicked="#saveConfig" text="Save" />
|
||||
@@ -21,16 +21,16 @@
|
||||
<Button mnemonicParsing="false" onMouseClicked="#cancelConfig" text="Cancel" />
|
||||
</items>
|
||||
</ToolBar>
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.3979933110367893" layoutY="40.0" prefHeight="363.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="40.0">
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.3979933110367893" layoutY="40.0" prefHeight="363.0" prefWidth="800.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="40.0">
|
||||
<items>
|
||||
<ScrollPane fx:id="treeScroll" fitToHeight="true" fitToWidth="true" prefHeight="361.0" prefWidth="174.0">
|
||||
<content>
|
||||
<TreeView fx:id="deviceTree" prefHeight="359.0" prefWidth="233.0" />
|
||||
<TreeView fx:id="deviceTree" prefHeight="359.0" prefWidth="300" />
|
||||
</content>
|
||||
</ScrollPane>
|
||||
<ScrollPane fx:id="settingsScroll" fitToHeight="true" fitToWidth="true" prefHeight="361.0" prefWidth="416.0">
|
||||
<content>
|
||||
<VBox fx:id="settingsVbox" prefHeight="360.0" prefWidth="354.0" />
|
||||
<VBox fx:id="settingsVbox" prefHeight="360.0" prefWidth="500" />
|
||||
</content>
|
||||
<padding>
|
||||
<Insets right="3.0" />
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Slider?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
@@ -19,9 +16,9 @@
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.TilePane?>
|
||||
|
||||
<AnchorPane id="AnchorPane" fx:id="rootPane" prefHeight="384.0" prefWidth="560.0" style="-fx-background-color: black;" stylesheets="@../styles/style.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jace.JaceUIController">
|
||||
<AnchorPane id="AnchorPane" fx:id="rootPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" style="-fx-background-color: black;" stylesheets="@../styles/style.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jace.JaceUIController">
|
||||
<children>
|
||||
<StackPane fx:id="stackPane" prefHeight="384.0" prefWidth="560.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<StackPane fx:id="stackPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<children>
|
||||
<ImageView fx:id="appleScreen" fitHeight="384.0" fitWidth="560.0" pickOnBounds="true" style="-fx-background-color: BLACK;" />
|
||||
<HBox fx:id="notificationBox" alignment="BOTTOM_RIGHT" fillHeight="false" maxHeight="45.0" minHeight="45.0" mouseTransparent="true" prefHeight="45.0" prefWidth="560.0" StackPane.alignment="BOTTOM_CENTER" />
|
||||
@@ -30,7 +27,7 @@
|
||||
<Button fx:id="menuButton" layoutX="494.0" layoutY="14.0" mnemonicParsing="false" styleClass="menuButton" text="☰" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
<BorderPane fx:id="controlOverlay" visible="false">
|
||||
<BorderPane fx:id="controlOverlay" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308">
|
||||
<center>
|
||||
<HBox maxHeight="64.0" prefHeight="64.0" styleClass="uiSpeedSlider" BorderPane.alignment="CENTER">
|
||||
<children>
|
||||
@@ -68,7 +65,7 @@
|
||||
<top>
|
||||
<HBox fillHeight="false" nodeOrientation="LEFT_TO_RIGHT" BorderPane.alignment="CENTER">
|
||||
<children>
|
||||
<TilePane hgap="5.0" nodeOrientation="LEFT_TO_RIGHT" vgap="5.0" HBox.hgrow="NEVER">
|
||||
<TilePane hgap="5.0" nodeOrientation="LEFT_TO_RIGHT" prefColumns="2" vgap="5.0" HBox.hgrow="NEVER">
|
||||
<children>
|
||||
<Button contentDisplay="TOP" mnemonicParsing="false" styleClass="uiActionButton" text="Info">
|
||||
<graphic>
|
||||
@@ -90,20 +87,30 @@
|
||||
</Button>
|
||||
</children>
|
||||
</TilePane>
|
||||
<GridPane prefHeight="70.0" prefWidth="467.0">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="RIGHT" hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
|
||||
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" minWidth="10.0" prefWidth="100.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label alignment="CENTER_RIGHT" prefHeight="53.0" prefWidth="201.0" styleClass="musicLabel" text="Speaker:" textAlignment="RIGHT" GridPane.rowIndex="1" />
|
||||
<Slider fx:id="speakerToggle" blockIncrement="1.0" majorTickUnit="1.0" max="1.0" maxWidth="32.0" minWidth="32.0" minorTickCount="0" prefWidth="32.0" snapToTicks="true" GridPane.columnIndex="1" GridPane.rowIndex="1" />
|
||||
</children>
|
||||
</GridPane>
|
||||
<BorderPane HBox.hgrow="ALWAYS">
|
||||
<right>
|
||||
<GridPane alignment="TOP_RIGHT" BorderPane.alignment="CENTER">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" />
|
||||
<ColumnConstraints halignment="LEFT" hgrow="NEVER" minWidth="10.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label alignment="CENTER_RIGHT" styleClass="musicLabel" text="Speaker:" textAlignment="RIGHT" />
|
||||
<Slider fx:id="speakerToggle" blockIncrement="1.0" majorTickUnit="1.0" max="1.0" maxWidth="-Infinity" minWidth="-Infinity" minorTickCount="0" prefWidth="32.0" snapToTicks="true" GridPane.columnIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets />
|
||||
</GridPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
</Slider>
|
||||
</children>
|
||||
</GridPane>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</children>
|
||||
</HBox>
|
||||
</top>
|
||||
|
||||
@@ -866,7 +866,8 @@ xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,
|
||||
0300000008100000e501000019040000,Anbernic Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a4,start:b11,x:b4,y:b3,platform:Mac OS X,
|
||||
03000000a30c00002700000003030000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||
03000000a30c00002800000003030000,Astro City Mini,a:b2,b:b1,back:b8,leftx:a3,lefty:a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||
03000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
|
||||
#03000000050b00000045000031000000,ASUS Gamepad (wrong dpad buttons),a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
|
||||
03000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,x:b2,y:b3,back:b10,dpdown:b13,dpleft:b14,dpright:b12,dpup:b11,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b9,platform:Mac OS X,
|
||||
03000000050b00000579000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b15,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b42,paddle1:b9,paddle2:b11,rightshoulder:b7,rightstick:b16,righttrigger:a4,rightx:a2,righty:a3,start:b13,x:b3,y:b4,platform:Mac OS X,
|
||||
03000000050b00000679000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b15,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b23,rightshoulder:b7,rightstick:b16,righttrigger:a4,rightx:a2,righty:a3,start:b13,x:b3,y:b4,platform:Mac OS X,
|
||||
03000000503200000110000047010000,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X,
|
||||
@@ -1177,8 +1178,8 @@ xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,
|
||||
03000000790000003018000011010000,Arcade Fightstick F300,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
|
||||
03000000a30c00002700000011010000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||
03000000a30c00002800000011010000,Astro City Mini,a:b2,b:b1,back:b8,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||
05000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
|
||||
05000000050b00000045000040000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
|
||||
05000000050b00000045000031000000,ASUS Gamepad Linux 1,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
|
||||
05000000050b00000045000040000000,ASUS Gamepad Linux 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
|
||||
03000000050b00000579000011010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b36,paddle1:b52,paddle2:b53,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
|
||||
05000000050b00000679000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b21,paddle1:b22,paddle2:b23,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
|
||||
03000000503200000110000000000000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux,
|
||||
|
||||
BIN
src/main/resources/jace/data/hayes-micromodem-8308a271.rom
Normal file
BIN
src/main/resources/jace/data/hayes-micromodem-8308a271.rom
Normal file
Binary file not shown.
BIN
src/main/resources/jace/data/sc01a.bin
Normal file
BIN
src/main/resources/jace/data/sc01a.bin
Normal file
Binary file not shown.
@@ -27,13 +27,18 @@
|
||||
}
|
||||
|
||||
.menuButton, .uiActionButton, .uiSpeedSlider ImageView, .uiSpeedSlider Slider, .uiSpeedSlider AnchorPane, .musicLabel, GridPane {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.75);
|
||||
-fx-background-color: rgba(0, 0, 0, 0.85);
|
||||
-fx-text-fill: #a0FFa0
|
||||
}
|
||||
|
||||
.menuButton:hover, .uiActionButton:hover, .uiSpeedSlider:hover Slider, Slider:hover {
|
||||
-fx-background-color: rgba(0,64,0,0.85);
|
||||
-fx-text-fill: #a0FFa0
|
||||
}
|
||||
|
||||
.musicLabel {
|
||||
-fx-text-alignment: right;
|
||||
-fx-font-size:18pt;
|
||||
-fx-font-size:14pt;
|
||||
}
|
||||
|
||||
.uiActionButton ImageView, .uiSpeedSlider ImageView {
|
||||
|
||||
23
src/test/java/jace/AbstractFXTest.java
Normal file
23
src/test/java/jace/AbstractFXTest.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package jace;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
public abstract class AbstractFXTest {
|
||||
public static boolean fxInitialized = false;
|
||||
@BeforeClass
|
||||
public static void initJfxRuntime() {
|
||||
if (!fxInitialized) {
|
||||
fxInitialized = true;
|
||||
Platform.startup(() -> {});
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void shutdown() {
|
||||
Emulator.abort();
|
||||
Platform.exit();
|
||||
}
|
||||
}
|
||||
44
src/test/java/jace/ProgramException.java
Normal file
44
src/test/java/jace/ProgramException.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package jace;
|
||||
|
||||
import jace.apple2e.MOS65C02;
|
||||
|
||||
public class ProgramException extends Exception {
|
||||
int breakpointNumber;
|
||||
String processorStats;
|
||||
String programLocation;
|
||||
public ProgramException(String message, int breakpointNumber) {
|
||||
super(message.replaceAll("<<.*>>", ""));
|
||||
this.breakpointNumber = breakpointNumber;
|
||||
this.processorStats = Emulator.withComputer(c-> ((MOS65C02) c.getCpu()).getState(), "N/A");
|
||||
// Look for a string pattern <<programLocation>> in the message and extract if found
|
||||
int start = message.indexOf("<<");
|
||||
if (start != -1) {
|
||||
int end = message.indexOf(">>", start);
|
||||
if (end != -1) {
|
||||
this.programLocation = message.substring(start + 2, end);
|
||||
}
|
||||
} else {
|
||||
this.programLocation = "N/A";
|
||||
}
|
||||
}
|
||||
public int getBreakpointNumber() {
|
||||
return breakpointNumber;
|
||||
}
|
||||
public String getProcessorStats() {
|
||||
return processorStats;
|
||||
}
|
||||
public String getProgramLocation() {
|
||||
return programLocation;
|
||||
}
|
||||
public String getMessage() {
|
||||
String message = super.getMessage();
|
||||
if (getBreakpointNumber() >= 0) {
|
||||
message += " at breakpoint " + getBreakpointNumber();
|
||||
}
|
||||
message += " \nStats: " + getProcessorStats();
|
||||
if (getProgramLocation() != null) {
|
||||
message += " \n at " + getProgramLocation();
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
481
src/test/java/jace/TestProgram.java
Normal file
481
src/test/java/jace/TestProgram.java
Normal file
@@ -0,0 +1,481 @@
|
||||
package jace;
|
||||
|
||||
import static jace.TestUtils.runAssemblyCode;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jace.apple2e.MOS65C02;
|
||||
import jace.core.Computer;
|
||||
|
||||
public class TestProgram {
|
||||
// Tests could be run in any order so it is really important that all registers/flags are preserved!
|
||||
public static enum Flag {
|
||||
CARRY_SET("BCS +", "Carry should be set"),
|
||||
CARRY_CLEAR("BCC +", "Carry should be clear"),
|
||||
ZERO_SET("BEQ +", "Zero should be set"),
|
||||
IS_ZERO("BEQ +", "Zero should be clear"),
|
||||
ZERO_CLEAR("BNE +", "Zero should be clear"),
|
||||
NOT_ZERO("BNE +", "Zero should be clear"),
|
||||
NEGATIVE("BMI +", "Negative should be set"),
|
||||
POSITIVE("BPL +", "Negative should be clear"),
|
||||
OVERFLOW_SET("BVS +", "Overflow should be set"),
|
||||
OVERFLOW_CLEAR("BVC +", "Overflow should be clear"),
|
||||
DECIMAL_SET("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00001000
|
||||
BEQ ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Decimal should be set"),
|
||||
DECIMAL_CLEAR("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00001000
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Decimal should be clear"),
|
||||
INTERRUPT_SET("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00000100
|
||||
BEQ ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Interrupt should be set"),
|
||||
INTERRUPT_CLEAR("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00000100
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Interrupt should be clear"),;
|
||||
String code;
|
||||
String condition;
|
||||
Flag(String code, String condition) {
|
||||
this.code = code;
|
||||
this.condition = condition;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
Consumer<Byte> timerHandler;
|
||||
Consumer<Byte> tickCountHandler;
|
||||
Consumer<Byte> errorHandler;
|
||||
Consumer<Byte> stopHandler;
|
||||
Consumer<Byte> progressHandler;
|
||||
Consumer<Byte> traceHandler;
|
||||
|
||||
public TestProgram() {
|
||||
lines.add(TestProgram.UNIT_TEST_MACROS);
|
||||
}
|
||||
|
||||
public TestProgram(String line1) {
|
||||
this();
|
||||
lines.add(line1);
|
||||
}
|
||||
|
||||
public TestProgram(String line1, int tickCount) {
|
||||
this();
|
||||
assertTimed(line1, tickCount);
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
|
||||
int timerStart = 0;
|
||||
int timerLastMark = 0;
|
||||
int timerEnd = 0;
|
||||
int timerLastEllapsed = 0;
|
||||
int lastBreakpoint = -1;
|
||||
int maxTicks = 10000;
|
||||
boolean programCompleted = false;
|
||||
boolean programReportedError = false;
|
||||
boolean programRunning = false;
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<Integer> timings = new ArrayList<>();
|
||||
|
||||
ProgramException lastError = null;
|
||||
public static String UNIT_TEST_MACROS = """
|
||||
!cpu 65c02
|
||||
!macro extendedOp .code, .val {!byte $FC, .code, .val}
|
||||
!macro startTimer {+extendedOp $10, $80}
|
||||
!macro markTimer {+extendedOp $10, $81}
|
||||
!macro stopTimer {+extendedOp $10, $82}
|
||||
!macro assertTicks .ticks {+extendedOp $11, .ticks}
|
||||
!macro stop .p1, .p2 {
|
||||
+extendedOp .p1, .p2
|
||||
+extendedOp $13, $ff
|
||||
+traceOff
|
||||
-
|
||||
JMP -
|
||||
}
|
||||
!macro throwError .errorCode {+stop $12, .errorCode}
|
||||
!macro recordError .errorCode {+extendedOp $12, .errorCode}
|
||||
!macro success {+stop $14, $ff}
|
||||
!macro breakpoint .num {+extendedOp $14, .num}
|
||||
!macro traceOn {+extendedOp $15, $01}
|
||||
!macro traceOff {+extendedOp $15, $00}
|
||||
!macro resetRegs {
|
||||
LDA #0
|
||||
LDX #0
|
||||
LDY #0
|
||||
PHA
|
||||
PLP
|
||||
TXS
|
||||
PHP
|
||||
}
|
||||
+resetRegs
|
||||
""";
|
||||
public static String INDENT = " ";
|
||||
|
||||
public void attach() {
|
||||
timerHandler = this::handleTimer;
|
||||
tickCountHandler = this::countAndCompareTicks;
|
||||
errorHandler = this::recordError;
|
||||
stopHandler = b->stop();
|
||||
progressHandler = this::recordProgress;
|
||||
traceHandler = this::handleTrace;
|
||||
|
||||
Emulator.withComputer(c-> {
|
||||
MOS65C02 cpu = (MOS65C02) c.getCpu();
|
||||
cpu.registerExtendedCommandHandler(0x10, timerHandler);
|
||||
cpu.registerExtendedCommandHandler(0x11, tickCountHandler);
|
||||
cpu.registerExtendedCommandHandler(0x12, errorHandler);
|
||||
cpu.registerExtendedCommandHandler(0x13, stopHandler);
|
||||
cpu.registerExtendedCommandHandler(0x14, progressHandler);
|
||||
cpu.registerExtendedCommandHandler(0x15, traceHandler);
|
||||
});
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
Emulator.withComputer(c-> {
|
||||
MOS65C02 cpu = (MOS65C02) c.getCpu();
|
||||
cpu.unregisterExtendedCommandHandler(timerHandler);
|
||||
cpu.unregisterExtendedCommandHandler(tickCountHandler);
|
||||
cpu.unregisterExtendedCommandHandler(errorHandler);
|
||||
cpu.unregisterExtendedCommandHandler(stopHandler);
|
||||
cpu.unregisterExtendedCommandHandler(progressHandler);
|
||||
cpu.unregisterExtendedCommandHandler(traceHandler);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleTimer(byte val) {
|
||||
switch (val) {
|
||||
case (byte)0x80:
|
||||
timerStart = tickCount;
|
||||
timerLastMark = tickCount;
|
||||
break;
|
||||
case (byte)0x82:
|
||||
timerEnd = tickCount;
|
||||
// Fall through
|
||||
case (byte)0x81:
|
||||
// Don't count the time spent on the timer commands!
|
||||
timerLastEllapsed = (tickCount - timerLastMark) - 4;
|
||||
timerLastMark = tickCount;
|
||||
break;
|
||||
default:
|
||||
lastError = new ProgramException("Unknown timer command %s".formatted(val), lastBreakpoint);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void countAndCompareTicks(byte val) {
|
||||
int expectedTickCountNum = val;
|
||||
int expectedTickCount = timings.get(expectedTickCountNum);
|
||||
String errorMessage = lastError != null ? lastError.getProgramLocation() : "";
|
||||
if (timerLastEllapsed != expectedTickCount) {
|
||||
lastError = new ProgramException("Expected %s ticks, instead counted %s <<%s>>".formatted(expectedTickCount, timerLastEllapsed, errorMessage), lastBreakpoint);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void recordError(byte v) {
|
||||
int val = v & 0x0ff;
|
||||
|
||||
if (val >= 0 && val < errors.size()) {
|
||||
lastError = new ProgramException(errors.get(val), lastBreakpoint);
|
||||
} else if (val == 255) {
|
||||
lastError = null;
|
||||
} else {
|
||||
lastError = new ProgramException("Error %s".formatted(val), lastBreakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public TestProgram defineError(String error) {
|
||||
errors.add(error);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
programReportedError = lastError != null;
|
||||
programRunning = false;
|
||||
}
|
||||
|
||||
private void recordProgress(byte val) {
|
||||
if (val == (byte)0xff) {
|
||||
programCompleted = true;
|
||||
return;
|
||||
} else {
|
||||
lastBreakpoint = val;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTrace(byte val) {
|
||||
if (val == (byte)0x01) {
|
||||
System.out.println("Trace on");
|
||||
Emulator.withComputer(c->c.getCpu().setTraceEnabled(true));
|
||||
} else {
|
||||
System.out.println("Trace off");
|
||||
Emulator.withComputer(c->c.getCpu().setTraceEnabled(false));
|
||||
}
|
||||
}
|
||||
|
||||
public TestProgram assertTimed(String line, int ticks) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
int errNum = errors.size();
|
||||
errors.add("Expected %s ticks for %s <<%s>>".formatted(ticks, line, caller));
|
||||
int timingNum = timings.size();
|
||||
timings.add(ticks);
|
||||
lines.add(INDENT+"+startTimer");
|
||||
lines.add(line);
|
||||
lines.add(INDENT+"+markTimer");
|
||||
lines.add(INDENT+"+recordError %s".formatted(errNum));
|
||||
lines.add(INDENT+"+assertTicks %s ; Check for %s cycles".formatted(timingNum, ticks));
|
||||
lines.add(INDENT+"+recordError %s".formatted(255));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestProgram add(String line) {
|
||||
lines.add(line);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestProgram assertEquals(String message) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
_test(TestProgram.INDENT + TestProgram.Flag.IS_ZERO.code, message + "<<" + caller + ">>");
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestProgram assertFlags(TestProgram.Flag... flags) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
for (TestProgram.Flag flag : flags)
|
||||
_test(TestProgram.INDENT + flag.code, flag.condition + "<<" + caller + ">>");
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test the A register for a specific value */
|
||||
public TestProgram assertA(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "A != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CMP #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition + " in " + caller);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test X register for a specific value */
|
||||
public TestProgram assertX(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "X != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CPX #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test Y register for a specific value */
|
||||
public TestProgram assertY(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "Y != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CPY #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test an address for a specific value. If the value is incorrect, leave it in A for inspection */
|
||||
public TestProgram assertAddrVal(int address, int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "$%s != %s <<%s>>".formatted(Integer.toHexString(address), Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
PHA
|
||||
LDA $%s
|
||||
CMP #%s
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
LDA $%s
|
||||
""".formatted(Integer.toHexString(address), val, val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Use provided code to test a condition. If successful it should jump or branch to +
|
||||
* If unsuccessful the error condition will be reported.
|
||||
*
|
||||
* @param code The code to test
|
||||
* @param condition The condition to report if the test fails
|
||||
*/
|
||||
public TestProgram test(String code, String condition) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
_test(code, condition + "<<" + caller + ">>");
|
||||
return this;
|
||||
}
|
||||
|
||||
private void _test(String code, String condition) {
|
||||
int errorNum = errors.size();
|
||||
errors.add(condition);
|
||||
lines.add("""
|
||||
; << Test %s
|
||||
%s
|
||||
+throwError %s
|
||||
+ ; >> Test """.formatted(condition, code, errorNum));
|
||||
}
|
||||
|
||||
public TestProgram throwError(String error) {
|
||||
_test("", error);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Note the current breakpoint, helpful in understanding error locations
|
||||
*
|
||||
* @param num Error number to report in any error that occurs
|
||||
*/
|
||||
public TestProgram breakpoint(int num) {
|
||||
lines.add(INDENT + "+breakpoint %s".formatted(num));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the registers to 0, clear the stack, and clear flags
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public TestProgram resetRegisters() {
|
||||
lines.add(INDENT + "+resetRegs");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on or off tracing
|
||||
*
|
||||
* @param state True to turn on tracing, false to turn it off
|
||||
*/
|
||||
public TestProgram setTrace(boolean state) {
|
||||
lines.add(INDENT +"+trace%s".formatted(state ? "On" : "Off"));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the program as unassembled code
|
||||
*
|
||||
* @return The program as a string
|
||||
*/
|
||||
public String build() {
|
||||
lines.add(INDENT +"+success");
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the program for a specific number of ticks, or until it completes (whichever comes first)
|
||||
*
|
||||
* @param ticks The number of ticks to run the program
|
||||
* @throws ProgramException If the program reports an error
|
||||
*/
|
||||
public void runForTicks(int ticks) throws ProgramException {
|
||||
this.maxTicks = ticks;
|
||||
run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the program until it completes, reports an error, or reaches the maximum number of ticks
|
||||
*
|
||||
* @throws ProgramException If the program reports an error
|
||||
*/
|
||||
public void run() throws ProgramException {
|
||||
Computer computer = Emulator.withComputer(c->c, null);
|
||||
MOS65C02 cpu = (MOS65C02) computer.getCpu();
|
||||
|
||||
attach();
|
||||
programRunning = true;
|
||||
String program = build();
|
||||
// We have to run the program more carefully so just load it first
|
||||
try {
|
||||
runAssemblyCode(program, 0);
|
||||
} catch (Exception e) {
|
||||
throw new ProgramException(e.getMessage() + "\n" + program, -1);
|
||||
}
|
||||
cpu.resume();
|
||||
try {
|
||||
for (int i=0; i < maxTicks; i++) {
|
||||
cpu.doTick();
|
||||
tickCount++;
|
||||
if (cpu.interruptSignalled) {
|
||||
if (lastError == null) {
|
||||
lastError = new ProgramException("Interrupt signalled by BRK opcode", lastBreakpoint);
|
||||
}
|
||||
programReportedError=true;
|
||||
}
|
||||
if (programReportedError) {
|
||||
throw lastError;
|
||||
}
|
||||
if (!programRunning) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cpu.suspend();
|
||||
detach();
|
||||
}
|
||||
assertFalse("Test reported an error", programReportedError);
|
||||
assertFalse("Program never ended fully after " + tickCount + " ticks; got to breakpoint " + lastBreakpoint, programRunning);
|
||||
assertTrue("Test did not complete fully", programCompleted);
|
||||
}
|
||||
}
|
||||
173
src/test/java/jace/TestUtils.java
Normal file
173
src/test/java/jace/TestUtils.java
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2023 org.badvision.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author brobert
|
||||
*/
|
||||
public class TestUtils {
|
||||
private TestUtils() {
|
||||
// Utility class has no constructor
|
||||
}
|
||||
|
||||
public static void initComputer() {
|
||||
Utility.setHeadlessMode(true);
|
||||
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);
|
||||
}
|
||||
|
||||
public static void runAssemblyCode(String code, int ticks) throws Exception {
|
||||
runAssemblyCode(code, 0x6000, ticks);
|
||||
}
|
||||
|
||||
public static void runAssemblyCode(String code, int addr, int ticks) throws Exception {
|
||||
CPU cpu = Emulator.withComputer(c->c.getCpu(), null);
|
||||
HeadlessProgram program = new HeadlessProgram(Program.DocumentType.assembly);
|
||||
program.setValue("*=$"+Integer.toHexString(addr)+"\n "+code+"\n NOP\n RTS");
|
||||
program.execute();
|
||||
if (ticks > 0) {
|
||||
cpu.resume();
|
||||
for (int i=0; i < ticks; i++) {
|
||||
cpu.doTick();
|
||||
}
|
||||
cpu.suspend();
|
||||
}
|
||||
}
|
||||
|
||||
public static Device createSimpleDevice(Runnable r, String name) {
|
||||
return new Device() {
|
||||
@Override
|
||||
public void tick() {
|
||||
r.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconfigure() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeviceName() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
262
src/test/java/jace/apple2e/CpuUnitTest.java
Normal file
262
src/test/java/jace/apple2e/CpuUnitTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
74
src/test/java/jace/apple2e/CycleCountTest.java
Normal file
74
src/test/java/jace/apple2e/CycleCountTest.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2023 org.badvision.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace.apple2e;
|
||||
|
||||
import static jace.TestUtils.initComputer;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.ProgramException;
|
||||
import jace.TestProgram;
|
||||
import jace.core.SoundMixer;
|
||||
|
||||
|
||||
/**
|
||||
* More advanced cycle counting tests. These help ensure CPU runs correctly so things
|
||||
* like vapor lock and speaker sound work as expected.
|
||||
* @author brobert
|
||||
*/
|
||||
public class CycleCountTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite test which ensures the speaker beep is the right pitch.
|
||||
* Test that the wait routine for beep cycles correctly.
|
||||
* Calling WAIT with A=#$c (12) should take 535 cycles
|
||||
* according to the tech ref notes: =1/2*(26+27*A+5*A^2) where A = 12 (0x0c)
|
||||
* The BELL routine has an additional 12 cycles per iteration plus 1 extra cycle in the first iteration.
|
||||
* e.g. 2 iterations take 1093 cycles
|
||||
*
|
||||
* @throws ProgramException
|
||||
*/
|
||||
@Test
|
||||
public void testDirectBeeperCycleCount() throws ProgramException {
|
||||
new TestProgram("""
|
||||
SPKR = $C030
|
||||
jmp BELL
|
||||
WAIT sec
|
||||
WAIT2 pha
|
||||
WAIT3 sbc #$01
|
||||
bne WAIT3
|
||||
pla
|
||||
sbc #$01
|
||||
bne WAIT2
|
||||
rts
|
||||
BELL +markTimer
|
||||
ldy #$02
|
||||
BELL2 lda #$0c
|
||||
jsr WAIT
|
||||
lda SPKR
|
||||
dey
|
||||
bne BELL2
|
||||
""", 1093).run();
|
||||
}
|
||||
|
||||
}
|
||||
804
src/test/java/jace/apple2e/Full65C02Test.java
Normal file
804
src/test/java/jace/apple2e/Full65C02Test.java
Normal file
@@ -0,0 +1,804 @@
|
||||
/*
|
||||
* Copyright 2024 Brendan Robert
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace.apple2e;
|
||||
|
||||
import static jace.TestProgram.Flag.CARRY_CLEAR;
|
||||
import static jace.TestProgram.Flag.CARRY_SET;
|
||||
import static jace.TestProgram.Flag.DECIMAL_CLEAR;
|
||||
import static jace.TestProgram.Flag.DECIMAL_SET;
|
||||
import static jace.TestProgram.Flag.INTERRUPT_CLEAR;
|
||||
import static jace.TestProgram.Flag.INTERRUPT_SET;
|
||||
import static jace.TestProgram.Flag.IS_ZERO;
|
||||
import static jace.TestProgram.Flag.NEGATIVE;
|
||||
import static jace.TestProgram.Flag.NOT_ZERO;
|
||||
import static jace.TestProgram.Flag.OVERFLOW_CLEAR;
|
||||
import static jace.TestProgram.Flag.OVERFLOW_SET;
|
||||
import static jace.TestProgram.Flag.POSITIVE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.ProgramException;
|
||||
import jace.TestProgram;
|
||||
import jace.TestUtils;
|
||||
import jace.core.Computer;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
import jace.core.SoundMixer;
|
||||
|
||||
/**
|
||||
* Basic test functionality to assert correct 6502 decode and execution.
|
||||
*
|
||||
* @author blurry
|
||||
*/
|
||||
public class Full65C02Test {
|
||||
|
||||
static Computer computer;
|
||||
public static MOS65C02 cpu;
|
||||
static RAM128k ram;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
TestUtils.initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
computer = Emulator.withComputer(c->c, null);
|
||||
cpu = (MOS65C02) computer.getCpu();
|
||||
ram = (RAM128k) computer.getMemory();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardownClass() {
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
computer.pause();
|
||||
cpu.clearState();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ADC: All CPU flags/modes */
|
||||
public void testAdditionCPUFlags() throws ProgramException {
|
||||
new TestProgram()
|
||||
// Add 1 w/o carry; 1+1 = 2
|
||||
.assertTimed("ADC #1", 2)
|
||||
.assertA(1)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Add 1 w/ carry 1+0+c = 2
|
||||
.add("SEC")
|
||||
.assertTimed("ADC #0", 2)
|
||||
.assertA(2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: ADD 8 w/o carry; 2+8 = 10
|
||||
.add("SED")
|
||||
.assertTimed("ADC #8", 3)
|
||||
.assertA(0x10)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: ADD 9 w/ carry; 10+9+c = 20
|
||||
.add("SEC")
|
||||
.assertTimed("ADC #09", 3)
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: Overflow check; 20 + 99 + C = 20 (carry, no overflow)
|
||||
.add("""
|
||||
SED
|
||||
SEC
|
||||
LDA #$20
|
||||
ADC #$99
|
||||
""")
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: Overflow check; 20 + 64 + C = 85 (overflow)
|
||||
.add("ADC #$64")
|
||||
.assertA(0x85)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
// Overflow check; 0x7F + 0x01 = 0x80 (overflow)
|
||||
.add("CLD")
|
||||
.add("LDA #$7F")
|
||||
.assertTimed("ADC #1", 2)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ADC: All addressing modes -- these are used in many opcodes so we mostly just need to test here */
|
||||
public void testAdditionAddressingModes() throws ProgramException {
|
||||
// Start test by filling zero page and $1000...$10FF with 0...255
|
||||
new TestProgram("""
|
||||
LDX #0
|
||||
- TXA
|
||||
STA $00,X
|
||||
STA $1000,X
|
||||
INX
|
||||
BNE -
|
||||
LDA #1
|
||||
""")
|
||||
// 1: AB,X
|
||||
.add("LDX #$7F")
|
||||
.assertTimed("ADC $1000,X", 4)
|
||||
.assertA(0x80)
|
||||
.assertFlags(OVERFLOW_SET, CARRY_CLEAR, NEGATIVE)
|
||||
// 2: AB,Y
|
||||
.add("LDY #$20")
|
||||
.assertTimed("ADC $1000,Y", 4)
|
||||
.assertA(0xA0)
|
||||
.assertFlags(OVERFLOW_CLEAR, CARRY_CLEAR, NEGATIVE)
|
||||
// 3: izp ($09) == 0x100f ==> A+=f
|
||||
.assertTimed("ADC ($0f)", 5)
|
||||
.assertA(0xAF)
|
||||
// 4: izx ($00,x) where X=f = 0x100f ==> A+=f
|
||||
.add("LDX #$0F")
|
||||
.assertTimed("ADC ($00,x)", 6)
|
||||
.assertA(0xBE)
|
||||
// 5: izy ($00),y where Y=20 = 0x102F ==> A+=2F
|
||||
.add("LDY #$21")
|
||||
.assertTimed("ADC ($0F),y", 5)
|
||||
.assertA(0xEE)
|
||||
// 6: zpx $00,x where X=10 ==> A+=10
|
||||
.add("LDX #$10")
|
||||
.assertTimed("ADC $00,x", 4)
|
||||
.assertA(0xFE)
|
||||
// 7: zp $01 ==> A+=01
|
||||
.assertTimed("ADC $01", 3)
|
||||
.assertA(0xFF)
|
||||
// 8: abs $1001 ==> A+=01
|
||||
.assertTimed("ADC $1001", 4)
|
||||
.assertA(0x00)
|
||||
.assertFlags(IS_ZERO, OVERFLOW_CLEAR, CARRY_SET, POSITIVE)
|
||||
// Now check boundary conditions on indexed addressing for timing differences
|
||||
.add("LDX #$FF")
|
||||
.assertTimed("ADC $10FF,X", 5)
|
||||
.add("LDY #$FF")
|
||||
.assertTimed("ADC $10FF,Y", 5)
|
||||
.assertTimed("ADC ($0F),Y", 6)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ABS: All CPU flags/modes */
|
||||
public void testSubtraction() throws ProgramException {
|
||||
new TestProgram("SEC")
|
||||
// 0-1 = -1
|
||||
.assertTimed("SBC #1", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// 127 - -1 = 128 (Overflow)
|
||||
.add("SEC")
|
||||
.add("LDA #$7F")
|
||||
.assertTimed("SBC #$FF", 2)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
// -128 - 1 = -129 (overflow)
|
||||
.add("SEC")
|
||||
.add("LDA #$80")
|
||||
.assertTimed("SBC #$01", 2)
|
||||
.assertA(0x7F)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE)
|
||||
// 20-10=10 (no overflow)
|
||||
.add("LDA #$30")
|
||||
.assertTimed("SBC #$10", 2)
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 20-5=15
|
||||
.add("SED")
|
||||
.assertTimed("SBC #$05", 3)
|
||||
.assertA(0x15)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 0x15-0x05=0x10
|
||||
.assertTimed("SBC #$05", 3)
|
||||
.assertA(0x10)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 99-19=80
|
||||
.add("LDA #$99")
|
||||
.assertTimed("SBC #$19", 3)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// Decimal: 99-50=49 (unintuitively causes overflow)
|
||||
.add("LDA #$99")
|
||||
.assertTimed("SBC #$50", 3)
|
||||
.assertA(0x49)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE)
|
||||
// Decimal: 19 - 22 = 97 (arithmetic underflow clears carry)
|
||||
.add("LDA #$19")
|
||||
.assertTimed("SBC #$22", 3)
|
||||
.assertA(0x97)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// Test cycle counts for other addressing modes
|
||||
.add("CLD")
|
||||
.assertTimed("SBC $1000", 4)
|
||||
.assertTimed("SBC $1000,x", 4)
|
||||
.assertTimed("SBC $1000,y", 4)
|
||||
.assertTimed("SBC ($00)", 5)
|
||||
.assertTimed("SBC ($00,X)", 6)
|
||||
.assertTimed("SBC ($00),Y", 5)
|
||||
.assertTimed("SBC $00", 3)
|
||||
.assertTimed("SBC $00,X", 4)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* Full test of ADC and SBC with binary coded decimal mode */
|
||||
public void testBCD() throws ProgramException, URISyntaxException, IOException {
|
||||
Path resource = Paths.get(getClass().getResource("/jace/bcd_test.asm").toURI());
|
||||
String testCode = Files.readString(resource);
|
||||
TestProgram test = new TestProgram(testCode);
|
||||
test.defineError("Error when performing ADC operation");
|
||||
test.defineError("Error when performing SBC operation");
|
||||
try {
|
||||
test.runForTicks(50000000);
|
||||
} catch (ProgramException e) {
|
||||
// Dump memory from 0x0006 to 0x0015
|
||||
for (int i = 0x0006; i <= 0x0015; i++) {
|
||||
System.out.printf("%04X: %02X\n", i, ram.read(i, TYPE.READ_DATA, false, false));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
/* Test of the processor flags */
|
||||
public void testFlags() throws ProgramException {
|
||||
new TestProgram()
|
||||
// Test explicit flag set/clear commands (and the tests by way of this cover all branch instructions)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SEC", 2)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLC", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SED", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_SET, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLD", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SEI", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_SET)
|
||||
.assertTimed("CLI", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
// Set overflow flag by hacking the P register
|
||||
.add("""
|
||||
LDA #%01000000
|
||||
PHA
|
||||
PLP
|
||||
""")
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLV", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
// Test Zero and Negative flags (within reason, the ADC/SBC tests cover these more thoroughly)
|
||||
.assertTimed("LDA #0",2 )
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDA #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDY #0", 2)
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDY #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDX #0", 2)
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDX #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test stack operations */
|
||||
@Test
|
||||
public void testStack() throws ProgramException {
|
||||
new TestProgram()
|
||||
.assertTimed("TSX", 2)
|
||||
.assertX(0xFF)
|
||||
.assertTimed("LDA #255", 2)
|
||||
.assertTimed("PHA", 3)
|
||||
.assertTimed("LDX #11", 2)
|
||||
.assertTimed("PHX", 3)
|
||||
.assertTimed("LDY #12", 2)
|
||||
.assertTimed("PHY", 3)
|
||||
.assertTimed("PHP", 3)
|
||||
.assertTimed("TSX", 2)
|
||||
.assertX(0xFB)
|
||||
.assertTimed("PLP", 4)
|
||||
.add("LDA #0")
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("PLA", 4)
|
||||
.assertA(12)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.add("LDY #$FF")
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("PLY", 4)
|
||||
.assertY(11)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("PLX", 4)
|
||||
.assertX(255)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test logic operations */
|
||||
@Test
|
||||
public void testLogic() throws ProgramException {
|
||||
// OR, AND, EOR
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("AND #0xAA", 2)
|
||||
.assertA(0x00)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("ORA #0xAA", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("EOR #0xFF", 2)
|
||||
.assertA(0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.run();
|
||||
TestProgram cycleCounts = new TestProgram();
|
||||
for (String opcode : new String[] {"AND", "ORA", "EOR"}) {
|
||||
cycleCounts.assertTimed(opcode + " $1000", 4)
|
||||
.assertTimed(opcode + " $1000,x", 4)
|
||||
.assertTimed(opcode + " $1000,y", 4)
|
||||
.assertTimed(opcode + " ($00)", 5)
|
||||
.assertTimed(opcode + " ($00,X)", 6)
|
||||
.assertTimed(opcode + " ($00),Y", 5)
|
||||
.assertTimed(opcode + " $00", 3)
|
||||
.assertTimed(opcode + " $00,X", 4);
|
||||
}
|
||||
cycleCounts.run();
|
||||
// ASL
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.add("STA $1000")
|
||||
.assertTimed("ASL", 2)
|
||||
.assertA(0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ASL", 2)
|
||||
.assertA(0x54)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("ASL $1000", 6)
|
||||
.assertAddrVal(0x1000, 0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ASL $1000,x", 7)
|
||||
.assertTimed("ASL $00", 5)
|
||||
.assertTimed("ASL $00,x", 6)
|
||||
.run();
|
||||
// LSR
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.add("STA $1000")
|
||||
.assertTimed("LSR", 2)
|
||||
.assertA(0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("LSR", 2)
|
||||
.assertA(0x15)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_CLEAR)
|
||||
.assertTimed("LSR $1000", 6)
|
||||
.assertAddrVal(0x1000, 0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("LSR $1000,x", 7)
|
||||
.assertTimed("LSR $00", 5)
|
||||
.assertTimed("LSR $00,x", 6)
|
||||
.run();
|
||||
// BIT
|
||||
new TestProgram()
|
||||
.add("LDA #$FF")
|
||||
.add("STA $FF")
|
||||
.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", 3)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, 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)
|
||||
.run();
|
||||
// ROL
|
||||
new TestProgram()
|
||||
.add("LDA #0x55")
|
||||
.add("STA $1000")
|
||||
.assertTimed("ROL", 2)
|
||||
.assertA(0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ROL", 2)
|
||||
.assertA(0x54)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.add("CLC")
|
||||
.assertTimed("ROL $1000", 6)
|
||||
.assertAddrVal(0x1000, 0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ROL $1000,x", 7)
|
||||
.assertTimed("ROL $00", 5)
|
||||
.assertTimed("ROL $00,x", 6)
|
||||
.run();
|
||||
// ROR
|
||||
new TestProgram()
|
||||
.add("LDA #0x55")
|
||||
.add("STA $1000")
|
||||
.assertTimed("ROR", 2)
|
||||
.assertA(0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("ROR", 2)
|
||||
.assertA(0x95)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ROR $1000", 6)
|
||||
.assertAddrVal(0x1000, 0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("ROR $1000,x", 7)
|
||||
.assertTimed("ROR $00", 5)
|
||||
.assertTimed("ROR $00,x", 6)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Increment/Decrement instructions */
|
||||
@Test
|
||||
public void testIncDec() throws ProgramException {
|
||||
new TestProgram()
|
||||
.add("LDA #0")
|
||||
.add("STA $1000")
|
||||
.assertTimed("INC", 2)
|
||||
.assertA(1)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("DEC", 2)
|
||||
.assertA(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.add("LDA #$FF")
|
||||
.assertTimed("INC", 2)
|
||||
.assertA(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("DEC", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("INC $1000", 6)
|
||||
.assertAddrVal(0x1000, 1)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("DEC $1000", 6)
|
||||
.assertAddrVal(0x1000, 0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("INC $1000,x", 7)
|
||||
.assertTimed("DEC $1000,x", 7)
|
||||
.assertTimed("INC $00", 5)
|
||||
.assertTimed("DEC $00", 5)
|
||||
.assertTimed("INC $00,x", 6)
|
||||
.assertTimed("DEC $00,x", 6)
|
||||
// INX/DEX/INY/DEY
|
||||
.add("LDX #0")
|
||||
.assertTimed("INX", 2)
|
||||
.assertX(1)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("DEX", 2)
|
||||
.assertX(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("DEX", 2)
|
||||
.assertX(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("INX", 2)
|
||||
.assertX(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.add("LDY #0")
|
||||
.assertTimed("INY", 2)
|
||||
.assertY(1)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("DEY", 2)
|
||||
.assertY(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("DEY", 2)
|
||||
.assertY(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("INY", 2)
|
||||
.assertY(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
|
||||
.run();
|
||||
}
|
||||
|
||||
/* All compare instructions CMP, CPX, CPY */
|
||||
@Test
|
||||
public void testComparisons() throws ProgramException {
|
||||
new TestProgram()
|
||||
.add("LDA #0")
|
||||
.assertTimed("CMP #0", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("CMP #1", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.add("LDA #$FF")
|
||||
.assertTimed("CMP #0", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET)
|
||||
.assertTimed("CMP #$FF", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
.add("LDX #0")
|
||||
.assertTimed("CPX #0", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("CPX #1", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.add("LDX #$FF")
|
||||
.assertTimed("CPX #0", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET)
|
||||
.assertTimed("CPX #$FF", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
.add("LDY #0")
|
||||
.assertTimed("CPY #0", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("CPY #1", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.add("LDY #$FF")
|
||||
.assertTimed("CPY #0", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET)
|
||||
.assertTimed("CPY #$FF", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, CARRY_SET)
|
||||
// Cycle count other modes
|
||||
.assertTimed("CMP $1000", 4)
|
||||
.assertTimed("CMP $1000,x", 4)
|
||||
.assertTimed("CMP $1000,y", 4)
|
||||
.assertTimed("CMP ($00)", 5)
|
||||
.assertTimed("CMP ($00,X)", 6)
|
||||
.assertTimed("CMP ($00),Y", 5)
|
||||
.assertTimed("CMP $00", 3)
|
||||
.assertTimed("CMP $00,X", 4)
|
||||
.assertTimed("CPX $1000", 4)
|
||||
.assertTimed("CPX $10", 3)
|
||||
.assertTimed("CPY $1000", 4)
|
||||
.assertTimed("CPY $10", 3)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Load/Store/Transfer operations */
|
||||
@Test
|
||||
public void testLoadStore() throws ProgramException {
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0", 2)
|
||||
.assertA(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.add("LDA #$FF")
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDX #0", 2)
|
||||
.assertX(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.add("LDX #$FE")
|
||||
.assertX(0xFE)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDY #0", 2)
|
||||
.assertY(0)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.add("LDY #$FD")
|
||||
.assertY(0xFD)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("STA $1000", 4)
|
||||
.assertAddrVal(0x1000, 0xFF)
|
||||
.assertTimed("STX $1001", 4)
|
||||
.assertAddrVal(0x1001, 0xFE)
|
||||
.assertTimed("STY $1002", 4)
|
||||
.assertAddrVal(0x1002, 0xFD)
|
||||
.assertTimed("LDA $1002", 4)
|
||||
.assertA(0xFD)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDX $1000", 4)
|
||||
.assertX(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDY $1001", 4)
|
||||
.assertY(0xFE)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
// Cycle count for other LDA modes
|
||||
.assertTimed("LDA $1000,x", 4)
|
||||
.assertTimed("LDA $1000,y", 4)
|
||||
.assertTimed("LDA ($00)", 5)
|
||||
.assertTimed("LDA ($00,X)", 6)
|
||||
.assertTimed("LDA ($00),Y", 5)
|
||||
.assertTimed("LDA $00", 3)
|
||||
.assertTimed("LDA $00,X", 4)
|
||||
// Cycle counts for other STA modes
|
||||
.assertTimed("STA $1000,x", 5)
|
||||
.assertTimed("STA $1000,y", 5)
|
||||
.assertTimed("STA ($00)", 5)
|
||||
.assertTimed("STA ($00,X)", 6)
|
||||
.assertTimed("STA ($00),Y", 6)
|
||||
.assertTimed("STA $00", 3)
|
||||
.assertTimed("STA $00,X", 4)
|
||||
// Cycle counts for other LDX and LDY modes
|
||||
.assertTimed("LDX $1000", 4)
|
||||
.assertTimed("LDX $1000,y", 4)
|
||||
.assertTimed("LDX $00", 3)
|
||||
.assertTimed("LDX $00,y", 4)
|
||||
.assertTimed("LDY $1000", 4)
|
||||
.assertTimed("LDY $1000,x", 4)
|
||||
.assertTimed("LDY $00", 3)
|
||||
.assertTimed("LDY $00,x", 4)
|
||||
// Cycle counts for other STX and STY modes
|
||||
.assertTimed("STX $1000", 4)
|
||||
.assertTimed("STX $00", 3)
|
||||
.assertTimed("STX $00, Y", 4)
|
||||
.assertTimed("STY $1000", 4)
|
||||
.assertTimed("STY $00", 3)
|
||||
.assertTimed("STY $00, X", 4)
|
||||
// STZ
|
||||
.assertTimed("STZ $1000", 4)
|
||||
.assertAddrVal(0x1000, 0)
|
||||
.assertTimed("STZ $1000,x", 5)
|
||||
.assertTimed("STZ $00", 3)
|
||||
.assertTimed("STZ $00,x", 4)
|
||||
.run();
|
||||
// Now test the transfer instructions
|
||||
new TestProgram()
|
||||
.add("LDA #10")
|
||||
.add("LDX #20")
|
||||
.add("LDY #30")
|
||||
.assertTimed("TAX", 2)
|
||||
.assertX(10)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("TAY", 2)
|
||||
.assertY(10)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.add("LDA #$FF")
|
||||
.assertTimed("TAX", 2)
|
||||
.assertX(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("TAY", 2)
|
||||
.assertY(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.add("LDA #0")
|
||||
.assertTimed("TXA", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.add("LDA #0")
|
||||
.assertTimed("TYA", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("TSX", 2)
|
||||
.assertX(0xFF)
|
||||
.assertTimed("TXS", 2)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test branch instructions */
|
||||
@Test
|
||||
public void testBranches() throws ProgramException {
|
||||
new TestProgram()
|
||||
// Zero and Negative flags
|
||||
.add("LDA #0")
|
||||
.assertTimed("BEQ *+2",3)
|
||||
.assertTimed("BNE *+2",2)
|
||||
.assertTimed("BPL *+2",3)
|
||||
.assertTimed("BMI *+2",2)
|
||||
.add("LDA #$FF")
|
||||
.assertTimed("BEQ *+2",2)
|
||||
.assertTimed("BNE *+2",3)
|
||||
.assertTimed("BPL *+2",2)
|
||||
.assertTimed("BMI *+2",3)
|
||||
.assertTimed("BRA *+2", 3)
|
||||
// Carry flag
|
||||
.assertTimed("BCC *+2", 3)
|
||||
.assertTimed("BCS *+2", 2)
|
||||
.add("SEC")
|
||||
.assertTimed("BCC *+2", 2)
|
||||
.assertTimed("BCS *+2", 3)
|
||||
// Overflow flag
|
||||
.add("CLV")
|
||||
.assertTimed("BVC *+2", 3)
|
||||
.assertTimed("BVS *+2", 2)
|
||||
.add("""
|
||||
lda #$40
|
||||
sta $1000
|
||||
bit $1000
|
||||
""")
|
||||
.assertTimed("BVC *+2", 2)
|
||||
.assertTimed("BVS *+2", 3)
|
||||
.assertTimed("NOP", 2)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test JMP */
|
||||
@Test
|
||||
public void testJmp() throws ProgramException {
|
||||
new TestProgram()
|
||||
.add("LDA #0")
|
||||
.assertTimed("""
|
||||
JMP +
|
||||
LDA #$FF
|
||||
NOP
|
||||
NOP
|
||||
+
|
||||
""", 3)
|
||||
.assertA(0)
|
||||
// Testing indirect jump using self-modifying code
|
||||
// Load the address of jmp target and store at $300
|
||||
.add("""
|
||||
LDA #<jmpTarget
|
||||
STA $300
|
||||
LDA #>jmpTarget
|
||||
STA $301
|
||||
LDX #$FF
|
||||
""")
|
||||
.assertTimed("JMP ($300)", 6)
|
||||
.add("""
|
||||
LDX #0
|
||||
jmpTarget LDA #$88
|
||||
""")
|
||||
.assertA(0x88)
|
||||
.assertX(0xFF)
|
||||
// Perform similar test using indirect,x addressing
|
||||
.add("""
|
||||
LDA #<(jmpTargetX)
|
||||
STA $310
|
||||
LDA #>(jmpTargetX)
|
||||
STA $311
|
||||
LDX #$10
|
||||
LDY #$FF
|
||||
""")
|
||||
.assertTimed("JMP ($300,X)", 6)
|
||||
.add("""
|
||||
LDY #88
|
||||
jmpTargetX LDA #$88
|
||||
""")
|
||||
.assertA(0x88)
|
||||
.assertY(0xFF)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test JSR */
|
||||
@Test
|
||||
public void testJsr() throws ProgramException {
|
||||
// "Easy" test that JSR + RTS work as expected and together take 12 cycles
|
||||
new TestProgram()
|
||||
.add("""
|
||||
jmp +
|
||||
sub1 rts
|
||||
+throwError 69
|
||||
+
|
||||
""")
|
||||
.assertTimed("""
|
||||
JSR sub1
|
||||
""", 12)
|
||||
.run();
|
||||
// Check that JSR pushes the expected PC values to the stack
|
||||
new TestProgram()
|
||||
.add("""
|
||||
jmp start
|
||||
test
|
||||
plx
|
||||
ply
|
||||
phy
|
||||
phx
|
||||
rts
|
||||
""")
|
||||
.test("", "RTS did not return to the correct address")
|
||||
.add("""
|
||||
start
|
||||
jsr test
|
||||
ret
|
||||
""")
|
||||
.test("""
|
||||
cpy #>ret
|
||||
beq +
|
||||
""", "Y = MSB of return address")
|
||||
.test("""
|
||||
inx
|
||||
cpx #<ret
|
||||
beq +
|
||||
""", "X = LSB of return address-1")
|
||||
.run();
|
||||
}
|
||||
}
|
||||
80
src/test/java/jace/apple2e/VideoDHGRTest.java
Normal file
80
src/test/java/jace/apple2e/VideoDHGRTest.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package jace.apple2e;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
// This is mostly to provide execution coverage to catch null pointer or index out of range exceptions
|
||||
public class VideoDHGRTest extends AbstractFXTest {
|
||||
WritableImage image = new WritableImage(560, 192);
|
||||
|
||||
private VideoDHGR video;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
video = new VideoDHGR();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitHgrDhgrTables() {
|
||||
// Test the initialization of HGR_TO_DHGR and HGR_TO_DHGR_BW tables
|
||||
assertNotNull(video.HGR_TO_DHGR);
|
||||
assertNotNull(video.HGR_TO_DHGR_BW);
|
||||
// Add more assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitCharMap() {
|
||||
// Test the initialization of CHAR_MAP1, CHAR_MAP2, and CHAR_MAP3 arrays
|
||||
assertNotNull(video.CHAR_MAP1);
|
||||
assertNotNull(video.CHAR_MAP2);
|
||||
assertNotNull(video.CHAR_MAP3);
|
||||
// Add more assertions here
|
||||
}
|
||||
|
||||
private void writeToScreen() {
|
||||
video.getCurrentWriter().displayByte(image, 0, 0, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, 0, 4, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, 0, 190, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, -1, 0, 0, 0);
|
||||
video.getCurrentWriter().actualWriter().displayByte(image, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetYOffset() {
|
||||
// Run through all possible combinations of soft switches to ensure the correct Y offset is returned each time
|
||||
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
|
||||
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
|
||||
String state = "";
|
||||
for (int j=0; j < switches.length; j++) {
|
||||
switches[j].getSwitch().setState((i & (1 << j)) != 0);
|
||||
state += switches[j].getSwitch().getName() + "=" + (switches[j].getSwitch().getState() ? "1" : "0") + " ";
|
||||
}
|
||||
video.configureVideoMode();
|
||||
int address = video.getCurrentWriter().getYOffset(0);
|
||||
int expected = SoftSwitches.TEXT.isOn() || SoftSwitches.HIRES.isOff() ? (SoftSwitches.PAGE2.isOn() ? 0x0800 : 0x0400)
|
||||
: (SoftSwitches.PAGE2.isOn() ? 0x04000 : 0x02000);
|
||||
assertEquals("Address for mode not correct: " + state, expected, address);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayByte() {
|
||||
// Run through all possible combinations of soft switches to ensure the video writer executes without error
|
||||
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
|
||||
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
|
||||
for (int j=0; j < switches.length; j++) {
|
||||
switches[j].getSwitch().setState((i & (1 << j)) != 0);
|
||||
}
|
||||
video.configureVideoMode();
|
||||
writeToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Add more test cases for other methods in the VideoDHGR class
|
||||
|
||||
}
|
||||
85
src/test/java/jace/apple2e/VideoNTSCTest.java
Normal file
85
src/test/java/jace/apple2e/VideoNTSCTest.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package jace.apple2e;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
// This is mostly to provide execution coverage to catch null pointer or index out of range exceptions
|
||||
public class VideoNTSCTest extends AbstractFXTest {
|
||||
WritableImage image = new WritableImage(560, 192);
|
||||
|
||||
private VideoNTSC video;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
video = new VideoNTSC();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitHgrDhgrTables() {
|
||||
// Test the initialization of HGR_TO_DHGR and HGR_TO_DHGR_BW tables
|
||||
assertNotNull(video.HGR_TO_DHGR);
|
||||
assertNotNull(video.HGR_TO_DHGR_BW);
|
||||
// Add more assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitCharMap() {
|
||||
// Test the initialization of CHAR_MAP1, CHAR_MAP2, and CHAR_MAP3 arrays
|
||||
assertNotNull(video.CHAR_MAP1);
|
||||
assertNotNull(video.CHAR_MAP2);
|
||||
assertNotNull(video.CHAR_MAP3);
|
||||
// Add more assertions here
|
||||
}
|
||||
|
||||
private void writeToScreen() {
|
||||
video.getCurrentWriter().displayByte(image, 0, 0, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, 0, 4, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, 0, 190, 0, 0);
|
||||
video.getCurrentWriter().displayByte(image, -1, 0, 0, 0);
|
||||
video.getCurrentWriter().actualWriter().displayByte(image, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetYOffset() {
|
||||
// Run through all possible combinations of soft switches to ensure the correct Y offset is returned each time
|
||||
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
|
||||
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
|
||||
String state = "";
|
||||
for (int j=0; j < switches.length; j++) {
|
||||
switches[j].getSwitch().setState((i & (1 << j)) != 0);
|
||||
state += switches[j].getSwitch().getName() + "=" + (switches[j].getSwitch().getState() ? "1" : "0") + " ";
|
||||
}
|
||||
video.configureVideoMode();
|
||||
int address = video.getCurrentWriter().getYOffset(0);
|
||||
int expected = SoftSwitches.TEXT.isOn() || SoftSwitches.HIRES.isOff() ? (SoftSwitches.PAGE2.isOn() ? 0x0800 : 0x0400)
|
||||
: (SoftSwitches.PAGE2.isOn() ? 0x04000 : 0x02000);
|
||||
assertEquals("Address for mode not correct: " + state, expected, address);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayByte() {
|
||||
// Run through all possible combinations of soft switches to ensure the video writer executes without error
|
||||
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
|
||||
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
|
||||
for (int j=0; j < switches.length; j++) {
|
||||
switches[j].getSwitch().setState((i & (1 << j)) != 0);
|
||||
}
|
||||
video.configureVideoMode();
|
||||
writeToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayModes() {
|
||||
for (VideoNTSC.VideoMode mode : VideoNTSC.VideoMode.values()) {
|
||||
VideoNTSC.setVideoMode(mode, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
112
src/test/java/jace/applesoft/ApplesoftTest.java
Normal file
112
src/test/java/jace/applesoft/ApplesoftTest.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package jace.applesoft;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.TestUtils;
|
||||
import jace.apple2e.MOS65C02;
|
||||
import jace.apple2e.RAM128k;
|
||||
import jace.core.Computer;
|
||||
import jace.core.SoundMixer;
|
||||
import jace.ide.Program;
|
||||
import jace.ide.Program.DocumentType;
|
||||
|
||||
public class ApplesoftTest {
|
||||
|
||||
static Computer computer;
|
||||
public static MOS65C02 cpu;
|
||||
static RAM128k ram;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
TestUtils.initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
computer = Emulator.withComputer(c->c, null);
|
||||
cpu = (MOS65C02) computer.getCpu();
|
||||
ram = (RAM128k) computer.getMemory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromStringTest() {
|
||||
String programSource = "10 PRINT \"Hello, World!\"\n\n20 PRINT \"Goodbye!\"\n";
|
||||
ApplesoftHandler handler = new ApplesoftHandler();
|
||||
// We want to test as much as we can but right now it's heavily integrated with the UI
|
||||
Program program = new Program(DocumentType.applesoft, Collections.emptyMap()) {
|
||||
String value;
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
};
|
||||
program.setValue(programSource);
|
||||
var compileResult = handler.compile(program);
|
||||
assertNotNull(compileResult.getCompiledAsset());
|
||||
assertTrue(compileResult.isSuccessful());
|
||||
assertTrue(compileResult.getErrors().isEmpty());
|
||||
assertTrue(compileResult.getWarnings().isEmpty());
|
||||
assertTrue(compileResult.getOtherMessages().isEmpty());
|
||||
assertTrue(compileResult.getRawOutput().isEmpty());
|
||||
assertEquals(2, compileResult.getCompiledAsset().lines.size());
|
||||
Line line1 = compileResult.getCompiledAsset().lines.get(0);
|
||||
assertEquals(10, line1.getNumber());
|
||||
assertEquals(1, line1.getCommands().size());
|
||||
Command command1 = line1.getCommands().get(0);
|
||||
assertEquals(0xBA, command1.parts.get(0).getByte() & 0x0ff);
|
||||
String match = "";
|
||||
for (int idx=1; idx < command1.parts.size(); idx++) {
|
||||
match += command1.parts.get(idx).toString();
|
||||
}
|
||||
assertEquals("\"Hello, World!\"", match);
|
||||
// Does nothing but test coverage is test coverage
|
||||
handler.clean(null);
|
||||
|
||||
// Now let's try to execute and see if we can read the program back
|
||||
handler.execute(compileResult);
|
||||
|
||||
ApplesoftProgram program2 = Emulator.withComputer(c->ApplesoftProgram.fromMemory(c.getMemory()), null);
|
||||
assertEquals(2, program2.getLength());
|
||||
Line line2 = program2.lines.get(0);
|
||||
assertEquals(10, line2.getNumber());
|
||||
assertEquals(1, line2.getCommands().size());
|
||||
Command command2 = line2.getCommands().get(0);
|
||||
assertEquals(0xBA, command2.parts.get(0).getByte() & 0x0ff);
|
||||
match = "";
|
||||
for (int idx=1; idx < command2.parts.size(); idx++) {
|
||||
match += command2.parts.get(idx).toString();
|
||||
}
|
||||
assertEquals("\"Hello, World!\"", match);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringTest() {
|
||||
Line line1 = Line.fromString("10 print \"Hello, world!\"");
|
||||
Line line2 = Line.fromString("20 print \"Goodbye!\"");
|
||||
ApplesoftProgram program = new ApplesoftProgram();
|
||||
program.lines.add(line1);
|
||||
program.lines.add(line2);
|
||||
String programSource = program.toString();
|
||||
assertEquals("10 PRINT \"Hello, world!\"\n20 PRINT \"Goodbye!\"\n", programSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void relocateVariablesTest() {
|
||||
ApplesoftProgram program = new ApplesoftProgram();
|
||||
Line line1 = Line.fromString("10 print \"Hello, world!\"");
|
||||
Line line2 = Line.fromString("20 print \"Goodbye!\"");
|
||||
program.lines.add(line1);
|
||||
program.lines.add(line2);
|
||||
program.relocateVariables(0x6000);
|
||||
// We need better assertions here but for now we just want to make sure it doesn't crash
|
||||
}
|
||||
}
|
||||
585
src/test/java/jace/core/MemoryTest.java
Normal file
585
src/test/java/jace/core/MemoryTest.java
Normal file
@@ -0,0 +1,585 @@
|
||||
/*
|
||||
* Copyright 2023 org.badvision.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace.core;
|
||||
import static jace.TestUtils.initComputer;
|
||||
import static jace.TestUtils.runAssemblyCode;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
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;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.ProgramException;
|
||||
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.
|
||||
* @author brobert
|
||||
*/
|
||||
public class MemoryTest {
|
||||
static Computer computer;
|
||||
static MOS65C02 cpu;
|
||||
static RAM128k ram;
|
||||
static String MEMORY_TEST_COMMONS;
|
||||
static String MACHINE_IDENTIFICATION;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() throws IOException, URISyntaxException {
|
||||
initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
computer = Emulator.withComputer(c->c, null);
|
||||
cpu = (MOS65C02) computer.getCpu();
|
||||
ram = (RAM128k) computer.getMemory();
|
||||
ram.addExecutionTrap("COUT intercept", 0x0FDF0, (e)->{
|
||||
char c = (char) (cpu.A & 0x07f);
|
||||
if (c == '\r') {
|
||||
System.out.println();
|
||||
} else {
|
||||
System.out.print(c);
|
||||
}
|
||||
});
|
||||
MEMORY_TEST_COMMONS = Files.readString(Paths.get(MemoryTest.class.getResource("/jace/memory_test_commons.asm").toURI()));
|
||||
MACHINE_IDENTIFICATION = Files.readString(Paths.get(MemoryTest.class.getResource("/jace/machine_identification.asm").toURI()));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void resetEmulator() {
|
||||
computer.pause();
|
||||
cpu.clearState();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void resetSoftSwitches() {
|
||||
// Reset softswitches
|
||||
for (SoftSwitches softswitch : SoftSwitches.values()) {
|
||||
softswitch.getSwitch().reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assertMemoryConfiguredCorrectly() {
|
||||
assertEquals("Active read bank 3 should be main memory page 3",
|
||||
ram.mainMemory.getMemoryPage(3),
|
||||
ram.activeRead.getMemoryPage(3));
|
||||
|
||||
assertEquals("Active write bank 3 should be main memory page 3",
|
||||
ram.mainMemory.getMemoryPage(3),
|
||||
ram.activeWrite.getMemoryPage(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenerRelevance() throws Exception {
|
||||
AtomicInteger anyEventCaught = new AtomicInteger();
|
||||
RAMListener anyListener = new RAMListener("Execution test", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
|
||||
@Override
|
||||
protected void doConfig() {
|
||||
setScopeStart(0x0100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
anyEventCaught.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
AtomicInteger readAnyEventCaught = new AtomicInteger();
|
||||
RAMListener readAnyListener = new RAMListener("Execution test 1", RAMEvent.TYPE.READ, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
|
||||
@Override
|
||||
protected void doConfig() {
|
||||
setScopeStart(0x0100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
readAnyEventCaught.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
AtomicInteger writeEventCaught = new AtomicInteger();
|
||||
RAMListener writeListener = new RAMListener("Execution test 2", RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
|
||||
@Override
|
||||
protected void doConfig() {
|
||||
setScopeStart(0x0100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
writeEventCaught.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
AtomicInteger executeEventCaught = new AtomicInteger();
|
||||
RAMListener executeListener = new RAMListener("Execution test 3", RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
|
||||
@Override
|
||||
protected void doConfig() {
|
||||
setScopeStart(0x0100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
executeEventCaught.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
RAMEvent readDataEvent = new RAMEvent(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0);
|
||||
RAMEvent readOperandEvent = new RAMEvent(RAMEvent.TYPE.READ_OPERAND, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0);
|
||||
RAMEvent executeEvent = new RAMEvent(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0);
|
||||
RAMEvent writeEvent = new RAMEvent(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0);
|
||||
|
||||
// Any listener
|
||||
assertTrue("Any listener should handle all events", anyListener.isRelevant(readDataEvent));
|
||||
assertTrue("Any listener should handle all events", anyListener.isRelevant(readOperandEvent));
|
||||
assertTrue("Any listener should handle all events", anyListener.isRelevant(executeEvent));
|
||||
assertTrue("Any listener should handle all events", anyListener.isRelevant(writeEvent));
|
||||
|
||||
// Read listener
|
||||
assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(readDataEvent));
|
||||
assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(readOperandEvent));
|
||||
assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(executeEvent));
|
||||
assertFalse("Read listener should ignore write events", readAnyListener.isRelevant(writeEvent));
|
||||
|
||||
// Write listener
|
||||
assertFalse("Write listener should ignore all read events", writeListener.isRelevant(readDataEvent));
|
||||
assertFalse("Write listener should ignore all read events", writeListener.isRelevant(readOperandEvent));
|
||||
assertFalse("Write listener should ignore all read events", writeListener.isRelevant(executeEvent));
|
||||
assertTrue("Write listener should handle write events", writeListener.isRelevant(writeEvent));
|
||||
|
||||
// Execution listener
|
||||
assertTrue("Execute listener should only catch execution events", executeListener.isRelevant(executeEvent));
|
||||
assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(readDataEvent));
|
||||
assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(readOperandEvent));
|
||||
assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(writeEvent));
|
||||
|
||||
ram.addListener(anyListener);
|
||||
ram.addListener(executeListener);
|
||||
ram.addListener(readAnyListener);
|
||||
ram.addListener(writeListener);
|
||||
|
||||
runAssemblyCode("NOP", 0x0100, 2);
|
||||
|
||||
assertEquals("Should have no writes for 0x0100", 0, writeEventCaught.get());
|
||||
assertEquals("Should have read event for 0x0100", 1, readAnyEventCaught.get());
|
||||
assertEquals("Should have execute for 0x0100", 1, executeEventCaught.get());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adapted version of the Apple II Family Identification Program from:
|
||||
* http://www.1000bit.it/support/manuali/apple/technotes/misc/tn.misc.02.html00
|
||||
*
|
||||
* Adapted to ACME by Zellyn Hunter
|
||||
*
|
||||
* @throws ProgramException
|
||||
*/
|
||||
@Test
|
||||
public void machineIdentificationTest() throws ProgramException {
|
||||
TestProgram memoryDetectTestProgram = new TestProgram(MEMORY_TEST_COMMONS);
|
||||
memoryDetectTestProgram.add(MACHINE_IDENTIFICATION);
|
||||
// Assert this is an Apple //e
|
||||
memoryDetectTestProgram.assertAddrVal(0x0800, 0x04);
|
||||
// Assert this is an enhanced revision
|
||||
memoryDetectTestProgram.assertAddrVal(0x0801, 0x02);
|
||||
// Aser this is a 128k machine
|
||||
memoryDetectTestProgram.assertAddrVal(0x0802, 128);
|
||||
memoryDetectTestProgram.run();
|
||||
}
|
||||
|
||||
/*
|
||||
* Adapted from Zellyn Hunder's language card test:
|
||||
* https://github.com/zellyn/a2audit/blob/main/audit/langcard.asm
|
||||
*
|
||||
* Adjusted to use JACE hooks to perform assertions and error reporting
|
||||
*/
|
||||
@Test
|
||||
public void languageCardBankswitchTest() throws ProgramException {
|
||||
TestProgram lcTestProgram = new TestProgram(MEMORY_TEST_COMMONS);
|
||||
lcTestProgram.add("""
|
||||
;; Setup - store differing values in bank first and second banked areas.
|
||||
lda $C08B ; Read and write bank 1
|
||||
lda $C08B
|
||||
lda #$11
|
||||
sta $D17B ; $D17B is $53 in Apple II/plus/e/enhanced
|
||||
cmp $D17B
|
||||
""")
|
||||
.assertEquals("E0004: We tried to put the language card into read bank 1, write bank 1, but failed to write.")
|
||||
.add("""
|
||||
lda #$33
|
||||
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
|
||||
lda $C083
|
||||
lda #$22
|
||||
sta $D17B
|
||||
cmp $D17B
|
||||
""")
|
||||
.assertEquals("E0006: We tried to put the language card into read bank 2, write bank 2, but failed to write.")
|
||||
.add("""
|
||||
lda $C08B ; Read and write bank 1 with single access (only one needed if banked in already)
|
||||
lda #$11
|
||||
cmp $D17B
|
||||
""")
|
||||
.assertEquals("E000D: We tried to put the language card into read bank 1, but failed to read.")
|
||||
.add("""
|
||||
lda $C081 ; Read ROM with single access (only one needed to bank out)
|
||||
lda #$53
|
||||
cmp $D17B
|
||||
""")
|
||||
.assertEquals("E000E: We tried to put the language card into read ROM, but failed to read (from ROM).")
|
||||
.add("""
|
||||
;;; Main data-driven test. PCL,PCH holds the address of the next
|
||||
;;; data-driven test routine. We expect the various softswitches
|
||||
;;; to be reset each time we loop at .ddloop.
|
||||
.datadriventests
|
||||
lda #<.tests
|
||||
sta PCL
|
||||
lda #>.tests
|
||||
sta PCH
|
||||
;;; Main data-drive-test loop.
|
||||
.ddloop
|
||||
ldy #0
|
||||
|
||||
;; Initialize to known state:
|
||||
;; - $11 in $D17B bank 1 (ROM: $53)
|
||||
;; - $22 in $D17B bank 2 (ROM: $53)
|
||||
;; - $33 in $FE1F (ROM: $60)
|
||||
lda $C08B ; Read and write bank 1
|
||||
lda $C08B
|
||||
lda #$11
|
||||
sta $D17B
|
||||
lda #$33
|
||||
sta $FE1F
|
||||
lda $C083 ; Read and write bank 2
|
||||
lda $C083
|
||||
lda #$22
|
||||
sta $D17B
|
||||
lda $C080
|
||||
|
||||
jmp (PCL) ; Jump to test routine
|
||||
|
||||
|
||||
;; Test routine will JSR back to here, so the check data address is on the stack.
|
||||
|
||||
.test ;; ... test the quintiple of test values
|
||||
inc $D17B
|
||||
inc $FE1F
|
||||
|
||||
;; pull address off of stack: it points just below check data for this test.
|
||||
pla
|
||||
sta .checkdata
|
||||
pla
|
||||
sta .checkdata+1
|
||||
|
||||
;; .checkdata now points to d17b-current,fe1f-current,bank1,bank2,fe1f-ram test quintiple
|
||||
|
||||
;; Test current $D17B
|
||||
jsr NEXTCHECK
|
||||
cmp $D17B
|
||||
beq +
|
||||
lda $D17B
|
||||
pha
|
||||
jsr .printseq
|
||||
+print
|
||||
!text "$D17B TO CONTAIN $"
|
||||
+printed
|
||||
jsr CURCHECK
|
||||
jsr PRBYTE
|
||||
+print
|
||||
!text ", GOT $"
|
||||
+printed
|
||||
pla
|
||||
jsr PRBYTE
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp .datatesturl
|
||||
|
||||
+ ;; Test current $FE1F
|
||||
jsr NEXTCHECK
|
||||
cmp $FE1F
|
||||
beq +
|
||||
lda $FE1F
|
||||
pha
|
||||
jsr .printseq
|
||||
+print
|
||||
!text "$FE1F=$"
|
||||
+printed
|
||||
jsr CURCHECK
|
||||
jsr PRBYTE
|
||||
+print
|
||||
!text ", GOT $"
|
||||
+printed
|
||||
pla
|
||||
jsr PRBYTE
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp .datatesturl
|
||||
|
||||
+ ;; Test bank 1 $D17B
|
||||
lda $C088
|
||||
jsr NEXTCHECK
|
||||
cmp $D17B
|
||||
beq +
|
||||
lda $D17B
|
||||
pha
|
||||
jsr .printseq
|
||||
+print
|
||||
!text "$D17B IN RAM BANK 1 TO CONTAIN $"
|
||||
+printed
|
||||
jsr CURCHECK
|
||||
jsr PRBYTE
|
||||
+print
|
||||
!text ", GOT $"
|
||||
+printed
|
||||
pla
|
||||
jsr PRBYTE
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp .datatesturl
|
||||
|
||||
+ ;; Test bank 2 $D17B
|
||||
lda $C080
|
||||
jsr NEXTCHECK
|
||||
cmp $D17B
|
||||
beq +
|
||||
lda $D17B
|
||||
pha
|
||||
jsr .printseq
|
||||
+print
|
||||
!text "$D17B IN RAM BANK 2 TO CONTAIN $"
|
||||
+printed
|
||||
jsr CURCHECK
|
||||
jsr PRBYTE
|
||||
+print
|
||||
!text ", GOT $"
|
||||
+printed
|
||||
pla
|
||||
jsr PRBYTE
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp .datatesturl
|
||||
|
||||
+ ;; Test RAM $FE1F
|
||||
lda $C080
|
||||
jsr NEXTCHECK
|
||||
cmp $FE1F
|
||||
beq +
|
||||
lda $FE1F
|
||||
pha
|
||||
jsr .printseq
|
||||
+print
|
||||
!text "RAM $FE1F=$"
|
||||
+printed
|
||||
jsr CURCHECK
|
||||
jsr PRBYTE
|
||||
+print
|
||||
!text ", GOT $"
|
||||
+printed
|
||||
pla
|
||||
jsr PRBYTE
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp .datatesturl
|
||||
|
||||
+ ;; Jump PCL,PCH up to after the test data, and loop.
|
||||
jsr NEXTCHECK
|
||||
bne +
|
||||
+success
|
||||
+ ldx .checkdata
|
||||
ldy .checkdata+1
|
||||
stx PCL
|
||||
sty PCH
|
||||
jmp .ddloop
|
||||
|
||||
.datatesturl
|
||||
""")
|
||||
.throwError("E0007: This is a data-driven test of Language Card operation. We initialize $D17B in RAM bank 1 to $11, $D17B in RAM bank 2 to $22, and $FE1F in RAM to $33. Then, we perform a testdata-driven sequence of LDA and STA to the $C08X range. Finally we (try to) increment $D17B and $FE1F. Then we test (a) the current live value in $D17B, (b) the current live value in $FE1F, (c) the RAM bank 1 value of $D17B, (d) the RAM bank 2 value of $D17B, and (e) the RAM value of $FE1F, to see whether they match expected values. $D17B is usually $53 in ROM, and $FE1F is usally $60. For more information on the operation of the language card soft-switches, see Understanding the Apple IIe, by James Fielding Sather, Pg 5-24.")
|
||||
.add("""
|
||||
rts
|
||||
|
||||
.printseq
|
||||
+print
|
||||
!text "AFTER SEQUENCE OF:",$8D,"- LDA $C080",$8D
|
||||
+printed
|
||||
jsr PRINTTEST
|
||||
+print
|
||||
!text "- INC $D17B",$8D,"- INC $FE1F",$8D,"EXPECTED "
|
||||
+printed
|
||||
rts
|
||||
|
||||
.tests
|
||||
;; Format:
|
||||
;; Sequence of test instructions, finishing with `jsr .test`.
|
||||
;; - quint: expected current $d17b and fe1f, then d17b in bank1, d17b in bank 2, and fe1f
|
||||
;; (All sequences start with lda $C080, just to reset things to a known state.)
|
||||
;; 0-byte to terminate tests.
|
||||
|
||||
lda $C088 ; Read $C088 (RAM read, write protected)
|
||||
jsr .test ;
|
||||
!byte $11, $33, $11, $22, $33 ;
|
||||
jsr .test ;
|
||||
!byte $22, $33, $11, $22, $33 ;
|
||||
lda $C081 ; Read $C081 (ROM read, write disabled)
|
||||
jsr .test ;
|
||||
!byte $53, $60, $11, $22, $33
|
||||
lda $C081 ; Read $C081, $C089 (ROM read, bank 1 write)
|
||||
lda $C089 ;
|
||||
jsr .test ;
|
||||
!byte $53, $60, $54, $22, $61
|
||||
lda $C081 ; Read $C081, $C081 (read ROM, write RAM bank 2)
|
||||
lda $C081 ;
|
||||
jsr .test ;
|
||||
!byte $53, $60, $11, $54, $61
|
||||
lda $C081 ; Read $C081, $C081, write $C081 (read ROM, write RAM bank bank 2)
|
||||
lda $C081 ; See https://github.com/zellyn/a2audit/issues/3
|
||||
sta $C081 ;
|
||||
jsr .test ;
|
||||
!byte $53, $60, $11, $54, $61
|
||||
lda $C081 ; Read $C081, $C081; write $C081, $C081
|
||||
lda $C081 ; See https://github.com/zellyn/a2audit/issues/4
|
||||
sta $C081 ;
|
||||
sta $C081 ;
|
||||
jsr .test ;
|
||||
!byte $53, $60, $11, $54, $61
|
||||
lda $C08B ; Read $C08B (read RAM bank 1, no write)
|
||||
jsr .test ;
|
||||
!byte $11, $33, $11, $22, $33
|
||||
lda $C083 ; Read $C083 (read RAM bank 2, no write)
|
||||
jsr .test ;
|
||||
!byte $22, $33, $11, $22, $33
|
||||
lda $C08B ; Read $C08B, $C08B (read/write RAM bank 1)
|
||||
lda $C08B ;
|
||||
jsr .test ;
|
||||
!byte $12, $34, $12, $22, $34
|
||||
lda $C08F ; Read $C08F, $C087 (read/write RAM bank 2)
|
||||
lda $C087 ;
|
||||
jsr .test ;
|
||||
!byte $23, $34, $11, $23, $34
|
||||
lda $C087 ; Read $C087, read $C08D (read ROM, write bank 1)
|
||||
lda $C08D ;
|
||||
jsr .test ;
|
||||
!byte $53, $60, $54, $22, $61
|
||||
lda $C08B ; Read $C08B, write $C08B, read $C08B (read RAM bank 1, no write)
|
||||
sta $C08B ; (this one is tricky: reset WRTCOUNT by writing halfway)
|
||||
lda $C08B ;
|
||||
jsr .test ;
|
||||
!byte $11, $33, $11, $22, $33
|
||||
sta $C08B ; Write $C08B, write $C08B, read $C08B (read RAM bank 1, no write)
|
||||
sta $C08B ;
|
||||
lda $C08B ;
|
||||
jsr .test ;
|
||||
!byte $11, $33, $11, $22, $33
|
||||
clc ; Read $C083, $C083 (read/write RAM bank 2)
|
||||
ldx #0 ; Uses "6502 false read"
|
||||
inc $C083,x ;
|
||||
jsr .test ;
|
||||
!byte $23, $34, $11, $23, $34
|
||||
!byte 0
|
||||
""")
|
||||
// .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));
|
||||
}
|
||||
}
|
||||
84
src/test/java/jace/core/SoundTest.java
Normal file
84
src/test/java/jace/core/SoundTest.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package jace.core;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.core.SoundMixer.SoundBuffer;
|
||||
import jace.core.SoundMixer.SoundError;
|
||||
|
||||
public class SoundTest extends AbstractFXTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
System.out.println("Init sound");
|
||||
Utility.setHeadlessMode(false);
|
||||
SoundMixer.initSound();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
Utility.setHeadlessMode(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
//(Only use this to ensure the sound engine produces audible output, it's otherwise annoying to hear all the time)
|
||||
public void soundGenerationTest() throws SoundError {
|
||||
try {
|
||||
System.out.println("Performing sound test...");
|
||||
SoundMixer mixer = new SoundMixer();
|
||||
System.out.println("Attach mixer");
|
||||
mixer.attach();
|
||||
System.out.println("Allocate buffer");
|
||||
SoundBuffer buffer = SoundMixer.createBuffer(false);
|
||||
System.out.println("Generate sound");
|
||||
// for (int i = 0; i < 100000; i++) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed
|
||||
double x = Math.sin(i*i * 0.0001);
|
||||
buffer.playSample((short) (Short.MAX_VALUE * x));
|
||||
}
|
||||
System.out.println("Closing buffer");
|
||||
buffer.shutdown();
|
||||
System.out.println("Deactivating sound");
|
||||
mixer.detach();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// Commented out because it's annoying to hear all the time, but it worked without issues
|
||||
public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException {
|
||||
System.out.println("Performing speaker tick test...");
|
||||
SoundMixer.initSound();
|
||||
System.out.println("Create mixer");
|
||||
SoundMixer mixer = new SoundMixer();
|
||||
System.out.println("Attach mixer");
|
||||
mixer.attach();
|
||||
// We want to create and destroy lots of buffers to make sure we don't have any memory leaks
|
||||
// for (int i = 0; i < 10000; i++) {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
// Print status every 1000 iterations
|
||||
if (i % 1000 == 0) {
|
||||
System.out.println("Iteration %d".formatted(i));
|
||||
}
|
||||
SoundBuffer buffer = SoundMixer.createBuffer(false);
|
||||
for (int j = 0; j < SoundMixer.BUFFER_SIZE*2; j++) {
|
||||
// Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed
|
||||
double x = Math.sin(j*j * 0.0001);
|
||||
buffer.playSample((short) (Short.MAX_VALUE * x));
|
||||
}
|
||||
buffer.flush();
|
||||
buffer.shutdown();
|
||||
}
|
||||
// Assert buffers are empty
|
||||
assertEquals("All buffers should be empty", 0, mixer.getActiveBuffers());
|
||||
System.out.println("Deactivating sound");
|
||||
mixer.detach();
|
||||
}
|
||||
}
|
||||
102
src/test/java/jace/core/TimedDeviceTest.java
Normal file
102
src/test/java/jace/core/TimedDeviceTest.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package jace.core;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class TimedDeviceTest extends AbstractFXTest {
|
||||
|
||||
private TimedDevice timedDevice;
|
||||
public int countedTicks = 0;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
countedTicks = 0;
|
||||
timedDevice = new TimedDevice(true) {
|
||||
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return "Test";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeviceName() {
|
||||
return "Test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
countedTicks++;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSpeedInHz() {
|
||||
long newSpeed = 2000000;
|
||||
timedDevice.setSpeedInHz(newSpeed);
|
||||
assertEquals(newSpeed, timedDevice.getSpeedInHz());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSpeedInPercentage() {
|
||||
int ratio = 50;
|
||||
timedDevice.setSpeedInPercentage(ratio);
|
||||
assertEquals(ratio, timedDevice.getSpeedRatio());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxSpeed() {
|
||||
// Use temp max speed
|
||||
timedDevice.setMaxSpeed(false);
|
||||
timedDevice.enableTempMaxSpeed();
|
||||
assertTrue("Max speed enabled", timedDevice.isMaxSpeed());
|
||||
timedDevice.disableTempMaxSpeed();
|
||||
assertFalse("Max speed disabled", timedDevice.isMaxSpeed());
|
||||
// Run 250 cycles and make sure none were skipped
|
||||
timedDevice.setSpeedInHz(1000);
|
||||
timedDevice.resume();
|
||||
for (int i=0 ; i<250 ; i++) {
|
||||
timedDevice.enableTempMaxSpeed();
|
||||
timedDevice.doTick();
|
||||
}
|
||||
assertEquals("250 ticks were counted", 250, countedTicks);
|
||||
// Disable temp max speed
|
||||
timedDevice.disableTempMaxSpeed();
|
||||
countedTicks = 0;
|
||||
for (int i=0 ; i<250 ; i++) {
|
||||
timedDevice.doTick();
|
||||
}
|
||||
assertTrue("Should have counted fewer than 250 ticks", countedTicks < 250);
|
||||
// Now use max speed
|
||||
timedDevice.setMaxSpeed(true);
|
||||
countedTicks = 0;
|
||||
for (int i=0 ; i<250 ; i++) {
|
||||
timedDevice.doTick();
|
||||
}
|
||||
assertEquals("250 ticks were counted", 250, countedTicks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReconfigure() {
|
||||
timedDevice.reconfigure();
|
||||
// Add assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTicks() {
|
||||
timedDevice.setSpeedInHz(1000);
|
||||
timedDevice.resume();
|
||||
long now = System.nanoTime();
|
||||
for (countedTicks=0 ; countedTicks<250 ; ) {
|
||||
timedDevice.doTick();
|
||||
}
|
||||
assertEquals("250 ticks were counted", 250, countedTicks);
|
||||
long ellapsed = System.nanoTime() - now;
|
||||
assertTrue("About 250ms elapsed", ellapsed / 1000000 >= 240);
|
||||
}
|
||||
|
||||
}
|
||||
42
src/test/java/jace/core/UtilityTest.java
Normal file
42
src/test/java/jace/core/UtilityTest.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package jace.core;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class UtilityTest {
|
||||
|
||||
@Test
|
||||
public void testLevenshteinDistance() {
|
||||
String s1 = "kitten";
|
||||
String s2 = "sitting";
|
||||
int distance = Utility.levenshteinDistance(s1, s2);
|
||||
assertEquals(3, distance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdjustedLevenshteinDistance() {
|
||||
String s1 = "kitten";
|
||||
String s2 = "sitting";
|
||||
int adjustedDistance = Utility.adjustedLevenshteinDistance(s1, s2);
|
||||
assertEquals(4, adjustedDistance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRankMatch() {
|
||||
String s1 = "apple";
|
||||
String s2 = "banana";
|
||||
double score = Utility.rankMatch(s1, s2, 3);
|
||||
assertEquals(0, score, 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindBestMatch() {
|
||||
String match = "apple";
|
||||
Collection<String> search = Arrays.asList("banana", "orange", "apple pie");
|
||||
String bestMatch = Utility.findBestMatch(match, search);
|
||||
assertEquals("apple pie", bestMatch);
|
||||
}
|
||||
}
|
||||
121
src/test/java/jace/hardware/CardAppleMouseTest.java
Normal file
121
src/test/java/jace/hardware/CardAppleMouseTest.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package jace.hardware;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.core.RAMEvent;
|
||||
import jace.core.RAMEvent.SCOPE;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
import jace.core.RAMEvent.VALUE;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
public class CardAppleMouseTest extends AbstractFXTest {
|
||||
|
||||
private CardAppleMouse cardAppleMouse;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
cardAppleMouse = new CardAppleMouse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDeviceName() {
|
||||
assertEquals("Apple Mouse", cardAppleMouse.getDeviceName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReset() {
|
||||
cardAppleMouse.mode = 1;
|
||||
cardAppleMouse.clampWindow = new Rectangle2D(10, 10, 100, 100);
|
||||
cardAppleMouse.detach();
|
||||
|
||||
cardAppleMouse.reset();
|
||||
|
||||
assertEquals(0, cardAppleMouse.mode);
|
||||
assertEquals(new Rectangle2D(0, 0, 0x03ff, 0x03ff), cardAppleMouse.clampWindow);
|
||||
}
|
||||
|
||||
@Test
|
||||
// Test mouseHandler responses to mouse events
|
||||
public void testMouseHandler() {
|
||||
MouseEvent clickEvent = new MouseEvent(MouseEvent.MOUSE_CLICKED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null);
|
||||
MouseEvent releaseEvent = new MouseEvent(MouseEvent.MOUSE_RELEASED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null);
|
||||
MouseEvent dragEvent = new MouseEvent(MouseEvent.MOUSE_DRAGGED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null);
|
||||
|
||||
cardAppleMouse.mode = 1;
|
||||
cardAppleMouse.mouseHandler.handle(clickEvent);
|
||||
cardAppleMouse.mouseHandler.handle(dragEvent);
|
||||
cardAppleMouse.mouseHandler.handle(releaseEvent);
|
||||
assertEquals(1, cardAppleMouse.mode);
|
||||
}
|
||||
|
||||
@Test
|
||||
// Test firmware entry points
|
||||
public void testFirmware() {
|
||||
// Test reads
|
||||
RAMEvent event = new RAMEvent(TYPE.READ, SCOPE.ANY, VALUE.ANY, 0, 0, 0);
|
||||
cardAppleMouse.handleFirmwareAccess(0x80, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x81, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x82, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x83, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x84, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x85, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x86, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x87, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x88, TYPE.EXECUTE, 0, event);
|
||||
assertEquals(0x60, event.getNewValue());
|
||||
event.setNewValue(0x00);
|
||||
cardAppleMouse.handleFirmwareAccess(0x05, TYPE.READ, 0, event);
|
||||
assertEquals(0x38, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x07, TYPE.READ, 0, event);
|
||||
assertEquals(0x18, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x08, TYPE.READ, 0, event);
|
||||
assertEquals(0x01, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x0B, TYPE.READ, 0, event);
|
||||
assertEquals(0x01, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x0C, TYPE.READ, 0, event);
|
||||
assertEquals(0x20, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x11, TYPE.READ, 0, event);
|
||||
assertEquals(0x00, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x12, TYPE.READ, 0, event);
|
||||
assertEquals(0x080, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x13, TYPE.READ, 0, event);
|
||||
assertEquals(0x081, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x14, TYPE.READ, 0, event);
|
||||
assertEquals(0x082, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x15, TYPE.READ, 0, event);
|
||||
assertEquals(0x083, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x16, TYPE.READ, 0, event);
|
||||
assertEquals(0x084, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x17, TYPE.READ, 0, event);
|
||||
assertEquals(0x085, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x18, TYPE.READ, 0, event);
|
||||
assertEquals(0x086, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x19, TYPE.READ, 0, event);
|
||||
assertEquals(0x087, event.getNewValue());
|
||||
cardAppleMouse.handleFirmwareAccess(0x1A, TYPE.READ, 0, event);
|
||||
assertEquals(0x088, event.getNewValue());
|
||||
}
|
||||
|
||||
}
|
||||
54
src/test/java/jace/hardware/CardSSCTest.java
Normal file
54
src/test/java/jace/hardware/CardSSCTest.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package jace.hardware;
|
||||
|
||||
import static jace.hardware.CardSSC.ACIA_Command;
|
||||
import static jace.hardware.CardSSC.ACIA_Control;
|
||||
import static jace.hardware.CardSSC.ACIA_Data;
|
||||
import static jace.hardware.CardSSC.ACIA_Status;
|
||||
import static jace.hardware.CardSSC.SW1;
|
||||
import static jace.hardware.CardSSC.SW2_CTS;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.core.RAMEvent;
|
||||
import jace.core.RAMEvent.SCOPE;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
import jace.core.RAMEvent.VALUE;
|
||||
|
||||
public class CardSSCTest extends AbstractFXTest {
|
||||
|
||||
private CardSSC cardSSC;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
cardSSC = new CardSSC();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDeviceName() {
|
||||
assertEquals("Super Serial Card", cardSSC.getDeviceName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSlot() {
|
||||
cardSSC.setSlot(1);
|
||||
// assertEquals("Slot 1", cardSSC.activityIndicator.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReset() {
|
||||
cardSSC.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIOAccess() {
|
||||
RAMEvent event = new RAMEvent(TYPE.READ_DATA, SCOPE.ANY, VALUE.ANY, 0, 0, 0);
|
||||
int[] registers = {SW1, SW2_CTS, ACIA_Data, ACIA_Control, ACIA_Status, ACIA_Command};
|
||||
for (int register : registers) {
|
||||
cardSSC.handleIOAccess(register, TYPE.READ_DATA, 0, event);
|
||||
cardSSC.handleIOAccess(register, TYPE.WRITE, 0, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/test/java/jace/hardware/FloppyDiskTest.java
Normal file
62
src/test/java/jace/hardware/FloppyDiskTest.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package jace.hardware;
|
||||
import static jace.hardware.FloppyDisk.PRODOS_SECTOR_ORDER;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.hardware.FloppyDisk.SectorOrder;
|
||||
|
||||
public class FloppyDiskTest extends AbstractFXTest {
|
||||
|
||||
private FloppyDisk floppyDisk;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
floppyDisk = new FloppyDisk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readDisk_ValidDiskFile_Success() throws IOException {
|
||||
// Create a sample disk file
|
||||
byte[] diskData = new byte[232960];
|
||||
File diskFile = File.createTempFile("test_disk", ".dsk");
|
||||
diskFile.deleteOnExit();
|
||||
ByteArrayInputStream diskInputStream = new ByteArrayInputStream(diskData);
|
||||
|
||||
// Read the disk file
|
||||
floppyDisk.readDisk(diskInputStream, SectorOrder.DOS);
|
||||
|
||||
// Verify the disk properties
|
||||
assert(floppyDisk.isNibblizedImage);
|
||||
assertEquals(254, floppyDisk.volumeNumber);
|
||||
assertEquals(0, floppyDisk.headerLength);
|
||||
assertEquals(232960, floppyDisk.nibbles.length);
|
||||
assertEquals("Sector order not null", true, null != floppyDisk.currentSectorOrder);
|
||||
assertNull(floppyDisk.diskPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nibblize_ValidNibbles_Success() throws IOException {
|
||||
// Create a sample nibbles array
|
||||
byte[] nibbles = new byte[FloppyDisk.DISK_NIBBLE_LENGTH];
|
||||
for (int i = 0; i < nibbles.length; i++) {
|
||||
nibbles[i] = (byte) (i % 256);
|
||||
}
|
||||
floppyDisk.currentSectorOrder = PRODOS_SECTOR_ORDER;
|
||||
// Nibblize the nibbles array
|
||||
byte[] nibblizedData = floppyDisk.nibblize(nibbles);
|
||||
|
||||
// Verify the nibblized data
|
||||
assertEquals(FloppyDisk.DISK_NIBBLE_LENGTH, nibblizedData.length);
|
||||
// for (int i = 0; i < nibblizedData.length; i++) {
|
||||
// assertEquals((i % 256) >> 2, nibblizedData[i]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
44
src/test/java/jace/hardware/PassportMidiInterfaceTest.java
Normal file
44
src/test/java/jace/hardware/PassportMidiInterfaceTest.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package jace.hardware;
|
||||
|
||||
import static jace.hardware.PassportMidiInterface.ACIA_RECV;
|
||||
import static jace.hardware.PassportMidiInterface.ACIA_STATUS;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER1_LSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER1_MSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER2_LSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER2_MSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER3_LSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER3_MSB;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER_CONTROL_1;
|
||||
import static jace.hardware.PassportMidiInterface.TIMER_CONTROL_2;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.core.RAMEvent;
|
||||
import jace.core.RAMEvent.SCOPE;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
import jace.core.RAMEvent.VALUE;
|
||||
|
||||
|
||||
public class PassportMidiInterfaceTest extends AbstractFXTest {
|
||||
PassportMidiInterface midi = new PassportMidiInterface();
|
||||
|
||||
@Test
|
||||
public void testDeviceSelection() {
|
||||
assertNotNull(PassportMidiInterface.preferredMidiDevice.getSelections());
|
||||
assertNotEquals(0, PassportMidiInterface.preferredMidiDevice.getSelections().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIOAccess() {
|
||||
RAMEvent event = new RAMEvent(TYPE.READ_DATA, SCOPE.ANY, VALUE.ANY, 0, 0, 0);
|
||||
int[] registers = {ACIA_STATUS, ACIA_RECV, TIMER_CONTROL_1, TIMER_CONTROL_2, TIMER1_LSB, TIMER1_MSB, TIMER2_LSB, TIMER2_MSB, TIMER3_LSB, TIMER3_MSB};
|
||||
for (int register : registers) {
|
||||
midi.handleIOAccess(register, TYPE.READ_DATA, 0, event);
|
||||
midi.handleIOAccess(register, TYPE.WRITE, 0, event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
50
src/test/java/jace/hardware/mockingboard/PSGTest.java
Normal file
50
src/test/java/jace/hardware/mockingboard/PSGTest.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package jace.hardware.mockingboard;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class PSGTest {
|
||||
|
||||
private PSG psg;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
psg = new PSG(0, 100, 44100, "name", 255);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControl_InactiveCommand_NoAction() {
|
||||
psg.setControl(0); // Set control to inactive
|
||||
// Assert that no action is taken
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControl_LatchCommand_SelectedRegUpdated() {
|
||||
psg.setControl(1); // Set control to latch
|
||||
// Assert that selectedReg is updated correctly
|
||||
// Add your assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControl_ReadCommand_BusUpdated() {
|
||||
psg.setControl(2); // Set control to read
|
||||
// Assert that bus is updated correctly
|
||||
// Add your assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setControl_WriteCommand_RegUpdated() {
|
||||
psg.setControl(3); // Set control to write
|
||||
// Assert that the corresponding register is updated correctly
|
||||
// Add your assertions here
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateTest() {
|
||||
AtomicInteger out = new AtomicInteger();
|
||||
psg.update(out, false, out, false, out, false);
|
||||
psg.update(out, true, out, true, out, true);
|
||||
}
|
||||
}
|
||||
49
src/test/java/jace/hardware/mockingboard/R6522Test.java
Normal file
49
src/test/java/jace/hardware/mockingboard/R6522Test.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package jace.hardware.mockingboard;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class R6522Test {
|
||||
R6522 r6522 = new R6522() {
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return "name";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendOutputA(int value) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendOutputB(int value) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public int receiveOutputA() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int receiveOutputB() {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testWriteRegs() {
|
||||
for (R6522.Register reg : R6522.Register.values()) {
|
||||
r6522.writeRegister(reg.val, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadRegs() {
|
||||
for (R6522.Register reg : R6522.Register.values()) {
|
||||
r6522.readRegister(reg.val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
49
src/test/java/jace/hardware/mockingboard/VotraxTest.java
Normal file
49
src/test/java/jace/hardware/mockingboard/VotraxTest.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package jace.hardware.mockingboard;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.AbstractFXTest;
|
||||
import jace.core.SoundMixer;
|
||||
import jace.core.Utility;
|
||||
|
||||
public class VotraxTest extends AbstractFXTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
System.out.println("Init sound");
|
||||
Utility.setHeadlessMode(false);
|
||||
SoundMixer.PLAYBACK_ENABLED = true;
|
||||
SoundMixer.initSound();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVoicedSource() {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFricativeSource() {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMixer() throws Exception {
|
||||
|
||||
Votrax vo = new Votrax();
|
||||
vo.resume();
|
||||
System.out.println("Sound: ON for 2sec");
|
||||
Thread.sleep(2000);
|
||||
boolean stillRunning = vo.isRunning();
|
||||
vo.suspend();
|
||||
System.out.println("Sound: OFF");
|
||||
boolean overrun = vo.isRunning();
|
||||
|
||||
assertTrue("Playback was interrupted early", stillRunning);
|
||||
assertFalse("Playback didn't stop when suspended", overrun);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
83
src/test/java/jace/ide/ApplesoftTest.java
Normal file
83
src/test/java/jace/ide/ApplesoftTest.java
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package jace.ide;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static jace.TestUtils.initComputer;
|
||||
import jace.applesoft.ApplesoftProgram;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author blurry
|
||||
*/
|
||||
public class ApplesoftTest {
|
||||
|
||||
public ApplesoftTest() {
|
||||
}
|
||||
|
||||
static Byte[] lemonadeStandBinary;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass() throws URISyntaxException, IOException {
|
||||
initComputer();
|
||||
byte[] lemonadeStand = readBinary("/jace/lemonade_stand.bin");
|
||||
lemonadeStandBinary = ApplesoftProgram.toObjects(lemonadeStand);
|
||||
}
|
||||
|
||||
public static byte[] readBinary(String path) throws IOException, URISyntaxException {
|
||||
Path resource = Paths.get(ApplesoftTest.class.getResource(path).toURI());
|
||||
return Files.readAllBytes(resource);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDownClass() {
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deserializeBinaryTest() {
|
||||
ApplesoftProgram program = ApplesoftProgram.fromBinary(Arrays.asList(lemonadeStandBinary), 0x0801);
|
||||
assertNotNull(program);
|
||||
assertNotSame("", program.toString());
|
||||
assertEquals("Lemonade stand has 380 lines", 380, program.getLength());
|
||||
assertTrue("Should have last line 31114", program.toString().contains("31114 "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripStringComparisonTest() {
|
||||
ApplesoftProgram program = ApplesoftProgram.fromBinary(Arrays.asList(lemonadeStandBinary), 0x0801);
|
||||
String serialized = program.toString();
|
||||
ApplesoftProgram deserialized = ApplesoftProgram.fromString(serialized);
|
||||
String[] serializedLines = serialized.split("\\n");
|
||||
String[] researializedLines = deserialized.toString().split("\\n");
|
||||
assertEquals("Lemonade stand has 380 lines", 380, deserialized.getLength());
|
||||
assertArrayEquals("Program listing should be not change if re-keyed in as printed", serializedLines, researializedLines);
|
||||
}
|
||||
}
|
||||
1
src/test/resources/65x02_unit_tests
Submodule
1
src/test/resources/65x02_unit_tests
Submodule
Submodule src/test/resources/65x02_unit_tests added at 3ecec7e679
301
src/test/resources/jace/bcd_test.asm
Normal file
301
src/test/resources/jace/bcd_test.asm
Normal file
@@ -0,0 +1,301 @@
|
||||
; Modified and adapted from http://www.6502.org/tutorials/decimal_mode.html#B
|
||||
;
|
||||
; Verify decimal mode behavior
|
||||
; Written by Bruce Clark. This code is public domain.
|
||||
;
|
||||
; Returns:
|
||||
; ERROR = 0 if the test passed
|
||||
; ERROR = 1 if the test failed
|
||||
;
|
||||
; This routine requires 17 bytes of RAM -- 1 byte each for:
|
||||
; AR, CF, DA, DNVZC, ERROR, HA, HNVZC, N1, N1H, N1L, N2, N2L, NF, VF, and ZF
|
||||
; and 2 bytes for N2H
|
||||
;
|
||||
; Variables:
|
||||
; N1 and N2 are the two numbers to be added or subtracted
|
||||
; N1H, N1L, N2H, and N2L are the upper 4 bits and lower 4 bits of N1 and N2
|
||||
; DA and DNVZC are the actual accumulator and flag results in decimal mode
|
||||
; HA and HNVZC are the accumulator and flag results when N1 and N2 are
|
||||
; added or subtracted using binary arithmetic
|
||||
; AR, NF, VF, ZF, and CF are the predicted decimal mode accumulator and
|
||||
; flag results, calculated using binary arithmetic
|
||||
;
|
||||
; This program takes approximately 1 minute at 1 MHz (a few seconds more on
|
||||
; a 65C02 than a 6502 or 65816)
|
||||
;
|
||||
AR = $06
|
||||
NF = $07
|
||||
VF = $08
|
||||
CF = $09
|
||||
ZF = $0a
|
||||
DA = $0b
|
||||
DNVZC = $0c
|
||||
HA = $0d
|
||||
HNVZC = $0e
|
||||
N1 = $0f
|
||||
N1H = $10
|
||||
N1L = $11
|
||||
N2 = $12
|
||||
N2H = $13
|
||||
N2L = $15
|
||||
|
||||
TEST ;LDY #1 ; initialize Y (used to loop through carry flag values)
|
||||
;STY ERROR ; store 1 in ERROR until the test passes
|
||||
LDA #0 ; initialize N1 and N2
|
||||
STA N1
|
||||
STA N2
|
||||
LOOP1 LDA N2 ; N2L = N2 & $0F
|
||||
AND #$0F ; [1] see text
|
||||
STA N2L
|
||||
LDA N2 ; N2H = N2 & $F0
|
||||
AND #$F0 ; [2] see text
|
||||
STA N2H
|
||||
ORA #$0F ; N2H+1 = (N2 & $F0) + $0F
|
||||
STA N2H+1
|
||||
LOOP2 LDA N1 ; N1L = N1 & $0F
|
||||
AND #$0F ; [3] see text
|
||||
STA N1L
|
||||
LDA N1 ; N1H = N1 & $F0
|
||||
AND #$F0 ; [4] see text
|
||||
STA N1H
|
||||
JSR ADD
|
||||
JSR A65C02
|
||||
JSR COMPARE
|
||||
BNE ADD_ERROR
|
||||
JSR SUB
|
||||
JSR S65C02
|
||||
JSR COMPARE
|
||||
BNE SUB_ERROR
|
||||
INC N1 ; [5] see text
|
||||
BNE LOOP2 ; loop through all 256 values of N1
|
||||
INC N2 ; [6] see text
|
||||
BNE LOOP1 ; loop through all 256 values of N2
|
||||
DEY
|
||||
BPL LOOP1 ; loop through both values of the carry flag
|
||||
SUCCESS +success
|
||||
ADD_ERROR
|
||||
CPY #1 ; Set carry based on Y reg
|
||||
LDA DA
|
||||
LDX N1
|
||||
LDY N2
|
||||
+throwError 0
|
||||
SUB_ERROR
|
||||
CPY #1 ; Set carry based on Y reg
|
||||
LDA DA
|
||||
LDX N1
|
||||
LDY N2
|
||||
+throwError 1
|
||||
|
||||
; Calculate the actual decimal mode accumulator and flags, the accumulator
|
||||
; and flag results when N1 is added to N2 using binary arithmetic, the
|
||||
; predicted accumulator result, the predicted carry flag, and the predicted
|
||||
; V flag
|
||||
;
|
||||
ADD SED ; decimal mode
|
||||
CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1
|
||||
ADC N2
|
||||
STA DA ; actual accumulator result in decimal mode
|
||||
PHP
|
||||
PLA
|
||||
STA DNVZC ; actual flags result in decimal mode
|
||||
CLD ; binary mode
|
||||
CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1
|
||||
ADC N2
|
||||
STA HA ; accumulator result of N1+N2 using binary arithmetic
|
||||
|
||||
PHP
|
||||
PLA
|
||||
STA HNVZC ; flags result of N1+N2 using binary arithmetic
|
||||
CPY #1
|
||||
LDA N1L
|
||||
ADC N2L
|
||||
CMP #$0A
|
||||
LDX #0
|
||||
BCC A1
|
||||
INX
|
||||
ADC #5 ; add 6 (carry is set)
|
||||
AND #$0F
|
||||
SEC
|
||||
A1 ORA N1H
|
||||
;
|
||||
; if N1L + N2L < $0A, then add N2 & $F0
|
||||
; if N1L + N2L >= $0A, then add (N2 & $F0) + $0F + 1 (carry is set)
|
||||
;
|
||||
ADC N2H,X
|
||||
PHP
|
||||
BCS A2
|
||||
CMP #$A0
|
||||
BCC A3
|
||||
A2 ADC #$5F ; add $60 (carry is set)
|
||||
SEC
|
||||
A3 STA AR ; predicted accumulator result
|
||||
PHP
|
||||
PLA
|
||||
STA CF ; predicted carry result
|
||||
PLA
|
||||
;
|
||||
; note that all 8 bits of the P register are stored in VF
|
||||
;
|
||||
STA VF ; predicted V flags
|
||||
RTS
|
||||
|
||||
; Calculate the actual decimal mode accumulator and flags, and the
|
||||
; accumulator and flag results when N2 is subtracted from N1 using binary
|
||||
; arithmetic
|
||||
;
|
||||
SUB SED ; decimal mode
|
||||
CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1
|
||||
SBC N2
|
||||
STA DA ; actual accumulator result in decimal mode
|
||||
PHP
|
||||
PLA
|
||||
STA DNVZC ; actual flags result in decimal mode
|
||||
CLD ; binary mode
|
||||
CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1
|
||||
SBC N2
|
||||
STA HA ; accumulator result of N1-N2 using binary arithmetic
|
||||
|
||||
PHP
|
||||
PLA
|
||||
STA HNVZC ; flags result of N1-N2 using binary arithmetic
|
||||
RTS
|
||||
|
||||
; Calculate the predicted SBC accumulator result for the 6502 and 65816
|
||||
|
||||
;
|
||||
SUB1 CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1L
|
||||
SBC N2L
|
||||
LDX #0
|
||||
BCS S11
|
||||
INX
|
||||
SBC #5 ; subtract 6 (carry is clear)
|
||||
AND #$0F
|
||||
CLC
|
||||
S11 ORA N1H
|
||||
;
|
||||
; if N1L - N2L >= 0, then subtract N2 & $F0
|
||||
; if N1L - N2L < 0, then subtract (N2 & $F0) + $0F + 1 (carry is clear)
|
||||
;
|
||||
SBC N2H,X
|
||||
BCS S12
|
||||
SBC #$5F ; subtract $60 (carry is clear)
|
||||
S12 STA AR
|
||||
RTS
|
||||
|
||||
; Calculate the predicted SBC accumulator result for the 6502 and 65C02
|
||||
|
||||
;
|
||||
SUB2 CPY #1 ; set carry if Y = 1, clear carry if Y = 0
|
||||
LDA N1L
|
||||
SBC N2L
|
||||
LDX #0
|
||||
BCS S21
|
||||
INX
|
||||
AND #$0F
|
||||
CLC
|
||||
S21 ORA N1H
|
||||
;
|
||||
; if N1L - N2L >= 0, then subtract N2 & $F0
|
||||
; if N1L - N2L < 0, then subtract (N2 & $F0) + $0F + 1 (carry is clear)
|
||||
;
|
||||
SBC N2H,X
|
||||
BCS S22
|
||||
SBC #$5F ; subtract $60 (carry is clear)
|
||||
S22 CPX #0
|
||||
BEQ S23
|
||||
SBC #6
|
||||
S23 STA AR ; predicted accumulator result
|
||||
RTS
|
||||
|
||||
; Compare accumulator actual results to predicted results
|
||||
;
|
||||
; Return:
|
||||
; Z flag = 1 (BEQ branch) if same
|
||||
; Z flag = 0 (BNE branch) if different
|
||||
;
|
||||
COMPARE LDA DA
|
||||
CMP AR
|
||||
+breakpoint 1
|
||||
BNE C1
|
||||
LDA DNVZC ; [7] see text
|
||||
EOR NF
|
||||
AND #$80 ; mask off N flag
|
||||
+breakpoint 2
|
||||
BNE C1
|
||||
LDA DNVZC ; [8] see text
|
||||
EOR VF
|
||||
AND #$40 ; mask off V flag
|
||||
+breakpoint 3
|
||||
BNE C1 ; [9] see text
|
||||
LDA DNVZC
|
||||
EOR ZF ; mask off Z flag
|
||||
AND #2
|
||||
+breakpoint 4
|
||||
BNE C1 ; [10] see text
|
||||
LDA DNVZC
|
||||
EOR CF
|
||||
AND #1 ; mask off C flag
|
||||
+breakpoint 5
|
||||
C1 RTS
|
||||
|
||||
; These routines store the predicted values for ADC and SBC for the 6502,
|
||||
; 65C02, and 65816 in AR, CF, NF, VF, and ZF
|
||||
|
||||
A6502 LDA VF
|
||||
;
|
||||
; since all 8 bits of the P register were stored in VF, bit 7 of VF contains
|
||||
; the N flag for NF
|
||||
;
|
||||
STA NF
|
||||
LDA HNVZC
|
||||
STA ZF
|
||||
RTS
|
||||
|
||||
S6502 JSR SUB1
|
||||
LDA HNVZC
|
||||
STA NF
|
||||
STA VF
|
||||
STA ZF
|
||||
STA CF
|
||||
RTS
|
||||
|
||||
A65C02 LDA AR
|
||||
PHP
|
||||
PLA
|
||||
STA NF
|
||||
STA ZF
|
||||
RTS
|
||||
|
||||
S65C02 JSR SUB2
|
||||
LDA AR
|
||||
PHP
|
||||
PLA
|
||||
STA NF
|
||||
STA ZF
|
||||
LDA HNVZC
|
||||
STA VF
|
||||
STA CF
|
||||
RTS
|
||||
|
||||
A65816 LDA AR
|
||||
PHP
|
||||
PLA
|
||||
STA NF
|
||||
STA ZF
|
||||
RTS
|
||||
|
||||
S65816 JSR SUB1
|
||||
LDA AR
|
||||
PHP
|
||||
PLA
|
||||
STA NF
|
||||
STA ZF
|
||||
LDA HNVZC
|
||||
STA VF
|
||||
STA CF
|
||||
RTS
|
||||
396
src/test/resources/jace/machine_identification.asm
Normal file
396
src/test/resources/jace/machine_identification.asm
Normal file
@@ -0,0 +1,396 @@
|
||||
JSR IDENTIFY
|
||||
JMP END_DETECT_PROGRAM
|
||||
;;; From http://www.1000bit.it/support/manuali/apple/technotes/misc/tn.misc.02.html
|
||||
;;; *********************************************
|
||||
;;; * *
|
||||
;;; * Apple II Family Identification Program *
|
||||
;;; * *
|
||||
;;; * Version 2.2 *
|
||||
;;; * *
|
||||
;;; * March, 1990 *
|
||||
;;; * *
|
||||
;;; * Includes support for the Apple IIe Card *
|
||||
;;; * for the Macintosh LC. *
|
||||
;;; * *
|
||||
;;; *********************************************
|
||||
|
||||
; First, some global equates for the routine:
|
||||
IIplain = $01 ;Apple II
|
||||
IIplus = $02 ;Apple II+
|
||||
IIIem = $03 ;Apple /// in emulation mode
|
||||
IIe = $04 ;Apple IIe
|
||||
IIc = $05 ;Apple IIc
|
||||
IIeCard = $06 ;Apple IIe Card for the Macintosh LC
|
||||
|
||||
.safe = $0001 ;start of code relocated to zp
|
||||
.location = $06 ;zero page location to use
|
||||
|
||||
.test1 = $AA ;test byte #1
|
||||
.test2 = $55 ;lsr of test1
|
||||
.test3 = $88 ;test byte #3
|
||||
.test4 = $EE ;test byte #4
|
||||
|
||||
.begpage1 = $400 ;beginning of text page 1
|
||||
.begpage2 = $800 ;beginning of text page 2
|
||||
.begsprse = $C00 ;byte after text page 2
|
||||
|
||||
.clr80col = $C000 ;disable 80-column store
|
||||
.set80col = $C001 ;enable 80-column store
|
||||
.rdmainram = $C002 ;read main ram
|
||||
.rdcardram = $C003 ;read aux ram
|
||||
.wrmainram = $C004 ;write main ram
|
||||
.wrcardram = $C005 ;write aux ram
|
||||
.rdramrd = $C013 ;are we reading aux ram?
|
||||
.rdaltzp = $C016 ;are we reading aux zero page?
|
||||
.rd80col = $C018 ;are we using 80-columns?
|
||||
.rdtext = $C01A ;read if text is displayed
|
||||
.rdpage2 = $C01C ;read if page 2 is displayed
|
||||
.txtclr = $C050 ;switch in graphics
|
||||
.txtset = $C051 ;switch in text
|
||||
.txtpage1 = $C054 ;switch in page 1
|
||||
.txtpage2 = $C055 ;switch in page 2
|
||||
.ramin = $C080 ;read LC bank 2, write protected
|
||||
.romin = $C081 ;read ROM, 2 reads write enable LC
|
||||
.lcbank1 = $C08B ;LC bank 1 enable
|
||||
|
||||
.lc1 = $E000 ;bytes to save for LC
|
||||
.lc2 = $D000 ;save/restore routine
|
||||
.lc3 = $D400
|
||||
.lc4 = $D800
|
||||
|
||||
.idroutine = $FE1F ;IIgs id routine
|
||||
|
||||
; Start by saving the state of the language card banks and
|
||||
; by switching in main ROM.
|
||||
|
||||
IDENTIFY
|
||||
php ;save the processor state
|
||||
sei ;before disabling interrupts
|
||||
lda .lc1 ;save four bytes from
|
||||
sta .save ;ROM/RAM area for later
|
||||
lda .lc2 ;restoring of RAM/ROM
|
||||
sta .save+1 ;to original condition
|
||||
lda .lc3
|
||||
sta .save+2
|
||||
lda .lc4
|
||||
sta .save+3
|
||||
lda $C081 ;read ROM
|
||||
lda $C081
|
||||
lda #0 ;start by assuming unknown machine
|
||||
sta MACHINE
|
||||
sta ROMLEVEL
|
||||
.IdStart
|
||||
lda .location ;save zero page locations
|
||||
sta .save+4 ;for later restoration
|
||||
lda .location+1
|
||||
sta .save+5
|
||||
lda #$FB ;all ID bytes are in page $FB
|
||||
sta .location+1 ;save in zero page as high byte
|
||||
ldx #0 ;init pointer to start of ID table
|
||||
.loop lda .IDTable,x ;get the machine we are testing for
|
||||
sta MACHINE ;and save it
|
||||
lda .IDTable+1,x ;get the ROM level we are testing for
|
||||
sta ROMLEVEL ;and save it
|
||||
ora MACHINE ;are both zero?
|
||||
beq .matched ;yes - at end of list - leave
|
||||
|
||||
.loop2 inx ;bump index to loc/byte pair to test
|
||||
inx
|
||||
lda .IDTable,x ;get the byte that should be in ROM
|
||||
beq .matched ;if zero, we're at end of list
|
||||
sta .location ;save in zero page
|
||||
|
||||
ldy #0 ;init index for indirect addressing
|
||||
lda .IDTable+1,x ;get the byte that should be in ROM
|
||||
cmp (.location),y ;is it there?
|
||||
beq .loop2 ;yes, so keep on looping
|
||||
|
||||
.loop3 inx ;we didn't match. Scoot to the end of the
|
||||
inx ;line in the ID table so we can start
|
||||
lda .IDTable,x ;checking for another machine
|
||||
bne .loop3
|
||||
inx ;point to start of next line
|
||||
bne .loop ;should always be taken
|
||||
|
||||
.matched ; anop
|
||||
|
||||
; Here we check the 16-bit ID routine at idroutine ($FE1F). If it
|
||||
; returns with carry clear, we call it again in 16-bit
|
||||
; mode to provide more information on the machine.
|
||||
|
||||
!cpu 65816 {
|
||||
.idIIgs
|
||||
sec ;set the carry bit
|
||||
jsr .idroutine ;Apple IIgs ID Routine
|
||||
bcc .idIIgs2 ;it's a IIgs or equivalent
|
||||
jmp .IIgsOut ;nope, go check memory
|
||||
.idIIgs2
|
||||
lda MACHINE ;get the value for machine
|
||||
ora #$80 ;and set the high bit
|
||||
sta MACHINE ;put it back
|
||||
clc ;get ready to switch into native mode
|
||||
xce
|
||||
php ;save the processor status
|
||||
rep #$30 ;sets 16-bit registers
|
||||
!al { ;longa on
|
||||
!rl { ;longi on
|
||||
jsr .idroutine ;call the ID routine again
|
||||
sta .IIgsA ;16-bit store!
|
||||
stx .IIgsX ;16-bit store!
|
||||
sty .IIgsY ;16-bit store!
|
||||
plp ;restores 8-bit registers
|
||||
xce ;switches back to whatever it was before
|
||||
} ;longi off
|
||||
} ;longa off
|
||||
|
||||
ldy .IIgsY ;get the ROM vers number (starts at 0)
|
||||
cpy #$02 ;is it ROM 01 or 00?
|
||||
bcs .idIIgs3 ;if not, don't increment
|
||||
iny ;bump it up for romlevel
|
||||
.idIIgs3
|
||||
sty ROMLEVEL ;and put it there
|
||||
cpy #$01 ;is it the first ROM?
|
||||
bne .IIgsOut ;no, go on with things
|
||||
lda .IIgsY+1 ;check the other byte too
|
||||
bne .IIgsOut ;nope, it's a IIgs successor
|
||||
lda #$7F ;fix faulty ROM 00 on the IIgs
|
||||
sta .IIgsA
|
||||
.IIgsOut ; anop
|
||||
}
|
||||
|
||||
;;; ******************************************
|
||||
;;; * This part of the code checks for the *
|
||||
;;; * memory configuration of the machine. *
|
||||
;;; * If it's a IIgs, we've already stored *
|
||||
;;; * the total memory from above. If it's *
|
||||
;;; * a IIc or a IIe Card, we know it's *
|
||||
;;; * 128K; if it's a ][+, we know it's at *
|
||||
;;; * least 48K and maybe 64K. We won't *
|
||||
;;; * check for less than 48K, since that's *
|
||||
;;; * a really rare circumstance. *
|
||||
;;; ******************************************
|
||||
|
||||
.exit lda MACHINE ;get the machine kind
|
||||
bmi .exit128 ;it's a 16-bit machine (has 128K)
|
||||
cmp #IIc ;is it a IIc?
|
||||
beq .exit128 ;yup, it's got 128K
|
||||
cmp #IIeCard ;is it a IIe Card?
|
||||
beq .exit128 ;yes, it's got 128K
|
||||
cmp #IIe ;is it a IIe?
|
||||
bne .contexit ;yes, go muck with aux memory
|
||||
jmp .muckaux
|
||||
.contexit
|
||||
cmp #IIIem ;is it a /// in emulation?
|
||||
bne .exitII ;nope, it's a ][ or ][+
|
||||
lda #48 ;/// emulation has 48K
|
||||
jmp .exita
|
||||
.exit128
|
||||
lda #128 ;128K
|
||||
.exita sta MEMORY
|
||||
.exit1 lda .lc1 ;time to restore the LC
|
||||
cmp .save ;if all 4 bytes are the same
|
||||
bne .exit2 ;then LC was never on so
|
||||
lda .lc2 ;do nothing
|
||||
cmp .save+1
|
||||
bne .exit2
|
||||
lda .lc3
|
||||
cmp .save+2
|
||||
bne .exit2
|
||||
lda .lc4
|
||||
cmp .save+3
|
||||
beq .exit6
|
||||
.exit2 lda $C088 ;no match! so turn first LC
|
||||
lda .lc1 ;bank on and check
|
||||
cmp .save
|
||||
beq .exit3
|
||||
lda $C080
|
||||
jmp .exit6
|
||||
.exit3 lda .lc2
|
||||
cmp .save+1 ;if all locations check
|
||||
beq .exit4 ;then do more more else
|
||||
lda $C080 ;turn on bank 2
|
||||
jmp .exit6
|
||||
.exit4 lda .lc3 ;check second byte in bank 1
|
||||
cmp .save+2
|
||||
beq .exit5
|
||||
lda $C080 ;select bank 2
|
||||
jmp .exit6
|
||||
.exit5 lda .lc4 ;check third byte in bank 1
|
||||
cmp .save+3
|
||||
beq .exit6
|
||||
lda $C080 ;select bank 2
|
||||
.exit6 plp ;restore interrupt status
|
||||
lda .save+4 ;put zero page back
|
||||
sta .location
|
||||
lda .save+5 ;like we found it
|
||||
sta .location+1
|
||||
rts ;and go home.
|
||||
|
||||
.exitII
|
||||
lda .lcbank1 ;force in language card
|
||||
lda .lcbank1 ;bank 1
|
||||
ldx .lc2 ;save the byte there
|
||||
lda #.test1 ;use this as a test byte
|
||||
sta .lc2
|
||||
eor .lc2 ;if the same, should return zero
|
||||
bne .noLC
|
||||
lsr .lc2 ;check twice just to be sure
|
||||
lda #.test2 ;this is the shifted value
|
||||
eor .lc2 ;here's the second check
|
||||
bne .noLC
|
||||
stx .lc2 ;put it back!
|
||||
lda #64 ;there's 64K here
|
||||
jmp .exita
|
||||
.noLC lda #48 ;no restore - no LC!
|
||||
jmp .exita ;and get out of here
|
||||
|
||||
.muckaux
|
||||
ldx .rdtext ;remember graphics in X
|
||||
lda .rdpage2 ;remember current video display
|
||||
asl ;in the carry bit
|
||||
lda #.test3 ;another test character
|
||||
bit .rd80col ;remember video mode in N
|
||||
sta .set80col ;enable 80-column store
|
||||
php ;save N and C flags
|
||||
sta .txtpage2 ;set page two
|
||||
sta .txtset ;set text
|
||||
ldy .begpage1 ;save first character
|
||||
sta .begpage1 ;and replace it with test character
|
||||
lda .begpage1 ;get it back
|
||||
sty .begpage1 ;and put back what was there
|
||||
plp
|
||||
bcs .muck2 ;stay in page 2
|
||||
sta .txtpage1 ;restore page 1
|
||||
.muck1 bmi .muck2 ;stay in 80-columns
|
||||
sta $c000 ;turn off 80-columns
|
||||
.muck2 tay ;save returned character
|
||||
txa ;get graphics/text setting
|
||||
bmi .muck3
|
||||
sta .txtclr ;turn graphics back on
|
||||
.muck3 cpy #.test3 ;finally compare it
|
||||
bne .nocard ;no 80-column card!
|
||||
lda .rdramrd ;is aux memory being read?
|
||||
bmi .muck128 ;yup, there's 128K!
|
||||
lda .rdaltzp ;is aux zero page used?
|
||||
bmi .muck128 ;yup!
|
||||
ldy #.done-.start
|
||||
.move ldx .start-1,y ;swap section of zero page
|
||||
lda <.safe-1,y ;code needing safe location during
|
||||
stx <.safe-1,y ;reading of aux mem
|
||||
sta .start-1,Y
|
||||
dey
|
||||
bne .move
|
||||
jmp .safe ;jump to safe ground
|
||||
.back php ;save status
|
||||
ldy #.done-.start ;move zero page back
|
||||
.move2 lda .start-1,y
|
||||
sta .safe-1,y
|
||||
dey
|
||||
bne .move2
|
||||
pla
|
||||
bcs .noaux
|
||||
.isaux jmp .muck128 ;there is 128K
|
||||
|
||||
;;; * You can put your own routine at "noaux" if you wish to
|
||||
;;; * distinguish between 64K without an 80-column card and
|
||||
;;; * 64K with an 80-column card.
|
||||
|
||||
.noaux ; anop
|
||||
.nocard lda #64 ;only 64K
|
||||
jmp .exita
|
||||
.muck128
|
||||
jmp .exit128 ;there's 128K
|
||||
|
||||
;;; * This is the routine run in the safe area not affected
|
||||
;;; * by bank-switching the main and aux RAM.
|
||||
|
||||
.start lda #.test4 ;yet another test byte
|
||||
sta .wrcardram ;write to aux while on main zero page
|
||||
sta .rdcardram ;read aux ram as well
|
||||
sta .begpage2 ;check for sparse memory mapping
|
||||
lda .begsprse ;if sparse, these will be the same
|
||||
cmp #.test4 ;value since they're 1K apart
|
||||
bne .auxmem ;yup, there's 128K!
|
||||
asl .begsprse ;may have been lucky so we'll
|
||||
lda .begpage2 ;change the value and see what happens
|
||||
cmp .begsprse
|
||||
bne .auxmem
|
||||
sec ;oops, no auxiliary memory
|
||||
bcs .goback
|
||||
.auxmem clc
|
||||
.goback sta .wrmainram ;write main RAM
|
||||
sta .rdmainram ;read main RAM
|
||||
jmp .back ;continue with program in main mem
|
||||
.done nop ;end of relocated program marker
|
||||
|
||||
|
||||
;;; * The storage locations for the returned machine ID:
|
||||
|
||||
.IIgsA !word 0 ;16-bit field
|
||||
.IIgsX !word 0 ;16-bit field
|
||||
.IIgsY !word 0 ;16-bit field
|
||||
.save !fill 6,0 ;six bytes for saved data
|
||||
|
||||
.IDTable
|
||||
;dc I1'1,1' ;Apple ][
|
||||
;dc H'B3 38 00'
|
||||
!byte 1,1
|
||||
!byte $B3,$38,0
|
||||
|
||||
;dc I1'2,1' ;Apple ][+
|
||||
;dc H'B3 EA 1E AD 00'
|
||||
!byte 2,1
|
||||
!byte $B3,$EA,$1E,$AD,0
|
||||
|
||||
;dc I1'3,1' ;Apple /// (emulation)
|
||||
;dc H'B3 EA 1E 8A 00'
|
||||
!byte 3,1
|
||||
!byte $B3,$EA,$1E,$8A,0
|
||||
|
||||
;dc I1'4,1' ;Apple IIe (original)
|
||||
;dc H'B3 06 C0 EA 00'
|
||||
!byte 4,1
|
||||
!byte $B3,$06,$C0,$EA,0
|
||||
|
||||
; Note: You must check for the Apple IIe Card BEFORE you
|
||||
; check for the enhanced Apple IIe since the first
|
||||
; two identification bytes are the same.
|
||||
|
||||
;dc I1'6,1' ;Apple IIe Card for the Macintosh LC (1st release)
|
||||
;dc H'B3 06 C0 E0 DD 02 BE 00 00'
|
||||
!byte 6,1
|
||||
!byte $B3,$06,$C0,$E0,$DD,$02,$BE,$00,0
|
||||
|
||||
;dc I1'4,2' ;Apple IIe (enhanced)
|
||||
;dc H'B3 06 C0 E0 00'
|
||||
!byte 4,2
|
||||
!byte $B3,$06,$C0,$E0,0
|
||||
|
||||
;dc I1'5,1' ;Apple IIc (original)
|
||||
;dc H'B3 06 C0 00 BF FF 00'
|
||||
!byte 5,1
|
||||
!byte $B3,$06,$C0,$00,$BF,$FF,0
|
||||
|
||||
;dc I1'5,2' ;Apple IIc (3.5 ROM)
|
||||
;dc H'B3 06 C0 00 BF 00 00'
|
||||
!byte 5,2
|
||||
!byte $B3,$06,$C0,$00,$BF,$00,0
|
||||
|
||||
;dc I1'5,3' ;Apple IIc (Mem. Exp)
|
||||
;dc H'B3 06 C0 00 BF 03 00'
|
||||
!byte 5,3
|
||||
!byte $B3,$06,$C0,$00,$BF,$03,0
|
||||
|
||||
;dc I1'5,4' ;Apple IIc (Rev. Mem. Exp.)
|
||||
;dc H'B3 06 C0 00 BF 04 00'
|
||||
!byte 5,4
|
||||
!byte $B3,$06,$C0,$00,$BF,$04,0
|
||||
|
||||
;dc I1'5,5' ;Apple IIc Plus
|
||||
;dc H'B3 06 C0 00 BF 05 00'
|
||||
!byte 5,5
|
||||
!byte $B3,$06,$C0,$00,$BF,$05,0
|
||||
|
||||
;dc I1'0,0' ;end of table
|
||||
!byte 0,0
|
||||
END_DETECT_PROGRAM
|
||||
207
src/test/resources/jace/memory_test_commons.asm
Normal file
207
src/test/resources/jace/memory_test_commons.asm
Normal file
@@ -0,0 +1,207 @@
|
||||
jmp START
|
||||
MACHINE = $800 ;the type of Apple II
|
||||
ROMLEVEL = $801 ;which revision of the machine
|
||||
MEMORY = $802 ;how much memory (up to 128K)
|
||||
|
||||
LCRESULT = $10
|
||||
LCRESULT1 = $11
|
||||
|
||||
lda #0
|
||||
sta LCRESULT
|
||||
|
||||
;; Zero-page locations.
|
||||
SCRATCH = $1
|
||||
SCRATCH2 = $2
|
||||
SCRATCH3 = $3
|
||||
LCRESULT = $10
|
||||
LCRESULT1 = $11
|
||||
AUXRESULT = $12
|
||||
SOFTSWITCHRESULT = $13
|
||||
|
||||
CSW = $36
|
||||
KSW = $38
|
||||
|
||||
PCL=$3A
|
||||
PCH=$3B
|
||||
A1L=$3C
|
||||
A1H=$3D
|
||||
A2L=$3E
|
||||
A2H=$3F
|
||||
A3L=$40
|
||||
A3H=$41
|
||||
A4L=$42
|
||||
A4H=$43
|
||||
|
||||
!addr tmp0 = $f9
|
||||
!addr tmp1 = $fa
|
||||
!addr tmp2 = $fb
|
||||
!addr tmp3 = $fc
|
||||
!addr tmp4 = $fd
|
||||
!addr tmp5 = $fe
|
||||
!addr tmp6 = $ff
|
||||
.checkdata = tmp1
|
||||
|
||||
STRINGS = $8000
|
||||
!set LASTSTRING = STRINGS
|
||||
|
||||
KBD = $C000
|
||||
KBDSTRB = $C010
|
||||
|
||||
;; Monitor locations.
|
||||
;HOME = $FC58
|
||||
COUT = $FDED
|
||||
COUT1 = $FDF0
|
||||
KEYIN = $FD1B
|
||||
CROUT = $FD8E
|
||||
PRBYTE = $FDDA
|
||||
PRNTYX = $F940
|
||||
|
||||
;; Softswitch locations.
|
||||
RESET_80STORE = $C000
|
||||
SET_80STORE = $C001
|
||||
READ_80STORE = $C018
|
||||
|
||||
RESET_RAMRD = $C002
|
||||
SET_RAMRD = $C003
|
||||
READ_RAMRD = $C013
|
||||
|
||||
RESET_RAMWRT = $C004
|
||||
SET_RAMWRT = $C005
|
||||
READ_RAMWRT = $C014
|
||||
|
||||
RESET_INTCXROM = $C006
|
||||
SET_INTCXROM = $C007
|
||||
READ_INTCXROM = $C015
|
||||
|
||||
RESET_ALTZP = $C008
|
||||
SET_ALTZP = $C009
|
||||
READ_ALTZP = $C016
|
||||
|
||||
RESET_SLOTC3ROM = $C00A
|
||||
SET_SLOTC3ROM = $C00B
|
||||
READ_SLOTC3ROM = $C017
|
||||
|
||||
RESET_80COL = $C00C
|
||||
SET_80COL = $C00D
|
||||
READ_80COL = $C01F
|
||||
|
||||
RESET_ALTCHRSET = $C00E
|
||||
SET_ALTCHRSET = $C00F
|
||||
READ_ALTCHRSET = $C01E
|
||||
|
||||
RESET_TEXT = $C050
|
||||
SET_TEXT = $C051
|
||||
READ_TEXT = $C01A
|
||||
|
||||
RESET_MIXED = $C052
|
||||
SET_MIXED = $C053
|
||||
READ_MIXED = $C01B
|
||||
|
||||
RESET_PAGE2 = $C054
|
||||
SET_PAGE2 = $C055
|
||||
READ_PAGE2 = $C01C
|
||||
|
||||
RESET_HIRES = $C056
|
||||
SET_HIRES = $C057
|
||||
READ_HIRES = $C01D
|
||||
|
||||
RESET_AN3 = $C05E
|
||||
SET_AN3 = $C05F
|
||||
|
||||
RESET_INTC8ROM = $CFFF
|
||||
|
||||
;; Readable things without corresponding set/reset pairs.
|
||||
READ_HRAM_BANK2 = $C011
|
||||
READ_HRAMRD = $C012
|
||||
READ_VBL = $C019
|
||||
|
||||
print
|
||||
lda $C081
|
||||
lda $C081
|
||||
pla
|
||||
sta getch+1
|
||||
pla
|
||||
sta getch+2
|
||||
- inc getch+1
|
||||
bne getch
|
||||
inc getch+2
|
||||
getch lda $FEED ; FEED gets modified
|
||||
beq +
|
||||
jsr COUT
|
||||
jmp -
|
||||
+ rts
|
||||
|
||||
PRINTTEST
|
||||
-
|
||||
ldy #0
|
||||
lda (PCL),y
|
||||
cmp #$20
|
||||
beq +++
|
||||
lda #'-'
|
||||
jsr COUT
|
||||
lda #' '
|
||||
jsr COUT
|
||||
ldx #0
|
||||
lda (PCL,x)
|
||||
jsr $f88e
|
||||
ldx #3
|
||||
jsr $f8ea
|
||||
jsr $f953
|
||||
sta PCL
|
||||
sty PCH
|
||||
lda #$8D
|
||||
jsr COUT
|
||||
jmp -
|
||||
+++ rts
|
||||
|
||||
;;; Increment .checkdata pointer to the next memory location, and load
|
||||
;;; it into the accumulator. X and Y are preserved.
|
||||
NEXTCHECK
|
||||
inc .checkdata
|
||||
bne CURCHECK
|
||||
inc .checkdata+1
|
||||
CURCHECK
|
||||
sty SCRATCH
|
||||
ldy #0
|
||||
lda (.checkdata),y
|
||||
ldy SCRATCH
|
||||
ora #0
|
||||
rts
|
||||
|
||||
!macro print {
|
||||
jsr LASTSTRING
|
||||
!set TEMP = *
|
||||
* = LASTSTRING
|
||||
jsr print
|
||||
}
|
||||
!macro printed {
|
||||
!byte 0
|
||||
!set LASTSTRING=*
|
||||
* = TEMP
|
||||
}
|
||||
|
||||
START
|
||||
;;; Reset all soft-switches to known-good state. Burns $300 and $301 in main mem.
|
||||
RESETALL
|
||||
; The COUT hook isn't set up yet, so the monitor routine will crash unless we set it up
|
||||
lda #<COUT1
|
||||
sta CSW
|
||||
lda #>COUT1
|
||||
sta CSW+1
|
||||
; 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_INTCXROM
|
||||
;sta RESET_ALTZP
|
||||
sta RESET_SLOTC3ROM
|
||||
sta RESET_INTC8ROM
|
||||
;sta RESET_80COL
|
||||
sta RESET_ALTCHRSET
|
||||
sta SET_TEXT
|
||||
sta RESET_MIXED
|
||||
sta RESET_PAGE2
|
||||
sta RESET_HIRES
|
||||
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Reference in New Issue
Block a user