diff --git a/src/main/java/com/loomcom/symon/devices/Crtc.java b/src/main/java/com/loomcom/symon/devices/Crtc.java index 0adc848..479c6fe 100644 --- a/src/main/java/com/loomcom/symon/devices/Crtc.java +++ b/src/main/java/com/loomcom/symon/devices/Crtc.java @@ -65,6 +65,11 @@ public class Crtc extends Device { private int currentRegister = 0; + // Status bits + private boolean rowColumnAddressing = false; + private boolean displayEnableSkew = false; + private boolean cursorSkew = false; + private Memory memory; public Crtc(int deviceAddress, Memory memory) throws MemoryRangeException, IOException { @@ -118,8 +123,9 @@ public class Crtc extends Device { return null; } - public int[] getDmaAccess() { - return memory.getDmaAccess(); + public int getCharAtAddress(int address) throws MemoryAccessException { + // TODO: Row/Column addressing + return memory.read(address); } public int getHorizontalDisplayed() { @@ -162,6 +168,18 @@ public class Crtc extends Device { return pageSize; } + public boolean getRowColumnAddressing() { + return rowColumnAddressing; + } + + public boolean getDisplayEnableSkew() { + return displayEnableSkew; + } + + public boolean getCursorSkew() { + return cursorSkew; + } + private void setCurrentRegister(int registerNumber) { this.currentRegister = registerNumber; } @@ -180,7 +198,9 @@ public class Crtc extends Device { pageSize = horizontalDisplayed * verticalDisplayed; break; case MODE_CONTROL: - // TODO: Implement multiple addressing modes and cursor skew. + rowColumnAddressing = (data & 0x04) != 0; + displayEnableSkew = (data & 0x10) != 0; + cursorSkew = (data & 0x20) != 0; break; case SCAN_LINE: scanLinesPerRow = data; diff --git a/src/main/java/com/loomcom/symon/ui/VideoWindow.java b/src/main/java/com/loomcom/symon/ui/VideoWindow.java index ebd049e..3b57786 100644 --- a/src/main/java/com/loomcom/symon/ui/VideoWindow.java +++ b/src/main/java/com/loomcom/symon/ui/VideoWindow.java @@ -25,6 +25,7 @@ package com.loomcom.symon.ui; import com.loomcom.symon.devices.Crtc; import com.loomcom.symon.devices.DeviceChangeListener; +import com.loomcom.symon.exceptions.MemoryAccessException; import javax.swing.*; import java.awt.*; @@ -36,8 +37,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import java.util.logging.Logger; +import static java.lang.System.*; + /** * VideoWindow represents a graphics framebuffer backed by a 6545 CRTC. * Each time the window's VideoPanel is repainted, the video memory is @@ -65,7 +69,6 @@ public class VideoWindow extends JFrame implements DeviceChangeListener { private BufferedImage image; private int[] charRom; - private int[] videoRam; private int horizontalDisplayed; private int verticalDisplayed; @@ -85,17 +88,21 @@ public class VideoWindow extends JFrame implements DeviceChangeListener { 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(address)); + try { + 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(address)); + } + Graphics2D g2d = (Graphics2D) g; + if (shouldScale) { + g2d.scale(scaleX, scaleY); + } + g2d.drawImage(image, 0, 0, null); + } catch (MemoryAccessException ex) { + logger.log(Level.SEVERE, "Memory Access Exception, can't paint video window! " + ex.getMessage()); } - Graphics2D g2d = (Graphics2D)g; - if (shouldScale) { - g2d.scale(scaleX, scaleY); - } - g2d.drawImage(image, 0, 0, null); } @Override @@ -132,7 +139,6 @@ public class VideoWindow extends JFrame implements DeviceChangeListener { this.scheduler = Executors.newSingleThreadScheduledExecutor(); this.crtc = crtc; this.charRom = loadCharRom("/pet.rom"); - this.videoRam = crtc.getDmaAccess(); this.scaleX = scaleX; this.scaleY = scaleY; this.shouldScale = (scaleX > 1 || scaleY > 1); @@ -158,13 +164,6 @@ public class VideoWindow extends JFrame implements DeviceChangeListener { } - public void refreshDisplay() { - // TODO: Verify whether this is necessary. Does `repaint()' do anything if the window is not visible? - if (isVisible()) { - repaint(); - } - } - /** * Called by the CRTC on state change. */ @@ -239,15 +238,13 @@ public class VideoWindow extends JFrame implements DeviceChangeListener { * @param address The address of the character being requested. * @return An array of integers representing the pixel data. */ - private int[] getGlyph(int address) { - int chr = videoRam[address]; + private int[] getGlyph(int address) throws MemoryAccessException { + int chr = crtc.getCharAtAddress(address); 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]; - } + arraycopy(charRom, romOffset, glyph, 0, CHAR_WIDTH * Math.min(CHAR_HEIGHT, scanLinesPerRow)); // Overlay the cursor if (!hideCursor && crtc.isCursorEnabled() && crtc.getCursorPosition() == address) { diff --git a/src/test/java/com/loomcom/symon/CrtcTest.java b/src/test/java/com/loomcom/symon/CrtcTest.java index 3caaf9f..ce5683c 100644 --- a/src/test/java/com/loomcom/symon/CrtcTest.java +++ b/src/test/java/com/loomcom/symon/CrtcTest.java @@ -19,19 +19,17 @@ import static org.mockito.Mockito.*; public class CrtcTest { Crtc crtc; + Memory memory; @Mock DeviceChangeListener changeListener; - @Mock - Memory memory; @Before public void createDevices() throws Exception { + memory = new Memory(0, 0x7fff); + crtc = new Crtc(0x9000, memory); crtc.registerListener(changeListener); - - when(memory.startAddress()).thenReturn(0); - when(memory.endAddress()).thenReturn(0x7fff); } @Test @@ -340,4 +338,58 @@ public class CrtcTest { crtc.write(1, 0x80); // Can't position cursor } + @Test + public void shouldSetRowColumnAddressing() throws Exception { + assertEquals(false, crtc.getRowColumnAddressing()); + crtc.write(0, 8); // Select mode control register + crtc.write(1, 0x04); + assertEquals(true, crtc.getRowColumnAddressing()); + } + + @Test + public void shouldSetDisplayEnableSkew() throws Exception { + assertEquals(false, crtc.getDisplayEnableSkew()); + crtc.write(0, 8); // Select mode control register + crtc.write(1, 0x10); + assertEquals(true, crtc.getDisplayEnableSkew()); + } + + @Test + public void shouldSetCursorSkew() throws Exception { + assertEquals(false, crtc.getCursorSkew()); + crtc.write(0, 8); // Select mode control register + crtc.write(1, 0x20); + assertEquals(true, crtc.getCursorSkew()); + } + + @Test + public void shouldDoStraightBinaryAddressing() throws Exception { + crtc.write(0, 8); + crtc.write(1, 0); // Select straight binary addressing + + // Fill the memory with a repeating pattern + int videoMemoryBase = 0x7000; + int j = 0; + + for (int i = 0; i < 2048; i++) { + memory.write(videoMemoryBase + i, j); + if (j == 255) { + j = 0; + } else { + j++; + } + } + + // Now verify that straight-binary addressing of the CRTC works + j = 0; + + for (int i = 0; i < 2048; i++) { + assertEquals(j, crtc.getCharAtAddress(videoMemoryBase + i)); + if (j == 255) { + j = 0; + } else { + j++; + } + } + } }