MOCKINGBOARD WORKS AGAIN!

This commit is contained in:
Brendan Robert 2024-02-15 11:34:16 -06:00
parent 3cd8a2d276
commit 856a660fde
6 changed files with 143 additions and 267 deletions

View File

@ -100,7 +100,6 @@ public abstract class Card extends Device {
@Override
public void reconfigure() {
//super.reconfigure();
// Emulator.whileSuspended(c-> {
unregisterListeners();
registerListeners();

View File

@ -129,7 +129,9 @@ public abstract class Device implements Reconfigurable {
private void __doTickIsRunning() {
for (Device d : childrenArray) {
d.doTick();
if (d.isRunning() && !d.isPaused()) {
d.doTick();
}
}
if (waitCycles <= 0) {
tick();

View File

@ -43,9 +43,9 @@ public abstract class RAM implements Reconfigurable {
public PagedMemory activeRead;
public PagedMemory activeWrite;
private Set<RAMListener> listeners;
private Set<RAMListener>[] listenerMap;
private Set<RAMListener>[] ioListenerMap;
private final Set<RAMListener> listeners;
private final Set<RAMListener>[] listenerMap;
private final Set<RAMListener>[] ioListenerMap;
public Optional<Card>[] cards;
// card 0 = 80 column card firmware / system rom
public int activeSlot = 0;
@ -58,6 +58,8 @@ public abstract class RAM implements Reconfigurable {
@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();
@ -202,10 +204,12 @@ public abstract class RAM implements Reconfigurable {
}
}
@SuppressWarnings("unchecked")
private void refreshListenerMap() {
listenerMap = (Set<RAMListener>[]) new Set[256];
ioListenerMap = (Set<RAMListener>[]) new Set[256];
// Wipe out existing maps
for (int i = 0; i < 256; i++) {
listenerMap[i] = null;
ioListenerMap[i] = null;
}
listeners.forEach(this::addListenerRange);
}

View File

@ -19,11 +19,6 @@
package jace.hardware;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -31,8 +26,6 @@ import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.RAMListener;
@ -50,7 +43,7 @@ import jace.hardware.mockingboard.R6522;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Mockingboard")
public class CardMockingboard extends Card implements Runnable {
public class CardMockingboard extends Card {
// If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips
@ConfigurableField(name = "Volume", shortName = "vol",
@ -67,7 +60,6 @@ public class CardMockingboard extends Card implements Runnable {
defaultValue = "1020484",
description = "Clock rate of AY oscillators")
public int CLOCK_SPEED = 1020484;
public int SAMPLE_RATE = 48000;
@ConfigurableField(name = "Buffer size",
category = "Sound",
description = "Number of samples to generate on each pass")
@ -76,13 +68,14 @@ public class CardMockingboard extends Card implements Runnable {
public PSG[] chips;
// The 6522 controllr chips (always 2)
public R6522[] controllers;
static private int ticksBetweenPlayback = 200;
Lock timerSync = new ReentrantLock();
Condition cpuCountReached = timerSync.newCondition();
Condition playbackFinished = timerSync.newCondition();
@ConfigurableField(name = "Idle sample threshold", description = "Number of samples to wait before suspending sound")
private final int MAX_IDLE_SAMPLES = SAMPLE_RATE;
int[] left, right;
SoundBuffer buffer;
int ticksBetweenPlayback = 24;
int MAX_IDLE_TICKS = 1000000;
boolean activatedAfterReset = false;
boolean debug = false;
@Override
public String getDeviceName() {
return "Mockingboard";
@ -90,13 +83,14 @@ public class CardMockingboard extends Card implements Runnable {
public CardMockingboard() {
super();
activatedAfterReset = false;
left = new int[BUFFER_LENGTH];
right = new int[BUFFER_LENGTH];
controllers = new R6522[2];
for (int i = 0; i < 2; i++) {
// has to be final to be used inside of anonymous class below
final int j = i;
controllers[i] = new R6522() {
final int controller = j;
@Override
public void sendOutputA(int value) {
chips[j].setBus(value);
@ -132,39 +126,25 @@ public class CardMockingboard extends Card implements Runnable {
@Override
public String getShortName() {
return "timer" + j;
}
public void tick() {
super.tick();
if (controller == 0) {
doSoundTick();
}
}
}
};
addChildDevice(controllers[i]);
}
}
@Override
public void reset() {
activatedAfterReset = false;
suspend();
}
RAMListener mainListener = null;
boolean heatbeatUnclocked = false;
long heartbeatReclockTime = 0L;
long unclockTime = 5000L;
private void setUnclocked(boolean unclocked) {
heatbeatUnclocked = unclocked;
for (R6522 controller : controllers) {
controller.setUnclocked(unclocked);
}
heartbeatReclockTime = System.currentTimeMillis() + unclockTime;
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
resume();
if (chips == null) {
reconfigure();
}
int chip = 0;
for (PSG psg : chips) {
if (psg.getBaseReg() == (register & 0x0f0)) {
@ -181,12 +161,19 @@ public class CardMockingboard extends Card implements Runnable {
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
e.setNewValue(val);
// System.out.println("Read "+Integer.toHexString(register)+" == "+val);
if (debug) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
// System.out.println("Write "+Integer.toHexString(register)+" == "+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();
}
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
@ -195,75 +182,66 @@ public class CardMockingboard extends Card implements Runnable {
Emulator.withVideo(v->e.setNewValue(v.getFloatingBus()));
}
long ticksSinceLastPlayback = 0;
long idleTicks = 0;
@Override
public void tick() {
if (heatbeatUnclocked) {
if (System.currentTimeMillis() - heartbeatReclockTime >= unclockTime) {
setUnclocked(false);
} else {
for (R6522 c : controllers) {
if (c == null || !c.isRunning()) {
continue;
}
c.doTick();
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBetweenPlayback) {
ticksSinceLastPlayback -= ticksBetweenPlayback;
if (playSound(left, right)) {
idleTicks = 0;
} else {
idleTicks += ticksBetweenPlayback;
}
}
} catch (InterruptedException | ExecutionException | SoundError | NullPointerException ex) {
Logger.getLogger(CardMockingboard.class.getName()).log(Level.SEVERE, "Mockingboard playback encountered fatal exception", ex);
suspend();
// Do nothing, probably suspending CPU
}
}
public boolean isRunning() {
return super.isRunning() && playbackThread != null && playbackThread.isAlive();
}
private void doSoundTick() {
if (isRunning() && !pause) {
// buildMixerTable();
timerSync.lock();
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBetweenPlayback) {
cpuCountReached.signalAll();
while (isRunning() && ticksSinceLastPlayback >= ticksBetweenPlayback) {
if (!playbackFinished.await(1, TimeUnit.SECONDS)) {
// gripe("The mockingboard playback thread has stalled. Disabling mockingboard.");
suspendSound();
}
}
}
} catch (InterruptedException ex) {
suspend();
// Do nothing, probably suspending CPU
} finally {
timerSync.unlock();
}
if (idleTicks >= MAX_IDLE_TICKS) {
suspend();
}
}
@Override
public void reconfigure() {
boolean restart = suspend();
initPSG();
for (PSG chip : chips) {
chip.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SAMPLE_RATE);
chip.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
chip.reset();
}
long motherboardSpeed = Emulator.withComputer(c->c.getMotherboard().getSpeedInHz(), 1L);
ticksBetweenPlayback = (int) ((motherboardSpeed * BUFFER_LENGTH) / SoundMixer.RATE);
buildMixerTable();
super.reconfigure();
if (restart) {
resume();
}
}
///////////////////////////////////////////////////////////
public static int[] VolTable;
public void playSound(int[] left, int[] right) {
public boolean playSound(int[] left, int[] right) throws InterruptedException, ExecutionException, SoundError {
if (buffer == null) {
return false;
}
chips[0].update(left, true, left, false, left, false, BUFFER_LENGTH);
chips[1].update(right, true, right, false, right, false, BUFFER_LENGTH);
if (phasorMode) {
chips[2].update(left, false, left, false, left, false, BUFFER_LENGTH);
chips[3].update(right, false, right, false, right, false, BUFFER_LENGTH);
}
boolean nonZeroSamples = false;
for (int i=0; i < BUFFER_LENGTH; i++) {
buffer.playSample((short) left[i]);
buffer.playSample((short) right[i]);
if (left[i] != 0 || right[i] != 0) {
nonZeroSamples = true;
}
}
return nonZeroSamples;
}
public void buildMixerTable() {
@ -288,176 +266,61 @@ public class CardMockingboard extends Card implements Runnable {
VolTable[0] = 0;
}
Thread playbackThread = null;
boolean pause = false;
@Override
public void resume() {
pause = false;
if (!activatedAfterReset) {
// Do not re-activate until firmware access was made
return;
}
if (buffer == null || !buffer.isAlive()) {
try {
buffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) {
System.out.println("Error whhen trying to create sound buffer for Mockingboard: " + e.getMessage());
e.printStackTrace();
suspend();
}
}
if (chips == null) {
initPSG();
for (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SAMPLE_RATE);
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
psg.reset();
}
}
if (!isRunning()) {
setUnclocked(true);
for (R6522 controller : controllers) {
controller.attach();
controller.resume();
}
}
idleTicks = 0;
setPaused(false);
reconfigure();
super.resume();
if (playbackThread == null || !playbackThread.isAlive()) {
playbackThread = new Thread(this, "Mockingboard sound playback");
playbackThread.start();
}
}
@Override
public boolean suspend() {
super.suspend();
for (R6522 controller : controllers) {
controller.suspend();
controller.detach();
if (buffer != null) {
try {
buffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
System.out.println("Error when trying to shutdown sound buffer for Mockingboard: " + e.getMessage());
e.printStackTrace();
} finally {
buffer = null;
}
}
return suspendSound();
return super.suspend();
}
public boolean suspendSound() {
setRun(false);
if (playbackThread == null || !playbackThread.isAlive()) {
return false;
}
if (playbackThread != null) {
try {
playbackThread.join(500);
} catch (InterruptedException ex) {
}
}
playbackThread = null;
return true;
}
@Override
/**
* This is the audio playback thread
*/
public void run() {
SoundBuffer buffer;
try {
buffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
setRun(false);
return;
}
try {
if (buffer == null) {
setRun(false);
return;
}
System.out.println("Mockingboard playback started");
int bufferSize = SoundMixer.BUFFER_SIZE;
int[] left = new int[bufferSize];
int[] right = new int[bufferSize];
buildMixerTable();
ticksBetweenPlayback = (int) ((Motherboard.DEFAULT_SPEED * BUFFER_LENGTH) / SAMPLE_RATE);
System.out.println("Ticks between playback: "+ticksBetweenPlayback);
ticksSinceLastPlayback = 0;
int zeroSamples = 0;
setRun(true);
LockSupport.parkNanos(5000);
while (isRunning() && !Thread.interrupted()) {
while (isRunning() && !Emulator.withComputer(Computer::isRunning, false)) {
Thread.sleep(1000);
}
if (isRunning() && !Thread.interrupted()) {
playSound(left, right);
try {
for (int i=0; i < bufferSize; i++) {
buffer.playSample((short) left[i]);
buffer.playSample((short) right[i]);
}
timerSync.lock();
ticksSinceLastPlayback -= ticksBetweenPlayback;
} catch (ExecutionException | SoundError e) {
Logger.getLogger(CardMockingboard.class.getName()).log(Level.SEVERE, "Mockingboard playback encountered fatal exception", e);
try {
buffer.shutdown();
} catch (ExecutionException | SoundError e1) {
// Ignore shutdown errors, we're already reporting a fatal error
}
buffer=null;
setRun(false);
break;
} finally {
timerSync.unlock();
}
if (zeroSamples >= MAX_IDLE_SAMPLES) {
zeroSamples = 0;
pause = true;
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
while (pause && isRunning()) {
try {
Thread.sleep(50);
timerSync.lock();
playbackFinished.signalAll();
} catch (InterruptedException ex) {
return;
} catch (IllegalMonitorStateException ex) {
// Do nothing
} finally {
try {
timerSync.unlock();
} catch (IllegalMonitorStateException ex) {
// Do nothing -- this is probably caused by a suspension event
}
}
}
}
try {
timerSync.lock();
playbackFinished.signalAll();
while (isRunning() && ticksSinceLastPlayback < ticksBetweenPlayback) {
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
cpuCountReached.await();
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
}
} catch (InterruptedException ex) {
// Do nothing, probably killing playback thread on purpose
} finally {
timerSync.unlock();
}
}
}
} catch (InterruptedException ex) {
Logger.getLogger(CardMockingboard.class.getName()).log(Level.SEVERE, null, ex);
} finally {
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
System.out.println("Mockingboard playback stopped");
if (buffer != null && buffer.isAlive()) {
try {
buffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
// Ignore errors during shutdown
}
}
}
}
private void initPSG() {
if (phasorMode) {
chips = new PSG[4];
chips[0] = new PSG(0x10, CLOCK_SPEED * 2, SAMPLE_RATE, "AY1", 8);
chips[1] = new PSG(0x80, CLOCK_SPEED * 2, SAMPLE_RATE, "AY2", 8);
chips[2] = new PSG(0x10, CLOCK_SPEED * 2, SAMPLE_RATE, "AY3", 16);
chips[3] = new PSG(0x80, CLOCK_SPEED * 2, SAMPLE_RATE, "AY4", 16);
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 {
chips = new PSG[2];
chips[0] = new PSG(0, CLOCK_SPEED, SAMPLE_RATE, "AY1", 255);
chips[1] = new PSG(0x80, CLOCK_SPEED, SAMPLE_RATE, "AY2", 255);
chips[0] = new PSG(0, CLOCK_SPEED, SoundMixer.RATE, "AY1", 255);
chips[1] = new PSG(0x80, CLOCK_SPEED, SoundMixer.RATE, "AY2", 255);
}
}
@ -467,8 +330,8 @@ public class CardMockingboard extends Card implements Runnable {
}
// This fixes freezes when resizing the window, etc.
@Override
public boolean suspendWithCPU() {
return true;
}
// @Override
// public boolean suspendWithCPU() {
// return true;
// }
}

View File

@ -33,7 +33,7 @@ import java.util.Map;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class PSG {
boolean debug = false;
int baseReg;
/* register ids */
@ -129,22 +129,22 @@ public class PSG {
public void setControl(int c) {
BusControl cmd = BusControl.fromInt(c);
if (cmd == null) {
// System.out.println("Bad control param "+c);
if (debug) System.out.println("Bad control param "+c);
return;
}
switch (cmd) {
case inactive:
break;
case latch:
// System.out.println("PSG latched register "+selectedReg);
if (debug) System.out.println("PSG latched register "+selectedReg);
selectedReg = bus & 0x0f;
break;
case read:
bus = getReg(Reg.get(selectedReg));
// System.out.println("PSG read register "+selectedReg + " == "+bus);
if (debug) System.out.println("PSG read register "+selectedReg + " == "+bus);
break;
case write:
// System.out.println("PSG wrote register "+selectedReg + " == "+bus);
if (debug) System.out.println("PSG wrote register "+selectedReg + " == "+bus);
setReg(Reg.get(selectedReg), bus);
break;
}

View File

@ -19,14 +19,14 @@
package jace.hardware.mockingboard;
import jace.Emulator;
import jace.core.TimedDevice;
import jace.core.Device;
/**
* Implementation of 6522 VIA chip
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class R6522 extends TimedDevice {
public abstract class R6522 extends Device {
public static long SPEED = 1020484L; // (NTSC)
public R6522() {
@ -35,14 +35,14 @@ public abstract class R6522 extends TimedDevice {
timer1running = true;
timer1latch = 0x1fff;
timer1interruptEnabled = false;
setSpeedInHz(SPEED);
setRun(true);
// setSpeedInHz(SPEED);
// setRun(true);
}
@Override
public long defaultCyclesPerSecond() {
return SPEED;
}
// @Override
// public long defaultCyclesPerSecond() {
// return SPEED;
// }
// 6522 VIA
// http://www.applevault.com/twiki/Main/Mockingboard/6522.pdf
@ -141,7 +141,8 @@ public abstract class R6522 extends TimedDevice {
public int timer2counter = 0;
public boolean timer2running = false;
public boolean unclocked = false;
public boolean debug = false;
@Override
protected String getDeviceName() {
return "6522 VIA Chip";
@ -149,16 +150,18 @@ public abstract class R6522 extends TimedDevice {
@Override
public void tick() {
if (!unclocked) {
// 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) {
// System.out.println("Timer 1 generated interrupt");
if (debug) System.out.println("Timer 1 generated interrupt");
timer1IRQ = true;
Emulator.withComputer(c->c.getCpu().generateInterrupt());
}
@ -166,19 +169,23 @@ public abstract class R6522 extends TimedDevice {
}
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) {
setRun(false);
if (debug) System.out.println("No timers active, suspending");
suspend();
}
}
// }
}
public void setUnclocked(boolean unclocked) {
@ -198,7 +205,7 @@ public abstract class R6522 extends TimedDevice {
public void writeRegister(int reg, int val) {
int value = val & 0x0ff;
Register r = Register.fromInt(reg);
// System.out.println("Writing "+(value&0x0ff)+" to register "+r.toString());
if (debug) System.out.println(getShortName() + " Writing "+Integer.toHexString(value&0x0ff)+" to register "+r.toString());
switch (r) {
case ORB:
if (dataDirectionB == 0) {
@ -228,7 +235,6 @@ public abstract class R6522 extends TimedDevice {
timer1IRQ = false;
timer1counter = timer1latch;
timer1running = true;
setRun(true);
break;
case T1LH:
timer1latch = (timer1latch & 0x0ff) | (value << 8);
@ -242,7 +248,6 @@ public abstract class R6522 extends TimedDevice {
timer2IRQ = false;
timer2counter = timer2latch;
timer2running = true;
setRun(true);
break;
case SR:
// SHIFT REGISTER NOT IMPLEMENTED
@ -252,7 +257,6 @@ public abstract class R6522 extends TimedDevice {
timer1freerun = (value & 64) != 0;
if (timer1freerun) {
timer1running = true;
setRun(true);
}
break;
case PCR:
@ -277,6 +281,10 @@ public abstract class R6522 extends TimedDevice {
break;
default:
}
if (timer1running || timer2running) {
if (debug) System.out.println("One or more timers active, resuming");
resume();
}
}
// Whatever uses 6522 will want to know when it is outputting values
@ -287,7 +295,7 @@ public abstract class R6522 extends TimedDevice {
public int readRegister(int reg) {
Register r = Register.fromInt(reg);
// System.out.println("Reading register "+r.toString());
if (debug) System.out.println(getShortName() + " Reading register "+r.toString());
switch (r) {
case ORB -> {
if (dataDirectionB == 0x0ff) {