diff --git a/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/ZxSpectrumBusImpl.java b/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/ZxSpectrumBusImpl.java
index aec3cc459..8380240ab 100644
--- a/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/ZxSpectrumBusImpl.java
+++ b/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/ZxSpectrumBusImpl.java
@@ -18,6 +18,7 @@
*/
package net.emustudio.plugins.device.zxspectrum.bus;
+import net.emustudio.emulib.plugins.cpu.CPUContext;
import net.emustudio.emulib.plugins.cpu.TimedEventsProcessor;
import net.emustudio.emulib.plugins.memory.AbstractMemoryContext;
import net.emustudio.emulib.plugins.memory.MemoryContext;
@@ -27,15 +28,17 @@
import net.emustudio.plugins.device.zxspectrum.bus.api.ZxSpectrumBus;
import net.jcip.annotations.NotThreadSafe;
-import java.util.*;
-import java.util.function.Consumer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
/**
* ZX Spectrum bus (for 48K ZX spectrum).
*
* Adds memory & I/O port contention.
*
- * https://sinclair.wiki.zxnet.co.uk/wiki/Contended_memory#Timing_differences
+ * ZX Spectrum48 timing
* On the 16K and 48K models of ZX Spectrum, the memory from 0x4000 to 0x7fff is contended. If the contended
* memory is accessed 14335[2] or 14336 tstates after an interrupt (see the timing differences section below
* for information on the 14335/14336 issue), the Z80 will be delayed for 6 tstates. After 14336 tstates,
@@ -76,37 +79,56 @@
* Yes | Set | C:1, C:1, C:1, C:1
*/
@NotThreadSafe
-public class ZxSpectrumBusImpl extends AbstractMemoryContext implements ZxSpectrumBus {
- private final static int SCREEN_LINES = 192;
- private final static int CONTENTION_TSTATE_START = 14335;
- private static final int CPU_INTERRUPT_TSTATES = 69888;
+public class ZxSpectrumBusImpl extends AbstractMemoryContext implements ZxSpectrumBus, CPUContext.PassedCyclesListener {
+ private static final long LINE_TSTATES = 224;
+
+ // from 14335 to 14463, then 96 tstates pause to reach "end of line", then repeat.
+ private final static Map CONTENTION_MAP = new HashMap<>();
+
+ static {
+ CONTENTION_MAP.put(14335L, 6);
+ CONTENTION_MAP.put(14336L, 5);
+ CONTENTION_MAP.put(14337L, 4);
+ CONTENTION_MAP.put(14338L, 3);
+ CONTENTION_MAP.put(14339L, 2);
+ CONTENTION_MAP.put(14340L, 1);
+ CONTENTION_MAP.put(14343L, 6);
+ CONTENTION_MAP.put(14344L, 5);
+ CONTENTION_MAP.put(14345L, 4);
+ CONTENTION_MAP.put(14346L, 3);
+ CONTENTION_MAP.put(14347L, 2);
+ CONTENTION_MAP.put(14348L, 1);
+ CONTENTION_MAP.put(14351L, 6);
+ CONTENTION_MAP.put(14352L, 5);
+ CONTENTION_MAP.put(14353L, 4);
+ CONTENTION_MAP.put(14354L, 3);
+ CONTENTION_MAP.put(14355L, 2);
+ CONTENTION_MAP.put(14356L, 1);
+ CONTENTION_MAP.put(14359L, 6);
+ CONTENTION_MAP.put(14360L, 5);
+ CONTENTION_MAP.put(14361L, 4);
+ CONTENTION_MAP.put(14362L, 3);
+ CONTENTION_MAP.put(14363L, 2);
+ // CONTENTION_MAP.put(14364L, 1);
+ }
private ContextZ80 cpu;
private MemoryContext memory;
private volatile byte busData; // data on the bus
- private TimedEventsProcessor tep;
- private boolean isContended = false;
+
+ private long contentionCycles;
private final Map deferredAttachments = new HashMap<>();
public void initialize(ContextZ80 cpu, MemoryContext memory) {
this.cpu = Objects.requireNonNull(cpu);
this.memory = Objects.requireNonNull(memory);
- this.tep = cpu.getTimedEventsProcessor().orElseThrow(() -> new RuntimeException("CPU must provide TimedEventProcessor"));
for (Map.Entry attachment : deferredAttachments.entrySet()) {
if (!cpu.attachDevice(attachment.getKey(), new ContendedDeviceProxy(attachment.getValue()))) {
throw new RuntimeException("Could not attach device " + attachment.getValue().getName() + " to CPU");
}
}
-
- // 17 x from 14335 to 14463, then 96 tstates pause to reach "end of line", then repeat.
- // the t-state 14335 is the first screen line to be read.
- int cycles = CONTENTION_TSTATE_START + CPU_INTERRUPT_TSTATES;
- for (int line = 0; line < SCREEN_LINES; line++) {
- scheduleMemoryContention(cycles);
- cycles = cycles + 17 * 8 + 96 - 1;
- }
}
@@ -147,6 +169,16 @@ public void writeMemoryNotContended(int location, byte data) {
memory.write(location, data);
}
+ @Override
+ public void addPassedCyclesListener(CPUContext.PassedCyclesListener passedCyclesListener) {
+ cpu.addPassedCyclesListener(passedCyclesListener);
+ }
+
+ @Override
+ public void removePassedCyclesListener(CPUContext.PassedCyclesListener passedCyclesListener) {
+ cpu.removePassedCyclesListener(passedCyclesListener);
+ }
+
@Override
public Byte readData() {
return busData;
@@ -159,25 +191,25 @@ public void writeData(Byte data) {
@Override
public Byte read(int location) {
- setContended(location);
+ contendedMemory(location);
return memory.read(location);
}
@Override
public Byte[] read(int location, int count) {
- setContended(location);
+ contendedMemory(location);
return memory.read(location, count);
}
@Override
public void write(int location, Byte data) {
- setContended(location);
+ contendedMemory(location);
memory.write(location, data);
}
@Override
public void write(int location, Byte[] data, int count) {
- setContended(location);
+ contendedMemory(location);
memory.write(location, data, count);
}
@@ -206,25 +238,71 @@ public MemoryContextAnnotations annotations() {
return memory.annotations();
}
- private void setContended(int location) {
- this.isContended = location >= 0x4000 && location <= 0x7FFF;
+ private void contendedMemory(int location) {
+ if (location >= 0x4000 && location <= 0x7FFF) {
+ Integer cycles = CONTENTION_MAP.get(contentionCycles);
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ }
}
- private void scheduleMemoryContention(int startCycles) {
- // 17 x from 14335 to 14463, then 96 tstates pause to reach "end of line", then repeat.
- Runnable slowDownByOneCycle = () -> {
- if (isContended) {
- cpu.addCycles(1);
+ private void contendedPort(int portAddress) {
+ // High byte | |
+ // in 40 - 7F? | Low bit | Contention pattern
+ // ------------+---------+-------------------
+ // No | Reset | N:1, C:3
+ // No | Set | N:4
+ // Yes | Reset | C:1, C:3
+ // Yes | Set | C:1, C:1, C:1, C:1
+
+ if (portAddress >= 0x4000 && portAddress <= 0x7FFF) {
+ // after this, CPU adds 4 cycles for I/O.
+ if ((portAddress & 1) == 0) {
+ // Yes | Reset | C:1, C:3
+ Integer cycles = CONTENTION_MAP.get(contentionCycles); // at C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ cycles = CONTENTION_MAP.get(contentionCycles + 1); // after C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ } else {
+ // Yes | Set | C:1, C:1, C:1, C:1
+ Integer cycles = CONTENTION_MAP.get(contentionCycles); // at C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ cycles = CONTENTION_MAP.get(contentionCycles + 1); // 2x at C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ cycles = CONTENTION_MAP.get(contentionCycles + 2); // 3x at C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
+ cycles = CONTENTION_MAP.get(contentionCycles + 3); // after 3x at C:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
}
- };
-
- for (int i = 0; i < 17; i++) {
- for (int j = 0; j < 6; j++) {
- tep.schedule(startCycles + i * 8 + j, slowDownByOneCycle);
+ } else {
+ // No | Reset | N:1, C:3
+ if ((portAddress & 1) == 0) {
+ Integer cycles = CONTENTION_MAP.get(contentionCycles + 1); // after N:1
+ if (cycles != null) {
+ cpu.addCycles(cycles);
+ }
}
}
}
+ @Override
+ public void passedCycles(long tstates) {
+ contentionCycles = (contentionCycles + tstates) % (LINE_TSTATES + 14335);
+ }
+
private class ContendedDeviceProxy implements Context8080.CpuPortDevice {
private final Context8080.CpuPortDevice device;
@@ -234,15 +312,13 @@ private ContendedDeviceProxy(Context8080.CpuPortDevice device) {
@Override
public byte read(int portAddress) {
- setContended(portAddress);
- // TODO: portAddress & 1 == 0 ==> contended for 3 cycles only
+ contendedPort(portAddress);
return device.read(portAddress);
}
@Override
public void write(int portAddress, byte data) {
- setContended(portAddress);
- // TODO: portAddress & 1 == 0 ==> contended for 3 cycles only
+ contendedPort(portAddress);
device.write(portAddress, data);
}
diff --git a/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/api/ZxSpectrumBus.java b/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/api/ZxSpectrumBus.java
index 1b63267c0..14f427099 100644
--- a/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/api/ZxSpectrumBus.java
+++ b/plugins/device/zxspectrum-bus/src/main/java/net/emustudio/plugins/device/zxspectrum/bus/api/ZxSpectrumBus.java
@@ -19,6 +19,7 @@
package net.emustudio.plugins.device.zxspectrum.bus.api;
import net.emustudio.emulib.plugins.annotations.PluginContext;
+import net.emustudio.emulib.plugins.cpu.CPUContext;
import net.emustudio.emulib.plugins.cpu.TimedEventsProcessor;
import net.emustudio.emulib.plugins.device.DeviceContext;
import net.emustudio.emulib.plugins.memory.MemoryContext;
@@ -86,4 +87,8 @@ public interface ZxSpectrumBus extends DeviceContext, MemoryContext
* @param data data to write
*/
void writeMemoryNotContended(int location, byte data);
+
+ void addPassedCyclesListener(CPUContext.PassedCyclesListener passedCyclesListener);
+
+ void removePassedCyclesListener(CPUContext.PassedCyclesListener passedCyclesListener);
}
diff --git a/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/ULA.java b/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/ULA.java
index acf2ebd59..953ac857b 100644
--- a/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/ULA.java
+++ b/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/ULA.java
@@ -18,14 +18,12 @@
*/
package net.emustudio.plugins.device.zxspectrum.ula;
-import net.emustudio.emulib.plugins.cpu.TimedEventsProcessor;
import net.emustudio.plugins.cpu.intel8080.api.Context8080;
import net.emustudio.plugins.device.zxspectrum.bus.api.ZxSpectrumBus;
import net.emustudio.plugins.device.zxspectrum.ula.gui.Keyboard;
import java.awt.event.KeyEvent;
import java.util.Arrays;
-import java.util.NoSuchElementException;
import java.util.Objects;
/**
@@ -119,12 +117,6 @@ public void triggerInterrupt() {
bus.signalInterrupt(RST_7);
}
- public TimedEventsProcessor getTimedEventsProcessor() {
- return bus
- .getTimedEventsProcessor()
- .orElseThrow(() -> new NoSuchElementException("The CPU does not provide TimedEventProcessor"));
- }
-
public void readLine(int y) {
for (int x = 0; x < SCREEN_WIDTH; x++) {
videoMemory[x][y] = bus.readMemoryNotContended(0x4000 + lineStartOffsets[y] + x);
@@ -140,6 +132,10 @@ public int getBorderColor() {
return borderColor;
}
+ public ZxSpectrumBus getBus() {
+ return bus;
+ }
+
@Override
public byte read(int portAddress) {
// A zero in one of the five lowest bits means that the corresponding key is pressed.
diff --git a/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/gui/DisplayCanvas.java b/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/gui/DisplayCanvas.java
index d0f8da6ce..9fc77b732 100644
--- a/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/gui/DisplayCanvas.java
+++ b/plugins/device/zxspectrum-ula/src/main/java/net/emustudio/plugins/device/zxspectrum/ula/gui/DisplayCanvas.java
@@ -18,7 +18,7 @@
*/
package net.emustudio.plugins.device.zxspectrum.ula.gui;
-import net.emustudio.emulib.plugins.cpu.TimedEventsProcessor;
+import net.emustudio.emulib.plugins.cpu.CPUContext;
import net.emustudio.plugins.device.zxspectrum.ula.ULA;
import java.awt.*;
@@ -39,7 +39,7 @@
* A frame is (64+192+56)*224=69888 T states long, which means that the '50 Hz' interrupt is actually
* a 3.5MHz/69888=50.08 Hz interrupt.
*/
-public class DisplayCanvas extends Canvas implements AutoCloseable {
+public class DisplayCanvas extends Canvas implements AutoCloseable, CPUContext.PassedCyclesListener {
private static final int PRE_SCREEN_LINES = 64;
private static final int POST_SCREEN_LINES = 56;
private static final int BORDER_WIDTH = 48; // pixels
@@ -48,8 +48,18 @@ public class DisplayCanvas extends Canvas implements AutoCloseable {
public static final int SCREEN_IMAGE_WIDTH = 2 * BORDER_WIDTH + SCREEN_WIDTH * 8;
public static final int SCREEN_IMAGE_HEIGHT = PRE_SCREEN_LINES + SCREEN_HEIGHT + POST_SCREEN_LINES;
- private static final int REPAINT_CPU_TSTATES = 69888;
- private static final int LINE_CPU_TSTATES = 224;
+ private static final long FRAME_CPU_TSTATES = 69888;
+ private static final long LINE_CPU_TSTATES = 224;
+
+ // After an interrupt occurs, 64 line times (14336 T states; see below for exact timings) pass before
+ // the first byte of the screen (16384) is displayed. At least the last 48 of these are actual
+ // border-lines; the others may be either border or vertical retrace.
+ //
+ //Then the 192 screen+border lines are displayed, followed by 56 border lines again. Note that this
+ // means that a frame is (64+192+56)*224=69888 T states long, which means that the '50 Hz' interrupt is actually a 3.5MHz/69888=50.08 Hz interrupt. This fact can be seen by taking a clock program, and running it for an hour, after which it will be the expected 6 seconds fast. However, on a real Spectrum, the frequency of the interrupt varies slightly as the Spectrum gets hot; the reason for this is unknown, but placing a cooler onto the ULA has been observed to remove this effect.
+ private long frameCycleCounter = 0;
+ private long lineCycleCounter = 0;
+ private int lastLinePainted = 0;
private final BufferedImage screenImage = new BufferedImage(
SCREEN_IMAGE_WIDTH, SCREEN_IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
@@ -81,13 +91,12 @@ public class DisplayCanvas extends Canvas implements AutoCloseable {
private volatile Dimension size = new Dimension(0, 0);
private final ULA ula;
- private final TimedEventsProcessor tep;
private final PaintCycle paintCycle = new PaintCycle();
private int interrupts = 0;
public DisplayCanvas(ULA ula) {
this.ula = Objects.requireNonNull(ula);
- this.tep = ula.getTimedEventsProcessor();
+ ula.getBus().addPassedCyclesListener(this);
this.screenImage.setAccelerationPriority(1.0f);
this.screenImageData = ((DataBufferInt) this.screenImage.getRaster().getDataBuffer()).getData();
}
@@ -95,13 +104,9 @@ public DisplayCanvas(ULA ula) {
public void start() {
if (painting.compareAndSet(false, true)) {
createBufferStrategy(2);
-
- tep.schedule(REPAINT_CPU_TSTATES, this::triggerCpuInterrupt);
- tep.schedule(REPAINT_CPU_TSTATES, ula::onNextFrame);
- for (int i = 0; i < SCREEN_IMAGE_HEIGHT; i++) {
- int finalI = i;
- tep.schedule(REPAINT_CPU_TSTATES + i * LINE_CPU_TSTATES, () -> drawNextLine(finalI));
- }
+ frameCycleCounter = 0;
+ lineCycleCounter = 0;
+ lastLinePainted = 0;
}
}
@@ -187,14 +192,28 @@ public void setBounds(Rectangle r) {
@Override
public void close() {
- tep.remove(REPAINT_CPU_TSTATES, paintCycle);
- for (int i = 0; i < SCREEN_IMAGE_HEIGHT; i++) {
- int finalI = i;
- tep.remove(REPAINT_CPU_TSTATES + i * LINE_CPU_TSTATES, () -> drawNextLine(finalI));
- }
painting.set(false);
}
+ @Override
+ public void passedCycles(long tstates) {
+ if (painting.get()) {
+ frameCycleCounter += tstates;
+ lineCycleCounter += tstates;
+
+ for (int i = 0; i < lineCycleCounter / LINE_CPU_TSTATES; i++) {
+ drawNextLine(lastLinePainted++);
+ }
+ lineCycleCounter = lineCycleCounter % LINE_CPU_TSTATES;
+ if (frameCycleCounter >= FRAME_CPU_TSTATES) {
+ lastLinePainted = 0;
+ triggerCpuInterrupt();
+ ula.onNextFrame();
+ frameCycleCounter = frameCycleCounter % FRAME_CPU_TSTATES;
+ }
+ }
+ }
+
public class PaintCycle implements Runnable {
private BufferStrategy strategy;