Completely rewritten sound engine, fixed listener registration issues, Configuration global actions fixed.

This commit is contained in:
Brendan Robert 2023-10-23 00:28:55 -05:00
parent cd5903367b
commit b5997c3876
41 changed files with 706 additions and 315 deletions

View File

@ -0,0 +1,55 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickJavaProcess}"
},
{
"type": "java",
"name": "Current File",
"request": "launch",
"mainClass": "${file}"
},
{
"type": "java",
"name": "ConvertDiskImage",
"request": "launch",
"mainClass": "lawlesslegends/jace.ConvertDiskImage",
"projectName": "lawlesslegends"
},
{
"type": "java",
"name": "LawlessLegends",
"request": "launch",
"mainClass": "lawlesslegends/jace.LawlessLegends",
"projectName": "lawlesslegends"
},
{
"type": "java",
"name": "ApplesoftProgram",
"request": "launch",
"mainClass": "lawlesslegends/jace.applesoft.ApplesoftProgram",
"projectName": "lawlesslegends"
},
{
"type": "java",
"name": "AcmeCrossAssembler",
"request": "launch",
"mainClass": "lawlesslegends/jace.assembly.AcmeCrossAssembler",
"projectName": "lawlesslegends"
},
{
"type": "java",
"name": "Library",
"request": "launch",
"mainClass": "lawlesslegends/jace.ui.Library",
"projectName": "lawlesslegends"
}
]
}

View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.8bitbunch</groupId> <groupId>org.8bitbunch</groupId>
@ -13,6 +15,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>jace.LawlessLegends</mainClass> <mainClass>jace.LawlessLegends</mainClass>
<netbeans.hint.license>apache20</netbeans.hint.license> <netbeans.hint.license>apache20</netbeans.hint.license>
<lwjgl.version>3.3.3</lwjgl.version>
</properties> </properties>
<organization> <organization>
@ -43,7 +46,7 @@
<plugin> <plugin>
<groupId>com.gluonhq</groupId> <groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId> <artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.19</version> <version>1.0.21</version>
<configuration> <configuration>
<mainClass>jace.LawlessLegends</mainClass> <mainClass>jace.LawlessLegends</mainClass>
</configuration> </configuration>
@ -55,44 +58,44 @@
<configuration> <configuration>
<mainClass>lawlesslegends/jace.LawlessLegends</mainClass> <mainClass>lawlesslegends/jace.LawlessLegends</mainClass>
<executions> <executions>
<execution> <execution>
<!-- Default configuration for running --> <!-- Default configuration for running -->
<!-- Usage: mvn clean javafx:run --> <!-- Usage: mvn clean javafx:run -->
<id>default-cli</id> <id>default-cli</id>
</execution> </execution>
<execution> <execution>
<!-- Configuration for manual attach debugging --> <!-- Configuration for manual attach debugging -->
<!-- Usage: mvn clean javafx:run@debug --> <!-- Usage: mvn clean javafx:run@debug -->
<id>debug</id> <id>debug</id>
<configuration> <configuration>
<options> <options>
<option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:8000</option> <option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:8000</option>
</options> </options>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
<!-- Configuration for automatic IDE debugging --> <!-- Configuration for automatic IDE debugging -->
<id>ide-debug</id> <id>ide-debug</id>
<configuration> <configuration>
<options> <options>
<option>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address}</option> <option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:8000</option>
</options> </options>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
<!-- Configuration for automatic IDE profiling --> <!-- Configuration for automatic IDE profiling -->
<id>ide-profile</id> <id>ide-profile</id>
<configuration> <configuration>
<options> <options>
<option>${profiler.jvmargs.arg1}</option> <option>${profiler.jvmargs.arg1}</option>
<option>${profiler.jvmargs.arg2}</option> <option>${profiler.jvmargs.arg2}</option>
<option>${profiler.jvmargs.arg3}</option> <option>${profiler.jvmargs.arg3}</option>
<option>${profiler.jvmargs.arg4}</option> <option>${profiler.jvmargs.arg4}</option>
<option>${profiler.jvmargs.arg5}</option> <option>${profiler.jvmargs.arg5}</option>
</options> </options>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@ -116,8 +119,8 @@
</artifact> </artifact>
<moduleInfoSource> <moduleInfoSource>
module nestedvm { module nestedvm {
exports org.ibex.nestedvm; exports org.ibex.nestedvm;
exports org.ibex.nestedvm.util; exports org.ibex.nestedvm.util;
} }
</moduleInfoSource> </moduleInfoSource>
</module> </module>
@ -130,13 +133,25 @@
</plugins> </plugins>
</build> </build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-bom</artifactId>
<version>${lwjgl.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>4.13.2</version> <version>4.13.2</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.xerial.thirdparty</groupId> <groupId>org.xerial.thirdparty</groupId>
<artifactId>nestedvm</artifactId> <artifactId>nestedvm</artifactId>
@ -175,18 +190,69 @@
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId> <artifactId>javafx-swing</artifactId>
<version>18</version> <version>20</version>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.lwjgl</groupId> <groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId> <artifactId>lwjgl</artifactId>
<version>3.3.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>javazoom</groupId> <groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.jlayer</groupId>
<artifactId>jlayer</artifactId> <artifactId>jlayer</artifactId>
<version>1.0.1</version> <version>1.0.2-gdx</version>
</dependency> </dependency>
</dependencies> </dependencies>
</project> <profiles>
<profile>
<id>lwjgl-natives-linux-amd64</id>
<activation>
<os>
<family>unix</family>
<arch>amd64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-linux</lwjgl.natives>
</properties>
</profile>
<profile>
<id>lwjgl-natives-macos-x86_64</id>
<activation>
<os>
<family>mac</family>
<arch>x86_64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-macos</lwjgl.natives>
</properties>
</profile>
<profile>
<id>lwjgl-natives-windows-amd64</id>
<activation>
<os>
<family>windows</family>
<arch>amd64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-windows</lwjgl.natives>
</properties>
</profile>
</profiles>
</project>

View File

@ -35,7 +35,7 @@ import jace.lawless.LawlessComputer;
public class Emulator { public class Emulator {
public static Emulator instance; public static Emulator instance;
public static EmulatorUILogic logic = new EmulatorUILogic(); private static EmulatorUILogic logic;
public static Thread mainThread; public static Thread mainThread;
// public static void main(String... args) { // public static void main(String... args) {
@ -44,6 +44,13 @@ public class Emulator {
// } // }
private final LawlessComputer computer; private final LawlessComputer computer;
public static EmulatorUILogic getUILogic() {
if (logic == null) {
logic = new EmulatorUILogic();
}
return logic;
}
public static Emulator getInstance(List<String> args) { public static Emulator getInstance(List<String> args) {
Emulator i = getInstance(); Emulator i = getInstance();

View File

@ -18,6 +18,8 @@
*/ */
package jace; package jace;
import static jace.core.Utility.gripe;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -44,7 +46,6 @@ import jace.config.Reconfigurable;
import jace.core.Debugger; import jace.core.Debugger;
import jace.core.RAM; import jace.core.RAM;
import jace.core.RAMListener; import jace.core.RAMListener;
import static jace.core.Utility.gripe;
import jace.ide.IdeController; import jace.ide.IdeController;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@ -90,7 +91,7 @@ public class EmulatorUILogic implements Reconfigurable {
category = "General", category = "General",
name = "Show Drives" name = "Show Drives"
) )
public boolean showDrives = false; public boolean showDrives = Emulator.withComputer(c->!c.PRODUCTION_MODE, false);
public static void updateCPURegisters(MOS65C02 cpu) { public static void updateCPURegisters(MOS65C02 cpu) {
// DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel(); // DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();

View File

@ -246,9 +246,9 @@ public class JaceUIController {
}); });
speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue())); speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
Platform.runLater(() -> { Platform.runLater(() -> {
speedSlider.setValue(Emulator.logic.speedSetting); speedSlider.setValue(Emulator.getUILogic().speedSetting);
// Kind of redundant but make sure speed is properly set as if the user did it // Kind of redundant but make sure speed is properly set as if the user did it
setSpeed(Emulator.logic.speedSetting); setSpeed(Emulator.getUILogic().speedSetting);
}); });
musicSelection.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> musicSelection.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
Emulator.withComputer(computer -> Emulator.withComputer(computer ->
@ -268,7 +268,7 @@ public class JaceUIController {
} }
public void setSpeed(double speed) { public void setSpeed(double speed) {
Emulator.logic.speedSetting = (int) speed; Emulator.getUILogic().speedSetting = (int) speed;
double speedRatio = convertSpeedToRatio(speed); double speedRatio = convertSpeedToRatio(speed);
if (speedSlider.getValue() != speed) { if (speedSlider.getValue() != speed) {
Platform.runLater(()->speedSlider.setValue(speed)); Platform.runLater(()->speedSlider.setValue(speed));
@ -404,7 +404,7 @@ public class JaceUIController {
private List<MediaConsumer> getMediaConsumers() { private List<MediaConsumer> getMediaConsumers() {
List<MediaConsumer> consumers = new ArrayList<>(); List<MediaConsumer> consumers = new ArrayList<>();
Emulator.withComputer(c -> consumers.add(((LawlessComputer) c).getUpgradeHandler())); Emulator.withComputer(c -> consumers.add(((LawlessComputer) c).getUpgradeHandler()));
if (Emulator.logic.showDrives) { if (Emulator.getUILogic().showDrives) {
Emulator.withMemory(m -> { Emulator.withMemory(m -> {
for (Optional<Card> card : m.getAllCards()) { for (Optional<Card> card : m.getAllCards()) {
card.filter(c -> c instanceof MediaConsumerParent).ifPresent(parent -> card.filter(c -> c instanceof MediaConsumerParent).ifPresent(parent ->

View File

@ -138,7 +138,7 @@ public class LawlessLegends extends Application {
new Thread(()->{ new Thread(()->{
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Booting with watchdog"); Logger.getLogger(getClass().getName()).log(Level.WARNING, "Booting with watchdog");
RAMListener startListener = c.getMemory(). RAMListener startListener = c.getMemory().
observe(RAMEvent.TYPE.EXECUTE, 0x2000, (e) -> { observe("Lawless Legends watchdog", RAMEvent.TYPE.EXECUTE, 0x2000, (e) -> {
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot was detected, watchdog terminated."); Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot was detected, watchdog terminated.");
romStarted = true; romStarted = true;
}); });

View File

@ -76,9 +76,9 @@ public class Apple2e extends Computer {
@ConfigurableField(name = "Slot 5", shortName = "s5card") @ConfigurableField(name = "Slot 5", shortName = "s5card")
public DeviceSelection<Cards> card5 = new DeviceSelection<>(Cards.class, null); public DeviceSelection<Cards> card5 = new DeviceSelection<>(Cards.class, null);
@ConfigurableField(name = "Slot 6", shortName = "s6card") @ConfigurableField(name = "Slot 6", shortName = "s6card")
public DeviceSelection<Cards> card6 = new DeviceSelection<>(Cards.class, Cards.DiskIIDrive); public DeviceSelection<Cards> card6 = new DeviceSelection<>(Cards.class, Cards.DiskIIDrive, true);
@ConfigurableField(name = "Slot 7", shortName = "s7card") @ConfigurableField(name = "Slot 7", shortName = "s7card")
public DeviceSelection<Cards> card7 = new DeviceSelection<>(Cards.class, Cards.MassStorage); public DeviceSelection<Cards> card7 = new DeviceSelection<>(Cards.class, Cards.MassStorage, true);
@ConfigurableField(name = "Debug rom", shortName = "debugRom", description = "Use debugger //e rom") @ConfigurableField(name = "Debug rom", shortName = "debugRom", description = "Use debugger //e rom")
public boolean useDebugRom = false; public boolean useDebugRom = false;
@ConfigurableField(name = "Helpful hints", shortName = "hints") @ConfigurableField(name = "Helpful hints", shortName = "hints")
@ -434,7 +434,7 @@ public class Apple2e extends Computer {
private void enableHints() { private void enableHints() {
if (hints.isEmpty()) { if (hints.isEmpty()) {
hints.add(getMemory().observe(RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{ hints.add(getMemory().observe("Helpful hints", RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{
animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS); animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS);
animationSchedule = animationSchedule =
animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS); animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS);

View File

@ -53,9 +53,9 @@ abstract public class RAM128k extends RAM {
Function<Computer, ? extends RAM128k> factory; Function<Computer, ? extends RAM128k> factory;
String name; String name;
Class clazz; Class<? extends RAM128k> clazz;
RamCards(String name, Class clazz, Function<Computer, ? extends RAM128k> factory) { RamCards(String name, Class<? extends RAM128k> clazz, Function<Computer, ? extends RAM128k> factory) {
this.factory = factory; this.factory = factory;
this.name = name; this.name = name;
this.clazz = clazz; this.clazz = clazz;
@ -409,7 +409,7 @@ abstract public class RAM128k extends RAM {
* *
*/ */
@Override @Override
public synchronized void configureActiveMemory() { public void configureActiveMemory() {
String auxZpConfiguration = getAuxZPConfiguration(); String auxZpConfiguration = getAuxZPConfiguration();
String readConfiguration = getReadConfiguration() + auxZpConfiguration; String readConfiguration = getReadConfiguration() + auxZpConfiguration;
String writeConfiguration = getWriteConfiguration() + auxZpConfiguration; String writeConfiguration = getWriteConfiguration() + auxZpConfiguration;

View File

@ -24,6 +24,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Timer; import java.util.Timer;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -37,6 +38,7 @@ import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
import jace.core.SoundGeneratorDevice; import jace.core.SoundGeneratorDevice;
import jace.core.SoundMixer; import jace.core.SoundMixer;
import jace.core.SoundMixer.SoundBuffer;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
/** /**
@ -108,23 +110,15 @@ public class Speaker extends SoundGeneratorDevice {
* Manifestation of the apple speaker softswitch * Manifestation of the apple speaker softswitch
*/ */
private boolean speakerBit = false; private boolean speakerBit = false;
//
/**
* Locking semaphore to prevent race conditions when working with buffer or
* related variables
*/
private final Object bufferLock = new Object();
/** /**
* Double-buffer used for playing processed sound -- as one is played the * Double-buffer used for playing processed sound -- as one is played the
* other fills up. * other fills up.
*/ */
private byte[] primaryBuffer;
private byte[] secondaryBuffer;
private int bufferPos = 0;
private Timer playbackTimer; private Timer playbackTimer;
private double TICKS_PER_SAMPLE = ((double) Motherboard.DEFAULT_SPEED) / SoundMixer.RATE; private double TICKS_PER_SAMPLE = ((double) Motherboard.DEFAULT_SPEED) / SoundMixer.RATE;
private double TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE); private double TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE);
private RAMListener listener = null; private RAMListener listener = null;
private SoundBuffer buffer = null;
/** /**
* Creates a new instance of Speaker * Creates a new instance of Speaker
@ -148,11 +142,14 @@ public class Speaker extends SoundGeneratorDevice {
playbackTimer = null; playbackTimer = null;
} }
speakerBit = false; speakerBit = false;
if (sdl != null && sdl.isOpen()) { if (buffer != null) {
sdl.stop(); try {
sdl.close(); buffer.shutdown();
} catch (InterruptedException | ExecutionException e) {
// Ignore
}
} }
sdl = null; buffer = null;
return result; return result;
} }
@ -162,19 +159,17 @@ public class Speaker extends SoundGeneratorDevice {
*/ */
@Override @Override
public void resume() { public void resume() {
if (sdl == null || !sdl.isOpen()) { if (buffer == null || !buffer.isAlive()) {
sdl = computer.mixer.getLine(); buffer = SoundMixer.createBuffer(false);
if (sdl != null) { }
sdl.start(); if (buffer != null) {
counter = 0; counter = 0;
idleCycles = 0; idleCycles = 0;
level = 0; level = 0;
bufferPos = 0; } else {
} else { Logger.getLogger(getClass().getName()).severe("Unable to get audio buffer for speaker!");
Logger.getLogger(getClass().getName()).severe("Unable to get audio line for speaker!"); detach();
detach(); return;
return;
}
} }
if (force1mhz) { if (force1mhz) {
@ -187,27 +182,13 @@ public class Speaker extends SoundGeneratorDevice {
setRun(true); setRun(true);
} }
public void playCurrentBuffer() {
byte[] buffer;
int len;
synchronized (bufferLock) {
len = bufferPos;
buffer = primaryBuffer;
primaryBuffer = secondaryBuffer;
bufferPos = 0;
}
secondaryBuffer = buffer;
if (sdl != null && len > 0) {
sdl.write(buffer, 0, len);
}
}
/** /**
* Reset idle counter whenever sound playback occurs * Reset idle counter whenever sound playback occurs
*/ */
public void resetIdle() { public void resetIdle() {
idleCycles = 0; idleCycles = 0;
if (!isRunning()) { if (!isRunning()) {
speakerBit = false;
resume(); resume();
} }
} }
@ -237,28 +218,32 @@ public class Speaker extends SoundGeneratorDevice {
} }
private void toggleSpeaker(RAMEvent e) { private void toggleSpeaker(RAMEvent e) {
if (e.getType() == RAMEvent.TYPE.WRITE) { // if (e.getType() == RAMEvent.TYPE.WRITE) {
level += 2; // level += 2;
} // }
speakerBit = !speakerBit; speakerBit = !speakerBit;
resetIdle(); resetIdle();
} }
private void playSample(int sample) { private void playSample(int sample) {
if (sdl == null || !sdl.isOpen()) { if (buffer == null || !buffer.isAlive()) {
resume(); Logger.getLogger(getClass().getName()).severe("Audio buffer not initalized properly!");
buffer = SoundMixer.createBuffer(false);
}
try {
buffer.playSample((short) sample);
} catch (InterruptedException | ExecutionException e) {
// TODO: Do we need to really worry about this?
e.printStackTrace();
} }
int bytes = SoundMixer.BITS >> 3;
// Prepare sound output in little endian format
for (int i = 0; i < bytes; i++) {
primaryBuffer[i] = primaryBuffer[i+bytes] = (byte) (sample & 0x0ff);
sample >>= 8;
}
sdl.write(primaryBuffer, 0, bytes*2);
if (fileOutputActive) { if (fileOutputActive) {
byte[] bytes = new byte[2];
bytes[0] = (byte) (sample & 0x0ff);
bytes[1] = (byte) ((sample >> 8) & 0x0ff);
try { try {
out.write(primaryBuffer, 0, bytes*2); out.write(bytes, 0, 2);
} catch (IOException ex) { } catch (IOException ex) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Error recording sound", ex); Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Error recording sound", ex);
toggleFileOutput(); toggleFileOutput();
@ -271,7 +256,7 @@ public class Speaker extends SoundGeneratorDevice {
* Add a memory event listener for C03x for capturing speaker events * Add a memory event listener for C03x for capturing speaker events
*/ */
private void configureListener() { private void configureListener() {
listener = computer.getMemory().observe(RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker); listener = computer.getMemory().observe("Speaker", RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker);
} }
private void removeListener() { private void removeListener() {
@ -295,14 +280,7 @@ public class Speaker extends SoundGeneratorDevice {
@Override @Override
public final void reconfigure() { public final void reconfigure() {
super.reconfigure(); super.reconfigure();
if (primaryBuffer != null && secondaryBuffer != null) {
return;
}
BUFFER_SIZE = 20000 * (SoundMixer.BITS >> 3);
primaryBuffer = new byte[BUFFER_SIZE];
secondaryBuffer = new byte[BUFFER_SIZE];
} }
@Override @Override

View File

@ -719,8 +719,8 @@ public class VideoDHGR extends Video {
} }
private void registerDirtyFlagChecks() { private void registerDirtyFlagChecks() {
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x0400, 0x0bff, this::registerTextDirtyFlag); computer.getMemory().observe("Check for text changes", RAMEvent.TYPE.WRITE, 0x0400, 0x0bff, this::registerTextDirtyFlag);
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x02000, 0x05fff, this::registerHiresDirtyFlag); computer.getMemory().observe("Check for graphics changes", RAMEvent.TYPE.WRITE, 0x02000, 0x05fff, this::registerHiresDirtyFlag);
} }
@Override @Override

View File

@ -423,11 +423,11 @@ public class VideoNTSC extends VideoDHGR {
return; return;
} }
RAM memory = computer.getMemory(); RAM memory = computer.getMemory();
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05e, (e) -> { rgbStateListeners.add(memory.observe("NTSC: AN3 state change", RAMEvent.TYPE.ANY, 0x0c05e, (e) -> {
an3 = false; an3 = false;
rgbStateChange(); rgbStateChange();
})); }));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05f, (e) -> { rgbStateListeners.add(memory.observe("NTSC: 80COL state change", RAMEvent.TYPE.ANY, 0x0c05f, (e) -> {
if (!an3) { if (!an3) {
f2 = f1; f2 = f1;
f1 = SoftSwitches._80COL.getState(); f1 = SoftSwitches._80COL.getState();
@ -435,7 +435,7 @@ public class VideoNTSC extends VideoDHGR {
an3 = true; an3 = true;
rgbStateChange(); rgbStateChange();
})); }));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.EXECUTE, 0x0fa62, (e) -> { rgbStateListeners.add(memory.observe("NTSC: Reset hook for reverting RGB mode", RAMEvent.TYPE.EXECUTE, 0x0fa62, (e) -> {
// When reset hook is called, reset the graphics mode // When reset hook is called, reset the graphics mode
// This is useful in case a program is running that // This is useful in case a program is running that
// is totally clueless how to set the RGB state correctly. // is totally clueless how to set the RGB state correctly.

View File

@ -40,7 +40,7 @@ public class IntC8SoftSwitch extends SoftSwitch {
super("InternalC8Rom", false); super("InternalC8Rom", false);
// INTC8Rom should activate whenever C3xx memory is accessed and SLOTC3ROM is off // INTC8Rom should activate whenever C3xx memory is accessed and SLOTC3ROM is off
addListener( addListener(
new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { new RAMListener("Softswitch " + getName() + " on", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0C300); setScopeStart(0x0C300);
@ -57,7 +57,7 @@ public class IntC8SoftSwitch extends SoftSwitch {
// INTCXRom shoud deactivate whenever CFFF is accessed // INTCXRom shoud deactivate whenever CFFF is accessed
addListener( addListener(
new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { new RAMListener("Softswitch " + getName() + " off", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0CFFF); setScopeStart(0x0CFFF);

View File

@ -202,7 +202,7 @@ public class ApplesoftProgram {
*/ */
private void whenReady(Runnable r) { private void whenReady(Runnable r) {
Emulator.withMemory(memory->{ Emulator.withMemory(memory->{
memory.addListener(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { memory.addListener(new RAMListener("Applesoft: Trap GOTO command", RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(GOTO_CMD); setScopeStart(GOTO_CMD);

View File

@ -87,36 +87,36 @@ public abstract class Cheats extends Device {
} }
} }
public RAMListener bypassCode(int address, int addressEnd) { public RAMListener bypassCode(String name, int address, int addressEnd) {
int noOperation = MOS65C02.COMMAND.NOP.ordinal(); int noOperation = MOS65C02.COMMAND.NOP.ordinal();
return addCheat(RAMEvent.TYPE.READ, (e) -> e.setNewValue(noOperation), address, addressEnd); return addCheat(name, RAMEvent.TYPE.READ, (e) -> e.setNewValue(noOperation), address, addressEnd);
} }
public RAMListener forceValue(int value, int... address) { public RAMListener forceValue(String name, int value, int... address) {
return addCheat(RAMEvent.TYPE.ANY, (e) -> e.setNewValue(value), address); return addCheat(name, RAMEvent.TYPE.ANY, (e) -> e.setNewValue(value), address);
} }
public RAMListener forceValue(int value, Boolean auxFlag, int... address) { public RAMListener forceValue(String name, int value, Boolean auxFlag, int... address) {
return addCheat(RAMEvent.TYPE.ANY, auxFlag, (e) -> e.setNewValue(value), address); return addCheat(name, RAMEvent.TYPE.ANY, auxFlag, (e) -> e.setNewValue(value), address);
} }
public RAMListener addCheat(RAMEvent.TYPE type, RAMEvent.RAMEventHandler handler, int... address) { public RAMListener addCheat(String name, RAMEvent.TYPE type, RAMEvent.RAMEventHandler handler, int... address) {
RAMListener listener; RAMListener listener;
if (address.length == 1) { if (address.length == 1) {
listener = computer.getMemory().observe(type, address[0], handler); listener = computer.getMemory().observe(getName() + ": " + name, type, address[0], handler);
} else { } else {
listener = computer.getMemory().observe(type, address[0], address[1], handler); listener = computer.getMemory().observe(getName() + ": " + name, type, address[0], address[1], handler);
} }
listeners.add(listener); listeners.add(listener);
return listener; return listener;
} }
public RAMListener addCheat(RAMEvent.TYPE type, Boolean auxFlag, RAMEvent.RAMEventHandler handler, int... address) { public RAMListener addCheat(String name, RAMEvent.TYPE type, Boolean auxFlag, RAMEvent.RAMEventHandler handler, int... address) {
RAMListener listener; RAMListener listener;
if (address.length == 1) { if (address.length == 1) {
listener = computer.getMemory().observe(type, address[0], auxFlag, handler); listener = computer.getMemory().observe(getName() + ": " + name, type, address[0], auxFlag, handler);
} else { } else {
listener = computer.getMemory().observe(type, address[0], address[1], auxFlag, handler); listener = computer.getMemory().observe(getName() + ": " + name, type, address[0], address[1], auxFlag, handler);
} }
listeners.add(listener); listeners.add(listener);
return listener; return listener;

View File

@ -1,5 +1,7 @@
package jace.cheat; package jace.cheat;
import javax.script.ScriptException;
import jace.core.RAMEvent; import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -9,7 +11,6 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.util.Callback; import javafx.util.Callback;
import javax.script.ScriptException;
/** /**
* *
@ -21,10 +22,11 @@ public class DynamicCheat extends RAMListener {
StringProperty expression; StringProperty expression;
BooleanProperty active; BooleanProperty active;
StringProperty name; StringProperty name;
String cheatName;
Callback<RAMEvent, Integer> expressionCallback; Callback<RAMEvent, Integer> expressionCallback;
public DynamicCheat(int address, String expr) { public DynamicCheat(String cheatName, int address, String expr) {
super(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY); super(cheatName, RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY);
id = (int) (Math.random() * 10000000); id = (int) (Math.random() * 10000000);
addr = new SimpleIntegerProperty(address); addr = new SimpleIntegerProperty(address);
expression = new SimpleStringProperty(expr); expression = new SimpleStringProperty(expr);
@ -106,18 +108,19 @@ public class DynamicCheat extends RAMListener {
public static final String DELIMITER = ";"; public static final String DELIMITER = ";";
public String serialize() { public String serialize() {
return escape(name.get()) + DELIMITER return escape(cheatName) + DELIMITER + escape(name.get()) + DELIMITER
+ escape("$"+Integer.toHexString(addr.get())) + DELIMITER + escape("$"+Integer.toHexString(addr.get())) + DELIMITER
+ escape(expression.get()); + escape(expression.get());
} }
static public DynamicCheat deserialize(String in) { static public DynamicCheat deserialize(String in) {
String[] parts = in.split(DELIMITER); String[] parts = in.split(DELIMITER);
String name = unescape(parts[0]); String cheatName = unescape(parts[0]);
Integer addr = Integer.parseInt(parts[1].substring(1), 16); String name = unescape(parts[1]);
String expr = unescape(parts[2]); Integer addr = Integer.parseInt(parts[2].substring(1), 16);
String expr = unescape(parts[3]);
DynamicCheat out = new DynamicCheat(addr, expr); DynamicCheat out = new DynamicCheat(cheatName, addr, expr);
out.name.set(name); out.name.set(name);
return out; return out;
} }

View File

@ -302,7 +302,7 @@ public class MetaCheat extends Cheats {
} }
} }
if (memoryViewListener == null) { if (memoryViewListener == null) {
memoryViewListener = memory.observe(RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent); memoryViewListener = memory.observe("Metacheat memory viewer", RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent);
listeners.add(memoryViewListener); listeners.add(memoryViewListener);
} }
}); });

View File

@ -71,21 +71,21 @@ public class MontezumasRevengeCheats extends Cheats {
@Override @Override
public void registerListeners() { public void registerListeners() {
if (repulsiveHack) { if (repulsiveHack) {
addCheat(RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518); addCheat("Repulsive", RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518);
} }
if (featherFall) { if (featherFall) {
addCheat(RAMEvent.TYPE.WRITE, this::featherFallBehavior, PLAYER_Y); addCheat("Feather fall", RAMEvent.TYPE.WRITE, this::featherFallBehavior, PLAYER_Y);
// Bypass the part that realizes you should die when you hit the floor // Bypass the part that realizes you should die when you hit the floor
bypassCode(0x6bb3, 0x6bb4); bypassCode("Feather fall code hack", 0x6bb3, 0x6bb4);
} }
if (moonJump) { if (moonJump) {
addCheat(RAMEvent.TYPE.WRITE, this::moonJumpBehavior, Y_VELOCITY); addCheat("Moon jump", RAMEvent.TYPE.WRITE, this::moonJumpBehavior, Y_VELOCITY);
} }
if (infiniteLives) { if (infiniteLives) {
forceValue(11, LIVES); forceValue("Infinite lives", 11, LIVES);
} }
if (safePassage) { if (safePassage) {
@ -103,22 +103,22 @@ public class MontezumasRevengeCheats extends Cheats {
memory.write(0x0f51, (byte) 0b00001000, false, false); memory.write(0x0f51, (byte) 0b00001000, false, false);
memory.write(0x0f52, (byte) 0b10000100, false, false); memory.write(0x0f52, (byte) 0b10000100, false, false);
memory.write(0x0f53, (byte) 0b11010101, false, false); memory.write(0x0f53, (byte) 0b11010101, false, false);
forceValue(32, FLOOR_TIMER); forceValue("Hack floor timer", 32, FLOOR_TIMER);
forceValue(32, HAZARD_TIMER); forceValue("Hack hazard timer", 32, HAZARD_TIMER);
forceValue(1, HAZARD_FLAG); forceValue("Hack hazard flag", 1, HAZARD_FLAG);
}); });
} }
if (scoreHack) { if (scoreHack) {
// Score: 900913 // Score: 900913
forceValue(0x90, SCORE); forceValue("Hack score 1", 0x90, SCORE);
forceValue(0x09, SCORE + 1); forceValue("Hack score 2", 0x09, SCORE + 1);
forceValue(0x13, SCORE + 2); forceValue("Hack score 3", 0x13, SCORE + 2);
} }
if (snakeCharmer) { if (snakeCharmer) {
// Skip the code that determines you're touching an enemy // Skip the code that determines you're touching an enemy
bypassCode(0x07963, 0x07964); bypassCode("Snake charmer", 0x07963, 0x07964);
} }
if (mouseHack) { if (mouseHack) {
EmulatorUILogic.addMouseListener(listener); EmulatorUILogic.addMouseListener(listener);

View File

@ -172,19 +172,19 @@ public class PrinceOfPersiaCheats extends Cheats {
@Override @Override
public void registerListeners() { public void registerListeners() {
if (velocityHack) { if (velocityHack) {
addCheat(RAMEvent.TYPE.READ_DATA, true, this::velocityHackBehavior, CharYVel); addCheat("Hack velocity", RAMEvent.TYPE.READ_DATA, true, this::velocityHackBehavior, CharYVel);
} }
if (invincibilityHack) { if (invincibilityHack) {
forceValue(3, true, KidStrength); forceValue("Hack invincibility", 3, true, KidStrength);
} }
if (sleepHack) { if (sleepHack) {
forceValue(0, true, EnemyAlert); forceValue("Go to sleep!", 0, true, EnemyAlert);
} }
if (swordHack) { if (swordHack) {
forceValue(1, true, hasSword); forceValue("Can haz sword", 1, true, hasSword);
} }
if (timeHack) { if (timeHack) {
forceValue(0x69, true, MinLeft); forceValue("Hack time", 0x69, true, MinLeft);
} }
if (mouseHack) { if (mouseHack) {
EmulatorUILogic.addMouseListener(listener); EmulatorUILogic.addMouseListener(listener);

View File

@ -62,6 +62,11 @@ import javafx.scene.image.ImageView;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
public class Configuration implements Reconfigurable { public class Configuration implements Reconfigurable {
public EmulatorUILogic ui;
public Configuration() {
ui = Emulator.getUILogic();
}
static ConfigurableField getConfigurableFieldInfo(Reconfigurable subject, String settingName) { static ConfigurableField getConfigurableFieldInfo(Reconfigurable subject, String settingName) {
Field f; Field f;
@ -262,13 +267,12 @@ public class Configuration implements Reconfigurable {
} }
} }
public static ConfigNode BASE; public static ConfigNode BASE;
public static EmulatorUILogic ui = Emulator.logic;
@ConfigurableField(name = "Autosave Changes", description = "If unchecked, changes are only saved when the Save button is pressed.") @ConfigurableField(name = "Autosave Changes", description = "If unchecked, changes are only saved when the Save button is pressed.")
public static boolean saveAutomatically = false; public static boolean saveAutomatically = false;
public static void buildTree() { public static void buildTree() {
BASE = new ConfigNode(new Configuration()); BASE = new ConfigNode(new Configuration());
Set visited = new LinkedHashSet(); Set<ConfigNode> visited = new LinkedHashSet<>();
buildTree(BASE, visited); buildTree(BASE, visited);
Emulator.withComputer(c->{ Emulator.withComputer(c->{
ConfigNode computer = new ConfigNode(BASE, c); ConfigNode computer = new ConfigNode(BASE, c);

View File

@ -18,7 +18,6 @@
*/ */
package jace.core; package jace.core;
import jace.Emulator;
import jace.apple2e.SoftSwitches; import jace.apple2e.SoftSwitches;
/** /**
@ -103,10 +102,10 @@ public abstract class Card extends Device {
@Override @Override
public void reconfigure() { public void reconfigure() {
//super.reconfigure(); //super.reconfigure();
Emulator.whileSuspended(c-> { // Emulator.whileSuspended(c-> {
unregisterListeners(); unregisterListeners();
registerListeners(); registerListeners();
}); // });
} }
public void notifyVBLStateChanged(boolean state) { public void notifyVBLStateChanged(boolean state) {
@ -121,12 +120,12 @@ public abstract class Card extends Device {
RAM memory = computer.getMemory(); RAM memory = computer.getMemory();
int baseIO = 0x0c080 + slot * 16; int baseIO = 0x0c080 + slot * 16;
int baseRom = 0x0c000 + slot * 256; int baseRom = 0x0c000 + slot * 256;
ioListener = memory.observe(RAMEvent.TYPE.ANY, baseIO, baseIO + 15, (e) -> { ioListener = memory.observe("Slot " + getSlot() + " " + getDeviceName() + " IO access", RAMEvent.TYPE.ANY, baseIO, baseIO + 15, (e) -> {
int address = e.getAddress() & 0x0f; int address = e.getAddress() & 0x0f;
handleIOAccess(address, e.getType(), e.getNewValue(), e); handleIOAccess(address, e.getType(), e.getNewValue(), e);
}); });
firmwareListener = memory.observe(RAMEvent.TYPE.ANY, baseRom, baseRom + 255, (e) -> { firmwareListener = memory.observe("Slot " + getSlot() + " " + getDeviceName() + " CX Firmware access", RAMEvent.TYPE.ANY, baseRom, baseRom + 255, (e) -> {
computer.getMemory().setActiveCard(slot); computer.getMemory().setActiveCard(slot);
// Sather 6-4: Writes will still go through even when CXROM inhibits slot ROM // Sather 6-4: Writes will still go through even when CXROM inhibits slot ROM
if (SoftSwitches.CXROM.isOff() || !e.getType().isRead()) { if (SoftSwitches.CXROM.isOff() || !e.getType().isRead()) {
@ -134,7 +133,7 @@ public abstract class Card extends Device {
} }
}); });
c8firmwareListener = memory.observe(RAMEvent.TYPE.ANY, 0xc800, 0xcfff, (e) -> { c8firmwareListener = memory.observe("Slot " + getSlot() + " " + getDeviceName() + " C8 Firmware access", RAMEvent.TYPE.ANY, 0xc800, 0xcfff, (e) -> {
if (SoftSwitches.CXROM.isOff() && SoftSwitches.INTC8ROM.isOff() if (SoftSwitches.CXROM.isOff() && SoftSwitches.INTC8ROM.isOff()
&& computer.getMemory().getActiveSlot() == slot) { && computer.getMemory().getActiveSlot() == slot) {
handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e); handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e);

View File

@ -135,6 +135,7 @@ public class Motherboard extends TimedDevice {
addChildDevice(speaker); addChildDevice(speaker);
} catch (Throwable t) { } catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out"); System.out.println("Unable to initalize sound -- deactivating speaker out");
t.printStackTrace();
} }
} else { } else {
System.out.println("Speaker not enabled, leaving it off."); System.out.println("Speaker not enabled, leaving it off.");

View File

@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import jace.Emulator;
import jace.apple2e.SoftSwitches; import jace.apple2e.SoftSwitches;
import jace.config.Reconfigurable; import jace.config.Reconfigurable;
@ -56,6 +57,7 @@ public abstract class RAM implements Reconfigurable {
* *
* @param computer * @param computer
*/ */
@SuppressWarnings("unchecked")
public RAM(Computer computer) { public RAM(Computer computer) {
this.computer = computer; this.computer = computer;
listeners = new ConcurrentSkipListSet<>(); listeners = new ConcurrentSkipListSet<>();
@ -95,11 +97,10 @@ public abstract class RAM implements Reconfigurable {
cards[slot] = Optional.of(c); cards[slot] = Optional.of(c);
c.setSlot(slot); c.setSlot(slot);
c.attach(); c.attach();
configureActiveMemory();
} }
public void removeCard(Card c) { public void removeCard(Card c) {
c.suspend();
c.detach();
removeCard(c.getSlot()); removeCard(c.getSlot());
} }
@ -198,14 +199,15 @@ public abstract class RAM implements Reconfigurable {
} }
} }
@SuppressWarnings("unchecked")
private void refreshListenerMap() { private void refreshListenerMap() {
listenerMap = (Set<RAMListener>[]) new Set[256]; listenerMap = (Set<RAMListener>[]) new Set[256];
ioListenerMap = (Set<RAMListener>[]) new Set[256]; ioListenerMap = (Set<RAMListener>[]) new Set[256];
listeners.forEach(this::addListenerRange); listeners.forEach(this::addListenerRange);
} }
public RAMListener observe(RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) { public RAMListener observe(String observerationName, RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { return addListener(new RAMListener(observerationName, type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(address); setScopeStart(address);
@ -218,8 +220,8 @@ public abstract class RAM implements Reconfigurable {
}); });
} }
public RAMListener observe(RAMEvent.TYPE type, int address, Boolean auxFlag, RAMEvent.RAMEventHandler handler) { public RAMListener observe(String observerationName, RAMEvent.TYPE type, int address, Boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { return addListener(new RAMListener(observerationName, type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(address); setScopeStart(address);
@ -234,8 +236,8 @@ public abstract class RAM implements Reconfigurable {
}); });
} }
public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, RAMEvent.RAMEventHandler handler) { public RAMListener observe(String observerationName, RAMEvent.TYPE type, int addressStart, int addressEnd, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { return addListener(new RAMListener(observerationName, type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(addressStart); setScopeStart(addressStart);
@ -249,8 +251,8 @@ public abstract class RAM implements Reconfigurable {
}); });
} }
public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, Boolean auxFlag, RAMEvent.RAMEventHandler handler) { public RAMListener observe(String observerationName, RAMEvent.TYPE type, int addressStart, int addressEnd, Boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { return addListener(new RAMListener(observerationName, type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(addressStart); setScopeStart(addressStart);
@ -280,14 +282,12 @@ public abstract class RAM implements Reconfigurable {
return l; return l;
} }
listeners.add(l); listeners.add(l);
computer.cpu.whileSuspended(()->{ Emulator.whileSuspended((c)->addListenerRange(l));
addListenerRange(l);
});
return l; return l;
} }
public RAMListener addExecutionTrap(int address, Consumer<RAMEvent> handler) { public RAMListener addExecutionTrap(String observerationName, int address, Consumer<RAMEvent> handler) {
RAMListener listener = new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { RAMListener listener = new RAMListener(observerationName, RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(address); setScopeStart(address);
@ -307,9 +307,7 @@ public abstract class RAM implements Reconfigurable {
return; return;
} }
listeners.remove(l); listeners.remove(l);
computer.cpu.whileSuspended(()->{ Emulator.whileSuspended(c->refreshListenerMap());
refreshListenerMap();
});
} }
private byte _callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue) { private byte _callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue) {

View File

@ -39,14 +39,17 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler, Comparabl
private int valueStart; private int valueStart;
private int valueEnd; private int valueEnd;
private int valueAmount; private int valueAmount;
private String name;
/** /**
* Creates a new instance of RAMListener * Creates a new instance of RAMListener
* @param name
* @param t * @param t
* @param s * @param s
* @param v * @param v
*/ */
public RAMListener(RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) { public RAMListener(String name, RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) {
setName(name);
setType(t); setType(t);
setScope(s); setScope(s);
setValue(v); setValue(v);
@ -77,6 +80,14 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler, Comparabl
this.value = value; this.value = value;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScopeStart() { public int getScopeStart() {
return scopeStart; return scopeStart;
} }
@ -161,18 +172,23 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler, Comparabl
@Override @Override
public int compareTo(RAMListener o) { public int compareTo(RAMListener o) {
if (o.scopeStart == scopeStart) { if (o.name == name) {
if (o.scopeEnd == scopeEnd) { if (o.scopeStart == scopeStart) {
if (o.type == type) { if (o.scopeEnd == scopeEnd) {
return Integer.compare(o.hashCode(), hashCode()); if (o.type == type) {
// Ignore hash codes -- combination of name, address range and type should identify similar listeners.
return (int) 0;
} else {
return Integer.compare(o.type.ordinal(), type.ordinal());
}
} else { } else {
return Integer.compare(o.type.ordinal(), type.ordinal()); return Integer.compare(o.scopeEnd, scopeEnd);
} }
} else { } else {
return Integer.compare(o.scopeEnd, scopeEnd); return Integer.compare(o.scopeStart, scopeStart);
} }
} else { } else {
return Integer.compare(o.scopeStart, scopeStart); return o.name.compareTo(name);
} }
} }
} }

View File

@ -106,7 +106,7 @@ public abstract class SoftSwitch {
exclusionActivate.add(i); exclusionActivate.add(i);
} }
} }
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { RAMListener l = new RAMListener("Softswitch toggle " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(beginAddr); setScopeStart(beginAddr);
@ -136,7 +136,7 @@ public abstract class SoftSwitch {
exclusionActivate.add(i); exclusionActivate.add(i);
} }
} }
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { RAMListener l = new RAMListener("Softswitch on " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(beginAddr); setScopeStart(beginAddr);
@ -169,7 +169,7 @@ public abstract class SoftSwitch {
exclusionDeactivate.add(i); exclusionDeactivate.add(i);
} }
} }
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { RAMListener l = new RAMListener("Softswitch off " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(beginAddr); setScopeStart(beginAddr);
@ -201,7 +201,7 @@ public abstract class SoftSwitch {
} }
} }
// RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { // RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { RAMListener l = new RAMListener("Softswitch read state " + name, RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(beginAddr); setScopeStart(beginAddr);

View File

@ -18,14 +18,25 @@
*/ */
package jace.core; package jace.core;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL; import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC; import org.lwjgl.openal.ALC;
import org.lwjgl.openal.ALC10; import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALCCapabilities; import org.lwjgl.openal.ALCCapabilities;
import org.lwjgl.openal.ALCapabilities; import org.lwjgl.openal.ALCapabilities;
import jace.Emulator;
import jace.config.ConfigurableField; import jace.config.ConfigurableField;
/** /**
@ -41,8 +52,9 @@ public class SoundMixer extends Device {
/** /**
* Bits per sample * Bits per sample
* Making this configurable requires too much effort and not a lot of benefit
*/ */
@ConfigurableField(name = "Bits per sample", shortName = "bits") // @ConfigurableField(name = "Bits per sample", shortName = "bits")
public static int BITS = 16; public static int BITS = 16;
/** /**
* Sample playback rate * Sample playback rate
@ -51,38 +63,206 @@ public class SoundMixer extends Device {
public static int RATE = 48000; public static int RATE = 48000;
@ConfigurableField(name = "Mute", shortName = "mute") @ConfigurableField(name = "Mute", shortName = "mute")
public static boolean MUTE = false; public static boolean MUTE = false;
@ConfigurableField(name = "Buffer size", shortName = "buffer")
public static int BUFFER_SIZE = 512;
public static boolean PLAYBACK_ENABLED = false;
// Innocent until proven guilty by a failed initialization
public static boolean PLAYBACK_DRIVER_DETECTED = true;
public static boolean PLAYBACK_INITIALIZED = false;
private final String defaultDeviceName; private static String defaultDeviceName;
private long audioDevice; private static long audioDevice = -1;
private long audioContext; private static long audioContext = -1;
private ALCCapabilities audioCapabilities; private static ALCCapabilities audioCapabilities;
private ALCapabilities audioLibCapabilities; private static ALCapabilities audioLibCapabilities;
// In case the OpenAL implementation wants to be run in a single thread, use a single thread executor
protected static ExecutorService soundThreadExecutor = Executors.newSingleThreadExecutor();
public SoundMixer(Computer computer) { public SoundMixer(Computer computer) {
super(computer); super(computer);
defaultDeviceName = ALC10.alcGetString(0, ALC10.ALC_DEFAULT_DEVICE_SPECIFIER); defaultDeviceName = ALC10.alcGetString(0, ALC10.ALC_DEFAULT_DEVICE_SPECIFIER);
}
public static <T> T performSoundFunction(Callable<T> operation) {
return performSoundFunction(operation, false);
}
public static <T> T performSoundFunction(Callable<T> operation, boolean ignoreError) {
Future<T> result = soundThreadExecutor.submit(operation);
try {
if (!ignoreError) {
Future<Integer> error = soundThreadExecutor.submit(AL10::alGetError);
int err;
err = error.get();
if (err != AL10.AL_NO_ERROR) {
throw new RuntimeException(AL10.alGetString(err));
}
}
return result.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("Error when executing sound action: " + e.getMessage());
e.printStackTrace();
}
return null;
}
public static void performSoundOperation(Runnable operation) {
performSoundOperation(operation, false);
}
public static void performSoundOperation(Runnable operation, boolean ignoreError) {
performSoundFunction(()->{
operation.run();
return null;
}, ignoreError);
}
public static void performSoundOperationAsync(Runnable operation) {
soundThreadExecutor.submit(operation);
}
protected static void initSound() {
performSoundOperation(()->{
if (!PLAYBACK_INITIALIZED) {
audioDevice = ALC10.alcOpenDevice(defaultDeviceName);
audioContext = ALC10.alcCreateContext(audioDevice, new int[]{0});
ALC10.alcMakeContextCurrent(audioContext);
audioCapabilities = ALC.createCapabilities(audioDevice);
audioLibCapabilities = AL.createCapabilities(audioCapabilities);
if (!audioLibCapabilities.OpenAL10) {
PLAYBACK_DRIVER_DETECTED = false;
Logger.getLogger(SoundMixer.class.getName()).warning("OpenAL 1.0 not supported");
Emulator.withComputer(c->c.mixer.detach());
}
PLAYBACK_INITIALIZED = true;
} else {
ALC10.alcMakeContextCurrent(audioContext);
}
});
} }
// Lots of inspiration from https://www.youtube.com/watch?v=dLrqBTeipwg // Lots of inspiration from https://www.youtube.com/watch?v=dLrqBTeipwg
@Override @Override
public void attach() { public void attach() {
if (!PLAYBACK_DRIVER_DETECTED) {
Logger.getLogger(SoundMixer.class.getName()).warning("Sound driver not detected");
return;
}
super.attach(); super.attach();
audioDevice = ALC10.alcOpenDevice(defaultDeviceName); initSound();
// TODO: Other attributes? PLAYBACK_ENABLED = true;
audioContext = ALC10.alcCreateContext(audioDevice, new int[]{0}); }
ALC10.alcMakeContextCurrent(audioContext);
audioCapabilities = ALC.createCapabilities(audioDevice); private static List<SoundBuffer> buffers = new ArrayList<>();
audioLibCapabilities = AL.createCapabilities(audioCapabilities); public static SoundBuffer createBuffer(boolean stereo) {
if (!audioLibCapabilities.OpenAL10) { if (!PLAYBACK_ENABLED) {
Logger.getLogger(SoundMixer.class.getName()).warning("OpenAL 1.0 not supported"); return null;
detach(); }
SoundBuffer buffer = new SoundBuffer(stereo);
buffers.add(buffer);
return buffer;
}
public static class SoundBuffer {
public static int MAX_BUFFER_ID;
private ShortBuffer currentBuffer;
private ShortBuffer alternateBuffer;
private int audioFormat;
private int currentBufferId;
private int alternateBufferId;
private int sourceId;
private boolean isAlive;
private int buffersGenerated = 0;
public SoundBuffer(boolean stereo) {
initSound();
currentBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
alternateBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
currentBufferId = performSoundFunction(AL10::alGenBuffers);
alternateBufferId = performSoundFunction(AL10::alGenBuffers);
sourceId = performSoundFunction(AL10::alGenSources);
audioFormat = stereo ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16;
isAlive = true;
}
public boolean isAlive() {
return isAlive;
}
/* If stereo, call this once for left and then again for right sample */
public void playSample(short sample) throws InterruptedException, ExecutionException {
if (!isAlive) {
Logger.getLogger(SoundMixer.class.getName()).warning("Playback attempted on stopped buffer!");
return;
}
currentBuffer.put(sample);
if (!currentBuffer.hasRemaining()) {
buffersGenerated++;
currentBuffer.flip();
if (buffersGenerated > 2) {
int[] unqueueBuffers = new int[]{currentBufferId};
performSoundOperation(()->{
int buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
while (buffersProcessed < 1) {
Thread.onSpinWait();
buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
}
});
performSoundOperation(()->{
AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers);
});
}
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE));
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId));
performSoundOperationAsync(()->{
if (AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
AL10.alSourcePlay(sourceId);
}
});
// Swap AL buffers
int tempId = currentBufferId;
currentBufferId = alternateBufferId;
alternateBufferId = tempId;
// Swap Java buffers
ShortBuffer tempBuffer = currentBuffer;
currentBuffer = alternateBuffer;
alternateBuffer = tempBuffer;
}
}
public void shutdown() throws InterruptedException, ExecutionException {
if (!isAlive) {
return;
}
isAlive = false;
performSoundOperation(()->AL10.alSourceStop(sourceId));
performSoundOperation(()->AL10.alDeleteSources(sourceId));
performSoundOperation(()->AL10.alDeleteBuffers(alternateBufferId));
performSoundOperation(()->AL10.alDeleteBuffers(currentBufferId));
} }
} }
@Override @Override
public void detach() { public void detach() {
ALC10.alcDestroyContext(audioContext); if (!PLAYBACK_ENABLED) {
ALC10.alcCloseDevice(audioDevice); return;
}
MUTE = true; MUTE = true;
PLAYBACK_ENABLED = false;
for (SoundBuffer buffer : buffers) {
try {
buffer.shutdown();
} catch (InterruptedException | ExecutionException e) {
Logger.getLogger(SoundMixer.class.getName()).warning("Error when detaching sound mixer: " + e.getMessage());
}
}
buffers.clear();
PLAYBACK_INITIALIZED = false;
performSoundOperation(()->ALC10.alcDestroyContext(audioContext), true);
performSoundOperation(()->ALC10.alcCloseDevice(audioDevice), true);
super.detach(); super.detach();
} }
@ -100,10 +280,11 @@ public class SoundMixer extends Device {
@Override @Override
public synchronized void reconfigure() { public synchronized void reconfigure() {
if (MUTE) { PLAYBACK_ENABLED = PLAYBACK_DRIVER_DETECTED && !MUTE;
detach(); if (PLAYBACK_ENABLED) {
} else {
attach(); attach();
} else {
detach();
} }
} }

View File

@ -27,7 +27,6 @@ import java.util.logging.Logger;
import jace.EmulatorUILogic; import jace.EmulatorUILogic;
import jace.config.ConfigurableField; import jace.config.ConfigurableField;
import jace.config.Name; import jace.config.Name;
import jace.config.Reconfigurable;
import jace.core.Card; import jace.core.Card;
import jace.core.Computer; import jace.core.Computer;
import jace.core.RAMEvent; import jace.core.RAMEvent;
@ -46,7 +45,7 @@ import jace.library.MediaConsumerParent;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/ */
@Name("Disk ][ Controller") @Name("Disk ][ Controller")
public class CardDiskII extends Card implements Reconfigurable, MediaConsumerParent { public class CardDiskII extends Card implements MediaConsumerParent {
DiskIIDrive currentDrive; DiskIIDrive currentDrive;
DiskIIDrive drive1 = new DiskIIDrive(computer); DiskIIDrive drive1 = new DiskIIDrive(computer);
@ -128,7 +127,8 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
case 0xC: case 0xC:
// read/write latch // read/write latch
currentDrive.write(); currentDrive.write();
e.setNewValue(currentDrive.readLatch()); int latch = currentDrive.readLatch();
e.setNewValue(latch);
break; break;
case 0xF: case 0xF:
// write mode // write mode

View File

@ -348,7 +348,7 @@ public class CardMockingboard extends Card implements Runnable {
public void run() { public void run() {
SourceDataLine out = null; SourceDataLine out = null;
try { try {
out = computer.mixer.getLine(); // out = computer.mixer.getLine();
if (out == null) { if (out == null) {
setRun(false); setRun(false);
return; return;

View File

@ -140,7 +140,7 @@ public class CardRamworks extends RAM128k {
private RAMListener bankSelectListener; private RAMListener bankSelectListener;
@Override @Override
public void attach() { public void attach() {
bankSelectListener = observe(RAMEvent.TYPE.WRITE, BANK_SELECT, (e) -> { bankSelectListener = observe("Ramworks bank select", RAMEvent.TYPE.WRITE, BANK_SELECT, (e) -> {
currentBank = e.getNewValue(); currentBank = e.getNewValue();
configureActiveMemory(); configureActiveMemory();
}); });

View File

@ -18,6 +18,14 @@
*/ */
package jace.hardware; package jace.hardware;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import jace.EmulatorUILogic; import jace.EmulatorUILogic;
import jace.core.Computer; import jace.core.Computer;
import jace.library.MediaConsumer; import jace.library.MediaConsumer;
@ -25,12 +33,6 @@ import jace.library.MediaEntry;
import jace.library.MediaEntry.MediaFile; import jace.library.MediaEntry.MediaFile;
import jace.state.StateManager; import jace.state.StateManager;
import jace.state.Stateful; import jace.state.Stateful;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.LockSupport;
import javafx.scene.control.Label; import javafx.scene.control.Label;
/** /**
@ -50,11 +52,13 @@ public class DiskIIDrive implements MediaConsumer {
this.computer = computer; this.computer = computer;
} }
public boolean DEBUG = true;
FloppyDisk disk; FloppyDisk disk;
// Number of milliseconds to wait between last write and update to disk image // Number of milliseconds to wait between last write and update to disk image
public static long WRITE_UPDATE_DELAY = 1000; public static long WRITE_UPDATE_DELAY = 1000;
// Flag to halt if any writes to floopy occur when updating physical disk image // Flag to halt if any writes to floopy occur when updating physical disk image
boolean diskUpdatePending = false; AtomicBoolean diskUpdatePending = new AtomicBoolean();
// Last time of write operation // Last time of write operation
long lastWriteTime; long lastWriteTime;
// Managed thread to update disk image in background // Managed thread to update disk image in background
@ -86,7 +90,7 @@ public class DiskIIDrive implements MediaConsumer {
driveOn = false; driveOn = false;
magnets = 0; magnets = 0;
dirtyTracks = new HashSet<>(); dirtyTracks = new HashSet<>();
diskUpdatePending = false; diskUpdatePending.set(false);
} }
void step(int register) { void step(int register) {
@ -113,13 +117,18 @@ public class DiskIIDrive implements MediaConsumer {
} }
nibbleOffset = 0; nibbleOffset = 0;
//System.out.printf("new half track %d\n", currentHalfTrack); if (DEBUG) {
System.out.printf("step %d, new half track %d\n", register, halfTrack);
}
} }
} }
} }
} }
void setOn(boolean b) { void setOn(boolean b) {
if (DEBUG) {
System.out.println("Drive setOn: "+b);
}
driveOn = b; driveOn = b;
} }
@ -155,17 +164,20 @@ public class DiskIIDrive implements MediaConsumer {
void write() { void write() {
if (writeMode) { if (writeMode) {
while (diskUpdatePending) { while (diskUpdatePending.get()) {
// If another thread requested writes to block (e.g. because of disk activity), wait for it to finish! // If another thread requested writes to block (e.g. because of disk activity), wait for it to finish!
LockSupport.parkNanos(1000); Thread.onSpinWait();
} }
if (disk != null) { // Holding the lock should block any other threads from writing to disk
// Do nothing if write-protection is enabled! synchronized (diskUpdatePending) {
if (getMediaEntry() == null || !getMediaEntry().writeProtected) { if (disk != null) {
dirtyTracks.add(trackStartOffset / FloppyDisk.TRACK_NIBBLE_LENGTH); // Do nothing if write-protection is enabled!
disk.nibbles[trackStartOffset + nibbleOffset++] = latch; if (getMediaEntry() == null || !getMediaEntry().writeProtected) {
triggerDiskUpdate(); dirtyTracks.add(trackStartOffset / FloppyDisk.TRACK_NIBBLE_LENGTH);
StateManager.markDirtyValue(disk.nibbles, computer); disk.nibbles[trackStartOffset + nibbleOffset++] = latch;
triggerDiskUpdate();
StateManager.markDirtyValue(disk.nibbles, computer);
}
} }
} }
@ -192,19 +204,20 @@ public class DiskIIDrive implements MediaConsumer {
} }
private void updateDisk() { private void updateDisk() {
// Signal disk update is underway // Signal disk update is underway
diskUpdatePending = true; synchronized (diskUpdatePending) {
// Update all tracks as necessary diskUpdatePending.set(true);
if (disk != null) { // Update all tracks as necessary
dirtyTracks.stream().forEach((track) -> { if (disk != null) {
disk.updateTrack(track); dirtyTracks.stream().forEach((track) -> {
}); disk.updateTrack(track);
});
}
// Empty out dirty list
dirtyTracks.clear();
} }
// Empty out dirty list
dirtyTracks.clear();
// Signal disk update is completed // Signal disk update is completed
diskUpdatePending = false; diskUpdatePending.set(false);
} }
private void triggerDiskUpdate() { private void triggerDiskUpdate() {
@ -226,6 +239,9 @@ public class DiskIIDrive implements MediaConsumer {
} }
void insertDisk(File diskPath) throws IOException { void insertDisk(File diskPath) throws IOException {
if (DEBUG) {
System.out.println("inserting disk " + diskPath.getAbsolutePath() + " into drive");
}
disk = new FloppyDisk(diskPath, computer); disk = new FloppyDisk(diskPath, computer);
dirtyTracks = new HashSet<>(); dirtyTracks = new HashSet<>();
// Emulator state has changed significantly, reset state manager // Emulator state has changed significantly, reset state manager
@ -300,14 +316,16 @@ public class DiskIIDrive implements MediaConsumer {
@Override @Override
public boolean isAccepted(MediaEntry e, MediaFile f) { public boolean isAccepted(MediaEntry e, MediaFile f) {
if (f == null) return false; if (f == null) return false;
// System.out.println("Type is accepted: "+f.path+"; "+e.type.toString()+": "+e.type.is140kb); if (DEBUG) {
System.out.println("Type is accepted: "+f.path+"; "+e.type.toString()+": "+e.type.is140kb);
}
return e.type.is140kb; return e.type.is140kb;
} }
private void waitForPendingWrites() { private void waitForPendingWrites() {
while (diskUpdatePending || !dirtyTracks.isEmpty()) { while (diskUpdatePending.get()) {
// If the current disk has unsaved changes, wait!!! // If the current disk has unsaved changes, wait!!!
LockSupport.parkNanos(1000); Thread.onSpinWait();
} }
} }
} }

View File

@ -18,9 +18,6 @@
*/ */
package jace.hardware; package jace.hardware;
import jace.core.Computer;
import jace.state.StateManager;
import jace.state.Stateful;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -32,6 +29,10 @@ import java.util.Arrays;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import jace.core.Computer;
import jace.state.StateManager;
import jace.state.Stateful;
/** /**
* Representation of a 140kb floppy disk image. This also performs conversions * Representation of a 140kb floppy disk image. This also performs conversions
* as needed. Internally, the emulator will always use a "nibblized" disk * as needed. Internally, the emulator will always use a "nibblized" disk
@ -180,6 +181,17 @@ public class FloppyDisk {
writeJunkBytes(output, 38 - gap2); writeJunkBytes(output, 38 - gap2);
} }
} }
// Write output to stdout for debugging purposes
if (DEBUG) {
System.out.println("Nibblized disk:");
for (int i = 0; i < output.size(); i++) {
System.out.print(Integer.toString(output.toByteArray()[i] & 0x0ff, 16) + " ");
if (i % 16 == 255) {
System.out.println();
}
}
System.out.println();
}
return output.toByteArray(); return output.toByteArray();
} }
@ -298,17 +310,23 @@ public class FloppyDisk {
byte[] trackNibbles = new byte[TRACK_NIBBLE_LENGTH]; byte[] trackNibbles = new byte[TRACK_NIBBLE_LENGTH];
byte[] trackData = new byte[SECTOR_COUNT * 256]; byte[] trackData = new byte[SECTOR_COUNT * 256];
// Copy track into temporary buffer // Copy track into temporary buffer
// System.out.println("Nibblized track "+track); if (DEBUG) {
// System.out.printf("%04d:",0); System.out.println("Nibblized track "+track);
for (int i = 0, pos = track * TRACK_NIBBLE_LENGTH; i < TRACK_NIBBLE_LENGTH; i++, pos++) { System.out.printf("%04d:",0);
trackNibbles[i] = nibbles[pos]; }
// System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" "); for (int i = 0, pos = track * TRACK_NIBBLE_LENGTH; i < TRACK_NIBBLE_LENGTH; i++, pos++) {
// if (i % 16 == 15) { trackNibbles[i] = nibbles[pos];
// System.out.println(); if (DEBUG) {
// System.out.printf("%04d:",i+1); System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" ");
// } if (i % 16 == 15) {
System.out.println();
System.out.printf("%04d:",i+1);
}
}
}
if (DEBUG) {
System.out.println();
} }
// System.out.println();
int pos = 0; int pos = 0;
for (int i = 0; i < SECTOR_COUNT; i++) { for (int i = 0; i < SECTOR_COUNT; i++) {
@ -318,7 +336,9 @@ public class FloppyDisk {
int trackVerify = decodeOddEven(trackNibbles[pos + 5], trackNibbles[pos + 6]); int trackVerify = decodeOddEven(trackNibbles[pos + 5], trackNibbles[pos + 6]);
// Locate sector number // Locate sector number
int sector = decodeOddEven(trackNibbles[pos + 7], trackNibbles[pos + 8]); int sector = decodeOddEven(trackNibbles[pos + 7], trackNibbles[pos + 8]);
// System.out.println("Writing track " + track + ", getting address block for T" + trackVerify + ".S" + sector + " found at NIB offset "+pos); if (DEBUG) {
System.out.println("Writing track " + track + ", getting address block for T" + trackVerify + ".S" + sector + " found at NIB offset "+pos);
}
// Skip to end of address block // Skip to end of address block
pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa /*, 0x0eb this is sometimes being written as FF??*/); pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa /*, 0x0eb this is sometimes being written as FF??*/);
// Locate start of sector data // Locate start of sector data
@ -326,7 +346,9 @@ public class FloppyDisk {
// Determine offset in output data for sector // Determine offset in output data for sector
//int offset = reverseLoopkup(currentSectorOrder, sector) * 256; //int offset = reverseLoopkup(currentSectorOrder, sector) * 256;
int offset = currentSectorOrder[sector] * 256; int offset = currentSectorOrder[sector] * 256;
// System.out.println("Sector "+sector+" maps to physical sector "+reverseLoopkup(currentSectorOrder, sector)); if (DEBUG) {
System.out.println("Sector "+sector+" maps to physical sector "+reverseLoopkup(currentSectorOrder, sector));
}
// Decode sector data // Decode sector data
denibblizeSector(trackNibbles, pos + 3, trackData, offset); denibblizeSector(trackNibbles, pos + 3, trackData, offset);
// Skip to end of sector // Skip to end of sector

View File

@ -201,7 +201,7 @@ public class Joystick extends Device {
RAMListener listener; RAMListener listener;
private void registerListeners() { private void registerListeners() {
listener = computer.getMemory().observe(RAMEvent.TYPE.ANY, 0x0c070, 0x0c07f, this::initJoystickRead); listener = computer.getMemory().observe("Joystick I/O", RAMEvent.TYPE.ANY, 0x0c070, 0x0c07f, this::initJoystickRead);
} }
private void removeListeners() { private void removeListeners() {

View File

@ -1,5 +1,8 @@
package jace.hardware; package jace.hardware;
import java.util.Calendar;
import java.util.Optional;
import jace.EmulatorUILogic; import jace.EmulatorUILogic;
import jace.apple2e.SoftSwitches; import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField; import jace.config.ConfigurableField;
@ -8,8 +11,6 @@ import jace.core.Device;
import jace.core.RAMEvent; import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
import jace.core.Utility; import jace.core.Utility;
import java.util.Calendar;
import java.util.Optional;
import javafx.scene.control.Label; import javafx.scene.control.Label;
/** /**
@ -31,7 +32,7 @@ public class NoSlotClock extends Device {
public boolean patchProdosClock = false; public boolean patchProdosClock = false;
Optional<Label> clockIcon; Optional<Label> clockIcon;
private final RAMListener listener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { private final RAMListener listener = new RAMListener("No slot clock read", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0C100); setScopeStart(0x0C100);

View File

@ -52,8 +52,8 @@ public class ZipWarpAccelerator extends Device {
public ZipWarpAccelerator(Computer computer) { public ZipWarpAccelerator(Computer computer) {
super(computer); super(computer);
zipListener = computer.getMemory().observe(RAMEvent.TYPE.ANY, ENABLE_ADDR, SET_SPEED, this::handleZipChipEvent); zipListener = computer.getMemory().observe("Zip chip access", RAMEvent.TYPE.ANY, ENABLE_ADDR, SET_SPEED, this::handleZipChipEvent);
transwarpListener = computer.getMemory().observe(RAMEvent.TYPE.ANY, TRANSWARP, this::handleTranswarpEvent); transwarpListener = computer.getMemory().observe("Transwarp access", RAMEvent.TYPE.ANY, TRANSWARP, this::handleTranswarpEvent);
} }
private void handleZipChipEvent(RAMEvent e) { private void handleZipChipEvent(RAMEvent e) {
@ -190,7 +190,7 @@ public class ZipWarpAccelerator extends Device {
private void turnOffAcceleration() { private void turnOffAcceleration() {
// The UI Logic retains the user's desired normal speed, reset to that // The UI Logic retains the user's desired normal speed, reset to that
Emulator.logic.reconfigure(); Emulator.getUILogic().reconfigure();
} }
@Override @Override

View File

@ -63,6 +63,7 @@ public class LawlessComputer extends Apple2e {
if (showBootAnimation && PRODUCTION_MODE) { if (showBootAnimation && PRODUCTION_MODE) {
(new Thread(this::startAnimation)).start(); (new Thread(this::startAnimation)).start();
} else { } else {
cpu.setPaused(false);
finishColdStart(); finishColdStart();
} }
} }

View File

@ -47,14 +47,14 @@ public class LawlessHacks extends Cheats {
@Override @Override
public void registerListeners() { public void registerListeners() {
// Observe graphics changes // Observe graphics changes
addCheat(RAMEvent.TYPE.ANY, (e) -> { addCheat("Lawless Legends Graphics Modes", RAMEvent.TYPE.ANY, (e) -> {
int addr = e.getAddress(); int addr = e.getAddress();
if (addr >= MODE_SOFTSWITCH_MIN && e.getAddress() <= MODE_SOFTSWITCH_MAX) { if (addr >= MODE_SOFTSWITCH_MIN && e.getAddress() <= MODE_SOFTSWITCH_MAX) {
// System.out.println("Trapped " + e.getType().toString() + " to $" + Integer.toHexString(e.getAddress())); // System.out.println("Trapped " + e.getType().toString() + " to $" + Integer.toHexString(e.getAddress()));
setEngineByOrdinal(e.getAddress() - MODE_SOFTSWITCH_MIN); setEngineByOrdinal(e.getAddress() - MODE_SOFTSWITCH_MIN);
} }
}, MODE_SOFTSWITCH_MIN, MODE_SOFTSWITCH_MAX); }, MODE_SOFTSWITCH_MIN, MODE_SOFTSWITCH_MAX);
addCheat(RAMEvent.TYPE.WRITE, (e) -> { addCheat("Lawless Legends Music Commands", RAMEvent.TYPE.WRITE, (e) -> {
// System.out.println(Integer.toHexString(e.getAddress()) + " => " + Integer.toHexString(e.getNewValue() & 0x0ff)); // System.out.println(Integer.toHexString(e.getAddress()) + " => " + Integer.toHexString(e.getNewValue() & 0x0ff));
playSound(e.getNewValue()); playSound(e.getNewValue());
}, SFX_TRIGGER); }, SFX_TRIGGER);

View File

@ -157,7 +157,7 @@ public class MetacheatUI {
@FXML @FXML
void addCheat(ActionEvent event) { void addCheat(ActionEvent event) {
cheatEngine.addCheat(new DynamicCheat(0, "?")); cheatEngine.addCheat(new DynamicCheat(event.toString(), 0, "?"));
} }
@FXML @FXML
@ -412,7 +412,7 @@ public class MetacheatUI {
Label addCheat = new Label("Cheat >>"); Label addCheat = new Label("Cheat >>");
addCheat.setOnMouseClicked((mouseEvent) -> { addCheat.setOnMouseClicked((mouseEvent) -> {
Platform.runLater(() -> addCheat(addr, watch.getValue())); Platform.runLater(() -> addCheat("Memory View " + Integer.toHexString(addr), addr, watch.getValue()));
}); });
watch.getChildren().add(addCheat); watch.getChildren().add(addCheat);
@ -539,7 +539,7 @@ public class MetacheatUI {
Label addCheat = new Label("Cheat >>"); Label addCheat = new Label("Cheat >>");
addCheat.setOnMouseClicked((mouseEvent) -> { addCheat.setOnMouseClicked((mouseEvent) -> {
addCheat(addr, watch.getValue()); addCheat("Metacheat " + Integer.toHexString(addr), addr, watch.getValue());
}); });
addCheat.setTextFill(Color.WHITE); addCheat.setTextFill(Color.WHITE);
watch.getChildren().add(addCheat); watch.getChildren().add(addCheat);
@ -556,8 +556,8 @@ public class MetacheatUI {
return watch; return watch;
} }
private void addCheat(int addr, int val) { private void addCheat(String name, int addr, int val) {
cheatEngine.addCheat(new DynamicCheat(addr, String.valueOf(val))); cheatEngine.addCheat(new DynamicCheat(name, addr, String.valueOf(val)));
} }
int currentlyInspecting = 0; int currentlyInspecting = 0;

View File

@ -118,7 +118,7 @@ class Watch extends VBox {
holdListener = null; holdListener = null;
} else { } else {
value = Emulator.withComputer(c->c.getMemory().readRaw(address) & 0x0ff, 0); value = Emulator.withComputer(c->c.getMemory().readRaw(address) & 0x0ff, 0);
holdListener = outer.cheatEngine.forceValue(value, address); holdListener = outer.cheatEngine.forceValue("Watch force value", value, address);
} }
} }

View File

@ -0,0 +1,35 @@
package jace;
import java.util.concurrent.ExecutionException;
import jace.core.SoundMixer;
import jace.core.SoundMixer.SoundBuffer;
public class SoundTest {
// @Test
public void soundGenerationTest() {
try {
System.out.println("Performing sound test...");
System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer(null);
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++) {
// Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed
double x = Math.sin(i*i * 0.0001);
buffer.playSample((short) (Short.MAX_VALUE * x));
}
System.out.println("Closing buffer");
buffer.shutdown();
System.out.println("Deactivating sound");
mixer.detach();
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -14,17 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package jace.core; package jace.core;
import java.util.concurrent.atomic.AtomicInteger; import static jace.TestUtils.initComputer;
import static jace.TestUtils.runAssemblyCode;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import jace.Emulator; import jace.Emulator;
import static jace.TestUtils.initComputer;
import static jace.TestUtils.runAssemblyCode;
import jace.apple2e.MOS65C02; import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k; import jace.apple2e.RAM128k;
@ -50,7 +51,7 @@ public class MemoryListenerTest {
@Test @Test
public void testListenerRelevance() { public void testListenerRelevance() {
AtomicInteger anyEventCaught = new AtomicInteger(); AtomicInteger anyEventCaught = new AtomicInteger();
RAMListener anyListener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { RAMListener anyListener = new RAMListener("Execution test", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0100); setScopeStart(0x0100);
@ -63,7 +64,7 @@ public class MemoryListenerTest {
}; };
AtomicInteger readAnyEventCaught = new AtomicInteger(); AtomicInteger readAnyEventCaught = new AtomicInteger();
RAMListener readAnyListener = new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { RAMListener readAnyListener = new RAMListener("Execution test 1", RAMEvent.TYPE.READ, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0100); setScopeStart(0x0100);
@ -76,7 +77,7 @@ public class MemoryListenerTest {
}; };
AtomicInteger writeEventCaught = new AtomicInteger(); AtomicInteger writeEventCaught = new AtomicInteger();
RAMListener writeListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { RAMListener writeListener = new RAMListener("Execution test 2", RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0100); setScopeStart(0x0100);
@ -89,7 +90,7 @@ public class MemoryListenerTest {
}; };
AtomicInteger executeEventCaught = new AtomicInteger(); AtomicInteger executeEventCaught = new AtomicInteger();
RAMListener executeListener = new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { RAMListener executeListener = new RAMListener("Execution test 3", RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override @Override
protected void doConfig() { protected void doConfig() {
setScopeStart(0x0100); setScopeStart(0x0100);

View File

@ -15,17 +15,18 @@
*/ */
package jace.cpu; package jace.cpu;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import org.junit.BeforeClass;
import org.junit.Test;
import jace.Emulator;
import static jace.TestUtils.assemble; import static jace.TestUtils.assemble;
import static jace.TestUtils.createSimpleDevice; import static jace.TestUtils.createSimpleDevice;
import static jace.TestUtils.initComputer; import static jace.TestUtils.initComputer;
import static jace.TestUtils.runAssemblyCode; import static jace.TestUtils.runAssemblyCode;
import static org.junit.Assert.assertEquals;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.BeforeClass;
import org.junit.Test;
import jace.Emulator;
import jace.apple2e.MOS65C02; import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k; import jace.apple2e.RAM128k;
import jace.core.Computer; import jace.core.Computer;
@ -90,7 +91,7 @@ WAIT3 sbc #$01
AtomicInteger breakpointEncountered = new AtomicInteger(); AtomicInteger breakpointEncountered = new AtomicInteger();
AtomicInteger cycleCount = new AtomicInteger(); AtomicInteger cycleCount = new AtomicInteger();
// This listener will increment our breakpoint counter if it reaches our desired stoppoing point in time // This listener will increment our breakpoint counter if it reaches our desired stoppoing point in time
RAMListener l = ram.addExecutionTrap(0x01000, e -> breakpointEncountered.incrementAndGet()); RAMListener l = ram.addExecutionTrap("Halting test", 0x01000, e -> breakpointEncountered.incrementAndGet());
// This faux device counts the number of cycles executed // This faux device counts the number of cycles executed
cpu.addChildDevice(createSimpleDevice(()->{ cpu.addChildDevice(createSimpleDevice(()->{
if (breakpointEncountered.get() == 0) { if (breakpointEncountered.get() == 0) {
@ -116,7 +117,7 @@ WAIT3 sbc #$01
AtomicInteger breakpointEncountered = new AtomicInteger(); AtomicInteger breakpointEncountered = new AtomicInteger();
AtomicInteger cycleCount = new AtomicInteger(); AtomicInteger cycleCount = new AtomicInteger();
// This listener will increment our breakpoint counter if it reaches our desired stoppoing point in time // This listener will increment our breakpoint counter if it reaches our desired stoppoing point in time
RAMListener l = ram.addExecutionTrap(0x01000, e -> breakpointEncountered.incrementAndGet()); RAMListener l = ram.addExecutionTrap("Halting test", 0x01000, e -> breakpointEncountered.incrementAndGet());
// This faux device counts the number of cycles executed // This faux device counts the number of cycles executed
cpu.addChildDevice(createSimpleDevice(()->{ cpu.addChildDevice(createSimpleDevice(()->{