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;