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