Checkpoint of my Swing UI refactoring. Usable for output-only programs, but only barely.
This commit is contained in:
1 parent
5ce81ef82c
commit
ed36690e9e
13 files changed
+776
-659
No files matched your search
@@ -1,34 +1,28 @@
|
||||
package com.loomcom.symon;
|
||||
|
||||
import java.util.logging.*;
|
||||
|
||||
import com.loomcom.symon.devices.*;
|
||||
import com.loomcom.symon.exceptions.*;
|
||||
import com.loomcom.symon.ui.UiUpdater;
|
||||
import com.loomcom.symon.devices.Acia;
|
||||
import com.loomcom.symon.devices.Memory;
|
||||
import com.loomcom.symon.exceptions.MemoryAccessException;
|
||||
import com.loomcom.symon.exceptions.MemoryRangeException;
|
||||
import com.loomcom.symon.exceptions.SymonException;
|
||||
import com.loomcom.symon.ui.PreferencesDialog;
|
||||
import com.loomcom.symon.ui.StatusPanel;
|
||||
import com.loomcom.symon.ui.Console;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.io.*;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* Main entry point and control for the Symon Simulator.
|
||||
* This class is responsible for creating the UI and starting
|
||||
* the IO threads that pass data to and from the simulated
|
||||
* Bus and Cpu.
|
||||
*/
|
||||
public class Simulator implements Runnable {
|
||||
|
||||
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
|
||||
|
||||
private Bus bus;
|
||||
private Cpu cpu;
|
||||
private Acia acia;
|
||||
private Memory ram;
|
||||
private Memory rom;
|
||||
private boolean isRunning = false;
|
||||
|
||||
private int updatesRequested = 0;
|
||||
|
||||
private UiUpdater uiUpdater;
|
||||
public class Simulator implements ActionListener, Observer {
|
||||
|
||||
// Constants used by the simulated system. These define the memory map.
|
||||
private static final int BUS_BOTTOM = 0x0000;
|
||||
private static final int BUS_TOP = 0xffff;
|
||||
|
||||
@@ -40,22 +34,316 @@ public class Simulator implements Runnable {
|
||||
|
||||
public static final int ACIA_BASE = 0xc000;
|
||||
|
||||
private static final int MAX_REQUESTS_BETWEEN_UPDATES = 25000;
|
||||
// Since it is very expensive to update the UI with Swing's Event Dispatch Thread, we can't afford
|
||||
// to refresh the view on every simualted clock cycle. Instead, we will only refresh the view after this
|
||||
// number of steps when running normally.
|
||||
private static final int MAX_STEPS_BETWEEN_UPDATES = 15000;
|
||||
|
||||
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
|
||||
|
||||
// The simulated peripherals
|
||||
private final Bus bus;
|
||||
private final Cpu cpu;
|
||||
private final Acia acia;
|
||||
private final Memory ram;
|
||||
private final Memory rom;
|
||||
|
||||
// A counter to keep track of the number of UI updates that have been
|
||||
// requested
|
||||
private int stepsSinceLastUpdate = 0;
|
||||
|
||||
private JFrame mainWindow;
|
||||
private RunLoop runLoop;
|
||||
private Console console;
|
||||
private StatusPanel statusPane;
|
||||
|
||||
private JButton runStopButton;
|
||||
private JButton stepButton;
|
||||
private JButton resetButton;
|
||||
|
||||
// TODO: loadMenuItem seriously violates encapsulation!
|
||||
// A far better solution would be to extend JMenu and add callback
|
||||
// methods to enable and disable menus as required.
|
||||
|
||||
// Menu Items
|
||||
private JMenuItem loadMenuItem;
|
||||
|
||||
private JFileChooser fileChooser;
|
||||
private Preferences preferences;
|
||||
|
||||
private StringBuffer aciaBuffer;
|
||||
|
||||
public Simulator() throws MemoryRangeException {
|
||||
this.aciaBuffer = new StringBuffer(512);
|
||||
this.acia = new Acia(ACIA_BASE);
|
||||
this.bus = new Bus(BUS_BOTTOM, BUS_TOP);
|
||||
this.cpu = new Cpu();
|
||||
this.ram = new Memory(MEMORY_BASE, MEMORY_SIZE, false);
|
||||
// TODO: Load this ROM from a file, naturally!
|
||||
|
||||
// TODO: Load this ROM from a file, of course!
|
||||
this.rom = new Memory(ROM_BASE, ROM_SIZE, false);
|
||||
|
||||
bus.addCpu(cpu);
|
||||
bus.addDevice(acia);
|
||||
bus.addDevice(ram);
|
||||
bus.addDevice(rom);
|
||||
}
|
||||
|
||||
public void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
|
||||
/**
|
||||
* Display the main simulator UI.
|
||||
*/
|
||||
public void createAndShowUi() {
|
||||
mainWindow = new JFrame();
|
||||
mainWindow.setTitle("Symon 6502 Simulator");
|
||||
mainWindow.setResizable(false);
|
||||
mainWindow.getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// The Menu
|
||||
mainWindow.setJMenuBar(createMenuBar());
|
||||
|
||||
// UI components used for I/O.
|
||||
this.console = new com.loomcom.symon.ui.Console();
|
||||
this.statusPane = new StatusPanel();
|
||||
|
||||
// File Chooser
|
||||
fileChooser = new JFileChooser();
|
||||
preferences = new PreferencesDialog(mainWindow, true);
|
||||
|
||||
// Panel for Console and Buttons
|
||||
JPanel controlsContainer = new JPanel();
|
||||
JPanel buttonContainer = new JPanel();
|
||||
Dimension buttonPanelSize = new Dimension(console.getWidth(), 36);
|
||||
|
||||
buttonContainer.setMinimumSize(buttonPanelSize);
|
||||
buttonContainer.setMaximumSize(buttonPanelSize);
|
||||
buttonContainer.setPreferredSize(buttonPanelSize);
|
||||
|
||||
controlsContainer.setLayout(new BorderLayout());
|
||||
buttonContainer.setLayout(new FlowLayout());
|
||||
|
||||
runStopButton = new JButton("Run");
|
||||
stepButton = new JButton("Step");
|
||||
resetButton = new JButton("Reset");
|
||||
|
||||
buttonContainer.add(runStopButton);
|
||||
buttonContainer.add(stepButton);
|
||||
buttonContainer.add(resetButton);
|
||||
|
||||
// Left side - console and buttons
|
||||
controlsContainer.add(console, BorderLayout.PAGE_START);
|
||||
controlsContainer.add(buttonContainer, BorderLayout.PAGE_END);
|
||||
|
||||
mainWindow.getContentPane().add(controlsContainer, BorderLayout.LINE_START);
|
||||
mainWindow.getContentPane().add(statusPane, BorderLayout.LINE_END);
|
||||
|
||||
runStopButton.addActionListener(this);
|
||||
stepButton.addActionListener(this);
|
||||
resetButton.addActionListener(this);
|
||||
|
||||
mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
|
||||
console.requestFocus();
|
||||
|
||||
mainWindow.pack();
|
||||
mainWindow.setVisible(true);
|
||||
}
|
||||
|
||||
private JMenuBar createMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
loadMenuItem = new JMenuItem("Load Program");
|
||||
loadMenuItem.setMnemonic(KeyEvent.VK_L);
|
||||
|
||||
JMenuItem prefsItem = new JMenuItem("Preferences...");
|
||||
prefsItem.setMnemonic(KeyEvent.VK_P);
|
||||
|
||||
JMenuItem quitItem = new JMenuItem("Quit");
|
||||
quitItem.setMnemonic(KeyEvent.VK_Q);
|
||||
|
||||
loadMenuItem.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent actionEvent) {
|
||||
handleProgramLoad();
|
||||
}
|
||||
});
|
||||
|
||||
prefsItem.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent actionEvent) {
|
||||
showAndUpdatePreferences();
|
||||
}
|
||||
});
|
||||
|
||||
quitItem.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent actionEvent) {
|
||||
handleQuit();
|
||||
}
|
||||
});
|
||||
|
||||
fileMenu.add(loadMenuItem);
|
||||
fileMenu.add(prefsItem);
|
||||
fileMenu.add(quitItem);
|
||||
|
||||
return menuBar;
|
||||
}
|
||||
|
||||
public void showAndUpdatePreferences() {
|
||||
preferences.getDialog().setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive an ActionEvent from the UI, and act on it.
|
||||
*/
|
||||
public void actionPerformed(ActionEvent actionEvent) {
|
||||
if (actionEvent.getSource() == resetButton) {
|
||||
handleReset();
|
||||
} else if (actionEvent.getSource() == stepButton) {
|
||||
handleStep();
|
||||
} else if (actionEvent.getSource() == runStopButton) {
|
||||
if (runLoop != null && runLoop.isRunning()) {
|
||||
runLoop.requestStop();
|
||||
runLoop.interrupt();
|
||||
runLoop = null;
|
||||
} else {
|
||||
// Spin up the new run loop
|
||||
runLoop = new RunLoop();
|
||||
runLoop.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a file chooser prompting the user to load a binary program.
|
||||
* After the user selects a file, read it in starting at PROGRAM_START_ADDRESS.
|
||||
*/
|
||||
private void handleProgramLoad() {
|
||||
try {
|
||||
int retVal = fileChooser.showOpenDialog(mainWindow);
|
||||
if (retVal == JFileChooser.APPROVE_OPTION) {
|
||||
File f = fileChooser.getSelectedFile();
|
||||
if (f.canRead()) {
|
||||
long fileSize = f.length();
|
||||
|
||||
if (fileSize > MEMORY_SIZE) {
|
||||
throw new IOException("Program will not fit in available memory.");
|
||||
} else {
|
||||
byte[] program = new byte[(int) fileSize];
|
||||
int i = 0;
|
||||
FileInputStream fis = new FileInputStream(f);
|
||||
BufferedInputStream bis = new BufferedInputStream(fis);
|
||||
DataInputStream dis = new DataInputStream(bis);
|
||||
while (dis.available() != 0) {
|
||||
program[i++] = dis.readByte();
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
console.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// Now load the program at the starting address.
|
||||
loadProgram(program, preferences.getProgramStartAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.log(Level.SEVERE, "Unable to read file: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
} catch (MemoryAccessException ex) {
|
||||
logger.log(Level.SEVERE, "Memory access error loading program");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReset() {
|
||||
if (runLoop != null && runLoop.isRunning()) {
|
||||
runLoop.requestStop();
|
||||
runLoop.interrupt();
|
||||
runLoop = null;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.log(Level.INFO, "Reset requested. Resetting CPU and clearing memory.");
|
||||
// Reset and clear memory
|
||||
cpu.reset();
|
||||
ram.fill(0x00);
|
||||
// Clear the console.
|
||||
console.reset();
|
||||
// Update status.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
updateConsoleAndStatus(cpu);
|
||||
}
|
||||
});
|
||||
} catch (MemoryAccessException ex) {
|
||||
logger.log(Level.SEVERE, "Exception during simulator reset: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step once, and immediately refresh the UI.
|
||||
*/
|
||||
private void handleStep() {
|
||||
try {
|
||||
step();
|
||||
// The simulator is lazy about updating the UI for performance reasons, so always request an
|
||||
// immediate update after stepping manually.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
updateConsoleAndStatus(cpu);
|
||||
}
|
||||
});
|
||||
} catch (SymonException ex) {
|
||||
logger.log(Level.SEVERE, "Exception during simulator step: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to quit.
|
||||
*/
|
||||
private void handleQuit() {
|
||||
if (runLoop != null && runLoop.isRunning()) {
|
||||
runLoop.requestStop();
|
||||
runLoop.interrupt();
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single step of the simulated system.
|
||||
*/
|
||||
private void step() throws MemoryAccessException {
|
||||
|
||||
cpu.step();
|
||||
|
||||
// Read from the ACIA
|
||||
while (acia.hasTxChar()) {
|
||||
aciaBuffer.append((char) acia.txRead());
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (stepsSinceLastUpdate++ > MAX_STEPS_BETWEEN_UPDATES) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
updateConsoleAndStatus(cpu);
|
||||
}
|
||||
});
|
||||
stepsSinceLastUpdate = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a program into memory at the simulatorDidStart address.
|
||||
*/
|
||||
private void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
|
||||
cpu.setResetVector(startAddress);
|
||||
|
||||
int addr = startAddress, i;
|
||||
@@ -65,93 +353,117 @@ public class Simulator implements Runnable {
|
||||
|
||||
logger.log(Level.INFO, "Loaded " + i + " bytes at address 0x" +
|
||||
Integer.toString(startAddress, 16));
|
||||
}
|
||||
|
||||
public void setUiUpdater(UiUpdater uiUpdater) {
|
||||
this.uiUpdater = uiUpdater;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
public void requestStop() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public void reset() throws MemoryAccessException {
|
||||
// After loading, be sure to reset and
|
||||
// Reset (but don't clear memory, naturally)
|
||||
cpu.reset();
|
||||
}
|
||||
|
||||
public void clearMemory() {
|
||||
ram.fill(0x00);
|
||||
}
|
||||
|
||||
public int getProcessorStatus() {
|
||||
return cpu.getProcessorStatus();
|
||||
}
|
||||
|
||||
public Cpu getCpu() {
|
||||
return this.cpu;
|
||||
}
|
||||
|
||||
public long memorySize() {
|
||||
return MEMORY_SIZE;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
logger.log(Level.INFO, "Entering 'run' on main Simulator thread");
|
||||
isRunning = true;
|
||||
|
||||
// Immediately update the UI.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.simulatorDidStart();
|
||||
updateConsoleAndStatus(cpu);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
while (isRunning && !cpu.getBreakFlag()) {
|
||||
step();
|
||||
}
|
||||
} catch (SymonException ex) {
|
||||
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
logger.log(Level.INFO, "Exiting 'run'. BREAK=" + cpu.getBreakBit() + "; RUN_FLAG=" + isRunning);
|
||||
isRunning = false;
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.simulatorDidStop();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void step() throws MemoryAccessException, FifoUnderrunException {
|
||||
|
||||
cpu.step();
|
||||
|
||||
// TODO: ACIA interrupt handling. For now, poll ACIA on each step.
|
||||
|
||||
// Read from the ACIA and add to the UiUpdater buffer
|
||||
while (acia.hasTxChar()) {
|
||||
uiUpdater.consoleWrite(acia.txRead());
|
||||
/**
|
||||
* Update the UI with the latest state of the simulator.
|
||||
*/
|
||||
private void updateConsoleAndStatus(Cpu cpu) {
|
||||
// If we have accumulated any ACIA output in our temporary
|
||||
// buffer since the last UI update, write it to the console,
|
||||
// then clear the buffer.
|
||||
if (aciaBuffer.length() > 0) {
|
||||
console.print(aciaBuffer.toString());
|
||||
console.repaint();
|
||||
aciaBuffer.delete(0, aciaBuffer.length());
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (updatesRequested++ > MAX_REQUESTS_BETWEEN_UPDATES) {
|
||||
// Now update the state
|
||||
statusPane.updateState(cpu);
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Create the main UI window
|
||||
Simulator simulator = new Simulator();
|
||||
simulator.createAndShowUi();
|
||||
// Reset the simulator.
|
||||
simulator.handleReset();
|
||||
} catch (MemoryRangeException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration has changed. Re-load.
|
||||
*
|
||||
* @param observable
|
||||
* @param o
|
||||
*/
|
||||
public void update(Observable observable, Object o) {
|
||||
// Instance equality should work here, there is only one instance.
|
||||
if (observable == preferences) {
|
||||
// TODO: Update system based on state. (i.e., update ACIA address, and raise a dialog if it
|
||||
// overlaps with anything)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main run thread.
|
||||
*/
|
||||
class RunLoop extends Thread {
|
||||
private boolean isRunning = false;
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
public void requestStop() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
logger.log(Level.INFO, "Starting main run loop.");
|
||||
isRunning = true;
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.updateUi();
|
||||
// Don't allow step while the simulator is running
|
||||
stepButton.setEnabled(false);
|
||||
loadMenuItem.setEnabled(false);
|
||||
// Toggle the state of the run button
|
||||
runStopButton.setText("Stop");
|
||||
}
|
||||
});
|
||||
updatesRequested = 0;
|
||||
|
||||
try {
|
||||
// TODO: Interrupts - both software and hardware. i.e., jump to address stored in FFFE/FFFF on BRK.
|
||||
while (isRunning && !cpu.getBreakFlag()) {
|
||||
step();
|
||||
}
|
||||
} catch (SymonException ex) {
|
||||
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
// Allow step while the simulator is stopped
|
||||
stepButton.setEnabled(true);
|
||||
loadMenuItem.setEnabled(true);
|
||||
runStopButton.setText("Run");
|
||||
// Update state.
|
||||
updateConsoleAndStatus(cpu);
|
||||
}
|
||||
});
|
||||
|
||||
logger.log(Level.INFO, "Exiting main run loop. BREAK=" + cpu.getBreakBit() + "; RUN_FLAG=" + isRunning);
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user