diff --git a/pom.xml b/pom.xml
index c25f237..127ff87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,7 +130,7 @@
org.codehaus.mojo
cobertura-maven-plugin
- 2.4
+ 2.6
false
@@ -167,6 +167,21 @@
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.2.1
+
+ com.loomcom.symon.Simulator
+
+
+
+
+ java
+
+
+
+
@@ -175,7 +190,7 @@
org.codehaus.mojo
cobertura-maven-plugin
- 2.4
+ 2.6
diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java
index db7f944..4b19002 100644
--- a/src/main/java/com/loomcom/symon/Cpu.java
+++ b/src/main/java/com/loomcom/symon/Cpu.java
@@ -161,10 +161,8 @@ public class Cpu implements InstructionTable {
irAddressMode = (state.ir >> 2) & 0x07;
irOpMode = state.ir & 0x03;
- // Increment PC
incrementPC();
- // Clear the illegal opcode trap.
clearOpTrap();
// Decode the instruction and operands
@@ -175,7 +173,6 @@ public class Cpu implements InstructionTable {
incrementPC();
}
- // Increment step counter
state.stepCounter++;
// Get the data from the effective address (if any)
@@ -412,18 +409,17 @@ public class Cpu implements InstructionTable {
}
state.pc = address(bus.read(lo), bus.read(hi));
- /* TODO: For accuracy, allow a flag to enable broken behavior
- * of early 6502s:
- *
- * "An original 6502 has does not correctly fetch the target
- * address if the indirect vector falls on a page boundary
- * (e.g. $xxFF where xx is and value from $00 to $FF). In this
- * case fetches the LSB from $xxFF as expected but takes the MSB
- * from $xx00. This is fixed in some later chips like the 65SC02
- * so for compatibility always ensure the indirect vector is not
- * at the end of the page."
- * (http://www.obelisk.demon.co.uk/6502/reference.html#JMP)
- */
+ /* TODO: For accuracy, allow a flag to enable broken behavior of early 6502s:
+ *
+ * "An original 6502 has does not correctly fetch the target
+ * address if the indirect vector falls on a page boundary
+ * (e.g. $xxFF where xx is and value from $00 to $FF). In this
+ * case fetches the LSB from $xxFF as expected but takes the MSB
+ * from $xx00. This is fixed in some later chips like the 65SC02
+ * so for compatibility always ensure the indirect vector is not
+ * at the end of the page."
+ * (http://www.obelisk.demon.co.uk/6502/reference.html#JMP)
+ */
break;
@@ -724,6 +720,7 @@ public class Cpu implements InstructionTable {
break;
/** Unimplemented Instructions ****************************************/
+ // TODO: Create a flag to enable highly-accurate emulation of unimplemented instructions.
default:
setOpTrap();
break;
diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java
index 3f5022d..dd4844a 100644
--- a/src/main/java/com/loomcom/symon/Simulator.java
+++ b/src/main/java/com/loomcom/symon/Simulator.java
@@ -24,6 +24,7 @@
package com.loomcom.symon;
import com.loomcom.symon.devices.Acia;
+import com.loomcom.symon.devices.Crtc;
import com.loomcom.symon.devices.Memory;
import com.loomcom.symon.devices.Via;
import com.loomcom.symon.exceptions.FifoUnderrunException;
@@ -37,11 +38,7 @@ import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.*;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
import java.io.*;
-import java.util.Observable;
-import java.util.Observer;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -69,6 +66,10 @@ public class Simulator {
// ACIA at $8800-$8803
private static final int ACIA_BASE = 0x8800;
+ // CRTC at $9000-$9001
+ private static final int CRTC_BASE = 0x9000;
+ private static final int VIDEO_RAM_BASE = 0x7000;
+
// 16KB ROM at $C000-$FFFF
private static final int ROM_BASE = 0xC000;
private static final int ROM_SIZE = 0x4000;
@@ -96,12 +97,18 @@ public class Simulator {
private final Cpu cpu;
private final Acia acia;
private final Via via;
+ private final Crtc crtc;
private final Memory ram;
private Memory rom;
+ // Number of CPU steps between CRT repaints.
+ // TODO: Dynamically refresh the value at runtime based on performance figures to reach ~ 30fps.
+ private long stepsBetweenCrtcRefreshes = 2500;
+
// A counter to keep track of the number of UI updates that have been
// requested
private int stepsSinceLastUpdate = 0;
+ private int stepsSinceLastCrtcRefresh = 0;
// The number of steps to run per click of the "Step" button
private int stepsPerClick = 1;
@@ -123,10 +130,7 @@ public class Simulator {
*/
private MemoryWindow memoryWindow;
- /**
- * The Zero Page Window shows the contents of page 0.
- */
- private JFrame zeroPageWindow;
+ private VideoWindow videoWindow;
private SimulatorMenu menuBar;
@@ -153,6 +157,7 @@ public class Simulator {
this.bus = new Bus(BUS_BOTTOM, BUS_TOP);
this.cpu = new Cpu();
this.ram = new Memory(MEMORY_BASE, MEMORY_SIZE, false);
+ this.crtc = new Crtc(CRTC_BASE, ram, VIDEO_RAM_BASE);
bus.addCpu(cpu);
bus.addDevice(ram);
@@ -259,6 +264,9 @@ public class Simulator {
// Prepare the memory window
memoryWindow = new MemoryWindow(bus);
+ // Prepare the video window
+ videoWindow = new VideoWindow(crtc.getCrtPanel());
+
mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// The Menu. This comes last, because it relies on other components having
@@ -266,10 +274,10 @@ public class Simulator {
menuBar = new SimulatorMenu();
mainWindow.setJMenuBar(menuBar);
- console.requestFocus();
-
mainWindow.pack();
mainWindow.setVisible(true);
+
+ console.requestFocus();
}
private void handleStart() {
@@ -365,6 +373,11 @@ public class Simulator {
logger.severe("Console type-ahead buffer underrun!");
}
+ if (stepsSinceLastCrtcRefresh++ > stepsBetweenCrtcRefreshes) {
+ videoWindow.refreshDisplay();
+ stepsSinceLastCrtcRefresh = 0;
+ }
+
// This is a very expensive update, and we're doing it without
// a delay, so we don't want to overwhelm the Swing event processing thread
// with requests. Limit the number of ui updates that can be performed.
@@ -671,6 +684,23 @@ public class Simulator {
}
}
+ class ToggleVideoWindowAction extends AbstractAction {
+ public ToggleVideoWindowAction() {
+ super("Video Window", null);
+ putValue(SHORT_DESCRIPTION, "Show or Hide the Video Window");
+ }
+
+ public void actionPerformed(ActionEvent actionEvent) {
+ synchronized (videoWindow) {
+ if (videoWindow.isVisible()) {
+ videoWindow.setVisible(false);
+ } else {
+ videoWindow.setVisible(true);
+ }
+ }
+ }
+ }
+
class SimulatorMenu extends JMenuBar {
// Menu Items
private JMenuItem loadProgramItem;
@@ -758,6 +788,15 @@ public class Simulator {
});
viewMenu.add(showMemoryTable);
+ final JCheckBoxMenuItem showVideoWindow = new JCheckBoxMenuItem(new ToggleVideoWindowAction());
+ videoWindow.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ showVideoWindow.setSelected(false);
+ }
+ });
+ viewMenu.add(showVideoWindow);
+
add(viewMenu);
}
diff --git a/src/main/java/com/loomcom/symon/devices/Crtc.java b/src/main/java/com/loomcom/symon/devices/Crtc.java
new file mode 100644
index 0000000..95cee0b
--- /dev/null
+++ b/src/main/java/com/loomcom/symon/devices/Crtc.java
@@ -0,0 +1,101 @@
+package com.loomcom.symon.devices;
+
+import com.loomcom.symon.exceptions.MemoryAccessException;
+import com.loomcom.symon.exceptions.MemoryRangeException;
+import com.loomcom.symon.ui.CrtPanel;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+
+/**
+ * Simulation of a 6545 CRTC and virtual CRT output.
+ */
+public class Crtc extends Device {
+
+ public static final int CHAR_WIDTH = 8;
+ public static final int CHAR_HEIGHT = 8;
+ public static final int SCAN_LINES = 9;
+ public static final int COLUMNS = 40;
+ public static final int ROWS = 25;
+ public static final int SCALE = 2;
+
+ public static final int REGISTER_SELECT = 0;
+ public static final int REGISTER_WRITE = 1;
+
+ public static String CHAR_ROM_RESOURCE = "/pet.rom";
+
+ private CrtPanel crtPanel;
+ private int currentRegister = 0;
+
+ public Crtc(int deviceAddress, Memory memory, int videoRamStartAddress) throws MemoryRangeException, IOException {
+ super(deviceAddress, 2, "CRTC");
+ this.crtPanel = new CrtPanel(loadCharRom(CHAR_ROM_RESOURCE), memory.getDmaAccess(), COLUMNS, ROWS,
+ CHAR_WIDTH, CHAR_HEIGHT,
+ SCALE, SCALE, videoRamStartAddress);
+ }
+
+ private byte[] loadCharRom(String resource) throws IOException {
+ BufferedInputStream bis = null;
+ try {
+ bis = new BufferedInputStream(this.getClass().getResourceAsStream(resource));
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ while (bis.available() > 0) {
+ bos.write(bis.read());
+ }
+ bos.flush();
+ bos.close();
+ return bos.toByteArray();
+ } finally {
+ if (bis != null) {
+ bis.close();
+ }
+ }
+ }
+
+ public CrtPanel getCrtPanel() {
+ return crtPanel;
+ }
+
+ @Override
+ public void write(int address, int data) throws MemoryAccessException {
+ switch (address) {
+ case REGISTER_SELECT:
+ setCurrentRegister(data);
+ case REGISTER_WRITE:
+ writeRegisterValue(data);
+ default:
+ throw new MemoryAccessException("No such address.");
+ }
+ }
+
+ @Override
+ public int read(int address) throws MemoryAccessException {
+ switch (address) {
+ case REGISTER_SELECT:
+ return status();
+ case REGISTER_WRITE:
+ return 0;
+ default:
+ throw new MemoryAccessException("No such address.");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return null;
+ }
+
+ private int status() {
+ return 0;
+ }
+
+ private void setCurrentRegister(int registerNumber) {
+ this.currentRegister = registerNumber;
+ }
+
+ private void writeRegisterValue(int data) {
+
+ }
+}
diff --git a/src/main/java/com/loomcom/symon/devices/Memory.java b/src/main/java/com/loomcom/symon/devices/Memory.java
index 8272652..32c65ae 100644
--- a/src/main/java/com/loomcom/symon/devices/Memory.java
+++ b/src/main/java/com/loomcom/symon/devices/Memory.java
@@ -109,4 +109,7 @@ public class Memory extends Device {
return "Memory: " + getMemoryRange().toString();
}
+ public int[] getDmaAccess() {
+ return mem;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/loomcom/symon/ui/CrtPanel.java b/src/main/java/com/loomcom/symon/ui/CrtPanel.java
new file mode 100644
index 0000000..a54a97a
--- /dev/null
+++ b/src/main/java/com/loomcom/symon/ui/CrtPanel.java
@@ -0,0 +1,275 @@
+package com.loomcom.symon.ui;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.concurrent.*;
+
+/**
+ * Simulates a CRT Display backed by a 6545 CRTC.
+ */
+public class CrtPanel extends JPanel {
+
+ // Character width and height are hardware-implementation specific
+ // and cannot be modified at runtime.
+ private final int charWidth;
+ private final int charHeight;
+ private final int scaleX, scaleY;
+ private final boolean shouldScale;
+
+ private Dimension dimensions;
+ private BufferedImage image;
+
+ private int[] charRom;
+ private int[] videoRam;
+
+ /* Fields corresponding to internal registers in the MOS/Rockwell 6545 */
+
+ // R1 - Horizontal Displayed
+ private final int horizontalDisplayed;
+ // R6 - Vertical Displayed
+ private final int verticalDisplayed;
+ // R9 - Scan Lines: Number of scan lines per character, including spacing.
+ private int scanLinesPerRow = 9;
+ // R10 - Cursor Start
+ private int cursorStartLine;
+ // R11 - Cursor End
+ private int cursorStopLine;
+ private boolean cursorEnabled;
+ private int cursorBlinkDelay;
+ private boolean cursorBlinkEnabled;
+ // R12, R13 - Display Start Address: The starting address in the video RAM of the displayed page.
+ private int startAddress;
+ // R14, R15 - Cursor Position
+ private int cursorPosition;
+
+ // The size, in bytes, of a displayed page of characters.
+ private int pageSize;
+
+ private ScheduledExecutorService scheduler;
+ private ScheduledFuture> cursorBlinker;
+
+ private class CursorBlinker implements Runnable {
+ public void run() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (cursorBlinkEnabled) {
+ cursorEnabled = !cursorEnabled;
+ repaint();
+ }
+ }
+ });
+ }
+ }
+
+ public CrtPanel(byte[] charRom, int[] videoRam,
+ int horizontalDisplayed, int verticalDisplayed,
+ int charWidth, int charHeight,
+ int scaleX, int scaleY,
+ int startAddress) {
+ super();
+
+ this.charRom = convertCharRom(charRom, charWidth);
+ this.videoRam = videoRam;
+ this.horizontalDisplayed = horizontalDisplayed;
+ this.verticalDisplayed = verticalDisplayed;
+ this.pageSize = horizontalDisplayed * verticalDisplayed;
+ this.scaleX = scaleX;
+ this.scaleY = scaleY;
+ this.startAddress = startAddress;
+ this.charWidth = charWidth;
+ this.charHeight = charHeight;
+ this.scanLinesPerRow = charHeight + 1;
+ this.cursorStartLine = 0;
+ this.cursorStopLine = charHeight - 1;
+ this.cursorBlinkEnabled = true;
+ this.cursorBlinkDelay = 500; // ms
+
+ this.shouldScale = (this.scaleX > 1 || this.scaleY > 1);
+
+ buildImage();
+
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ cursorBlinker = scheduler.scheduleAtFixedRate(new CursorBlinker(), cursorBlinkDelay, cursorBlinkDelay, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ for (int i = 0; i < pageSize; i++) {
+ int address = startAddress + i;
+ int originX = (i % horizontalDisplayed) * charWidth;
+ int originY = (i / horizontalDisplayed) * scanLinesPerRow;
+ image.getRaster().setPixels(originX, originY, charWidth, scanLinesPerRow, getGlyph(i, videoRam[address]));
+ }
+ Graphics2D g2d = (Graphics2D)g;
+ if (shouldScale) {
+ g2d.scale(scaleX, scaleY);
+ }
+ g2d.drawImage(image, 0, 0, null);
+ }
+
+ public void setStartAddress(int address) {
+ startAddress = address;
+ repaint();
+ }
+
+ public int getStartAddress() {
+ return startAddress;
+ }
+
+ /**
+ * Returns an array of pixels (including extra scanlines, if any) corresponding to the
+ * Character ROM plus cursor overlay (if any). The cursor overlay simulates an XOR
+ * of the Character Rom output and the 6545 Cursor output.
+ *
+ * @param position The position within the character field, from 0 to (horizontalDisplayed * verticalDisplayed)
+ * @param chr The character value within the ROM to display.
+ * @return
+ */
+ private int[] getGlyph(int position, int chr) {
+ int romOffset = (chr & 0xff) * (charHeight * charWidth);
+ int[] glyph = new int[charWidth * scanLinesPerRow];
+
+ // Populate the character
+ for (int i = 0; i < (charWidth * Math.min(charHeight, scanLinesPerRow)); i++) {
+ glyph[i] = charRom[romOffset + i];
+ }
+
+ // Overlay the cursor
+ if (cursorEnabled && cursorPosition == position) {
+ int cursorStart = Math.min(glyph.length, cursorStartLine * charWidth);
+ int cursorStop = Math.min(glyph.length, (cursorStopLine + 1) * charWidth);
+
+ for (int i = cursorStart; i < cursorStop; i++) {
+ glyph[i] ^= 0xff;
+ }
+ }
+
+ return glyph;
+ }
+
+ @Override
+ public int getWidth() {
+ return (int) dimensions.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return (int) dimensions.getHeight();
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return dimensions;
+ }
+
+ @Override
+ public Dimension getMaximumSize() {
+ return dimensions;
+ }
+
+ @Override
+ public Dimension getMinimumSize() {
+ return dimensions;
+ }
+
+ public int getCursorStartLine() {
+ return cursorStartLine;
+ }
+
+ public void setCursorStartLine(int cursorStartLine) {
+ this.cursorStartLine = cursorStartLine;
+ }
+
+ public int getCursorStopLine() {
+ return cursorStopLine;
+ }
+
+ public void setCursorStopLine(int cursorStopLine) {
+ this.cursorStopLine = cursorStopLine;
+ }
+
+ public int getCursorBlinkDelay() {
+ return cursorBlinkDelay;
+ }
+
+ public void setCursorBlinkDelay(int cursorBlinkDelay) {
+ this.cursorBlinkDelay = cursorBlinkDelay;
+ }
+
+ public int getCursorPosition() {
+ return cursorPosition;
+ }
+
+ public void setCursorPosition(int cursorPosition) {
+ this.cursorPosition = cursorPosition;
+ }
+
+ public boolean isCursorEnabled() {
+ return cursorEnabled;
+ }
+
+ public void setCursorEnabled(boolean cursorEnabled) {
+ this.cursorEnabled = cursorEnabled;
+ }
+
+ public boolean isCursorBlinkEnabled() {
+ return cursorBlinkEnabled;
+ }
+
+ public void setCursorBlinkEnabled(boolean cursorBlinkEnabled) {
+ if (cursorBlinkEnabled && cursorBlinker == null) {
+ cursorBlinker = scheduler.scheduleAtFixedRate(new CursorBlinker(),
+ cursorBlinkDelay,
+ cursorBlinkDelay,
+ TimeUnit.MILLISECONDS);
+ } else if (!cursorBlinkEnabled && cursorBlinker != null) {
+ cursorBlinker.cancel(true);
+ cursorBlinker = null;
+ }
+
+ this.cursorBlinkEnabled = cursorBlinkEnabled;
+ repaint();
+ }
+
+ public void setScanLinesPerRow(int scanLinesPerRow) {
+ this.scanLinesPerRow = scanLinesPerRow;
+ buildImage();
+ }
+
+ public int getScanLinesPerRow() {
+ return scanLinesPerRow;
+ }
+
+ private void buildImage() {
+ int rasterWidth = charWidth * horizontalDisplayed;
+ int rasterHeight = scanLinesPerRow * verticalDisplayed;
+
+ this.image = new BufferedImage(rasterWidth, rasterHeight, BufferedImage.TYPE_BYTE_BINARY);
+ this.dimensions = new Dimension(rasterWidth * scaleX, rasterHeight * scaleY);
+ }
+
+
+ /**
+ * Convert a raw binary Character ROM image into an array of pixel data usable
+ * by the Raster underlying the display's BufferedImage.
+ *
+ * @param rawBytes
+ * @param charWidth
+ * @return
+ */
+ private int[] convertCharRom(byte[] rawBytes, int charWidth) {
+ int[] converted = new int[rawBytes.length * charWidth];
+
+ int romIndex = 0;
+ for (int i = 0; i < converted.length;) {
+ byte charRow = rawBytes[romIndex++];
+
+ for (int j = 7; j >= 0; j--) {
+ converted[i++] = ((charRow & (1 << j)) == 0) ? 0 : 0xff;
+ }
+ }
+ return converted;
+ }
+
+}
diff --git a/src/main/java/com/loomcom/symon/ui/VideoWindow.java b/src/main/java/com/loomcom/symon/ui/VideoWindow.java
new file mode 100644
index 0000000..edea06f
--- /dev/null
+++ b/src/main/java/com/loomcom/symon/ui/VideoWindow.java
@@ -0,0 +1,39 @@
+package com.loomcom.symon.ui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class VideoWindow extends JFrame {
+
+ private CrtPanel crtPanel;
+
+ public VideoWindow(CrtPanel crtPanel) {
+ this.crtPanel = crtPanel;
+ createUi();
+ }
+
+ public void createUi() {
+ setTitle("Composite Video");
+
+ int borderWidth = (int) (crtPanel.getWidth() * 0.08);
+ int borderHeight = (int) (crtPanel.getHeight() * 0.08);
+
+ JPanel containerPane = new JPanel();
+ containerPane.setBorder(BorderFactory.createEmptyBorder(borderHeight, borderWidth, borderHeight, borderWidth));
+ containerPane.setLayout(new BorderLayout());
+ containerPane.setBackground(Color.black);
+ containerPane.add(crtPanel, BorderLayout.CENTER);
+
+ getContentPane().add(containerPane);
+ setResizable(false);
+ pack();
+ }
+
+ public void refreshDisplay() {
+ // TODO: Verify whether this is necessary. Does `repaint()' do anything if the window is not visible?
+ if (isVisible()) {
+ repaint();
+ }
+ }
+
+}
diff --git a/src/main/resources/cga8.rom b/src/main/resources/cga8.rom
new file mode 100644
index 0000000..71f2c04
Binary files /dev/null and b/src/main/resources/cga8.rom differ
diff --git a/src/main/resources/pet.rom b/src/main/resources/pet.rom
new file mode 100644
index 0000000..0af9d26
Binary files /dev/null and b/src/main/resources/pet.rom differ