Lots of test coverage, tweaks and timing fixes

This commit is contained in:
Brendan Robert 2024-03-05 00:06:47 -06:00
parent 2651818ca2
commit dcf4638e1e
46 changed files with 1086 additions and 606 deletions

View File

@ -226,12 +226,6 @@
<version>21.0.2</version> <version>21.0.2</version>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<!-- <dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency> -->
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId> <artifactId>javafx-graphics</artifactId>
@ -271,11 +265,6 @@
<artifactId>lwjgl-stb</artifactId> <artifactId>lwjgl-stb</artifactId>
<classifier>${lwjgl.natives}</classifier> <classifier>${lwjgl.natives}</classifier>
</dependency> </dependency>
<!-- <dependency>
<groupId>com.badlogicgames.jlayer</groupId>
<artifactId>jlayer</artifactId>
<version>1.0.2-gdx</version>
</dependency> -->
</dependencies> </dependencies>
<profiles> <profiles>
<profile> <profile>

View File

@ -145,8 +145,8 @@ public class Emulator {
instance = this; instance = this;
computer = new LawlessComputer(); computer = new LawlessComputer();
Configuration.buildTree(); Configuration.buildTree();
computer.getMotherboard().suspend();
Configuration.loadSettings(); Configuration.loadSettings();
Configuration.applySettings(Configuration.BASE);
mainThread = Thread.currentThread(); mainThread = Thread.currentThread();
// EmulatorUILogic.registerDebugger(); // EmulatorUILogic.registerDebugger();
// computer.coldStart(); // computer.coldStart();

View File

@ -29,7 +29,6 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -79,12 +78,6 @@ public class EmulatorUILogic implements Reconfigurable {
}; };
} }
@ConfigurableField(
category = "General",
name = "Speed Setting"
)
public int speedSetting = 3;
@ConfigurableField( @ConfigurableField(
category = "General", category = "General",
name = "Show Drives" name = "Show Drives"
@ -564,8 +557,5 @@ public class EmulatorUILogic implements Reconfigurable {
@Override @Override
public void reconfigure() { public void reconfigure() {
// Null-safe so there are no errors in unit tests
Optional.ofNullable(LawlessLegends.getApplication())
.ifPresent(app->app.controller.setSpeed(speedSetting));
} }
} }

View File

@ -18,7 +18,6 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import jace.core.Card; import jace.core.Card;
import jace.core.Motherboard;
import jace.core.Utility; import jace.core.Utility;
import jace.core.Video; import jace.core.Video;
import jace.lawless.LawlessComputer; import jace.lawless.LawlessComputer;
@ -97,6 +96,9 @@ public class JaceUIController {
private final BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false); private final BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false);
public static final double MIN_SPEED = 0.5;
public static final double MAX_SPEED = 5.0;
@FXML @FXML
void initialize() { void initialize() {
assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'JaceUI.fxml'."; assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'JaceUI.fxml'.";
@ -191,15 +193,10 @@ public class JaceUIController {
return 0.5; return 0.5;
} else if (setting == 1.0) { } else if (setting == 1.0) {
return 1.0; return 1.0;
} else if (setting >= 10) { } else if (setting >= 5) {
return Double.MAX_VALUE; return Double.MAX_VALUE;
} else { } else {
double val = Math.pow(2.0, (setting - 1.0) / 1.5); return setting;
val = Math.floor(val * 2.0) / 2.0;
if (val > 2.0) {
val = Math.floor(val);
}
return val;
} }
} }
@ -221,14 +218,16 @@ public class JaceUIController {
primaryStage = ps; primaryStage = ps;
connectButtons(controlOverlay); connectButtons(controlOverlay);
speedSlider.setMinorTickCount(0); speedSlider.setMinorTickCount(3);
speedSlider.setMajorTickUnit(1); speedSlider.setMajorTickUnit(1);
speedSlider.setMax(MAX_SPEED);
speedSlider.setMin(MIN_SPEED);
speedSlider.setLabelFormatter(new StringConverter<Double>() { speedSlider.setLabelFormatter(new StringConverter<Double>() {
@Override @Override
public String toString(Double val) { public String toString(Double val) {
if (val < 1.0) { if (val <= MIN_SPEED) {
return "Half"; return "Half";
} else if (val >= 10.0) { } else if (val >= MAX_SPEED) {
return ""; return "";
} }
double v = convertSpeedToRatio(val); double v = convertSpeedToRatio(val);
@ -244,11 +243,10 @@ public class JaceUIController {
return 1.0; return 1.0;
} }
}); });
speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
Platform.runLater(() -> { Platform.runLater(() -> {
speedSlider.setValue(Emulator.getUILogic().speedSetting); double currentSpeed = (double) Emulator.withComputer(c->c.getMotherboard().getSpeedRatio(), 100) / 100.0;
// Kind of redundant but make sure speed is properly set as if the user did it speedSlider.valueProperty().set(currentSpeed);
setSpeed(Emulator.getUILogic().speedSetting); speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
}); });
musicSelection.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> musicSelection.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
Emulator.withComputer(computer -> Emulator.withComputer(computer ->
@ -268,29 +266,20 @@ public class JaceUIController {
} }
public void setSpeed(double speed) { public void setSpeed(double speed) {
Emulator.getUILogic().speedSetting = (int) speed; double newSpeed = Math.max(speed, MIN_SPEED);
double speedRatio = convertSpeedToRatio(speed);
if (speedSlider.getValue() != speed) { if (speedSlider.getValue() != speed) {
Platform.runLater(()->speedSlider.setValue(speed)); Platform.runLater(()->speedSlider.setValue(newSpeed));
} }
if (speedRatio >= 100.0) { if (newSpeed >= MAX_SPEED) {
Emulator.withComputer(c -> { Emulator.withComputer(c -> {
c.getMotherboard().setSpeedInPercentage(20000);
c.getMotherboard().setMaxSpeed(true); c.getMotherboard().setMaxSpeed(true);
}); });
// Motherboard.cpuPerClock = 10;
} else { } else {
if (speedRatio > 1000) {
Motherboard.cpuPerClock = 2;
} else {
Motherboard.cpuPerClock = 1;
}
Emulator.withComputer(c -> { Emulator.withComputer(c -> {
c.getMotherboard().setMaxSpeed(false); c.getMotherboard().setMaxSpeed(false);
c.getMotherboard().setSpeedInPercentage((int) (speedRatio * 100)); c.getMotherboard().setSpeedInPercentage((int) (newSpeed * 100));
}); });
} }
Emulator.withComputer(c -> c.getMotherboard().reconfigure());
} }
public void toggleAspectRatio() { public void toggleAspectRatio() {

View File

@ -141,7 +141,7 @@ public class LawlessLegends extends Application {
romStarted.set(true); romStarted.set(true);
}); });
romStarted.set(false); romStarted.set(false);
c.invokeColdStart(); c.coldStart();
try { try {
Thread.sleep(watchdogDelay); Thread.sleep(watchdogDelay);
if (!romStarted.get() || !c.isRunning() || c.getCpu().getProgramCounter() == 0xc700 || c.getCpu().getProgramCounter() == 0) { if (!romStarted.get() || !c.isRunning() || c.getCpu().getProgramCounter() == 0xc700 || c.getCpu().getProgramCounter() == 0) {
@ -162,12 +162,10 @@ public class LawlessLegends extends Application {
} }
public void resetEmulator() { public void resetEmulator() {
// Reset the emulator memory and reconfigure // Reset the emulator memory and restart
Emulator.withComputer(c -> { Emulator.withComputer(c -> {
c.pause();
c.getMemory().resetState(); c.getMemory().resetState();
c.reconfigure(); c.warmStart();
c.resume();
}); });
} }

View File

@ -61,8 +61,10 @@ import jace.state.Stateful;
*/ */
@Stateful @Stateful
public class Apple2e extends Computer { public class Apple2e extends Computer {
static int IRQ_VECTOR = 0x003F2; static int IRQ_VECTOR = 0x003F2;
@ConfigurableField(name = "Production mode", shortName = "production")
public boolean PRODUCTION_MODE = false;
@ConfigurableField(name = "Slot 1", shortName = "s1card") @ConfigurableField(name = "Slot 1", shortName = "s1card")
public DeviceSelection<Cards> card1 = new DeviceSelection<>(Cards.class, null); public DeviceSelection<Cards> card1 = new DeviceSelection<>(Cards.class, null);
@ConfigurableField(name = "Slot 2", shortName = "s2card") @ConfigurableField(name = "Slot 2", shortName = "s2card")
@ -92,9 +94,7 @@ public class Apple2e extends Computer {
@ConfigurableField(name = "No-Slot Clock Enabled", shortName = "clock", description = "If checked, no-slot clock will be enabled", enablesDevice = true) @ConfigurableField(name = "No-Slot Clock Enabled", shortName = "clock", description = "If checked, no-slot clock will be enabled", enablesDevice = true)
public boolean clockEnabled = true; public boolean clockEnabled = true;
@ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true) @ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true)
public boolean acceleratorEnabled = true; public boolean acceleratorEnabled = PRODUCTION_MODE;
@ConfigurableField(name = "Production mode", shortName = "production")
public boolean PRODUCTION_MODE = true;
public Joystick joystick1; public Joystick joystick1;
public Joystick joystick2; public Joystick joystick2;
@ -105,7 +105,7 @@ public class Apple2e extends Computer {
public ZipWarpAccelerator accelerator; public ZipWarpAccelerator accelerator;
FPSMonitorDevice fpsCounters; FPSMonitorDevice fpsCounters;
@ConfigurableField(name = "Show speed monitors", shortName = "showFps") @ConfigurableField(name = "Show speed monitors", shortName = "showFps")
public boolean showSpeedMonitors = false; public boolean showSpeedMonitors = !PRODUCTION_MODE;
/** /**
* Creates a new instance of Apple2e * Creates a new instance of Apple2e
@ -130,46 +130,42 @@ public class Apple2e extends Computer {
@Override @Override
public void coldStart() { public void coldStart() {
getMotherboard().whileSuspended(()->{ RAM128k r = (RAM128k) getMemory();
System.err.println("Cold starting computer: RESETTING SOFT SWITCHES"); System.err.println("Cold starting computer: RESETTING SOFT SWITCHES");
for (SoftSwitches s : SoftSwitches.values()) { r.resetState();
for (SoftSwitches s : SoftSwitches.values()) {
if ((s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset(); s.getSwitch().reset();
} }
reconfigure(); }
getMemory().configureActiveMemory(); // This isn't really authentic behavior but sometimes games like memory to have a consistent state when booting.
getVideo().configureVideoMode(); r.zeroAllRam();
for (Optional<Card> c : getMemory().getAllCards()) { // Sather 4-15:
c.ifPresent(Card::reset); // An open Apple (left Apple) reset causes meaningless values to be stored in two locations
} // of every memory page from Page $01 through Page $BF before the power-up byte is checked.
}); int offset = IRQ_VECTOR & 0x0ff;
reboot(); byte garbage = (byte) (Math.random() * 256.0);
} for (int page=1; page < 0xc0; page++) {
r.write(page << 8 + offset, garbage, false, true);
public void reboot() { r.write(page << 8 + 1 + offset, garbage, false, true);
RAM r = getMemory(); }
r.write(IRQ_VECTOR, (byte) 0x00, false, true);
r.write(IRQ_VECTOR + 1, (byte) 0x00, false, true);
r.write(IRQ_VECTOR + 2, (byte) 0x00, false, true);
warmStart(); warmStart();
} }
@Override @Override
public void warmStart() { public void warmStart() {
getMotherboard().whileSuspended(()->{ for (SoftSwitches s : SoftSwitches.values()) {
// This isn't really authentic behavior but sometimes games like memory to have a consistent state when booting. if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
for (SoftSwitches s : SoftSwitches.values()) { s.getSwitch().reset();
if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
} }
((RAM128k)getMemory()).zeroAllRam(); }
getMemory().configureActiveMemory(); getMemory().configureActiveMemory();
getVideo().configureVideoMode(); getVideo().configureVideoMode();
getCpu().reset(); getCpu().reset();
for (Optional<Card> c : getMemory().getAllCards()) { for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset); c.ifPresent(Card::reset);
} }
}); motherboard.disableTempMaxSpeed();
resume(); resume();
} }
@ -376,15 +372,9 @@ public class Apple2e extends Computer {
if (getMotherboard() == null) { if (getMotherboard() == null) {
return; return;
} }
getMotherboard().resume(); getMotherboard().resumeAll();
} }
// public boolean isRunning() {
// if (motherboard == null) {
// return false;
// }
// return motherboard.isRunning() && !motherboard.isPaused;
// }
private final List<RAMListener> hints = new ArrayList<>(); private final List<RAMListener> hints = new ArrayList<>();
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1); ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);

View File

@ -182,7 +182,6 @@ abstract public class RAM128k extends RAM {
} }
public final void zeroAllRam() { public final void zeroAllRam() {
// Format memory with FF FF 00 00 pattern
for (int i = 0; i < 0x0100; i++) { for (int i = 0; i < 0x0100; i++) {
blank.get(0)[i] = (byte) 0x0FF; blank.get(0)[i] = (byte) 0x0FF;
} }

View File

@ -94,7 +94,7 @@ public class Speaker extends Device {
* Playback volume (should be < 1423) * Playback volume (should be < 1423)
*/ */
@ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400") @ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400")
public static int VOLUME = 600; public static int VOLUME = 400;
/** /**
* Number of idle cycles until speaker playback is deactivated * Number of idle cycles until speaker playback is deactivated
*/ */
@ -118,6 +118,7 @@ public class Speaker extends Device {
public boolean suspend() { public boolean suspend() {
boolean result = super.suspend(); boolean result = super.suspend();
speakerBit = false; speakerBit = false;
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
if (buffer != null) { if (buffer != null) {
try { try {
buffer.shutdown(); buffer.shutdown();
@ -164,8 +165,7 @@ public class Speaker extends Device {
TICKS_PER_SAMPLE = Emulator.withComputer(c-> ((double) c.getMotherboard().getSpeedInHz()) / SoundMixer.RATE, 0.0); TICKS_PER_SAMPLE = Emulator.withComputer(c-> ((double) c.getMotherboard().getSpeedInHz()) / SoundMixer.RATE, 0.0);
} }
TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE); TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE);
super.resume();
setRun(true);
} }
/** /**
@ -195,6 +195,7 @@ public class Speaker extends Device {
counter += 1.0d; counter += 1.0d;
if (counter >= TICKS_PER_SAMPLE) { if (counter >= TICKS_PER_SAMPLE) {
playSample(level * VOLUME); playSample(level * VOLUME);
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
// Set level back to 0 // Set level back to 0
level = 0; level = 0;
@ -280,13 +281,11 @@ public class Speaker extends Device {
@Override @Override
public void attach() { public void attach() {
configureListener(); configureListener();
resume();
} }
@Override @Override
public void detach() { public void detach() {
removeListener(); removeListener();
suspend();
super.detach(); super.detach();
} }
} }

View File

@ -16,15 +16,9 @@
package jace.applesoft; package jace.applesoft;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jace.Emulator; import jace.Emulator;
@ -53,22 +47,7 @@ public class ApplesoftProgram {
public static final int RUNNING_FLAG = 0x076; public static final int RUNNING_FLAG = 0x076;
public static final int NOT_RUNNING = 0x0FF; public static final int NOT_RUNNING = 0x0FF;
public static final int GOTO_CMD = 0x0D944; //actually starts at D93E public static final int GOTO_CMD = 0x0D944; //actually starts at D93E
int startingAddress = 0x0801; public static final int START_ADDRESS = 0x0801;
public static void main(String... args) {
byte[] source = null;
File f = new File("/home/brobert/Documents/Personal/a2gameserver/lib/data/games/LEMONADE#fc0801");
try (FileInputStream in = new FileInputStream(f)) {
source = new byte[(int) f.length()];
in.read(source);
} catch (FileNotFoundException ex) {
Logger.getLogger(ApplesoftProgram.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(ApplesoftProgram.class.getName()).log(Level.SEVERE, null, ex);
}
ApplesoftProgram test = ApplesoftProgram.fromBinary(Arrays.asList(toObjects(source)));
System.out.println(test);
}
public static Byte[] toObjects(byte[] bytesPrim) { public static Byte[] toObjects(byte[] bytesPrim) {
Byte[] bytes = new Byte[bytesPrim.length]; Byte[] bytes = new Byte[bytesPrim.length];
@ -91,7 +70,7 @@ public class ApplesoftProgram {
} }
public static ApplesoftProgram fromBinary(List<Byte> binary) { public static ApplesoftProgram fromBinary(List<Byte> binary) {
return fromBinary(binary, 0x0801); return fromBinary(binary, START_ADDRESS);
} }
public static ApplesoftProgram fromBinary(List<Byte> binary, int startAddress) { public static ApplesoftProgram fromBinary(List<Byte> binary, int startAddress) {

View File

@ -643,22 +643,22 @@ public class Configuration implements Reconfigurable {
} }
} }
private static void printTree(ConfigNode n, String prefix, int i) { // private static void printTree(ConfigNode n, String prefix, int i) {
n.getAllSettingNames().stream().forEach((setting) -> { // n.getAllSettingNames().stream().forEach((setting) -> {
for (int j = 0; j < i; j++) { // for (int j = 0; j < i; j++) {
System.out.print(" "); // System.out.print(" ");
} // }
ConfigurableField f = null; // ConfigurableField f = null;
try { // try {
f = n.subject.getClass().getField(setting).getAnnotation(ConfigurableField.class); // f = n.subject.getClass().getField(setting).getAnnotation(ConfigurableField.class);
} catch (NoSuchFieldException | SecurityException ex) { // } catch (NoSuchFieldException | SecurityException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); // Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} // }
String sn = (f != null && !f.shortName().equals("")) ? f.shortName() : setting; // String sn = (f != null && !f.shortName().equals("")) ? f.shortName() : setting;
System.out.println(prefix + ">>" + setting + " (" + n.subject.getShortName() + "." + sn + ")"); // System.out.println(prefix + ">>" + setting + " (" + n.subject.getShortName() + "." + sn + ")");
}); // });
n.getChildren().stream().forEach((c) -> { // n.getChildren().stream().forEach((c) -> {
printTree(c, prefix + "." + c, i + 1); // printTree(c, prefix + "." + c, i + 1);
}); // });
} // }
} }

View File

@ -198,18 +198,10 @@ public class InvokableActionRegistryImpl extends InvokableActionRegistry {
return false; return false;
} }
}); });
annotation = createInvokableAction("Cold boot", "general", "Process startup sequence from power-up", "Full reset;reset emulator", true, false, new String[]{"Ctrl+Shift+Backspace", "Ctrl+Shift+Delete"}); 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) -> { putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> {
try { try {
((jace.core.Computer) o).invokeColdStart(); ((jace.core.Computer) o).invokeReset();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeColdStart", ex);
}
});
annotation = createInvokableAction("Warm boot", "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) -> {
try {
((jace.core.Computer) o).invokeWarmStart();
} catch (Exception ex) { } catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeWarmStart", ex); logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeWarmStart", ex);
} }

View File

@ -19,11 +19,11 @@ package jace.core;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.LawlessLegends; import jace.LawlessLegends;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField; import jace.config.ConfigurableField;
import jace.config.Configuration;
import jace.config.InvokableAction; import jace.config.InvokableAction;
import jace.config.Reconfigurable; import jace.config.Reconfigurable;
import jace.state.StateManager; import jace.state.StateManager;
@ -161,34 +161,37 @@ public abstract class Computer implements Reconfigurable {
} }
} }
/**
* If the user wants a full reset, use the coldStart method.
* This ensures a more consistent state of the machine.
* Some games make bad assumptions about the initial state of the machine
* and that fails to work if the machine is not reset to a known state first.
*/
@InvokableAction( @InvokableAction(
name = "Cold boot", name = "Reset",
description = "Process startup sequence from power-up",
category = "general",
alternatives = "Full reset;reset emulator",
defaultKeyMapping = {"Ctrl+Shift+Backspace", "Ctrl+Shift+Delete"})
public void invokeColdStart() {
try {
loadRom(false);
memory.resetState();
coldStart();
} catch (IOException e) {
Logger.getLogger(Computer.class.getName()).log(Level.SEVERE, "Failed to load system rom ROMs", e);
}
}
public abstract void coldStart();
@InvokableAction(
name = "Warm boot",
description = "Process user-initatiated reboot (ctrl+apple+reset)", description = "Process user-initatiated reboot (ctrl+apple+reset)",
category = "general", category = "general",
alternatives = "reboot;reset;three-finger-salute;restart", alternatives = "reboot;reset;three-finger-salute;restart",
defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"}) defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
public void invokeWarmStart() { public void invokeReset() {
warmStart(); if (SoftSwitches.PDL0.isOn()) {
coldStart();
} else {
warmStart();
}
} }
/**
* In a cold start, memory is reset (either two bytes per page as per Sather 4-15) or full-wipe
* Also video softswitches are reset
* Otherwise it does the same as warm start
**/
public abstract void coldStart();
/**
* In a warm start, memory is not reset, but the CPU and cards are reset
* All but video softswitches are reset, putting the MMU in a known state
*/
public abstract void warmStart(); public abstract void warmStart();
public Keyboard getKeyboard() { public Keyboard getKeyboard() {
@ -225,5 +228,6 @@ public abstract class Computer implements Reconfigurable {
stateManager = null; stateManager = null;
StateManager.getInstance(this).invalidate(); StateManager.getInstance(this).invalidate();
} }
Configuration.registerKeyHandlers();
} }
} }

View File

@ -222,7 +222,8 @@ public abstract class Device implements Reconfigurable {
public boolean suspend() { public boolean suspend() {
children.forEach(Device::suspend); // Suspending the parent device means the children are not going to run
// children.forEach(Device::suspend);
if (isRunning()) { if (isRunning()) {
setRun(false); setRun(false);
return true; return true;
@ -230,10 +231,17 @@ public abstract class Device implements Reconfigurable {
return false; return false;
} }
public void resumeAll() {
resume();
children.forEach(Device::resumeAll);
}
public void resume() { public void resume() {
children.forEach(Device::resume); // Resuming children pre-emptively might lead to unexpected behavior
// Don't do that unless we really mean to (such as cold-starting the computer)
// children.forEach(Device::resume);
if (!isRunning()) { if (!isRunning()) {
setRun(true); setRun(true);
waitCycles = 0; waitCycles = 0;
} }
} }

View File

@ -37,9 +37,6 @@ public abstract class IndependentTimedDevice extends TimedDevice {
// The actual worker that the device runs as // The actual worker that the device runs as
public Thread worker; public Thread worker;
public boolean hasStopped = true; public boolean hasStopped = true;
// From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles
@Override @Override
/* We really don't want to suspect the worker thread if we're running in it. /* We really don't want to suspect the worker thread if we're running in it.
@ -67,7 +64,7 @@ public abstract class IndependentTimedDevice extends TimedDevice {
public boolean isDeviceThread() { public boolean isDeviceThread() {
return worker != null && worker.isAlive() && Thread.currentThread() == worker; return Thread.currentThread() == worker;
} }
/** /**
@ -84,14 +81,14 @@ public abstract class IndependentTimedDevice extends TimedDevice {
public boolean suspend() { public boolean suspend() {
boolean result = super.suspend(); boolean result = super.suspend();
Thread w = worker; Thread w = worker;
worker = null;
if (w != null && w.isAlive()) { if (w != null && w.isAlive()) {
try { try {
w.interrupt(); w.interrupt();
w.join(1000); w.join(100);
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
} }
} }
worker = null;
return result; return result;
} }
@ -112,9 +109,9 @@ public abstract class IndependentTimedDevice extends TimedDevice {
} }
@Override @Override
public void resume() { public synchronized void resume() {
super.resume(); super.resume();
if (worker != null && worker.isAlive()) { if (worker != null && worker.isAlive()) {
return; return;
} }
Thread newWorker = new Thread(() -> { Thread newWorker = new Thread(() -> {

View File

@ -304,20 +304,12 @@ public class Keyboard implements Reconfigurable {
@InvokableAction(name = "Open Apple Key", alternatives = "OA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = "Alt", consumeKeyEvent = false) @InvokableAction(name = "Open Apple Key", alternatives = "OA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = "Alt", consumeKeyEvent = false)
public void openApple(boolean pressed) { public void openApple(boolean pressed) {
// boolean isRunning = computer.pause();
SoftSwitches.PB0.getSwitch().setState(pressed); SoftSwitches.PB0.getSwitch().setState(pressed);
// if (isRunning) {
// computer.resume();
// }
} }
@InvokableAction(name = "Closed Apple Key", alternatives = "CA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = {"Shortcut","Meta","Command"}, consumeKeyEvent = false) @InvokableAction(name = "Closed Apple Key", alternatives = "CA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = {"Shortcut","Meta","Command"}, consumeKeyEvent = false)
public void solidApple(boolean pressed) { public void solidApple(boolean pressed) {
// boolean isRunning = computer.pause();
SoftSwitches.PB1.getSwitch().setState(pressed); SoftSwitches.PB1.getSwitch().setState(pressed);
// if (isRunning) {
// computer.resume();
// }
} }
public static void pasteFromString(String text) { public static void pasteFromString(String text) {

View File

@ -37,7 +37,7 @@ public class Motherboard extends IndependentTimedDevice {
@ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true") @ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true")
public static boolean enableSpeaker = true; public static boolean enableSpeaker = true;
private Speaker speaker; public Speaker speaker;
void vblankEnd() { void vblankEnd() {
SoftSwitches.VBL.getSwitch().setState(true); SoftSwitches.VBL.getSwitch().setState(true);
@ -74,9 +74,6 @@ public class Motherboard extends IndependentTimedDevice {
public String getShortName() { public String getShortName() {
return "mb"; return "mb";
} }
@ConfigurableField(category = "advanced", shortName = "cpuPerClock", name = "CPU per clock", defaultValue = "1", description = "Number of extra CPU cycles per clock cycle (normal = 1)")
public static int cpuPerClock = 0;
public int clockCounter = 1;
private CPU _cpu = null; private CPU _cpu = null;
public CPU getCpu() { public CPU getCpu() {
@ -88,45 +85,32 @@ public class Motherboard extends IndependentTimedDevice {
@Override @Override
public void tick() { public void tick() {
// Extra CPU cycles requested, other devices are called by the TimedDevice abstraction
for (int i=1; i < cpuPerClock; i++) {
getCpu().doTick();
if (Speaker.force1mhz) {
speaker.tick();
}
}
}
@Override
public long defaultCyclesPerSecond() {
return NTSC_1MHZ;
} }
@Override @Override
public synchronized void reconfigure() { public synchronized void reconfigure() {
_cpu = null; _cpu = null;
whileSuspended(() -> { accelorationRequestors.clear();
accelorationRequestors.clear(); disableTempMaxSpeed();
disableTempMaxSpeed(); super.reconfigure();
super.reconfigure();
// Now create devices as needed, e.g. sound // Now create devices as needed, e.g. sound
if (enableSpeaker) { if (enableSpeaker) {
try { try {
if (speaker == null) { if (speaker == null) {
speaker = new Speaker(); speaker = new Speaker();
speaker.attach(); speaker.attach();
}
speaker.reconfigure();
addChildDevice(speaker);
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
t.printStackTrace();
} }
} else { speaker.reconfigure();
System.out.println("Speaker not enabled, leaving it off."); addChildDevice(speaker);
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
t.printStackTrace();
} }
}); } else {
System.out.println("Speaker not enabled, leaving it off.");
}
} }
HashSet<Object> accelorationRequestors = new HashSet<>(); HashSet<Object> accelorationRequestors = new HashSet<>();
@ -136,8 +120,7 @@ public class Motherboard extends IndependentTimedDevice {
} }
public void cancelSpeedRequest(Object requester) { public void cancelSpeedRequest(Object requester) {
accelorationRequestors.remove(requester); if (accelorationRequestors.remove(requester) && accelorationRequestors.isEmpty()) {
if (accelorationRequestors.isEmpty()) {
disableTempMaxSpeed(); disableTempMaxSpeed();
} }
} }

View File

@ -41,10 +41,13 @@ public abstract class RAM implements Reconfigurable {
public PagedMemory activeRead; public PagedMemory activeRead;
public PagedMemory activeWrite; public PagedMemory activeWrite;
private final Set<RAMListener> listeners; private final Set<RAMListener> listeners = new ConcurrentSkipListSet<>();
private final Set<RAMListener>[] listenerMap; @SuppressWarnings("unchecked")
private final Set<RAMListener>[] ioListenerMap; private final Set<RAMListener>[] listenerMap = (Set<RAMListener>[]) new Set[256];
public Optional<Card>[] cards; @SuppressWarnings("unchecked")
private final Set<RAMListener>[] ioListenerMap = (Set<RAMListener>[]) new Set[256];
@SuppressWarnings("unchecked")
public Optional<Card>[] cards = (Optional<Card>[]) new Optional[8];
// card 0 = 80 column card firmware / system rom // card 0 = 80 column card firmware / system rom
public int activeSlot = 0; public int activeSlot = 0;
@ -53,12 +56,7 @@ public abstract class RAM implements Reconfigurable {
* *
* @param computer * @param computer
*/ */
@SuppressWarnings("unchecked")
public RAM() { public RAM() {
listeners = new ConcurrentSkipListSet<>();
listenerMap = (Set<RAMListener>[]) new Set[256];
ioListenerMap = (Set<RAMListener>[]) new Set[256];
cards = (Optional<Card>[]) new Optional[8];
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
cards[i] = Optional.empty(); cards[i] = Optional.empty();
} }
@ -298,7 +296,7 @@ public abstract class RAM implements Reconfigurable {
} }
public RAMListener addListener(final RAMListener l) { public RAMListener addListener(final RAMListener l) {
if (listeners.contains(l)) { if (l == null || listeners.contains(l)) {
return l; return l;
} }
listeners.add(l); listeners.add(l);
@ -323,6 +321,9 @@ public abstract class RAM implements Reconfigurable {
} }
public void removeListener(final RAMListener l) { public void removeListener(final RAMListener l) {
if (l == null) {
return;
}
if (!listeners.contains(l)) { if (!listeners.contains(l)) {
return; return;
} }

View File

@ -28,12 +28,11 @@ import jace.config.ConfigurableField;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
public abstract class TimedDevice extends Device { public abstract class TimedDevice extends Device {
// From the holy word of Sather 3:5 (Table 3.1) :-) // From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles // This average speed averages in the "long" cycles
public static final long NTSC_1MHZ = 1020484L; public static final long NTSC_1MHZ = 1020484L;
public static final long PAL_1MHZ = 1015625L; public static final long PAL_1MHZ = 1015625L;
public static final long SYNC_FREQ_HZ = 30; // Check sync every 2 frames public static final long SYNC_FREQ_HZ = 60;
public static final double NANOS_PER_SECOND = 1000000000.0; public static final double NANOS_PER_SECOND = 1000000000.0;
public static final long NANOS_PER_MILLISECOND = 1000000L; public static final long NANOS_PER_MILLISECOND = 1000000L;
public static final long SYNC_SLOP = NANOS_PER_MILLISECOND * 10L; // 10ms slop for synchronization public static final long SYNC_SLOP = NANOS_PER_MILLISECOND * 10L; // 10ms slop for synchronization
@ -41,6 +40,7 @@ public abstract class TimedDevice extends Device {
@ConfigurableField(name = "Speed", description = "(Percentage)") @ConfigurableField(name = "Speed", description = "(Percentage)")
public int speedRatio = 100; public int speedRatio = 100;
@ConfigurableField(name = "Max speed") @ConfigurableField(name = "Max speed")
public boolean forceMaxspeed = false;
public boolean maxspeed = false; public boolean maxspeed = false;
private long cyclesPerSecond = defaultCyclesPerSecond(); private long cyclesPerSecond = defaultCyclesPerSecond();
private int cycleTimer = 0; private int cycleTimer = 0;
@ -120,29 +120,30 @@ public abstract class TimedDevice extends Device {
public final void setMaxSpeed(boolean enabled) { public final void setMaxSpeed(boolean enabled) {
maxspeed = enabled; maxspeed = enabled;
if (!enabled) { if (!enabled) {
disableTempMaxSpeed(); resetSyncTimer();
} }
} }
public final boolean isMaxSpeed() { public final boolean isMaxSpeed() {
return maxspeed; return forceMaxspeed || maxspeed;
} }
public final long getSpeedInHz() { public final long getSpeedInHz() {
return cyclesPerInterval * SYNC_FREQ_HZ; return cyclesPerSecond;
} }
public final void setSpeedInHz(long cyclesPerSecond) { public final void setSpeedInHz(long newSpeed) {
// System.out.println("Raw set speed for " + getName() + " to " + cyclesPerSecond + "hz"); // System.out.println("Raw set speed for " + getName() + " to " + cyclesPerSecond + "hz");
// Thread.dumpStack();
cyclesPerSecond = newSpeed;
speedRatio = (int) Math.round(cyclesPerSecond * 100.0 / defaultCyclesPerSecond()); speedRatio = (int) Math.round(cyclesPerSecond * 100.0 / defaultCyclesPerSecond());
cyclesPerInterval = cyclesPerSecond / SYNC_FREQ_HZ; cyclesPerInterval = cyclesPerSecond / SYNC_FREQ_HZ;
nanosPerInterval = (long) (cyclesPerInterval * NANOS_PER_SECOND / cyclesPerSecond); nanosPerInterval = (long) (cyclesPerInterval * NANOS_PER_SECOND / cyclesPerSecond);
// System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles"); // System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles");
resetSyncTimer(); resetSyncTimer();
} }
public final void setSpeedInPercentage(int ratio) { public final void setSpeedInPercentage(int ratio) {
// System.out.println("Setting " + getName() + " speed ratio to " + speedRatio);
cyclesPerSecond = defaultCyclesPerSecond() * ratio / 100; cyclesPerSecond = defaultCyclesPerSecond() * ratio / 100;
if (cyclesPerSecond == 0) { if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond(); cyclesPerSecond = defaultCyclesPerSecond();
@ -169,21 +170,17 @@ public abstract class TimedDevice extends Device {
} }
protected Long calculateResyncDelay() { protected Long calculateResyncDelay() {
if (!maxspeed && ++cycleTimer >= cyclesPerInterval) { if (!isMaxSpeed() && ++cycleTimer >= cyclesPerInterval) {
cycleTimer = 0; cycleTimer = 0;
if (tempSpeedDuration > 0) { if (tempSpeedDuration > 0) {
tempSpeedDuration -= cyclesPerInterval; tempSpeedDuration -= cyclesPerInterval;
if (tempSpeedDuration <= 0) { if (tempSpeedDuration <= 0) {
disableTempMaxSpeed(); disableTempMaxSpeed();
} }
} else if (nextSync < System.nanoTime()) {
// We're outside the expected range of timing so don't bother trying to sync
nextSync = System.nanoTime() + nanosPerInterval;
} else { } else {
// Return the number of nanoseconds we're ahead (subtract the slop allowed for synchronization deltas) long retVal = nextSync;
long returnVal = nextSync-SYNC_SLOP; nextSync = Math.min(nextSync + nanosPerInterval, System.nanoTime() + nanosPerInterval * 2); // Avoid drift (but not too much!
nextSync += nanosPerInterval; return retVal;
return returnVal;
} }
} }
return null; return null;

View File

@ -149,6 +149,10 @@ public class Utility {
return Optional.empty(); return Optional.empty();
} }
InputStream stream = Utility.class.getResourceAsStream("/jace/data/" + filename); InputStream stream = Utility.class.getResourceAsStream("/jace/data/" + filename);
if (stream == null) {
System.err.println("Could not load icon: " + filename);
return Optional.empty();
}
return Optional.of(new Image(stream)); return Optional.of(new Image(stream));
} }
@ -278,45 +282,6 @@ public class Utility {
return null; return null;
} }
public static void printStackTrace() {
System.out.println("START OF STACK TRACE:");
int skip = 2;
for (StackTraceElement s : Thread.currentThread().getStackTrace()) {
if (skip-- > 0) {
continue;
}
if (s.getClassName().startsWith("com.sun.javafx.event")) {
break;
}
System.out.println(" " + s.getClassName() + "." + s.getMethodName() + " (line " + s.getLineNumber() + ") " + (s.isNativeMethod() ? "NATIVE" : ""));
}
System.out.println("END OF STACK TRACE");
}
public static int parseHexInt(Object s) {
if (s == null) {
return -1;
}
if (s instanceof Integer integer) {
return integer;
}
String val = String.valueOf(s).trim();
int base = 10;
if (val.startsWith("$")) {
base = 16;
val = val.contains(" ") ? val.substring(1, val.indexOf(' ')) : val.substring(1);
} else if (val.startsWith("0x")) {
base = 16;
val = val.contains(" ") ? val.substring(2, val.indexOf(' ')) : val.substring(2);
}
try {
return Integer.parseInt(val, base);
} catch (NumberFormatException ex) {
gripe("This isn't a valid number: " + val + ". If you put a $ in front of that then I'll know you meant it to be a hex number.");
throw ex;
}
}
public static void gripe(final String message) { public static void gripe(final String message) {
gripe(message, false, null); gripe(message, false, null);
} }

View File

@ -125,10 +125,8 @@ public abstract class Video extends TimedDevice {
Runnable redrawScreen = () -> { Runnable redrawScreen = () -> {
if (visible != null && video != null) { if (visible != null && video != null) {
// if (computer.getRunningProperty().get()) { screenDirty = false;
screenDirty = false; visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0);
visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0);
// }
} }
}; };

View File

@ -73,7 +73,7 @@ public class CardAppleMouse extends Card {
@Stateful @Stateful
public int statusByte; public int statusByte;
@Stateful @Stateful
public Point2D lastMouseLocation; public Point2D lastMouseLocation = new Point2D(0, 0);
@Stateful @Stateful
public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff); public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff);
// By default, update 60 times a second -- roughly every VBL period (in theory) // By default, update 60 times a second -- roughly every VBL period (in theory)
@ -107,8 +107,16 @@ public class CardAppleMouse extends Card {
private void processMouseEvent(MouseEvent event) { private void processMouseEvent(MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) { if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
Node source = (Node) event.getSource(); double x = 0.0;
updateLocation(event.getSceneX(), event.getSceneY(), source.getBoundsInLocal()); double y = 0.0;
if (event.getSource() != null && event.getSource() instanceof Node) {
// This is a bit of a hack to get the mouse position in the local coordinate system of the source (the emulator screen
Node source = (Node) event.getSource();
Bounds bounds = source.getBoundsInLocal();
x=event.getSceneX() / bounds.getWidth();
y=event.getSceneY() / bounds.getHeight();
}
updateLocation(x, y);
event.consume(); event.consume();
} }
if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) { if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
@ -120,10 +128,8 @@ public class CardAppleMouse extends Card {
} }
} }
private void updateLocation(double x, double y, Bounds bounds) { private void updateLocation(double x, double y) {
double scaledX = x / bounds.getWidth(); lastMouseLocation = new Point2D(x, y);
double scaledY = y / bounds.getHeight();
lastMouseLocation = new Point2D(scaledX, scaledY);
movedSinceLastTick = true; movedSinceLastTick = true;
movedSinceLastRead = true; movedSinceLastRead = true;
} }
@ -210,6 +216,7 @@ public class CardAppleMouse extends Card {
case 0x08: case 0x08:
// Pascal signature byte // Pascal signature byte
e.setNewValue(0x001); e.setNewValue(0x001);
break;
case 0x011: case 0x011:
e.setNewValue(0x000); e.setNewValue(0x000);
break; break;
@ -376,7 +383,9 @@ public class CardAppleMouse extends Card {
* Screen holes are updated * Screen holes are updated
*/ */
private void initMouse() { private void initMouse() {
mouseActive.setText("Active"); if (mouseActive != null) {
mouseActive.setText("Active");
}
EmulatorUILogic.addIndicator(this, mouseActive, 2000); EmulatorUILogic.addIndicator(this, mouseActive, 2000);
setClampWindowX(0, 0x3ff); setClampWindowX(0, 0x3ff);
setClampWindowY(0, 0x3ff); setClampWindowY(0, 0x3ff);

View File

@ -84,18 +84,24 @@ public class CardDiskII extends Card implements MediaConsumerParent {
// Motherboard.cancelSpeedRequest(this); // Motherboard.cancelSpeedRequest(this);
} }
@SuppressWarnings("fallthrough")
@Override @Override
protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e) { protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e) {
// handle Disk ][ registers // handle Disk ][ registers
switch (register) { switch (register) {
case 0x0: case 0x0:
// Fall-through
case 0x1: case 0x1:
// Fall-through
case 0x2: case 0x2:
// Fall-through
case 0x3: case 0x3:
// Fall-through
case 0x4: case 0x4:
// Fall-through
case 0x5: case 0x5:
// Fall-through
case 0x6: case 0x6:
// Fall-through
case 0x7: case 0x7:
currentDrive.step(register); currentDrive.step(register);
break; break;

View File

@ -45,6 +45,9 @@ import jace.hardware.mockingboard.R6522;
public class CardMockingboard extends Card { public class CardMockingboard extends Card {
// If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips // If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips
@ConfigurableField(name = "Debug", category = "Sound", description = "Enable debug output")
public static boolean DEBUG = false;
@ConfigurableField(name = "Volume", shortName = "vol", @ConfigurableField(name = "Volume", shortName = "vol",
category = "Sound", category = "Sound",
description = "Mockingboard volume, 100=max, 0=silent") description = "Mockingboard volume, 100=max, 0=silent")
@ -68,7 +71,6 @@ public class CardMockingboard extends Card {
double ticksBetweenPlayback = 24.0; double ticksBetweenPlayback = 24.0;
int MAX_IDLE_TICKS = 1000000; int MAX_IDLE_TICKS = 1000000;
boolean activatedAfterReset = false; boolean activatedAfterReset = false;
boolean debug = false;
@Override @Override
public String getDeviceName() { public String getDeviceName() {
@ -127,6 +129,11 @@ public class CardMockingboard extends Card {
@Override @Override
public void reset() { public void reset() {
activatedAfterReset = false; activatedAfterReset = false;
if (chips != null) {
for (PSG p : chips) {
p.reset();
}
}
suspend(); suspend();
} }
RAMListener mainListener = null; RAMListener mainListener = null;
@ -145,7 +152,9 @@ public class CardMockingboard extends Card {
chip++; chip++;
} }
if (chip >= 2) { if (chip >= 2) {
System.err.println("Could not determine which PSG to communicate to for access to regsiter + " + Integer.toHexString(register)); if (DEBUG) {
System.err.println("Could not determine which PSG to communicate to for access to regsiter + " + Integer.toHexString(register));
}
Emulator.withVideo(v->e.setNewValue(v.getFloatingBus())); Emulator.withVideo(v->e.setNewValue(v.getFloatingBus()));
return; return;
} }
@ -153,24 +162,27 @@ public class CardMockingboard extends Card {
if (e.getType().isRead()) { if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f); int val = controller.readRegister(register & 0x0f);
e.setNewValue(val); e.setNewValue(val);
if (debug) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val); if (DEBUG) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
} else { } else {
controller.writeRegister(register & 0x0f, e.getNewValue()); controller.writeRegister(register & 0x0f, e.getNewValue());
if (debug) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue()); if (DEBUG) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
} }
// Any firmware access will reset the idle counter and wake up the card, this allows the timers to start running again // Any firmware access will reset the idle counter and wake up the card, this allows the timers to start running again
// Games such as "Skyfox" use the timer to detect if the card is present. // Games such as "Skyfox" use the timer to detect if the card is present.
idleTicks = 0; idleTicks = 0;
if (!isRunning() || isPaused()) { if (!isRunning() || isPaused()) {
activatedAfterReset = true; activatedAfterReset = true;
resume(); // ResumeAll is important so that the 6522's can start their timers
resumeAll();
} }
} }
@Override @Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// Oddly, all IO is done at the firmware address bank. It's a strange card. // Oddly, all IO is done at the firmware address bank. It's a strange card.
// System.out.println("MB I/O Access "+type.name()+" "+register+":"+value); if (DEBUG) {
System.out.println("MB I/O Access "+type.name()+" "+register+":"+value);
}
Emulator.withVideo(v->e.setNewValue(v.getFloatingBus())); Emulator.withVideo(v->e.setNewValue(v.getFloatingBus()));
} }
double ticksSinceLastPlayback = 0; double ticksSinceLastPlayback = 0;
@ -200,20 +212,16 @@ public class CardMockingboard extends Card {
@Override @Override
public void reconfigure() { public void reconfigure() {
boolean isActive = isRunning(); if (DEBUG) {
initPSG(); System.out.println("Reconfiguring Mockingboard");
for (PSG chip : chips) {
chip.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
chip.reset();
} }
ticksBetweenPlayback = (double) CLOCK_SPEED / (double) SoundMixer.RATE; ticksBetweenPlayback = (double) CLOCK_SPEED / (double) SoundMixer.RATE;
buildMixerTable(); initPSG();
super.reconfigure(); super.reconfigure();
if (isActive) { if (DEBUG) {
resume(); System.out.println("Reconfiguring Mockingboard completed");
} }
super.reconfigure();
} }
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
@ -222,6 +230,10 @@ public class CardMockingboard extends Card {
AtomicInteger left = new AtomicInteger(0); AtomicInteger left = new AtomicInteger(0);
AtomicInteger right = new AtomicInteger(0); AtomicInteger right = new AtomicInteger(0);
public boolean playSound() throws InterruptedException, ExecutionException, SoundError { public boolean playSound() throws InterruptedException, ExecutionException, SoundError {
if (phasorMode && chips.length != 4) {
System.err.println("Wrong number of chips for phasor mode, correcting this");
initPSG();
}
chips[0].update(left, true, left, false, left, false); chips[0].update(left, true, left, false, left, false);
chips[1].update(right, true, right, false, right, false); chips[1].update(right, true, right, false, right, false);
if (phasorMode) { if (phasorMode) {
@ -262,11 +274,22 @@ public class CardMockingboard extends Card {
@Override @Override
public void resume() { public void resume() {
if (DEBUG) {
System.out.println("Resuming Mockingboard");
Thread.dumpStack();
}
if (!activatedAfterReset) { if (!activatedAfterReset) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: not activated after reset, not resuming");
}
// Do not re-activate until firmware access was made // Do not re-activate until firmware access was made
return; return;
} }
initPSG();
if (buffer == null || !buffer.isAlive()) { if (buffer == null || !buffer.isAlive()) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: creating sound buffer");
}
try { try {
buffer = SoundMixer.createBuffer(true); buffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) { } catch (InterruptedException | ExecutionException | SoundError e) {
@ -275,21 +298,18 @@ public class CardMockingboard extends Card {
suspend(); suspend();
} }
} }
if (chips == null) {
initPSG();
for (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
psg.reset();
}
}
idleTicks = 0; idleTicks = 0;
setPaused(false);
reconfigure();
super.resume(); super.resume();
System.out.println("Resuming Mockingboard: resume completed");
} }
@Override @Override
public boolean suspend() { public boolean suspend() {
if (DEBUG) {
System.out.println("Suspending Mockingboard");
Thread.dumpStack();
}
if (buffer != null) { if (buffer != null) {
try { try {
buffer.shutdown(); buffer.shutdown();
@ -300,21 +320,28 @@ public class CardMockingboard extends Card {
buffer = null; buffer = null;
} }
} }
for (R6522 c : controllers) {
c.suspend();
}
return super.suspend(); return super.suspend();
} }
private void initPSG() { private void initPSG() {
if (phasorMode) { if (phasorMode && (chips == null || chips.length < 4)) {
chips = new PSG[4]; chips = new PSG[4];
chips[0] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY1", 8); chips[0] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY1", 8);
chips[1] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY2", 8); chips[1] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY2", 8);
chips[2] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY3", 16); chips[2] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY3", 16);
chips[3] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY4", 16); chips[3] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY4", 16);
} else { } else if (chips == null || chips.length != 2) {
chips = new PSG[2]; chips = new PSG[2];
chips[0] = new PSG(0, CLOCK_SPEED, SoundMixer.RATE, "AY1", 255); chips[0] = new PSG(0, CLOCK_SPEED, SoundMixer.RATE, "AY1", 255);
chips[1] = new PSG(0x80, CLOCK_SPEED, SoundMixer.RATE, "AY2", 255); chips[1] = new PSG(0x80, CLOCK_SPEED, SoundMixer.RATE, "AY2", 255);
} }
for (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
}
buildMixerTable();
} }
@Override @Override

View File

@ -54,8 +54,6 @@ public class CardSSC extends Card {
protected Thread listenThread; protected Thread listenThread;
private int lastInputByte = 0; private int lastInputByte = 0;
private boolean FULL_ECHO = true; private boolean FULL_ECHO = true;
private final boolean RECV_ACTIVE = true;
private boolean TRANS_ACTIVE = true;
// private boolean RECV_STRIP_LF = true; // private boolean RECV_STRIP_LF = true;
// private boolean TRANS_ADD_LF = true; // private boolean TRANS_ADD_LF = true;
@ConfigurableField(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life when idle (in milliseconds)") @ConfigurableField(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life when idle (in milliseconds)")
@ -65,7 +63,7 @@ public class CardSSC extends Card {
@ConfigurableField(name = "Add LF (send)", shortName = "addLF", defaultValue = "false", description = "Append linefeeds after outgoing carriage returns") @ConfigurableField(name = "Add LF (send)", shortName = "addLF", defaultValue = "false", description = "Append linefeeds after outgoing carriage returns")
public boolean TRANS_ADD_LF = false; public boolean TRANS_ADD_LF = false;
private boolean DTR = true; private boolean DTR = true;
public int SW1 = 0x01; // Read = Jumper block SW1 public static int SW1 = 0x01; // Read = Jumper block SW1
//Bit 0 = !SW1-6 //Bit 0 = !SW1-6
//Bit 1 = !SW1-5 //Bit 1 = !SW1-5
//Bit 4 = !SW1-4 //Bit 4 = !SW1-4
@ -75,7 +73,7 @@ public class CardSSC extends Card {
// 19200 baud (SW1-1,2,3,4 off) // 19200 baud (SW1-1,2,3,4 off)
// Communications mode (SW1-5,6 on) // Communications mode (SW1-5,6 on)
public int SW1_SETTING = 0x0F0; public int SW1_SETTING = 0x0F0;
public int SW2_CTS = 0x02; // Read = Jumper block SW2 and CTS public static int SW2_CTS = 0x02; // Read = Jumper block SW2 and CTS
//Bit 0 = !CTS //Bit 0 = !CTS
//SW2-6 = Allow interrupts (disable in ][, ][+) //SW2-6 = Allow interrupts (disable in ][, ][+)
//Bit 1 = !SW2-5 -- Generate LF after CR //Bit 1 = !SW2-5 -- Generate LF after CR
@ -87,10 +85,10 @@ public class CardSSC extends Card {
// 8 data bits (SW2-2 on) // 8 data bits (SW2-2 on)
// No parity (SW2-3 don't care, SW2-4 off) // No parity (SW2-3 don't care, SW2-4 off)
private final int SW2_SETTING = 0x04; private final int SW2_SETTING = 0x04;
public int ACIA_Data = 0x08; // Read=Receive / Write=transmit public static int ACIA_Data = 0x08; // Read=Receive / Write=transmit
public int ACIA_Status = 0x09; // Read=Status / Write=Reset public static int ACIA_Status = 0x09; // Read=Status / Write=Reset
public int ACIA_Command = 0x0A; public static int ACIA_Command = 0x0A;
public int ACIA_Control = 0x0B; public static int ACIA_Control = 0x0B;
public boolean PORT_CONNECTED = false; public boolean PORT_CONNECTED = false;
public boolean RECV_IRQ_ENABLED = false; public boolean RECV_IRQ_ENABLED = false;
public boolean TRANS_IRQ_ENABLED = false; public boolean TRANS_IRQ_ENABLED = false;
@ -296,19 +294,15 @@ public class CardSSC extends Card {
switch ((value >> 2) & 3) { switch ((value >> 2) & 3) {
case 0: case 0:
TRANS_IRQ_ENABLED = false; TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = false;
break; break;
case 1: case 1:
TRANS_IRQ_ENABLED = true; TRANS_IRQ_ENABLED = true;
TRANS_ACTIVE = true;
break; break;
case 2: case 2:
TRANS_IRQ_ENABLED = false; TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break; break;
case 3: case 3:
TRANS_IRQ_ENABLED = false; TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break; break;
} }
// 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0) // 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0)
@ -397,16 +391,17 @@ public class CardSSC extends Card {
} }
} }
private void setCTS(boolean b) throws InterruptedException { // CTS isn't used here -- it's assumed that we're always clear-to-send
PORT_CONNECTED = b; // private void setCTS(boolean b) throws InterruptedException {
if (b == false) { // PORT_CONNECTED = b;
reset(); // if (b == false) {
} // reset();
} // }
// }
private boolean getCTS() throws InterruptedException { // private boolean getCTS() throws InterruptedException {
return PORT_CONNECTED; // return PORT_CONNECTED;
} // }
private void triggerIRQ() { private void triggerIRQ() {
IRQ_TRIGGERED = true; IRQ_TRIGGERED = true;

View File

@ -7,13 +7,14 @@ import jace.core.Card;
import jace.hardware.massStorage.CardMassStorage; import jace.hardware.massStorage.CardMassStorage;
public enum Cards implements DeviceEnum<Card> { public enum Cards implements DeviceEnum<Card> {
DiskIIDrive("Disk II Floppy Controller", CardDiskII.class, CardDiskII::new),
MassStorage("Mass Storage", CardMassStorage.class, CardMassStorage::new),
AppleMouse("Apple Mouse", CardAppleMouse.class, CardAppleMouse::new), AppleMouse("Apple Mouse", CardAppleMouse.class, CardAppleMouse::new),
DiskIIDrive("Disk II Floppy Controller", CardDiskII.class, CardDiskII::new),
HayesMicroModem("Hayes MicroModem", CardHayesMicromodem.class, CardHayesMicromodem::new), HayesMicroModem("Hayes MicroModem", CardHayesMicromodem.class, CardHayesMicromodem::new),
MassStorage("Mass Storage", CardMassStorage.class, CardMassStorage::new),
Mockingboard("Mockingboard", CardMockingboard.class, CardMockingboard::new), Mockingboard("Mockingboard", CardMockingboard.class, CardMockingboard::new),
SuperSerialCard("Super Serial Card", CardSSC.class, CardSSC::new), PassportMidi("Passport MIDI", PassportMidiInterface.class, PassportMidiInterface::new),
RamFactor("RamFactor", CardRamFactor.class, CardRamFactor::new), RamFactor("RamFactor", CardRamFactor.class, CardRamFactor::new),
SuperSerialCard("Super Serial Card", CardSSC.class, CardSSC::new),
Thunderclock("Thunderclock", CardThunderclock.class, CardThunderclock::new); Thunderclock("Thunderclock", CardThunderclock.class, CardThunderclock::new);
Supplier<Card> factory; Supplier<Card> factory;

View File

@ -1,19 +1,18 @@
/** /**
* Copyright 2024 Brendan Robert * Copyright 2024 Brendan Robert
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ * */
package jace.hardware; package jace.hardware;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -42,7 +41,7 @@ import jace.core.RAMEvent.TYPE;
* operational notes taken from the Passport MIDI interface manual * operational notes taken from the Passport MIDI interface manual
* ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf * ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf
* *
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
@Name(value = "Passport Midi Interface", description = "MIDI sound card") @Name(value = "Passport Midi Interface", description = "MIDI sound card")
public class PassportMidiInterface extends Card { public class PassportMidiInterface extends Card {
@ -78,8 +77,9 @@ public class PassportMidiInterface extends Card {
for (MidiDevice.Info dev : devices) { for (MidiDevice.Info dev : devices) {
try { try {
MidiDevice device = MidiSystem.getMidiDevice(dev); MidiDevice device = MidiSystem.getMidiDevice(dev);
if (device.getMaxReceivers() > 0 || dev instanceof Synthesizer) if (device.getMaxReceivers() > 0 || dev instanceof Synthesizer) {
System.out.println("MIDI Device found: " + dev); System.out.println("MIDI Device found: " + dev);
}
out.put(dev.getName(), dev.getName()); out.put(dev.getName(), dev.getName());
} catch (MidiUnavailableException ex) { } catch (MidiUnavailableException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
@ -202,13 +202,11 @@ public class PassportMidiInterface extends Card {
@Override @Override
public void reset() { public void reset() {
// TODO: Deactivate card
suspend(); suspend();
} }
@Override @Override
public boolean suspend() { public boolean suspend() {
// TODO: Deactivate card
suspendACIA(); suspendACIA();
return super.suspend(); return super.suspend();
} }
@ -221,77 +219,70 @@ public class PassportMidiInterface extends Card {
@Override @Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
switch (type) { switch (type) {
case READ_DATA: case READ_DATA -> {
int returnValue = 0; int returnValue = 0;
switch (register) { switch (register) {
case ACIA_STATUS: case ACIA_STATUS ->
returnValue = getACIAStatus(); returnValue = getACIAStatus();
break; case ACIA_RECV ->
case ACIA_RECV:
returnValue = getACIARecieve(); returnValue = getACIARecieve();
break; case TIMER_CONTROL_1 -> // Technically it's not supposed to return anything...
//TODO: Implement PTM registers
case TIMER_CONTROL_1:
// Technically it's not supposed to return anything...
returnValue = getPTMStatus(); returnValue = getPTMStatus();
break; case TIMER_CONTROL_2 ->
case TIMER_CONTROL_2:
returnValue = getPTMStatus(); returnValue = getPTMStatus();
break; case TIMER1_LSB -> {
case TIMER1_LSB:
returnValue = (int) (ptmTimer[0].value & 0x0ff); returnValue = (int) (ptmTimer[0].value & 0x0ff);
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false; ptmTimer[0].irqRequested = false;
} }
break; }
case TIMER1_MSB: case TIMER1_MSB -> {
returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff; returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false; ptmTimer[0].irqRequested = false;
} }
break; }
case TIMER2_LSB: case TIMER2_LSB -> {
returnValue = (int) (ptmTimer[1].value & 0x0ff); returnValue = (int) (ptmTimer[1].value & 0x0ff);
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false; ptmTimer[1].irqRequested = false;
} }
break; }
case TIMER2_MSB: case TIMER2_MSB -> {
returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff; returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false; ptmTimer[1].irqRequested = false;
} }
break; }
case TIMER3_LSB: case TIMER3_LSB -> {
returnValue = (int) (ptmTimer[2].value & 0x0ff); returnValue = (int) (ptmTimer[2].value & 0x0ff);
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false; ptmTimer[2].irqRequested = false;
} }
break; }
case TIMER3_MSB: case TIMER3_MSB -> {
returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff; returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) { if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false; ptmTimer[2].irqRequested = false;
} }
break; }
default: default ->
System.out.println("Passport midi read unrecognized, port " + register); System.out.println("Passport midi read unrecognized, port " + register);
} }
//TODO: Implement PTM registers
e.setNewValue(returnValue); e.setNewValue(returnValue);
// System.out.println("Passport I/O read register " + register + " == " + returnValue); // System.out.println("Passport I/O read register " + register + " == " + returnValue);
break; }
case WRITE:
case WRITE -> {
int v = e.getNewValue() & 0x0ff; int v = e.getNewValue() & 0x0ff;
// System.out.println("Passport I/O write register " + register + " == " + v); // System.out.println("Passport I/O write register " + register + " == " + v);
switch (register) { switch (register) {
case ACIA_CONTROL: case ACIA_CONTROL ->
processACIAControl(v); processACIAControl(v);
break; case ACIA_SEND ->
case ACIA_SEND:
processACIASend(v); processACIASend(v);
break; case TIMER_CONTROL_1 -> {
case TIMER_CONTROL_1:
if (ptmTimer3Selected) { if (ptmTimer3Selected) {
// System.out.println("Configuring timer 3"); // System.out.println("Configuring timer 3");
ptmTimer[2].prescaledTimer = ((v & TIMER3_PRESCALED) != 0); ptmTimer[2].prescaledTimer = ((v & TIMER3_PRESCALED) != 0);
@ -305,38 +296,32 @@ public class PassportMidiInterface extends Card {
} }
processPTMConfiguration(ptmTimer[0], v); processPTMConfiguration(ptmTimer[0], v);
} }
break; }
case TIMER_CONTROL_2: case TIMER_CONTROL_2 -> {
// System.out.println("Configuring timer 2"); // System.out.println("Configuring timer 2");
ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0); ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0);
processPTMConfiguration(ptmTimer[1], v); processPTMConfiguration(ptmTimer[1], v);
break; }
case TIMER1_LSB: case TIMER1_LSB ->
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v; ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v;
break; case TIMER1_MSB ->
case TIMER1_MSB:
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8); ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8);
break; case TIMER2_LSB ->
case TIMER2_LSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v; ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v;
break; case TIMER2_MSB ->
case TIMER2_MSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8); ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8);
break; case TIMER3_LSB ->
case TIMER3_LSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v; ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v;
break; case TIMER3_MSB ->
case TIMER3_MSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8); ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8);
break; default ->
default:
System.out.println("Passport midi write unrecognized, port " + register); System.out.println("Passport midi write unrecognized, port " + register);
} }
}
break; default -> {
default: }
// Nothing
} }
// Nothing
} }
@Override @Override
@ -352,7 +337,7 @@ public class PassportMidiInterface extends Card {
if (t.irqEnabled) { if (t.irqEnabled) {
// System.out.println("Timer generating interrupt!"); // System.out.println("Timer generating interrupt!");
t.irqRequested = true; t.irqRequested = true;
Emulator.withComputer(c->c.getCpu().generateInterrupt()); Emulator.withComputer(c -> c.getCpu().generateInterrupt());
ptmStatusReadSinceIRQ = false; ptmStatusReadSinceIRQ = false;
} }
if (t.mode == TIMER_MODE.CONTINUOUS || t.mode == TIMER_MODE.FREQ_COMPARISON) { if (t.mode == TIMER_MODE.CONTINUOUS || t.mode == TIMER_MODE.FREQ_COMPARISON) {
@ -425,6 +410,7 @@ public class PassportMidiInterface extends Card {
} }
return status; return status;
} }
//------------------------------------------------------ ACIA //------------------------------------------------------ ACIA
/* /*
ACIA status register ACIA status register
@ -554,9 +540,8 @@ public class PassportMidiInterface extends Card {
continue; continue;
} }
System.out.println("MIDI Device found: " + dev); System.out.println("MIDI Device found: " + dev);
if ((preferredMidiDevice.getValue() == null && dev.getName().contains("Java Sound") && dev instanceof Synthesizer) || if ((preferredMidiDevice.getValue() == null && dev.getName().contains("Java Sound") && dev instanceof Synthesizer)
preferredMidiDevice.getValue().equalsIgnoreCase(dev.getName()) || preferredMidiDevice.getValue().equalsIgnoreCase(dev.getName())) {
) {
selectedDevice = MidiSystem.getMidiDevice(dev); selectedDevice = MidiSystem.getMidiDevice(dev);
break; break;
} }
@ -575,7 +560,6 @@ public class PassportMidiInterface extends Card {
} }
private void suspendACIA() { private void suspendACIA() {
// TODO: Stop ACIA thread...
if (midiOut != null) { if (midiOut != null) {
currentMessage = new ShortMessage(); currentMessage = new ShortMessage();
// Send a note-off on every channel // Send a note-off on every channel
@ -583,10 +567,10 @@ public class PassportMidiInterface extends Card {
try { try {
// All Notes Off // All Notes Off
currentMessage.setMessage(0x0B0 | channel, 123, 0); currentMessage.setMessage(0x0B0 | channel, 123, 0);
midiOut.send(currentMessage, 0); midiOut.send(currentMessage, 0);
// All Oscillators Off // All Oscillators Off
currentMessage.setMessage(0x0B0 | channel, 120, 0); currentMessage.setMessage(0x0B0 | channel, 120, 0);
midiOut.send(currentMessage, 0); midiOut.send(currentMessage, 0);
} catch (InvalidMidiDataException ex) { } catch (InvalidMidiDataException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
} }

View File

@ -18,7 +18,6 @@ package jace.hardware;
import jace.Emulator; import jace.Emulator;
import jace.config.ConfigurableField; import jace.config.ConfigurableField;
import jace.core.Device; import jace.core.Device;
import jace.core.Motherboard;
import jace.core.RAMEvent; import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
@ -175,14 +174,12 @@ public class ZipWarpAccelerator extends Device {
private void setSpeed(SPEED speed) { private void setSpeed(SPEED speed) {
speedValue = speed.val; speedValue = speed.val;
Emulator.withComputer(c -> { Emulator.withComputer(c -> {
// if (speed.max) { if (speed.max) {
// c.getMotherboard().setMaxSpeed(true); c.getMotherboard().setMaxSpeed(true);
// Motherboard.cpuPerClock = 3; } else {
// } else {
c.getMotherboard().setMaxSpeed(false); c.getMotherboard().setMaxSpeed(false);
c.getMotherboard().setSpeedInPercentage((int) (speed.ratio * 100)); c.getMotherboard().setSpeedInPercentage((int) (speed.ratio * 100));
Motherboard.cpuPerClock = 1; }
// }
c.getMotherboard().reconfigure(); c.getMotherboard().reconfigure();
}); });
} }

View File

@ -99,16 +99,12 @@ public class PSG {
List<SoundGenerator> channels; List<SoundGenerator> channels;
EnvelopeGenerator envelopeGenerator; EnvelopeGenerator envelopeGenerator;
NoiseGenerator noiseGenerator; NoiseGenerator noiseGenerator;
int CLOCK;
int SAMPLE_RATE;
public int bus; public int bus;
int selectedReg; int selectedReg;
String name;
Map<Reg, Integer> regValues; Map<Reg, Integer> regValues;
public int mask; public int mask;
public PSG(int base, int clock, int sample_rate, String name, int DDR_Mask) { public PSG(int base, int clock, int sample_rate, String name, int DDR_Mask) {
this.name = name;
this.mask = DDR_Mask; this.mask = DDR_Mask;
baseReg = base; baseReg = base;
channels = new ArrayList<>(); channels = new ArrayList<>();
@ -117,7 +113,7 @@ public class PSG {
} }
envelopeGenerator = new EnvelopeGenerator(clock, sample_rate); envelopeGenerator = new EnvelopeGenerator(clock, sample_rate);
noiseGenerator = new NoiseGenerator(clock, sample_rate); noiseGenerator = new NoiseGenerator(clock, sample_rate);
regValues = Collections.synchronizedMap(new EnumMap<Reg, Integer>(Reg.class)); regValues = Collections.synchronizedMap(new EnumMap<>(Reg.class));
reset(); reset();
} }
@ -132,20 +128,20 @@ public class PSG {
return; return;
} }
switch (cmd) { switch (cmd) {
case inactive: case inactive -> {
break; }
case latch: case latch -> {
if (debug) System.out.println("PSG latched register "+selectedReg); if (debug) System.out.println("PSG latched register "+selectedReg);
selectedReg = bus & 0x0f; selectedReg = bus & 0x0f;
break; }
case read: case read -> {
bus = getReg(Reg.get(selectedReg)); bus = getReg(Reg.get(selectedReg));
if (debug) System.out.println("PSG read register "+selectedReg + " == "+bus); if (debug) System.out.println("PSG read register "+selectedReg + " == "+bus);
break; }
case write: case write -> {
if (debug) System.out.println("PSG wrote register "+selectedReg + " == "+bus); if (debug) System.out.println("PSG wrote register "+selectedReg + " == "+bus);
setReg(Reg.get(selectedReg), bus); setReg(Reg.get(selectedReg), bus);
break; }
} }
} }
@ -154,14 +150,11 @@ public class PSG {
} }
public void setRate(int clock, int sample_rate) { public void setRate(int clock, int sample_rate) {
CLOCK = clock;
SAMPLE_RATE = sample_rate;
channels.stream().forEach((c) -> { channels.stream().forEach((c) -> {
c.setRate(clock, sample_rate); c.setRate(clock, sample_rate);
}); });
envelopeGenerator.setRate(clock, sample_rate); envelopeGenerator.setRate(clock, sample_rate);
noiseGenerator.setRate(clock, sample_rate); noiseGenerator.setRate(clock, sample_rate);
reset();
} }
public final void reset() { public final void reset() {
@ -208,50 +201,29 @@ public class PSG {
/* period. In that case, period = 0 is half as period = 1. */ /* period. In that case, period = 0 is half as period = 1. */
value = value & 0x0ff; value = value & 0x0ff;
switch (r) { switch (r) {
case ACoarse: case ACoarse, AFine -> channels.get(0).setPeriod(getReg(Reg.AFine) + (getReg(Reg.ACoarse) << 8));
case AFine: case BCoarse, BFine -> channels.get(1).setPeriod(getReg(Reg.BFine) + (getReg(Reg.BCoarse) << 8));
channels.get(0).setPeriod(getReg(Reg.AFine) + (getReg(Reg.ACoarse) << 8)); case CCoarse, CFine -> channels.get(2).setPeriod(getReg(Reg.CFine) + (getReg(Reg.CCoarse) << 8));
break; case NoisePeriod -> {
case BCoarse:
case BFine:
channels.get(1).setPeriod(getReg(Reg.BFine) + (getReg(Reg.BCoarse) << 8));
break;
case CCoarse:
case CFine:
channels.get(2).setPeriod(getReg(Reg.CFine) + (getReg(Reg.CCoarse) << 8));
break;
case NoisePeriod:
if (value == 0) value = 32; if (value == 0) value = 32;
noiseGenerator.setPeriod(value+16); noiseGenerator.setPeriod(value+16);
noiseGenerator.counter = 0; noiseGenerator.counter = 0;
break; }
case Enable: case Enable -> {
channels.get(0).setActive((value & 1) == 0); channels.get(0).setActive((value & 1) == 0);
channels.get(0).setNoiseActive((value & 8) == 0); channels.get(0).setNoiseActive((value & 8) == 0);
channels.get(1).setActive((value & 2) == 0); channels.get(1).setActive((value & 2) == 0);
channels.get(1).setNoiseActive((value & 16) == 0); channels.get(1).setNoiseActive((value & 16) == 0);
channels.get(2).setActive((value & 4) == 0); channels.get(2).setActive((value & 4) == 0);
channels.get(2).setNoiseActive((value & 32) == 0); channels.get(2).setNoiseActive((value & 32) == 0);
break; }
case AVol: case AVol -> channels.get(0).setAmplitude(value);
channels.get(0).setAmplitude(value); case BVol -> channels.get(1).setAmplitude(value);
break; case CVol -> channels.get(2).setAmplitude(value);
case BVol: case EnvFine, EnvCoarse -> envelopeGenerator.setPeriod(getReg(Reg.EnvFine) + 256 * getReg(Reg.EnvCoarse));
channels.get(1).setAmplitude(value); case EnvShape -> envelopeGenerator.setShape(value);
break; case PortA, PortB -> {
case CVol: }
channels.get(2).setAmplitude(value);
break;
case EnvFine:
case EnvCoarse:
envelopeGenerator.setPeriod(getReg(Reg.EnvFine) + 256 * getReg(Reg.EnvCoarse));
break;
case EnvShape:
envelopeGenerator.setShape(value);
break;
case PortA:
case PortB:
break;
} }
} }

View File

@ -33,15 +33,8 @@ public abstract class R6522 extends Device {
timer1running = true; timer1running = true;
timer1latch = 0x1fff; timer1latch = 0x1fff;
timer1interruptEnabled = false; timer1interruptEnabled = false;
// setSpeedInHz(SPEED);
// setRun(true);
} }
// @Override
// public long defaultCyclesPerSecond() {
// return SPEED;
// }
// 6522 VIA // 6522 VIA
// http://www.applevault.com/twiki/Main/Mockingboard/6522.pdf // http://www.applevault.com/twiki/Main/Mockingboard/6522.pdf
// I/O registers // I/O registers
@ -148,56 +141,49 @@ public abstract class R6522 extends Device {
@Override @Override
public void tick() { public void tick() {
// if (!unclocked) { if (timer1running) {
if (timer1running) { timer1counter--;
timer1counter--; if (debug && timer1counter % 1000 == 0)
if (debug && timer1counter % 1000 == 0) System.out.println(getShortName() + " Timer 1 counter: "+timer1counter+" Timer 1 interrupt enabled: "+timer1interruptEnabled);
System.out.println(getShortName() + " Timer 1 counter: "+timer1counter+" Timer 1 interrupt enabled: "+timer1interruptEnabled); if (timer1counter < 0) {
if (timer1counter < 0) { timer1counter = timer1latch;
timer1counter = timer1latch; if (!timer1freerun) {
if (!timer1freerun) { timer1running = false;
timer1running = false; }
} if (timer1interruptEnabled) {
if (timer1interruptEnabled) { if (debug) System.out.println("Timer 1 generated interrupt");
if (debug) System.out.println("Timer 1 generated interrupt"); timer1IRQ = true;
timer1IRQ = true; Emulator.withComputer(c->c.getCpu().generateInterrupt());
Emulator.withComputer(c->c.getCpu().generateInterrupt());
}
} }
} }
if (timer2running) { }
timer2counter--; if (timer2running) {
if (debug && timer2counter % 1000 == 0) timer2counter--;
System.out.println(getShortName() + " Timer 2 counter: "+timer2counter+" Timer 2 interrupt enabled: "+timer2interruptEnabled); if (debug && timer2counter % 1000 == 0)
if (timer2counter < 0) { System.out.println(getShortName() + " Timer 2 counter: "+timer2counter+" Timer 2 interrupt enabled: "+timer2interruptEnabled);
timer2running = false; if (timer2counter < 0) {
timer2counter = timer2latch; timer2running = false;
if (timer2interruptEnabled) { timer2counter = timer2latch;
if (debug) System.out.println("Timer 2 generated interrupt"); if (timer2interruptEnabled) {
timer2IRQ = true; if (debug) System.out.println("Timer 2 generated interrupt");
Emulator.withComputer(c->c.getCpu().generateInterrupt()); timer2IRQ = true;
} Emulator.withComputer(c->c.getCpu().generateInterrupt());
} }
} }
if (!timer1running && !timer2running) { }
if (debug) System.out.println("No timers active, suspending"); if (!timer1running && !timer2running) {
suspend(); if (debug) System.out.println("No timers active, suspending");
} suspend();
// } }
} }
public void setUnclocked(boolean unclocked) { public void setUnclocked(boolean unclocked) {
this.unclocked = unclocked; this.unclocked = unclocked;
} }
@Override
public void attach() {
// Start chip
}
@Override @Override
public void reconfigure() { public void reconfigure() {
// Reset // Nothing to do
} }
public void writeRegister(int reg, int val) { public void writeRegister(int reg, int val) {
@ -205,70 +191,59 @@ public abstract class R6522 extends Device {
Register r = Register.fromInt(reg); Register r = Register.fromInt(reg);
if (debug) System.out.println(getShortName() + " Writing "+Integer.toHexString(value&0x0ff)+" to register "+r.toString()); if (debug) System.out.println(getShortName() + " Writing "+Integer.toHexString(value&0x0ff)+" to register "+r.toString());
switch (r) { switch (r) {
case ORB: case ORB -> {
if (dataDirectionB == 0) { if (dataDirectionB == 0) {
break; break;
} }
sendOutputB(value & dataDirectionB); sendOutputB(value & dataDirectionB);
break; }
case ORA: case ORA -> {
// case ORAH: // case ORAH:
if (dataDirectionA == 0) { if (dataDirectionA == 0) {
break; break;
} }
sendOutputA(value & dataDirectionA); sendOutputA(value & dataDirectionA);
break; }
case DDRB: case DDRB -> dataDirectionB = value;
dataDirectionB = value; case DDRA -> dataDirectionA = value;
break; case T1CL, T1LL -> timer1latch = (timer1latch & 0x0ff00) | value;
case DDRA: case T1CH -> {
dataDirectionA = value;
break;
case T1CL:
case T1LL:
timer1latch = (timer1latch & 0x0ff00) | value;
break;
case T1CH:
timer1latch = (timer1latch & 0x0ff) | (value << 8); timer1latch = (timer1latch & 0x0ff) | (value << 8);
timer1IRQ = false; timer1IRQ = false;
timer1counter = timer1latch; timer1counter = timer1latch;
timer1running = true; timer1running = true;
break; }
case T1LH: case T1LH -> {
timer1latch = (timer1latch & 0x0ff) | (value << 8); timer1latch = (timer1latch & 0x0ff) | (value << 8);
timer1IRQ = false; timer1IRQ = false;
break; }
case T2CL: case T2CL -> timer2latch = (timer2latch & 0x0ff00) | value;
timer2latch = (timer2latch & 0x0ff00) | value; case T2CH -> {
break;
case T2CH:
timer2latch = (timer2latch & 0x0ff) | (value << 8); timer2latch = (timer2latch & 0x0ff) | (value << 8);
timer2IRQ = false; timer2IRQ = false;
timer2counter = timer2latch; timer2counter = timer2latch;
timer2running = true; timer2running = true;
break; }
case SR: case SR -> {
// SHIFT REGISTER NOT IMPLEMENTED }
break; case ACR -> {
case ACR:
// SHIFT REGISTER NOT IMPLEMENTED // SHIFT REGISTER NOT IMPLEMENTED
timer1freerun = (value & 64) != 0; timer1freerun = (value & 64) != 0;
if (timer1freerun) { if (timer1freerun) {
timer1running = true; timer1running = true;
} }
break; }
case PCR: case PCR -> {
// TODO: Implement if Votrax (SSI) is to be supported }
break; case IFR -> {
case IFR:
if ((value & 64) != 0) { if ((value & 64) != 0) {
timer1IRQ = false; timer1IRQ = false;
} }
if ((value & 32) != 0) { if ((value & 32) != 0) {
timer2IRQ = false; timer2IRQ = false;
} }
break; }
case IER: case IER -> {
boolean enable = (value & 128) != 0; boolean enable = (value & 128) != 0;
if ((value & 64) != 0) { if ((value & 64) != 0) {
timer1interruptEnabled = enable; timer1interruptEnabled = enable;
@ -276,9 +251,12 @@ public abstract class R6522 extends Device {
if ((value & 32) != 0) { if ((value & 32) != 0) {
timer2interruptEnabled = enable; timer2interruptEnabled = enable;
} }
break; }
default: default -> {
}
} }
// SHIFT REGISTER NOT IMPLEMENTED
// TODO: Implement if Votrax (SSI) is to be supported
if (timer1running || timer2running) { if (timer1running || timer2running) {
if (debug) System.out.println("One or more timers active, resuming"); if (debug) System.out.println("One or more timers active, resuming");
resume(); resume();

View File

@ -4,7 +4,6 @@ import jace.LawlessLegends;
import jace.apple2e.SoftSwitches; import jace.apple2e.SoftSwitches;
import jace.apple2e.softswitch.VideoSoftSwitch; import jace.apple2e.softswitch.VideoSoftSwitch;
import jace.core.Device; import jace.core.Device;
import jace.core.Motherboard;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -65,7 +64,6 @@ public class FPSMonitorDevice extends Device {
} }
void updateIcon() { void updateIcon() {
cpuPerClock = Math.max(Motherboard.cpuPerClock, 1);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long ellapsed = now - lastUpdate; long ellapsed = now - lastUpdate;
if (ellapsed < UPDATE_INTERVAL) { if (ellapsed < UPDATE_INTERVAL) {

View File

@ -28,19 +28,20 @@ public class LawlessComputer extends Apple2e {
boolean performedBootAnimation = false; boolean performedBootAnimation = false;
LawlessImageTool gameDiskHandler = new LawlessImageTool(); LawlessImageTool gameDiskHandler = new LawlessImageTool();
@ConfigurableField(name = "Boot Animation") @ConfigurableField(name = "Boot Animation")
public boolean showBootAnimation = true; public boolean showBootAnimation = PRODUCTION_MODE;
public LawlessComputer() { public LawlessComputer() {
super(); super();
} }
public void initLawlessLegendsConfiguration() { public void initLawlessLegendsConfiguration() {
this.cheatEngine.setValue(Cheats.Cheat.LawlessHacks); if (PRODUCTION_MODE) {
this.cheatEngine.setValue(Cheats.Cheat.LawlessHacks);
}
// this.activeCheatEngine = new LawlessHacks(this); // this.activeCheatEngine = new LawlessHacks(this);
// this.activeCheatEngine.attach(); // this.activeCheatEngine.attach();
blankTextPage1(); blankTextPage1();
reconfigure(); reconfigure();
Configuration.registerKeyHandlers();
} }
private void blankTextPage1() { private void blankTextPage1() {
@ -60,7 +61,7 @@ public class LawlessComputer extends Apple2e {
s.getSwitch().reset(); s.getSwitch().reset();
} }
}); });
if (showBootAnimation && PRODUCTION_MODE) { if (showBootAnimation) {
(new Thread(this::startAnimation)).start(); (new Thread(this::startAnimation)).start();
} else { } else {
getCpu().setPaused(false); getCpu().setPaused(false);
@ -187,7 +188,7 @@ public class LawlessComputer extends Apple2e {
public void finishColdStart() { public void finishColdStart() {
try { try {
waitForVBL(); waitForVBL();
reboot(); warmStart();
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
Logger.getLogger(LawlessComputer.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(LawlessComputer.class.getName()).log(Level.SEVERE, null, ex);
} }

View File

@ -5,8 +5,12 @@ import org.junit.BeforeClass;
import javafx.application.Platform; import javafx.application.Platform;
public abstract class AbstractFXTest { public abstract class AbstractFXTest {
public static boolean fxInitialized = false;
@BeforeClass @BeforeClass
public static void initJfxRuntime() { public static void initJfxRuntime() {
Platform.startup(() -> {}); if (!fxInitialized) {
fxInitialized = true;
Platform.startup(() -> {});
}
} }
} }

View File

@ -8,9 +8,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import jace.apple2e.Full65C02Test;
import jace.apple2e.MOS65C02; import jace.apple2e.MOS65C02;
import jace.core.Computer; import jace.core.Computer;
import jace.cpu.Full65C02Test;
public class TestProgram { public class TestProgram {
// Tests could be run in any order so it is really important that all registers/flags are preserved! // Tests could be run in any order so it is really important that all registers/flags are preserved!

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package jace.cpu; package jace.apple2e;
import static jace.TestUtils.initComputer; import static jace.TestUtils.initComputer;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package jace.cpu; package jace.apple2e;
import static jace.TestProgram.Flag.CARRY_CLEAR; import static jace.TestProgram.Flag.CARRY_CLEAR;
import static jace.TestProgram.Flag.CARRY_SET; import static jace.TestProgram.Flag.CARRY_SET;
@ -43,8 +43,6 @@ import jace.Emulator;
import jace.ProgramException; import jace.ProgramException;
import jace.TestProgram; import jace.TestProgram;
import jace.TestUtils; import jace.TestUtils;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.core.Computer; import jace.core.Computer;
import jace.core.RAMEvent.TYPE; import jace.core.RAMEvent.TYPE;
import jace.core.SoundMixer; import jace.core.SoundMixer;

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,38 @@
package jace.applesoft;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
public class ApplesoftTest {
@Test
public void fromStringTest() {
String programSource = "10 PRINT \"Hello, World!\"\n20 PRINT \"Goodbye!\"";
ApplesoftProgram program = ApplesoftProgram.fromString(programSource);
assertNotNull(program);
assertEquals(2, program.lines.size());
Line line1 = program.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);
}
@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);
}
}

View File

@ -2,9 +2,12 @@ package jace.core;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.Random;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import jace.AbstractFXTest; import jace.AbstractFXTest;
import jace.core.SoundMixer.SoundBuffer; import jace.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError; import jace.core.SoundMixer.SoundError;
@ -12,6 +15,18 @@ import jace.lawless.LawlessHacks;
import jace.lawless.Media; import jace.lawless.Media;
public class SoundTest extends AbstractFXTest { 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 // @Test
public void musicDecodeTest() { public void musicDecodeTest() {
// For every song in the music folder, decode it and print out the duration // For every song in the music folder, decode it and print out the duration
@ -39,19 +54,19 @@ public class SoundTest extends AbstractFXTest {
// } // }
} }
// @Test @Test
//(Only use this to ensure the sound engine produces audible output, it's otherwise annoying to hear all the time) //(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 { public void soundGenerationTest() throws SoundError {
try { try {
System.out.println("Performing sound test..."); System.out.println("Performing sound test...");
System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer(); SoundMixer mixer = new SoundMixer();
System.out.println("Attach mixer"); System.out.println("Attach mixer");
mixer.attach(); mixer.attach();
System.out.println("Allocate buffer"); System.out.println("Allocate buffer");
SoundBuffer buffer = SoundMixer.createBuffer(false); SoundBuffer buffer = SoundMixer.createBuffer(false);
System.out.println("Generate sound"); System.out.println("Generate sound");
for (int i = 0; i < 100000; i++) { // 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 // 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); double x = Math.sin(i*i * 0.0001);
buffer.playSample((short) (Short.MAX_VALUE * x)); buffer.playSample((short) (Short.MAX_VALUE * x));
@ -65,7 +80,7 @@ public class SoundTest extends AbstractFXTest {
} }
} }
// @Test @Test
// Commented out because it's annoying to hear all the time, but it worked without issues // Commented out because it's annoying to hear all the time, but it worked without issues
public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException { public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException {
System.out.println("Performing speaker tick test..."); System.out.println("Performing speaker tick test...");
@ -75,8 +90,9 @@ public class SoundTest extends AbstractFXTest {
System.out.println("Attach mixer"); System.out.println("Attach mixer");
mixer.attach(); mixer.attach();
// We want to create and destroy lots of buffers to make sure we don't have any memory leaks // 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 < 10000; i++) {
// Print status every 1000 iterations for (int i = 0; i < 1000; i++) {
// Print status every 1000 iterations
if (i % 1000 == 0) { if (i % 1000 == 0) {
System.out.println("Iteration %d".formatted(i)); System.out.println("Iteration %d".formatted(i));
} }
@ -100,14 +116,13 @@ public class SoundTest extends AbstractFXTest {
* Runs through 500 iterations of playing a random song for 1 second and switching songs * Runs through 500 iterations of playing a random song for 1 second and switching songs
*/ */
public void musicPlaybackTortureTest() throws InterruptedException { public void musicPlaybackTortureTest() throws InterruptedException {
SoundMixer.initSound();
System.out.println("Create mixer"); System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer(); SoundMixer mixer = new SoundMixer();
System.out.println("Attach mixer"); System.out.println("Attach mixer");
mixer.attach(); mixer.attach();
LawlessHacks lawlessHacks = new LawlessHacks(); LawlessHacks lawlessHacks = new LawlessHacks();
int track = 0; int track = 0;
Random rnd = new Random(); // Random rnd = new Random();
for (int i=0; i < 500; i++) { for (int i=0; i < 500; i++) {
System.out.println(">>>>>>>>> Cycle " + i); System.out.println(">>>>>>>>> Cycle " + i);
// Get a random song // Get a random song

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 jace.AbstractFXTest;
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 static jace.hardware.FloppyDisk.PRODOS_SECTOR_ORDER;
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, false);
// 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);
}
}
}