26 Commits

Author SHA1 Message Date
Badvision
7ae5cf18bd Add Klauss tests 2024-08-11 22:48:24 -05:00
Badvision
519c561537 Fixed some edge case language card issues and implemented false reads for absolute indexed addressing modes, improved test coverage 2024-08-05 00:36:49 -05:00
Brendan Robert
1c26ecad3d Update README.md 2024-08-04 11:14:52 -05:00
Badvision
751c66c53f Improved test coverage for timed device, applesoft basic editing 2024-08-04 11:02:57 -05:00
Brendan Robert
f34ba40ff0 Merge pull request #55 from badvision/votrax
Restoring tests
2024-08-02 22:53:13 -05:00
Brendan Robert
45dfabfe14 Merge branch 'main' into votrax 2024-08-02 22:53:05 -05:00
Badvision
d6252e5c8b Disable jacoco coverage for now, update joystick (and fix asus mappings for macos) 2024-08-01 17:38:07 -05:00
brobert@adobe.com
9e04a05726 Add Mac ARM support 2024-08-01 10:21:28 -05:00
Brendan Robert
7f598bcdc1 restore test coverage 2024-07-13 23:35:10 -05:00
Brendan Robert
dafd4453eb Add configurable file field support 2024-07-13 23:28:46 -05:00
Brendan Robert
3a8e55d9dd Experimenting with Votrax support (don't get your hopes up though) 2024-07-11 09:41:47 -05:00
Brendan Robert
1fb9f925fc Increase size of configuration window 2024-07-10 20:11:16 -05:00
Brendan Robert
2dfe146e54 #45: Cleaned up floating menu, Fixed reset button 2024-07-10 19:24:26 -05:00
Brendan Robert
bb90292ab5 #49 : Remove mouse click event handler for the time being 2024-07-10 16:09:24 -05:00
Brendan Robert
39dd9d81b5 #49: Expand a few configuration levels by default 2024-07-10 16:00:50 -05:00
Brendan Robert
88cd03a9e6 #44: Assume extra messages only needed when DEBUG flag is set 2024-07-10 13:37:05 -05:00
Brendan Robert
455151abf7 #44: Tighter sector order detection, at least for prodos volumes. 2024-07-10 13:34:09 -05:00
Brendan Robert
9769f3282a #44: Detect sector order, ignore extension (lots of invalid PO images that are not prodos-ordered!) 2024-07-10 12:42:07 -05:00
Brendan Robert
4c6a8f976c #42: Fixed joystick emulation of keyboard 2024-07-09 21:20:51 -05:00
Brendan Robert
6d1a5e7edd #41: Fix y-axis scaling issue 2024-07-09 20:07:03 -05:00
Brendan Robert
7e638dbf05 #41: Treat mouse clicks as joystick buttons 2024-07-09 19:53:47 -05:00
Brendan Robert
41310b43e7 Fix url and use multiline string 2024-07-07 18:02:22 -05:00
Brendan Robert
2d2753ed99 tweak spacing 2024-07-07 17:34:15 -05:00
Brendan Robert
0f475ba186 Retire old handle and defunct goo.gl link 2024-07-07 17:33:45 -05:00
Brendan Robert
f413c1baad Remove unused import 2024-07-07 17:27:39 -05:00
Brendan Robert
55d444477c Update build for windows icon, increment minor version number 2024-07-07 17:01:01 -05:00
51 changed files with 17018 additions and 126 deletions

1
.gitignore vendored
View File

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

View File

@@ -53,7 +53,7 @@ Because Jace provides an annotation processor for compilation, there is a chicke
## 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>

42
pom.xml
View File

@@ -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,15 +24,15 @@
</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>
<mainClass>jace.JaceApplication</mainClass>
<resourcesList>
<resourcesList>ceAppl
<resource>.*</resource>
</resourcesList>
<releaseConfiguration>
@@ -93,7 +93,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 +158,7 @@
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
<minimum>0.35</minimum>
</limit>
</limits>
</rule>
@@ -197,31 +197,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 +281,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 +307,7 @@
<source>17</source>
<target>17</target>
</configuration>
<version>3.11.0</version>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
@@ -336,6 +336,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>

View File

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

View File

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

View File

@@ -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;
@@ -415,6 +414,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_DATA, true, false);
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
@@ -422,6 +423,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_DATA, true, false);
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

@@ -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)
*/

View File

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

View 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);
}
}
}

View File

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

View File

@@ -0,0 +1,318 @@
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.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import 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: 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);
}
}
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 Optional<Double> tick() {
sampleCounter += samplesPerClock;
if (sampleCounter >= 1.0) {
sampleCounter -= 1.0;
return Optional.of(doGenerate());
} else {
return Optional.empty();
}
}
public abstract double doGenerate();
}
public static class Mixer extends Generator {
public List<Generator> inputs = new ArrayList<>();
public List<Double> gains = new ArrayList<>();
double volume=0.0;
public void addInput(Generator input) {
inputs.add(input);
gains.add(1.0);
}
public void setGain(int index, double gain) {
gains.set(index, gain);
}
public double doGenerate() {
double sample = 0.0;
for (int i = 0; i < inputs.size(); i++) {
sample += inputs.get(i).doGenerate() * gains.get(i);
}
return sample;
}
}
public static class SawGenerator extends Generator {
double pitch=440.0;
double sample = 0.0;
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;
}
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;
}
}
public float mixerGain = 32767.0f;
public int[] filters = new int[5];
public SawGenerator formantGenerator = new SawGenerator();
public NoiseGenerator noiseGenerator = new NoiseGenerator();
public Mixer mixer = new Mixer();
public static int FORMANT = 0;
public static int NOISE = 1;
private Thread playbackThread = null;
public Votrax() throws Exception {
// loadPhonemeData();
formantGenerator.setSampleRate(44100);
formantGenerator.setPitch(100);
noiseGenerator.setSampleRate(44100);
mixer.addInput(formantGenerator);
mixer.addInput(noiseGenerator);
mixer.setGain(FORMANT, 0.5);
mixer.setGain(NOISE, 0.1);
}
public void resume() {
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] = EXTEfx.alGenFilters();
if (alGetError() != AL_NO_ERROR) {
throw new Exception("Failed to create filter " + i);
}
if (EXTEfx.alIsFilter(filters[i])) {
// Set Filter type to Band-Pass and set parameters
EXTEfx.alFilteri(filters[i], EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_BANDPASS);
if (alGetError() != AL_NO_ERROR) {
System.out.println("Band pass filter not supported.");
} else {
EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAIN, 0.5f);
EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAINHF, 0.5f);
System.out.println("Band pass filter "+i+" created.");
}
}
}
}
public boolean suspend() {
destroyFilters();
playbackThread = null;
return super.suspend();
}
private void destroyFilters() {
for (int i = 0; i < 5; i++) {
if (EXTEfx.alIsFilter(filters[i])) {
EXTEfx.alDeleteFilters(filters[i]);
}
}
}
@Override
public String getShortName() {
return "Votrax";
}
@Override
protected String getDeviceName() {
return "Votrax SC-02 / SSI-263";
}
@Override
public void tick() {
}
}

View File

@@ -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" />

View File

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

View File

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

Binary file not shown.

View File

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

View 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();
}
}

View 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;
}
}

View File

@@ -0,0 +1,482 @@
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.Full65C02Test;
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);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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 jace.core.CPU;
import jace.core.Computer;
import jace.core.Device;
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 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;
}
};
}
}

View 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();
}
}

View 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", 2)
.assertFlags(IS_ZERO, POSITIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$FF", 2)
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_CLEAR)
.assertTimed("BIT $FF", 3)
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_SET)
.add("CLV")
.add("LDA #$40")
.assertTimed("BIT #$40", 2)
.assertFlags(NOT_ZERO, POSITIVE, OVERFLOW_CLEAR)
.assertTimed("BIT #$80", 2)
.assertFlags(IS_ZERO, NEGATIVE, 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();
}
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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);
}
}
}

View 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
}
}

View File

@@ -0,0 +1,505 @@
/*
* 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.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;
/**
* 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 setup() {
computer.pause();
cpu.clearState();
// 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);
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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());
}
}

View 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);
}
}
}

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

View 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);
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -0,0 +1,204 @@
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
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

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB