diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index dd4844a..7b2845c 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -152,17 +152,20 @@ public class Simulator { private static final String[] STEPS = {"1", "5", "10", "20", "50", "100"}; public Simulator() throws MemoryRangeException, IOException { - this.acia = new Acia(ACIA_BASE); - this.via = new Via(VIA_BASE); + this.ram = new Memory(MEMORY_BASE, MEMORY_SIZE, false); + 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); + this.via = new Via(VIA_BASE); + this.acia = new Acia(ACIA_BASE); + this.crtc = new Crtc(CRTC_BASE, ram); bus.addCpu(cpu); + bus.addDevice(ram); bus.addDevice(via); bus.addDevice(acia); + bus.addDevice(crtc); // TODO: Make this configurable, of course. File romImage = new File("rom.bin"); @@ -174,13 +177,14 @@ public class Simulator { " not found, loading empty R/W memory image."); this.rom = Memory.makeRAM(ROM_BASE, ROM_SIZE); } + bus.addDevice(rom); } /** * Display the main simulator UI. */ - public void createAndShowUi() { + public void createAndShowUi() throws IOException { mainWindow = new JFrame(); mainWindow.setTitle("Symon 6502 Simulator"); mainWindow.setResizable(false); @@ -264,8 +268,8 @@ public class Simulator { // Prepare the memory window memoryWindow = new MemoryWindow(bus); - // Prepare the video window - videoWindow = new VideoWindow(crtc.getCrtPanel()); + // Composite Video and 6545 CRTC + videoWindow = new VideoWindow(crtc, 2, 2); mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); diff --git a/src/main/java/com/loomcom/symon/devices/Crtc.java b/src/main/java/com/loomcom/symon/devices/Crtc.java index 95cee0b..04559c0 100644 --- a/src/main/java/com/loomcom/symon/devices/Crtc.java +++ b/src/main/java/com/loomcom/symon/devices/Crtc.java @@ -2,11 +2,9 @@ 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; +import java.util.logging.Logger; /** @@ -14,60 +12,101 @@ import java.io.IOException; */ 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; + private static final Logger logger = Logger.getLogger(Crtc.class.getName()); - public static final int REGISTER_SELECT = 0; - public static final int REGISTER_WRITE = 1; - public static String CHAR_ROM_RESOURCE = "/pet.rom"; + // Memory locations in the CRTC address space + public static final int REGISTER_SELECT = 0; + public static final int REGISTER_WRITE = 1; + + // Registers + public static final int HORIZONTAL_TOTAL = 0; + public static final int HORIZONTAL_DISPLAYED = 1; + public static final int HORIZONTAL_SYNC_POSITION = 2; + public static final int H_V_SYNC_WIDTHS = 3; + public static final int VERTICAL_TOTAL = 4; + public static final int VERTICAL_TOTAL_ADJUST = 5; + public static final int VERTICAL_DISPLAYED = 6; + public static final int VERTICAL_SYNC_POSITION = 7; + public static final int MODE_CONTROL = 8; + public static final int SCAN_LINE = 9; + public static final int CURSOR_START = 10; + public static final int CURSOR_END = 11; + public static final int DISPLAY_START_HIGH = 12; + public static final int DISPLAY_START_LOW = 13; + public static final int CURSOR_POSITION_HIGH = 14; + public static final int CURSOR_POSITION_LOW = 15; + public static final int LPEN_HIGH = 16; + public static final int LPEN_LOW = 17; + + + /* + * These will determine how the Character ROM is decoded, + * and are Character ROM dependent. + */ + + // R1 - Horizontal Displayed + private int horizontalDisplayed; + + // R6 - Vertical Displayed + private int verticalDisplayed; + + // R9 - Scan Lines: Number of scan lines per character, including spacing. + private int scanLinesPerRow; + + // R10 - Cursor Start / Cursor Mode + private int cursorStartLine; + private boolean cursorEnabled; + private int cursorBlinkRate; + + // R11 - Cursor End + private int cursorStopLine; + + // 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 CrtPanel crtPanel; private int currentRegister = 0; - public Crtc(int deviceAddress, Memory memory, int videoRamStartAddress) throws MemoryRangeException, IOException { + private Memory memory; + + public Crtc(int deviceAddress, Memory memory) 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); - } + this.memory = memory; - 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; + // Defaults + this.horizontalDisplayed = 40; + this.verticalDisplayed = 25; + this.scanLinesPerRow = 9; + this.cursorStartLine = 0; + this.cursorStopLine = 8; + this.startAddress = 0x7000; + this.cursorPosition = 0; + this.pageSize = horizontalDisplayed * verticalDisplayed; + this.cursorEnabled = true; + this.cursorBlinkRate = 500; } @Override public void write(int address, int data) throws MemoryAccessException { + logger.info("[write] Writing CRTC address=" + address + " data=" + data); switch (address) { case REGISTER_SELECT: setCurrentRegister(data); + break; case REGISTER_WRITE: writeRegisterValue(data); + break; default: throw new MemoryAccessException("No such address."); } + + notifyListeners(); } @Override @@ -87,15 +126,80 @@ public class Crtc extends Device { return null; } + public int[] getDmaAccess() { + return memory.getDmaAccess(); + } + private int status() { return 0; } + public int getHorizontalDisplayed() { + return horizontalDisplayed; + } + + public int getVerticalDisplayed() { + return verticalDisplayed; + } + + public int getScanLinesPerRow() { + return scanLinesPerRow; + } + + public int getCursorStartLine() { + return cursorStartLine; + } + + public int getCursorStopLine() { + return cursorStopLine; + } + + public int getCursorBlinkRate() { + return cursorBlinkRate; + } + + public boolean isCursorEnabled() { + return cursorEnabled; + } + + public int getStartAddress() { + return startAddress; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public int getPageSize() { + return pageSize; + } + private void setCurrentRegister(int registerNumber) { this.currentRegister = registerNumber; } private void writeRegisterValue(int data) { + logger.info("Writing CRTC Register #" + currentRegister + " with value " + String.format("$%02X", data)); + switch (currentRegister) { + case HORIZONTAL_DISPLAYED: + horizontalDisplayed = data; + pageSize = horizontalDisplayed * verticalDisplayed; + break; + case VERTICAL_DISPLAYED: + verticalDisplayed = data; + pageSize = horizontalDisplayed * verticalDisplayed; + break; + case MODE_CONTROL: + // TODO: Implement multiple addressing modes. + break; + case SCAN_LINE: + scanLinesPerRow = data; + break; + default: + logger.info("Ignoring."); + break; + } + notifyListeners(); } } diff --git a/src/main/java/com/loomcom/symon/devices/Device.java b/src/main/java/com/loomcom/symon/devices/Device.java index 7e995da..d6361eb 100644 --- a/src/main/java/com/loomcom/symon/devices/Device.java +++ b/src/main/java/com/loomcom/symon/devices/Device.java @@ -23,8 +23,13 @@ package com.loomcom.symon.devices; -import com.loomcom.symon.*; -import com.loomcom.symon.exceptions.*; +import com.loomcom.symon.Bus; +import com.loomcom.symon.MemoryRange; +import com.loomcom.symon.exceptions.MemoryAccessException; +import com.loomcom.symon.exceptions.MemoryRangeException; + +import java.util.HashSet; +import java.util.Set; /** * A memory-mapped IO Device. @@ -32,69 +37,93 @@ import com.loomcom.symon.exceptions.*; public abstract class Device implements Comparable { - /** The memory range for this device. */ - private MemoryRange memoryRange; + /** + * The memory range for this device. + */ + private MemoryRange memoryRange; - /** The name of the device. */ - private String name; + /** + * The name of the device. + */ + private String name; - /** Reference to the bus where this Device is attached. */ - private Bus bus; + /** + * Reference to the bus where this Device is attached. + */ + private Bus bus; - public Device(int address, int size, String name) - throws MemoryRangeException { - this.memoryRange = new MemoryRange(address, address + size - 1); - this.name = name; - } + /** + * Listeners to notify on update. + */ + private Set deviceChangeListeners; - public Device(int address, int size) throws MemoryRangeException { - this(address, size, null); - } - - /* Methods required to be implemented by inheriting classes. */ - public abstract void write(int address, int data) throws MemoryAccessException; - public abstract int read(int address) throws MemoryAccessException; - public abstract String toString(); - - public void setBus(Bus bus) { - this.bus = bus; - } - - public Bus getBus() { - return this.bus; - } - - public MemoryRange getMemoryRange() { - return memoryRange; - } - - public int endAddress() { - return memoryRange.endAddress(); - } - - public int startAddress() { - return memoryRange.startAddress(); - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Compares two devices. The sort order is defined by the sort - * order of the device's memory ranges. - */ - public int compareTo(Device other) { - if (other == null) { - throw new NullPointerException("Cannot compare to null."); + public Device(int address, int size, String name) + throws MemoryRangeException { + this.memoryRange = new MemoryRange(address, address + size - 1); + this.name = name; + this.deviceChangeListeners = new HashSet(); } - if (this == other) { - return 0; + + public Device(int address, int size) throws MemoryRangeException { + this(address, size, null); + } + + /* Methods required to be implemented by inheriting classes. */ + public abstract void write(int address, int data) throws MemoryAccessException; + + public abstract int read(int address) throws MemoryAccessException; + + public abstract String toString(); + + public Bus getBus() { + return this.bus; + } + + public void setBus(Bus bus) { + this.bus = bus; + } + + public MemoryRange getMemoryRange() { + return memoryRange; + } + + public int endAddress() { + return memoryRange.endAddress(); + } + + public int startAddress() { + return memoryRange.startAddress(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void registerListener(DeviceChangeListener listener) { + deviceChangeListeners.add(listener); + } + + public void notifyListeners() { + for (DeviceChangeListener l : deviceChangeListeners) { + l.deviceStateChanged(); + } + } + + /** + * Compares two devices. The sort order is defined by the sort + * order of the device's memory ranges. + */ + public int compareTo(Device other) { + if (other == null) { + throw new NullPointerException("Cannot compare to null."); + } + if (this == other) { + return 0; + } + return getMemoryRange().compareTo(other.getMemoryRange()); } - return getMemoryRange().compareTo(other.getMemoryRange()); - } } diff --git a/src/main/java/com/loomcom/symon/devices/DeviceChangeListener.java b/src/main/java/com/loomcom/symon/devices/DeviceChangeListener.java new file mode 100644 index 0000000..5d7565c --- /dev/null +++ b/src/main/java/com/loomcom/symon/devices/DeviceChangeListener.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Seth J. Morabito + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.devices; + +public interface DeviceChangeListener { + public void deviceStateChanged(); +} diff --git a/src/main/java/com/loomcom/symon/ui/Console.java b/src/main/java/com/loomcom/symon/ui/Console.java index e6511c3..39c565a 100644 --- a/src/main/java/com/loomcom/symon/ui/Console.java +++ b/src/main/java/com/loomcom/symon/ui/Console.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008-2013 Seth J. Morabito + * Copyright (c) 2013 Seth J. Morabito * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -23,14 +23,6 @@ package com.loomcom.symon.ui; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.util.logging.Level; -import java.util.logging.Logger; - import com.grahamedgecombe.jterminal.JTerminal; import com.grahamedgecombe.jterminal.vt100.Vt100TerminalModel; import com.loomcom.symon.exceptions.FifoUnderrunException; @@ -39,6 +31,11 @@ import com.loomcom.symon.util.FifoRingBuffer; import javax.swing.*; import javax.swing.border.BevelBorder; import javax.swing.border.Border; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; /** * The Console is a simulated 80 column x 24 row VT-100 terminal attached to diff --git a/src/main/java/com/loomcom/symon/ui/CrtPanel.java b/src/main/java/com/loomcom/symon/ui/CrtPanel.java deleted file mode 100644 index a54a97a..0000000 --- a/src/main/java/com/loomcom/symon/ui/CrtPanel.java +++ /dev/null @@ -1,275 +0,0 @@ -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/MemoryWindow.java b/src/main/java/com/loomcom/symon/ui/MemoryWindow.java index 43a9c7c..b358dbc 100644 --- a/src/main/java/com/loomcom/symon/ui/MemoryWindow.java +++ b/src/main/java/com/loomcom/symon/ui/MemoryWindow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008-2013 Seth J. Morabito + * Copyright (c) 2013 Seth J. Morabito * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -28,13 +28,9 @@ import com.loomcom.symon.exceptions.MemoryAccessException; import com.loomcom.symon.util.HexUtil; import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; -import javax.swing.border.MatteBorder; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import javax.swing.text.JTextComponent; import java.awt.*; diff --git a/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java b/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java index 6fda5c4..d42c011 100644 --- a/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java +++ b/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008-2013 Seth J. Morabito + * Copyright (c) 2013 Seth J. Morabito * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the diff --git a/src/main/java/com/loomcom/symon/ui/StatusPanel.java b/src/main/java/com/loomcom/symon/ui/StatusPanel.java index 1fa9f5d..addb4cc 100644 --- a/src/main/java/com/loomcom/symon/ui/StatusPanel.java +++ b/src/main/java/com/loomcom/symon/ui/StatusPanel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008-2013 Seth J. Morabito + * Copyright (c) 2013 Seth J. Morabito * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -26,7 +26,8 @@ package com.loomcom.symon.ui; import com.loomcom.symon.Cpu; import javax.swing.*; -import javax.swing.border.*; +import javax.swing.border.Border; +import javax.swing.border.EtchedBorder; import java.awt.*; /** diff --git a/src/main/java/com/loomcom/symon/ui/TraceLog.java b/src/main/java/com/loomcom/symon/ui/TraceLog.java index 99db88a..6b8981b 100644 --- a/src/main/java/com/loomcom/symon/ui/TraceLog.java +++ b/src/main/java/com/loomcom/symon/ui/TraceLog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008-2013 Seth J. Morabito + * Copyright (c) 2013 Seth J. Morabito * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -19,7 +19,6 @@ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * */ package com.loomcom.symon.ui; diff --git a/src/main/java/com/loomcom/symon/ui/VideoWindow.java b/src/main/java/com/loomcom/symon/ui/VideoWindow.java index edea06f..fdea2f2 100644 --- a/src/main/java/com/loomcom/symon/ui/VideoWindow.java +++ b/src/main/java/com/loomcom/symon/ui/VideoWindow.java @@ -1,34 +1,284 @@ +/* + * Copyright (c) 2013 Seth J. Morabito + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package com.loomcom.symon.ui; +import com.loomcom.symon.devices.Crtc; +import com.loomcom.symon.devices.DeviceChangeListener; + import javax.swing.*; import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; -public class VideoWindow extends JFrame { +public class VideoWindow extends JFrame implements DeviceChangeListener { - private CrtPanel crtPanel; + private static final Logger logger = Logger.getLogger(VideoWindow.class.getName()); + + private static final int CHAR_WIDTH = 8; + private static final int CHAR_HEIGHT = 8; + + private final int scaleX, scaleY; + private final boolean shouldScale; + + private BufferedImage image; + private int[] charRom; + private int[] videoRam; + + private int horizontalDisplayed; + private int verticalDisplayed; + private int scanLinesPerRow; + private int cursorBlinkRate; + private boolean showCursor; + + private Dimension dimensions; + private Crtc crtc; + + private ScheduledExecutorService scheduler; + private ScheduledFuture cursorBlinker; + + private class VideoPanel extends JPanel { + @Override + public void paintComponent(Graphics g) { + for (int i = 0; i < crtc.getPageSize(); i++) { + int address = crtc.getStartAddress() + i; + int originX = (i % horizontalDisplayed) * CHAR_WIDTH; + int originY = (i / horizontalDisplayed) * scanLinesPerRow; + image.getRaster().setPixels(originX, originY, CHAR_WIDTH, scanLinesPerRow, getGlyph(i, videoRam[address])); + } + Graphics2D g2d = (Graphics2D)g; + if (shouldScale) { + g2d.scale(scaleX, scaleY); + } + g2d.drawImage(image, 0, 0, null); + } + + @Override + public Dimension getMinimumSize() { + return dimensions; + } + + @Override + public Dimension getPreferredSize() { + return dimensions; + } - public VideoWindow(CrtPanel crtPanel) { - this.crtPanel = crtPanel; - createUi(); } - public void createUi() { + private class CursorBlinker implements Runnable { + public void run() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (cursorBlinkRate > 0) { + showCursor = !showCursor; + repaint(); + } + } + }); + } + } + + public VideoWindow(Crtc crtc, int scaleX, int scaleY) throws IOException { + crtc.registerListener(this); + + this.scheduler = Executors.newSingleThreadScheduledExecutor(); + this.crtc = crtc; + this.charRom = convertCharRom(loadCharRom("/pet.rom"), CHAR_WIDTH); + this.videoRam = crtc.getDmaAccess(); + this.scaleX = scaleX; + this.scaleY = scaleY; + this.shouldScale = (scaleX > 1 || scaleY > 1); + this.cursorBlinkRate = crtc.getCursorBlinkRate(); + + if (cursorBlinkRate > 0) { + this.cursorBlinker = scheduler.scheduleAtFixedRate(new CursorBlinker(), + cursorBlinkRate, + cursorBlinkRate, + TimeUnit.MILLISECONDS); + } + + // Capture some state from the CRTC that will define the + // window size. When these values change, the window will + // need to re-pack and redraw. + this.horizontalDisplayed = crtc.getHorizontalDisplayed(); + this.verticalDisplayed = crtc.getVerticalDisplayed(); + this.scanLinesPerRow = crtc.getScanLinesPerRow(); + + buildImage(); + + createAndShowUi(); + + } + + 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 void createAndShowUi() { setTitle("Composite Video"); - int borderWidth = (int) (crtPanel.getWidth() * 0.08); - int borderHeight = (int) (crtPanel.getHeight() * 0.08); + int borderWidth = 20; + int borderHeight = 20; 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); + containerPane.add(new VideoPanel(), BorderLayout.CENTER); + + getContentPane().add(containerPane, BorderLayout.CENTER); setResizable(false); pack(); } + private void buildImage() { + int rasterWidth = CHAR_WIDTH * horizontalDisplayed; + int rasterHeight = scanLinesPerRow * verticalDisplayed; + this.image = new BufferedImage(rasterWidth, rasterHeight, BufferedImage.TYPE_BYTE_BINARY); + this.dimensions = new Dimension(rasterWidth * scaleX, rasterHeight * scaleY); + } + + /** + * Called by the CRTC on state change. + */ + public void deviceStateChanged() { + + // Certain state + boolean repackNeeded = false; + + if (horizontalDisplayed != crtc.getHorizontalDisplayed()) { + horizontalDisplayed = crtc.getHorizontalDisplayed(); + repackNeeded = true; + } + + if (verticalDisplayed != crtc.getVerticalDisplayed()) { + verticalDisplayed = crtc.getVerticalDisplayed(); + repackNeeded = true; + } + + if (scanLinesPerRow != crtc.getScanLinesPerRow()) { + scanLinesPerRow = crtc.getScanLinesPerRow(); + repackNeeded = true; + } + + if (cursorBlinkRate != crtc.getCursorBlinkRate()) { + cursorBlinkRate = crtc.getCursorBlinkRate(); + + if (cursorBlinker != null) { + cursorBlinker.cancel(true); + cursorBlinker = null; + } + + if (cursorBlinkRate > 0) { + cursorBlinker = scheduler.scheduleAtFixedRate(new CursorBlinker(), + cursorBlinkRate, + cursorBlinkRate, + TimeUnit.MILLISECONDS); + } + } + + if (repackNeeded) { + buildImage(); + invalidate(); + pack(); + } + } + + /** + * 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; + } + + /** + * 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) * (CHAR_HEIGHT * CHAR_WIDTH); + int[] glyph = new int[CHAR_WIDTH * scanLinesPerRow]; + + // Populate the character + for (int i = 0; i < (CHAR_WIDTH * Math.min(CHAR_HEIGHT, scanLinesPerRow)); i++) { + glyph[i] = charRom[romOffset + i]; + } + + // Overlay the cursor + if (showCursor && crtc.isCursorEnabled() && crtc.getCursorPosition() == position) { + int cursorStart = Math.min(glyph.length, crtc.getCursorStartLine() * CHAR_WIDTH); + int cursorStop = Math.min(glyph.length, (crtc.getCursorStopLine() + 1) * CHAR_WIDTH); + + for (int i = cursorStart; i < cursorStop; i++) { + glyph[i] ^= 0xff; + } + } + + return glyph; + } + public void refreshDisplay() { // TODO: Verify whether this is necessary. Does `repaint()' do anything if the window is not visible? if (isVisible()) {