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

View File

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

View File

@ -29,7 +29,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
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(
category = "General",
name = "Show Drives"
@ -564,8 +557,5 @@ public class EmulatorUILogic implements Reconfigurable {
@Override
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 jace.core.Card;
import jace.core.Motherboard;
import jace.core.Utility;
import jace.core.Video;
import jace.lawless.LawlessComputer;
@ -97,6 +96,9 @@ public class JaceUIController {
private final BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false);
public static final double MIN_SPEED = 0.5;
public static final double MAX_SPEED = 5.0;
@FXML
void initialize() {
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;
} else if (setting == 1.0) {
return 1.0;
} else if (setting >= 10) {
} else if (setting >= 5) {
return Double.MAX_VALUE;
} else {
double val = Math.pow(2.0, (setting - 1.0) / 1.5);
val = Math.floor(val * 2.0) / 2.0;
if (val > 2.0) {
val = Math.floor(val);
}
return val;
return setting;
}
}
@ -221,14 +218,16 @@ public class JaceUIController {
primaryStage = ps;
connectButtons(controlOverlay);
speedSlider.setMinorTickCount(0);
speedSlider.setMinorTickCount(3);
speedSlider.setMajorTickUnit(1);
speedSlider.setMax(MAX_SPEED);
speedSlider.setMin(MIN_SPEED);
speedSlider.setLabelFormatter(new StringConverter<Double>() {
@Override
public String toString(Double val) {
if (val < 1.0) {
if (val <= MIN_SPEED) {
return "Half";
} else if (val >= 10.0) {
} else if (val >= MAX_SPEED) {
return "";
}
double v = convertSpeedToRatio(val);
@ -244,11 +243,10 @@ public class JaceUIController {
return 1.0;
}
});
speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
Platform.runLater(() -> {
speedSlider.setValue(Emulator.getUILogic().speedSetting);
// Kind of redundant but make sure speed is properly set as if the user did it
setSpeed(Emulator.getUILogic().speedSetting);
double currentSpeed = (double) Emulator.withComputer(c->c.getMotherboard().getSpeedRatio(), 100) / 100.0;
speedSlider.valueProperty().set(currentSpeed);
speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
});
musicSelection.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
Emulator.withComputer(computer ->
@ -268,29 +266,20 @@ public class JaceUIController {
}
public void setSpeed(double speed) {
Emulator.getUILogic().speedSetting = (int) speed;
double speedRatio = convertSpeedToRatio(speed);
double newSpeed = Math.max(speed, MIN_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 -> {
c.getMotherboard().setSpeedInPercentage(20000);
c.getMotherboard().setMaxSpeed(true);
});
// Motherboard.cpuPerClock = 10;
} else {
if (speedRatio > 1000) {
Motherboard.cpuPerClock = 2;
} else {
Motherboard.cpuPerClock = 1;
}
Emulator.withComputer(c -> {
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() {

View File

@ -141,7 +141,7 @@ public class LawlessLegends extends Application {
romStarted.set(true);
});
romStarted.set(false);
c.invokeColdStart();
c.coldStart();
try {
Thread.sleep(watchdogDelay);
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() {
// Reset the emulator memory and reconfigure
// Reset the emulator memory and restart
Emulator.withComputer(c -> {
c.pause();
c.getMemory().resetState();
c.reconfigure();
c.resume();
c.warmStart();
});
}

View File

@ -61,8 +61,10 @@ import jace.state.Stateful;
*/
@Stateful
public class Apple2e extends Computer {
static int IRQ_VECTOR = 0x003F2;
@ConfigurableField(name = "Production mode", shortName = "production")
public boolean PRODUCTION_MODE = false;
@ConfigurableField(name = "Slot 1", shortName = "s1card")
public DeviceSelection<Cards> card1 = new DeviceSelection<>(Cards.class, null);
@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)
public boolean clockEnabled = true;
@ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true)
public boolean acceleratorEnabled = true;
@ConfigurableField(name = "Production mode", shortName = "production")
public boolean PRODUCTION_MODE = true;
public boolean acceleratorEnabled = PRODUCTION_MODE;
public Joystick joystick1;
public Joystick joystick2;
@ -105,7 +105,7 @@ public class Apple2e extends Computer {
public ZipWarpAccelerator accelerator;
FPSMonitorDevice fpsCounters;
@ConfigurableField(name = "Show speed monitors", shortName = "showFps")
public boolean showSpeedMonitors = false;
public boolean showSpeedMonitors = !PRODUCTION_MODE;
/**
* Creates a new instance of Apple2e
@ -130,46 +130,42 @@ public class Apple2e extends Computer {
@Override
public void coldStart() {
getMotherboard().whileSuspended(()->{
System.err.println("Cold starting computer: RESETTING SOFT SWITCHES");
for (SoftSwitches s : SoftSwitches.values()) {
RAM128k r = (RAM128k) getMemory();
System.err.println("Cold starting computer: RESETTING SOFT SWITCHES");
r.resetState();
for (SoftSwitches s : SoftSwitches.values()) {
if ((s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
reconfigure();
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
});
reboot();
}
public void reboot() {
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);
}
// This isn't really authentic behavior but sometimes games like memory to have a consistent state when booting.
r.zeroAllRam();
// Sather 4-15:
// 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;
byte garbage = (byte) (Math.random() * 256.0);
for (int page=1; page < 0xc0; page++) {
r.write(page << 8 + offset, garbage, false, true);
r.write(page << 8 + 1 + offset, garbage, false, true);
}
warmStart();
}
@Override
public void warmStart() {
getMotherboard().whileSuspended(()->{
// This isn't really authentic behavior but sometimes games like memory to have a consistent state when booting.
for (SoftSwitches s : SoftSwitches.values()) {
if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
for (SoftSwitches s : SoftSwitches.values()) {
if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
((RAM128k)getMemory()).zeroAllRam();
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
getCpu().reset();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
});
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
getCpu().reset();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
motherboard.disableTempMaxSpeed();
resume();
}
@ -376,15 +372,9 @@ public class Apple2e extends Computer {
if (getMotherboard() == null) {
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<>();
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);

View File

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

View File

@ -94,7 +94,7 @@ public class Speaker extends Device {
* Playback volume (should be < 1423)
*/
@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
*/
@ -118,6 +118,7 @@ public class Speaker extends Device {
public boolean suspend() {
boolean result = super.suspend();
speakerBit = false;
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
if (buffer != null) {
try {
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_FLOOR = Math.floor(TICKS_PER_SAMPLE);
setRun(true);
super.resume();
}
/**
@ -195,6 +195,7 @@ public class Speaker extends Device {
counter += 1.0d;
if (counter >= TICKS_PER_SAMPLE) {
playSample(level * VOLUME);
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
// Set level back to 0
level = 0;
@ -280,13 +281,11 @@ public class Speaker extends Device {
@Override
public void attach() {
configureListener();
resume();
}
@Override
public void detach() {
removeListener();
suspend();
super.detach();
}
}

View File

@ -16,15 +16,9 @@
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.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import jace.Emulator;
@ -53,22 +47,7 @@ public class ApplesoftProgram {
public static final int RUNNING_FLAG = 0x076;
public static final int NOT_RUNNING = 0x0FF;
public static final int GOTO_CMD = 0x0D944; //actually starts at D93E
int startingAddress = 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 final int START_ADDRESS = 0x0801;
public static Byte[] toObjects(byte[] bytesPrim) {
Byte[] bytes = new Byte[bytesPrim.length];
@ -91,7 +70,7 @@ public class ApplesoftProgram {
}
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) {

View File

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

View File

@ -198,18 +198,10 @@ public class InvokableActionRegistryImpl extends InvokableActionRegistry {
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) -> {
try {
((jace.core.Computer) o).invokeColdStart();
} 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();
((jace.core.Computer) o).invokeReset();
} catch (Exception 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.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.LawlessLegends;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.config.Configuration;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
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(
name = "Cold boot",
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",
name = "Reset",
description = "Process user-initatiated reboot (ctrl+apple+reset)",
category = "general",
alternatives = "reboot;reset;three-finger-salute;restart",
defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
public void invokeWarmStart() {
warmStart();
public void invokeReset() {
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 Keyboard getKeyboard() {
@ -225,5 +228,6 @@ public abstract class Computer implements Reconfigurable {
stateManager = null;
StateManager.getInstance(this).invalidate();
}
Configuration.registerKeyHandlers();
}
}

View File

@ -222,7 +222,8 @@ public abstract class Device implements Reconfigurable {
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()) {
setRun(false);
return true;
@ -230,10 +231,17 @@ public abstract class Device implements Reconfigurable {
return false;
}
public void resumeAll() {
resume();
children.forEach(Device::resumeAll);
}
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()) {
setRun(true);
setRun(true);
waitCycles = 0;
}
}

View File

@ -37,9 +37,6 @@ public abstract class IndependentTimedDevice extends TimedDevice {
// The actual worker that the device runs as
public Thread worker;
public boolean hasStopped = true;
// From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles
@Override
/* 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() {
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() {
boolean result = super.suspend();
Thread w = worker;
worker = null;
if (w != null && w.isAlive()) {
try {
w.interrupt();
w.join(1000);
w.join(100);
} catch (InterruptedException ex) {
}
}
worker = null;
return result;
}
@ -112,9 +109,9 @@ public abstract class IndependentTimedDevice extends TimedDevice {
}
@Override
public void resume() {
public synchronized void resume() {
super.resume();
if (worker != null && worker.isAlive()) {
if (worker != null && worker.isAlive()) {
return;
}
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)
public void openApple(boolean pressed) {
// boolean isRunning = computer.pause();
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)
public void solidApple(boolean pressed) {
// boolean isRunning = computer.pause();
SoftSwitches.PB1.getSwitch().setState(pressed);
// if (isRunning) {
// computer.resume();
// }
}
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")
public static boolean enableSpeaker = true;
private Speaker speaker;
public Speaker speaker;
void vblankEnd() {
SoftSwitches.VBL.getSwitch().setState(true);
@ -74,9 +74,6 @@ public class Motherboard extends IndependentTimedDevice {
public String getShortName() {
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;
public CPU getCpu() {
@ -88,45 +85,32 @@ public class Motherboard extends IndependentTimedDevice {
@Override
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
public synchronized void reconfigure() {
_cpu = null;
whileSuspended(() -> {
accelorationRequestors.clear();
disableTempMaxSpeed();
super.reconfigure();
accelorationRequestors.clear();
disableTempMaxSpeed();
super.reconfigure();
// Now create devices as needed, e.g. sound
// Now create devices as needed, e.g. sound
if (enableSpeaker) {
try {
if (speaker == null) {
speaker = new Speaker();
speaker.attach();
}
speaker.reconfigure();
addChildDevice(speaker);
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
t.printStackTrace();
if (enableSpeaker) {
try {
if (speaker == null) {
speaker = new Speaker();
speaker.attach();
}
} else {
System.out.println("Speaker not enabled, leaving it off.");
speaker.reconfigure();
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<>();
@ -136,8 +120,7 @@ public class Motherboard extends IndependentTimedDevice {
}
public void cancelSpeedRequest(Object requester) {
accelorationRequestors.remove(requester);
if (accelorationRequestors.isEmpty()) {
if (accelorationRequestors.remove(requester) && accelorationRequestors.isEmpty()) {
disableTempMaxSpeed();
}
}

View File

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

View File

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

View File

@ -149,6 +149,10 @@ public class Utility {
return Optional.empty();
}
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));
}
@ -278,45 +282,6 @@ public class Utility {
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) {
gripe(message, false, null);
}

View File

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

View File

@ -73,7 +73,7 @@ public class CardAppleMouse extends Card {
@Stateful
public int statusByte;
@Stateful
public Point2D lastMouseLocation;
public Point2D lastMouseLocation = new Point2D(0, 0);
@Stateful
public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff);
// 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) {
if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
Node source = (Node) event.getSource();
updateLocation(event.getSceneX(), event.getSceneY(), source.getBoundsInLocal());
double x = 0.0;
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();
}
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) {
double scaledX = x / bounds.getWidth();
double scaledY = y / bounds.getHeight();
lastMouseLocation = new Point2D(scaledX, scaledY);
private void updateLocation(double x, double y) {
lastMouseLocation = new Point2D(x, y);
movedSinceLastTick = true;
movedSinceLastRead = true;
}
@ -210,6 +216,7 @@ public class CardAppleMouse extends Card {
case 0x08:
// Pascal signature byte
e.setNewValue(0x001);
break;
case 0x011:
e.setNewValue(0x000);
break;
@ -376,7 +383,9 @@ public class CardAppleMouse extends Card {
* Screen holes are updated
*/
private void initMouse() {
mouseActive.setText("Active");
if (mouseActive != null) {
mouseActive.setText("Active");
}
EmulatorUILogic.addIndicator(this, mouseActive, 2000);
setClampWindowX(0, 0x3ff);
setClampWindowY(0, 0x3ff);

View File

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

View File

@ -45,6 +45,9 @@ import jace.hardware.mockingboard.R6522;
public class CardMockingboard extends Card {
// 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",
category = "Sound",
description = "Mockingboard volume, 100=max, 0=silent")
@ -68,7 +71,6 @@ public class CardMockingboard extends Card {
double ticksBetweenPlayback = 24.0;
int MAX_IDLE_TICKS = 1000000;
boolean activatedAfterReset = false;
boolean debug = false;
@Override
public String getDeviceName() {
@ -127,6 +129,11 @@ public class CardMockingboard extends Card {
@Override
public void reset() {
activatedAfterReset = false;
if (chips != null) {
for (PSG p : chips) {
p.reset();
}
}
suspend();
}
RAMListener mainListener = null;
@ -145,7 +152,9 @@ public class CardMockingboard extends Card {
chip++;
}
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()));
return;
}
@ -153,24 +162,27 @@ public class CardMockingboard extends Card {
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
e.setNewValue(val);
if (debug) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
if (DEBUG) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
if (debug) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
if (DEBUG) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
}
// Any firmware access will reset the idle counter and wake up the card, this allows the timers to start running again
// Games such as "Skyfox" use the timer to detect if the card is present.
idleTicks = 0;
if (!isRunning() || isPaused()) {
activatedAfterReset = true;
resume();
// ResumeAll is important so that the 6522's can start their timers
resumeAll();
}
}
@Override
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.
// 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()));
}
double ticksSinceLastPlayback = 0;
@ -200,20 +212,16 @@ public class CardMockingboard extends Card {
@Override
public void reconfigure() {
boolean isActive = isRunning();
initPSG();
for (PSG chip : chips) {
chip.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
chip.reset();
if (DEBUG) {
System.out.println("Reconfiguring Mockingboard");
}
ticksBetweenPlayback = (double) CLOCK_SPEED / (double) SoundMixer.RATE;
buildMixerTable();
initPSG();
super.reconfigure();
if (isActive) {
resume();
if (DEBUG) {
System.out.println("Reconfiguring Mockingboard completed");
}
super.reconfigure();
}
///////////////////////////////////////////////////////////
@ -222,6 +230,10 @@ public class CardMockingboard extends Card {
AtomicInteger left = new AtomicInteger(0);
AtomicInteger right = new AtomicInteger(0);
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[1].update(right, true, right, false, right, false);
if (phasorMode) {
@ -262,11 +274,22 @@ public class CardMockingboard extends Card {
@Override
public void resume() {
if (DEBUG) {
System.out.println("Resuming Mockingboard");
Thread.dumpStack();
}
if (!activatedAfterReset) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: not activated after reset, not resuming");
}
// Do not re-activate until firmware access was made
return;
}
initPSG();
if (buffer == null || !buffer.isAlive()) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: creating sound buffer");
}
try {
buffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) {
@ -275,21 +298,18 @@ public class CardMockingboard extends Card {
suspend();
}
}
if (chips == null) {
initPSG();
for (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
psg.reset();
}
}
idleTicks = 0;
setPaused(false);
reconfigure();
super.resume();
System.out.println("Resuming Mockingboard: resume completed");
}
@Override
public boolean suspend() {
if (DEBUG) {
System.out.println("Suspending Mockingboard");
Thread.dumpStack();
}
if (buffer != null) {
try {
buffer.shutdown();
@ -300,21 +320,28 @@ public class CardMockingboard extends Card {
buffer = null;
}
}
for (R6522 c : controllers) {
c.suspend();
}
return super.suspend();
}
private void initPSG() {
if (phasorMode) {
if (phasorMode && (chips == null || chips.length < 4)) {
chips = new PSG[4];
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[2] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY3", 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[0] = new PSG(0, CLOCK_SPEED, SoundMixer.RATE, "AY1", 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

View File

@ -54,8 +54,6 @@ public class CardSSC extends Card {
protected Thread listenThread;
private int lastInputByte = 0;
private boolean FULL_ECHO = true;
private final boolean RECV_ACTIVE = true;
private boolean TRANS_ACTIVE = true;
// private boolean RECV_STRIP_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)")
@ -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")
public boolean TRANS_ADD_LF = false;
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 1 = !SW1-5
//Bit 4 = !SW1-4
@ -75,7 +73,7 @@ public class CardSSC extends Card {
// 19200 baud (SW1-1,2,3,4 off)
// Communications mode (SW1-5,6 on)
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
//SW2-6 = Allow interrupts (disable in ][, ][+)
//Bit 1 = !SW2-5 -- Generate LF after CR
@ -87,10 +85,10 @@ public class CardSSC extends Card {
// 8 data bits (SW2-2 on)
// No parity (SW2-3 don't care, SW2-4 off)
private final int SW2_SETTING = 0x04;
public int ACIA_Data = 0x08; // Read=Receive / Write=transmit
public int ACIA_Status = 0x09; // Read=Status / Write=Reset
public int ACIA_Command = 0x0A;
public int ACIA_Control = 0x0B;
public static int ACIA_Data = 0x08; // Read=Receive / Write=transmit
public static int ACIA_Status = 0x09; // Read=Status / Write=Reset
public static int ACIA_Command = 0x0A;
public static int ACIA_Control = 0x0B;
public boolean PORT_CONNECTED = false;
public boolean RECV_IRQ_ENABLED = false;
public boolean TRANS_IRQ_ENABLED = false;
@ -296,19 +294,15 @@ public class CardSSC extends Card {
switch ((value >> 2) & 3) {
case 0:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = false;
break;
case 1:
TRANS_IRQ_ENABLED = true;
TRANS_ACTIVE = true;
break;
case 2:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break;
case 3:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break;
}
// 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 {
PORT_CONNECTED = b;
if (b == false) {
reset();
}
}
// CTS isn't used here -- it's assumed that we're always clear-to-send
// private void setCTS(boolean b) throws InterruptedException {
// PORT_CONNECTED = b;
// if (b == false) {
// reset();
// }
// }
private boolean getCTS() throws InterruptedException {
return PORT_CONNECTED;
}
// private boolean getCTS() throws InterruptedException {
// return PORT_CONNECTED;
// }
private void triggerIRQ() {
IRQ_TRIGGERED = true;

View File

@ -7,13 +7,14 @@ import jace.core.Card;
import jace.hardware.massStorage.CardMassStorage;
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),
DiskIIDrive("Disk II Floppy Controller", CardDiskII.class, CardDiskII::new),
HayesMicroModem("Hayes MicroModem", CardHayesMicromodem.class, CardHayesMicromodem::new),
MassStorage("Mass Storage", CardMassStorage.class, CardMassStorage::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),
SuperSerialCard("Super Serial Card", CardSSC.class, CardSSC::new),
Thunderclock("Thunderclock", CardThunderclock.class, CardThunderclock::new);
Supplier<Card> factory;

View File

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

View File

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

View File

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

View File

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

View File

@ -28,19 +28,20 @@ public class LawlessComputer extends Apple2e {
boolean performedBootAnimation = false;
LawlessImageTool gameDiskHandler = new LawlessImageTool();
@ConfigurableField(name = "Boot Animation")
public boolean showBootAnimation = true;
public boolean showBootAnimation = PRODUCTION_MODE;
public LawlessComputer() {
super();
}
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.attach();
blankTextPage1();
reconfigure();
Configuration.registerKeyHandlers();
}
private void blankTextPage1() {
@ -60,7 +61,7 @@ public class LawlessComputer extends Apple2e {
s.getSwitch().reset();
}
});
if (showBootAnimation && PRODUCTION_MODE) {
if (showBootAnimation) {
(new Thread(this::startAnimation)).start();
} else {
getCpu().setPaused(false);
@ -187,7 +188,7 @@ public class LawlessComputer extends Apple2e {
public void finishColdStart() {
try {
waitForVBL();
reboot();
warmStart();
} catch (InterruptedException 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;
public abstract class AbstractFXTest {
public static boolean fxInitialized = false;
@BeforeClass
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.function.Consumer;
import jace.apple2e.Full65C02Test;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.cpu.Full65C02Test;
public class TestProgram {
// 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
* limitations under the License.
*/
package jace.cpu;
package jace.apple2e;
import static jace.TestUtils.initComputer;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jace.cpu;
package jace.apple2e;
import static jace.TestProgram.Flag.CARRY_CLEAR;
import static jace.TestProgram.Flag.CARRY_SET;
@ -43,8 +43,6 @@ import jace.Emulator;
import jace.ProgramException;
import jace.TestProgram;
import jace.TestUtils;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.core.Computer;
import jace.core.RAMEvent.TYPE;
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 java.util.Random;
import java.util.concurrent.ExecutionException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import jace.AbstractFXTest;
import jace.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError;
@ -12,6 +15,18 @@ import jace.lawless.LawlessHacks;
import jace.lawless.Media;
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
public void musicDecodeTest() {
// 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)
public void soundGenerationTest() throws SoundError {
try {
System.out.println("Performing sound test...");
System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer();
System.out.println("Attach mixer");
mixer.attach();
System.out.println("Allocate buffer");
SoundBuffer buffer = SoundMixer.createBuffer(false);
System.out.println("Generate sound");
for (int i = 0; i < 100000; i++) {
// for (int i = 0; i < 100000; i++) {
for (int i = 0; i < 100; i++) {
// Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed
double x = Math.sin(i*i * 0.0001);
buffer.playSample((short) (Short.MAX_VALUE * x));
@ -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
public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException {
System.out.println("Performing speaker tick test...");
@ -75,8 +90,9 @@ public class SoundTest extends AbstractFXTest {
System.out.println("Attach mixer");
mixer.attach();
// We want to create and destroy lots of buffers to make sure we don't have any memory leaks
for (int i = 0; i < 10000; i++) {
// Print status every 1000 iterations
// for (int i = 0; i < 10000; i++) {
for (int i = 0; i < 1000; i++) {
// Print status every 1000 iterations
if (i % 1000 == 0) {
System.out.println("Iteration %d".formatted(i));
}
@ -100,14 +116,13 @@ public class SoundTest extends AbstractFXTest {
* Runs through 500 iterations of playing a random song for 1 second and switching songs
*/
public void musicPlaybackTortureTest() throws InterruptedException {
SoundMixer.initSound();
System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer();
System.out.println("Attach mixer");
mixer.attach();
LawlessHacks lawlessHacks = new LawlessHacks();
int track = 0;
Random rnd = new Random();
// Random rnd = new Random();
for (int i=0; i < 500; i++) {
System.out.println(">>>>>>>>> Cycle " + i);
// 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);
}
}
}