1
0
mirror of https://github.com/sethm/symon.git synced 2025-04-12 23:37:07 +00:00

First pass at Video window

This commit is contained in:
Seth Morabito 2013-12-27 21:40:28 -08:00
parent a5af522c5c
commit e7e3c77e3f
9 changed files with 496 additions and 27 deletions

19
pom.xml
View File

@ -130,7 +130,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.4</version>
<version>2.6</version>
<configuration>
<check>
<haltOnFailure>false</haltOnFailure>
@ -167,6 +167,21 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>com.loomcom.symon.Simulator</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@ -175,7 +190,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.4</version>
<version>2.6</version>
</plugin>
</plugins>
</reporting>

View File

@ -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;

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -109,4 +109,7 @@ public class Memory extends Device {
return "Memory: " + getMemoryRange().toString();
}
public int[] getDmaAccess() {
return mem;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

BIN
src/main/resources/cga8.rom Normal file

Binary file not shown.

BIN
src/main/resources/pet.rom Normal file

Binary file not shown.