Skip to content

Commit

Permalink
[#314] Cleanup + refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
vbmacher committed Jan 2, 2024
1 parent 991f962 commit 11a54d3
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Optional;
import java.util.ResourceBundle;

@SuppressWarnings("unused")
@PluginRoot(type = PLUGIN_TYPE.DEVICE, title = "ZX Spectrum48K ULA")
public class DeviceImpl extends AbstractDevice {

Expand All @@ -42,6 +43,7 @@ public class DeviceImpl extends AbstractDevice {
private boolean guiIOset = false;

private ULA ula;
private PassedCyclesMediator passedCyclesMediator;
private DisplayWindow gui;

public DeviceImpl(long pluginID, ApplicationApi applicationApi, PluginSettings settings) {
Expand All @@ -54,6 +56,8 @@ public DeviceImpl(long pluginID, ApplicationApi applicationApi, PluginSettings s
public void initialize() throws PluginInitializationException {
ZxSpectrumBus bus = applicationApi.getContextPool().getDeviceContext(pluginID, ZxSpectrumBus.class);
this.ula = new ULA(bus);
this.passedCyclesMediator = new PassedCyclesMediator(ula);
bus.addPassedCyclesListener(passedCyclesMediator);
keyboard.addOnKeyListener(ula);
bus.attachDevice(0xFE, ula);
}
Expand Down Expand Up @@ -88,6 +92,7 @@ public void showGUI(JFrame parent) {
if (guiSupported) {
if (!guiIOset) {
this.gui = new DisplayWindow(parent, ula, keyboard);
passedCyclesMediator.setCanvas(gui.getCanvas());
GuiUtils.addKeyListener(gui, keyboard);
guiIOset = true;
this.gui.setVisible(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* This file is part of emuStudio.
*
* Copyright (C) 2006-2024 Peter Jakubčo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.emustudio.plugins.device.zxspectrum.ula;

import net.emustudio.emulib.plugins.cpu.CPUContext;
import net.emustudio.plugins.device.zxspectrum.ula.gui.DisplayCanvas;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import static net.emustudio.plugins.device.zxspectrum.ula.ZxParameters.*;

/**
* Triggers actions based on passed CPU cycles.
* <p>
* For <a href="https://worldofspectrum.org/faq/reference/48kreference.htm">ZX Spectrum 48K</a> the actions are:
* <p>
* - 0: CPU interrupt
* - 0 - 14336: first 64 lines. From those, at least 48 are border-lines, others are either border or vertical retraces
* After an interrupt occurs, 64 line times (14336 T states; see below for exact timings) pass before
* - 14337 - 57344: 192 screen lines are displayed
* - 57345 - 69888: 56 border-lines are displayed
* <p>
* This means a frame is (64+192+56)*224=69888 T states long, which means that the CPU interrupt occurs
* at 3.5MHz/69888=50.08 Hz.
*/
public class PassedCyclesMediator implements CPUContext.PassedCyclesListener {
private static final long LINE_CYCLES = 224;
private static final long FRAME_CYCLES = (PRE_SCREEN_LINES + SCREEN_HEIGHT + POST_SCREEN_LINES) * LINE_CYCLES; // 69888;

private long frameCycles = 0;
private long lineCycles = 0;
private int lastLinePainted = 0;

private final AtomicReference<DisplayCanvas> canvas = new AtomicReference<>();
private final ULA ula;

public PassedCyclesMediator(ULA ula) {
this.ula = Objects.requireNonNull(ula);
}

public void setCanvas(DisplayCanvas canvas) {
this.canvas.set(canvas);
}

@Override
public void passedCycles(long cycles) {
frameCycles += cycles;
lineCycles += cycles;

DisplayCanvas canvas = this.canvas.get();
if (canvas != null) {
if (lineCycles >= LINE_CYCLES) {
canvas.drawNextLine(lastLinePainted++);
}
}
lineCycles = lineCycles % LINE_CYCLES;
if (frameCycles >= FRAME_CYCLES) {
lastLinePainted = 0;
ula.onNextFrame();
frameCycles = frameCycles % FRAME_CYCLES;
if (canvas != null) {
canvas.runPaintCycle(); // expensive operation
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Map;
import java.util.Objects;

import static net.emustudio.plugins.device.zxspectrum.ula.ZxParameters.*;

/**
* Uncommitted Logic Array (ULA).
* <p>
Expand All @@ -36,7 +38,14 @@
* - <a href="http://www.breakintoprogram.co.uk/hardware/computers/zx-spectrum/screen-memory-layout">Screen Memory layout</a>
*
* <p>
* OUT:
* The ULA component in emuStudio is a "mediator" of interaction between host and emulator. That means, it handles:
* - keyboard
* - audio
* - video (maps RAM to video/attribute memory, video flash, border color)
* <p>
* From the ZX Spectrum point of view, it represents the port 0xFE (254).
* <p>
* Port 0xFE write:
* 7 6 5 4 3 2 1 0
* +-------------------------------+
* | | | | E | M | Border |
Expand All @@ -47,15 +56,14 @@
* - on host CTRL + letter/number = ZX symbol "shift" + letter/number
* - on host plain letter/number = ZX letter/number
* <p>
* IN: (bit 0 to bit 4 inclusive)
* Port 0xFE read (bit 0 to bit 4 inclusive):
* 0xfefe SHIFT, Z, X, C, V 0xeffe 0, 9, 8, 7, 6
* 0xfdfe A, S, D, F, G 0xdffe P, O, I, U, Y
* 0xfbfe Q, W, E, R, T 0xbffe ENTER, L, K, J, H
* 0xf7fe 1, 2, 3, 4, 5 0x7ffe SPACE, SYM SHIFT, M, N, B
* <p>
* The colour attribute data overlays the monochrome bitmap data and is arranged in a linear fashion from left to right,
* top to bottom.
* Each attribute byte colours is 8x8 character on the screen and is encoded as follows:
* top to bottom. Each attribute byte colours is 8x8 character on the screen and is encoded as follows:
* <p>
* 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* F | B | P2| P1| P0| I2| I1| I0|
Expand All @@ -67,18 +75,16 @@
* - I2 to I0 is the INK colour
*/
public class ULA implements Context8080.CpuPortDevice, Keyboard.OnKeyListener {
public static final int SCREEN_WIDTH = 32; // in bytes; each byte represents 8 pixels in a row, reversed
public static final int SCREEN_HEIGHT = 192;
public static final int ATTRIBUTE_HEIGHT = SCREEN_HEIGHT / 8;

private final static byte[] RST_7 = new byte[0x38]; // works for IM1 and IM2 modes
private final static byte[] KEY_SHIFT = new byte[]{0, 1};
private final static byte[] KEY_SYM_SHIFT = new byte[]{7, 2};
private final static int[] LINE_OFFSETS = computeLineOffsets();

private final byte[] keymap = new byte[8]; // keyboard state

// accessible from outside
public final byte[][] videoMemory = new byte[SCREEN_WIDTH][SCREEN_HEIGHT];
public final byte[][] attributeMemory = new byte[SCREEN_WIDTH][ATTRIBUTE_HEIGHT];
private final static int[] lineStartOffsets = computeLineStartOffsets();

// maps host characters to ZX Spectrum key "commands"
// Byte[] = {keymap index, "zero" value, shift, symshift}
Expand Down Expand Up @@ -127,8 +133,6 @@ public class ULA implements Context8080.CpuPortDevice, Keyboard.OnKeyListener {
CHAR_MAPPING.put((char) 127, new Byte[]{4, 1, 1, -1}); // delete
}

// The Spectrum's 'FLASH' effect is also produced by the ULA: Every 16 frames, the ink and paper of all flashing
// bytes is swapped; ie a normal to inverted to normal cycle takes 32 frames, which is (good as) 0.64 seconds.
public boolean videoFlash = false;
private int flashFramesCount = 0;

Expand All @@ -137,7 +141,6 @@ public class ULA implements Context8080.CpuPortDevice, Keyboard.OnKeyListener {
private int borderColor;
private boolean microphoneAndEarOut; // TODO: audio


public ULA(ZxSpectrumBus bus) {
this.bus = Objects.requireNonNull(bus);
Arrays.fill(keymap, (byte) 0xBF);
Expand All @@ -150,32 +153,22 @@ public void reset() {
}

public void onNextFrame() {
if (flashFramesCount == 15) {
bus.signalInterrupt(RST_7);
if (flashFramesCount == VIDEO_FLASH_FRAME) {
videoFlash = !videoFlash;
}
flashFramesCount = (flashFramesCount + 1) % 16;
flashFramesCount = (flashFramesCount + 1) % (VIDEO_FLASH_FRAME + 1);
}

public void readScreen() {
for (int x = 0; x < SCREEN_WIDTH; x++) {
for (int y = 0; y < SCREEN_HEIGHT; y++) {
videoMemory[x][y] = bus.readMemoryNotContended(0x4000 + lineStartOffsets[y] + x);
if (y < ATTRIBUTE_HEIGHT) {
int off = ((y >>> 3) << 8) | (((y & 0x07) << 5) | x);
int attributeAddress = 0x5800 + off;
attributeMemory[x][y] = bus.readMemoryNotContended(attributeAddress);
}
}
for (int y = 0; y < SCREEN_HEIGHT; y++) {
readLine(y);
}
}

public void triggerInterrupt() {
bus.signalInterrupt(RST_7);
}

public void readLine(int y) {
for (int x = 0; x < SCREEN_WIDTH; x++) {
videoMemory[x][y] = bus.readMemoryNotContended(0x4000 + lineStartOffsets[y] + x);
videoMemory[x][y] = bus.readMemoryNotContended(0x4000 + LINE_OFFSETS[y] + x);
if (y < ATTRIBUTE_HEIGHT) {
int off = ((y >>> 3) << 8) | (((y & 0x07) << 5) | x);
int attributeAddress = 0x5800 + off;
Expand All @@ -188,10 +181,6 @@ 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.
Expand Down Expand Up @@ -220,7 +209,7 @@ public byte read(int portAddress) {
// ENTER, L, K, J, H
result &= keymap[6];
} else if ((portAddress & 0x7FFE) == 0x7FFE) {
// SPACE, SYM SHFT, M, N, B
// SPACE, SYM SHIFT, M, N, B
result &= keymap[7];
}

Expand Down Expand Up @@ -271,7 +260,6 @@ public void onKeyUp(KeyEvent evt) {
keymap[command[0]] |= command[1];
}
}

}

@Override
Expand Down Expand Up @@ -302,7 +290,20 @@ public void onKeyDown(KeyEvent evt) {
}
}

private static int[] computeLineStartOffsets() {
/**
* Computes address offsets for each line in the screen.
* <p>
* The Spectrum’s screen memory starts at 0x4000 so the most significant three bits of our address will always be 010.
* The 5 least significant bits will always be the X (column) address. The 8 bits from 5-12 represent the pixel Y:
* <p>
* 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
* 0 1 0 Y7 Y6 Y2 Y1 Y0 Y5 Y4 Y3 X4 X3 X2 X1 X0
* <p>
* This method sets all X bits to 0, and then sets the Y bits according to the line number.
*
* @return array of offsets
*/
private static int[] computeLineOffsets() {
final int[] result = new int[SCREEN_HEIGHT];
for (int y = 0; y < SCREEN_HEIGHT; y++) {
result[y] = ((y & 0xC0) << 5) | ((y & 7) << 8) | ((y & 0x38) << 2);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* This file is part of emuStudio.
*
* Copyright (C) 2006-2024 Peter Jakubčo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.emustudio.plugins.device.zxspectrum.ula;

public class ZxParameters {
public static final int SCREEN_WIDTH = 32; // in bytes; each byte represents 8 pixels in a row, reversed
public static final int SCREEN_HEIGHT = 192;
public static final int ATTRIBUTE_HEIGHT = SCREEN_HEIGHT / 8;

public static final int PRE_SCREEN_LINES = 64;
public static final int POST_SCREEN_LINES = 56;
public static final int BORDER_WIDTH = 48; // pixels

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;

// The Spectrum's 'FLASH' effect is also produced by the ULA: Every 16 frames, the ink and paper of all flashing
// bytes is swapped; ie a normal to inverted to normal cycle takes 32 frames, which is (good as) 0.64 seconds.
public static final int VIDEO_FLASH_FRAME = 15;
}
Loading

0 comments on commit 11a54d3

Please sign in to comment.