From df88c54f90397f303bef69928576814c842634de Mon Sep 17 00:00:00 2001 From: Seth Morabito Date: Thu, 31 Dec 2015 16:09:46 -0800 Subject: [PATCH] Support for breakpoints - Adds a new window that allows adding and deleting breakpoints. Will halt the simulator when a breakpoint is reached. --- src/main/java/com/loomcom/symon/Cpu.java | 7 - src/main/java/com/loomcom/symon/Main.java | 4 +- .../java/com/loomcom/symon/Simulator.java | 107 +++++++---- .../loomcom/symon/ui/BreakpointsWindow.java | 175 ++++++++++++++++++ .../com/loomcom/symon/ui/MemoryWindow.java | 11 +- 5 files changed, 258 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java index 04e303d..68f4814 100644 --- a/src/main/java/com/loomcom/symon/Cpu.java +++ b/src/main/java/com/loomcom/symon/Cpu.java @@ -911,13 +911,6 @@ public class Cpu implements InstructionTable { this.clockPeriodInNs = clockPeriodInNs; } - /** - * @return The simulated clock period, in nanoseconds - */ - public long getClockPeriodInNs() { - return clockPeriodInNs; - } - /** * Return the current Cpu State. * diff --git a/src/main/java/com/loomcom/symon/Main.java b/src/main/java/com/loomcom/symon/Main.java index 26df834..5e4d8ff 100644 --- a/src/main/java/com/loomcom/symon/Main.java +++ b/src/main/java/com/loomcom/symon/Main.java @@ -97,8 +97,8 @@ public class Main { }); - Simulator.MAIN_CMD cmd = simulator.waitForCommand(); - if (cmd.equals(Simulator.MAIN_CMD.SELECTMACHINE)) { + Simulator.MainCommand cmd = simulator.waitForCommand(); + if (cmd.equals(Simulator.MainCommand.SELECTMACHINE)) { machineClass = null; } else { break; diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index a9cfccf..9783419 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -24,10 +24,7 @@ package com.loomcom.symon; import com.loomcom.symon.devices.Memory; -import com.loomcom.symon.exceptions.FifoUnderrunException; -import com.loomcom.symon.exceptions.MemoryAccessException; -import com.loomcom.symon.exceptions.MemoryRangeException; -import com.loomcom.symon.exceptions.SymonException; +import com.loomcom.symon.exceptions.*; import com.loomcom.symon.machines.Machine; import com.loomcom.symon.ui.*; import com.loomcom.symon.ui.Console; @@ -39,6 +36,8 @@ import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.*; import java.io.*; +import java.util.SortedSet; +import java.util.TreeSet; /** * Symon Simulator Interface and Control. @@ -53,9 +52,9 @@ public class Simulator { private final static Logger logger = LoggerFactory.getLogger(Simulator.class.getName()); // UI constants - private static final int DEFAULT_FONT_SIZE = 12; - private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, DEFAULT_FONT_SIZE); - private static final int CONSOLE_BORDER_WIDTH = 10; + private static final int DEFAULT_FONT_SIZE = 12; + private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, DEFAULT_FONT_SIZE); + private static final int CONSOLE_BORDER_WIDTH = 10; // Clock periods, in NS, for each speed. 0MHz, 1MHz, 2MHz, 3MHz, 4MHz, 5MHz, 6MHz, 7MHz, 8MHz. private static final long[] CLOCK_PERIODS = {0, 1000, 500, 333, 250, 200, 167, 143, 125}; @@ -105,38 +104,48 @@ public class Simulator { private final VideoWindow videoWindow; + private final BreakpointsWindow breakpointsWindow; + private SimulatorMenu menuBar; - private RunLoop runLoop; - private Console console; + private RunLoop runLoop; + private Console console; private StatusPanel statusPane; private JButton runStopButton; private JButton stepButton; private JComboBox stepCountBox; - private JFileChooser fileChooser; + private JFileChooser fileChooser; private PreferencesDialog preferences; + private SortedSet breakpoints; + private final Object commandMonitorObject = new Object(); - private MAIN_CMD command = MAIN_CMD.NONE; - public enum MAIN_CMD { + + private MainCommand command = MainCommand.NONE; + + public enum MainCommand { NONE, SELECTMACHINE } - + /** * The list of step counts that will appear in the "Step" drop-down. */ private static final String[] STEPS = {"1", "5", "10", "20", "50", "100"}; public Simulator(Class machineClass) throws Exception { + this.breakpoints = new TreeSet<>(); + this.machine = (Machine) machineClass.getConstructors()[0].newInstance(); // Initialize final fields in the constructor. this.traceLog = new TraceLog(); this.memoryWindow = new MemoryWindow(machine.getBus()); - if(machine.getCrtc() != null) { + this.breakpointsWindow = new BreakpointsWindow(breakpoints, mainWindow); + + if (machine.getCrtc() != null) { videoWindow = new VideoWindow(machine.getCrtc(), 2, 2); } else { videoWindow = null; @@ -235,9 +244,9 @@ public class Simulator { console.requestFocus(); handleReset(false); } - - public MAIN_CMD waitForCommand() { - synchronized(commandMonitorObject) { + + public MainCommand waitForCommand() { + synchronized (commandMonitorObject) { try { commandMonitorObject.wait(); } catch (InterruptedException ex) { @@ -246,7 +255,7 @@ public class Simulator { } return command; } - + private void handleStart() { // Shift focus to the console. @@ -274,7 +283,7 @@ public class Simulator { } try { - logger.info("Reset requested. Resetting CPU."); + logger.debug("Reset requested. Resetting CPU."); // Reset CPU machine.getCpu().reset(); // Clear the console. @@ -349,7 +358,6 @@ public class Simulator { updateVisibleState(); stepsSinceLastUpdate = 0; } - } /** @@ -389,7 +397,7 @@ public class Simulator { } public void run() { - logger.info("Starting main run loop."); + logger.debug("Starting main run loop."); isRunning = true; SwingUtilities.invokeLater(() -> { @@ -426,12 +434,12 @@ public class Simulator { } /** - * Returns true if the run loop should proceed to the next step. - * * @return True if the run loop should proceed to the next step. */ private boolean shouldContinue() { - return isRunning && !(preferences.getHaltOnBreak() && machine.getCpu().getInstruction() == 0x00); + return !breakpoints.contains(machine.getCpu().getProgramCounter()) && + isRunning && + !(preferences.getHaltOnBreak() && machine.getCpu().getInstruction() == 0x00); } } @@ -506,7 +514,7 @@ public class Simulator { if (fileSize != machine.getRomSize()) { throw new IOException("ROM file must be exactly " + String.valueOf(machine.getRomSize()) + " bytes."); } - + // Load the new ROM image Memory rom = Memory.makeROM(machine.getRomBase(), machine.getRomBase() + machine.getRomSize() - 1, romFile); machine.setRom(rom); @@ -551,7 +559,7 @@ public class Simulator { preferences.getDialog().setVisible(true); } } - + class SelectMachineAction extends AbstractAction { public SelectMachineAction() { super("Switch emulated machine...", null); @@ -560,19 +568,19 @@ public class Simulator { } public void actionPerformed(ActionEvent actionEvent) { - if(runLoop != null) { + if (runLoop != null) { runLoop.requestStop(); } memoryWindow.dispose(); traceLog.dispose(); - if(videoWindow != null) { + if (videoWindow != null) { videoWindow.dispose(); } mainWindow.dispose(); - command = MAIN_CMD.SELECTMACHINE; - synchronized(commandMonitorObject) { + command = MainCommand.SELECTMACHINE; + synchronized (commandMonitorObject) { commandMonitorObject.notifyAll(); } } @@ -682,6 +690,23 @@ public class Simulator { } } + class ToggleBreakpointWindowAction extends AbstractAction { + public ToggleBreakpointWindowAction() { + super("Breakpoints...", null); + putValue(SHORT_DESCRIPTION, "Show or Hide Breakpoints"); + } + + public void actionPerformed(ActionEvent actionEvent) { + synchronized (breakpointsWindow) { + if (breakpointsWindow.isVisible()) { + breakpointsWindow.setVisible(false); + } else { + breakpointsWindow.setVisible(true); + } + } + } + } + class SimulatorMenu extends JMenuBar { // Menu Items private JMenuItem loadProgramItem; @@ -779,7 +804,7 @@ public class Simulator { }); viewMenu.add(showMemoryTable); - if(videoWindow != null) { + if (videoWindow != null) { final JCheckBoxMenuItem showVideoWindow = new JCheckBoxMenuItem(new ToggleVideoWindowAction()); videoWindow.addWindowListener(new WindowAdapter() { @Override @@ -795,7 +820,15 @@ public class Simulator { /* * Simulator Menu */ + + JMenu simulatorMenu = new JMenu("Simulator"); + + // "Select Machine..." item. + JMenuItem selectMachineItem = new JMenuItem(new SelectMachineAction()); + simulatorMenu.add(selectMachineItem); + + // "Clock Speed" sub-menu JMenu speedSubMenu = new JMenu("Clock Speed"); ButtonGroup speedGroup = new ButtonGroup(); @@ -806,8 +839,16 @@ public class Simulator { simulatorMenu.add(speedSubMenu); - JMenuItem selectMachineItem = new JMenuItem(new SelectMachineAction()); - simulatorMenu.add(selectMachineItem); + // "Breakpoints" + final JCheckBoxMenuItem showBreakpoints = new JCheckBoxMenuItem(new ToggleBreakpointWindowAction()); + // Un-check the menu item if the user closes the window directly + breakpointsWindow.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + showBreakpoints.setSelected(false); + } + }); + simulatorMenu.add(showBreakpoints); add(simulatorMenu); } diff --git a/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java b/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java new file mode 100644 index 0000000..8f51129 --- /dev/null +++ b/src/main/java/com/loomcom/symon/ui/BreakpointsWindow.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016 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.util.HexUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.util.ArrayList; +import java.util.SortedSet; + +/** + * Simple window to enter breakpoints. + */ +public class BreakpointsWindow extends JFrame { + + private static final Logger logger = LoggerFactory.getLogger(BreakpointsWindow.class); + + private static final Dimension FRAME_SIZE = new Dimension(240, 280); + private static final String EMPTY_STRING = ""; + + private JFrame mainWindow; + private SortedSet breakpoints; + + /** + * Simple ListModel to back the list of breakpoints. + */ + private class BreakpointsListModel extends AbstractListModel { + private SortedSet breakpoints; + + public BreakpointsListModel(SortedSet breakpoints) { + this.breakpoints = breakpoints; + } + + @Override + public int getSize() { + return breakpoints.size(); + } + + @Override + public String getElementAt(int index) { + ArrayList values = new ArrayList<>(breakpoints); + return "$" + HexUtil.wordToHex(values.get(index)); + } + + public void addElement(Integer breakpoint) { + breakpoints.add(breakpoint); + ArrayList values = new ArrayList<>(breakpoints); + int index = values.indexOf(breakpoint); + fireIntervalAdded(this, index, index); + } + + public void removeElement(int index) { + ArrayList values = new ArrayList<>(breakpoints); + Integer breakpoint = values.get(index); + breakpoints.remove(breakpoint); + fireIntervalRemoved(this, index, index); + } + } + + public BreakpointsWindow(SortedSet breakpoints, + JFrame mainWindow) { + this.breakpoints = breakpoints; + this.mainWindow = mainWindow; + createUi(); + } + + private void createUi() { + setTitle("Breakpoints"); + + JPanel breakpointsPanel = new JPanel(); + JPanel controlPanel = new JPanel(); + + breakpointsPanel.setLayout(new BorderLayout()); + breakpointsPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + JButton addButton = new JButton("Add"); + JButton removeButton = new JButton("Del"); + removeButton.setEnabled(false); + + JTextField addTextField = new JTextField(5); + + BreakpointsListModel listModel = new BreakpointsListModel(breakpoints); + JList breakpointsList = new JList<>(listModel); + breakpointsList.setFont(new Font("Monospace", Font.PLAIN, 14)); + breakpointsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + JScrollPane scrollPane = new JScrollPane(breakpointsList); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + breakpointsPanel.add(scrollPane, BorderLayout.CENTER); + + + breakpointsList.addListSelectionListener(le -> { + int idx = breakpointsList.getSelectedIndex(); + + if (idx == -1) { + removeButton.setEnabled(false); + } else { + removeButton.setEnabled(true); + } + }); + + addButton.addActionListener(e -> { + int value = -1; + + String newBreakpoint = addTextField.getText(); + + if (newBreakpoint == null || newBreakpoint.isEmpty()) { + return; + } + + try { + value = Integer.parseInt(addTextField.getText(), 16); + } catch (NumberFormatException ex) { + logger.warn("Can't parse page number {}", newBreakpoint); + return; + } + + if (value < 0) { + return; + } + + listModel.addElement(value); + + logger.debug("Added breakpoint ${}", HexUtil.wordToHex(value)); + + addTextField.setText(EMPTY_STRING); + }); + + removeButton.addActionListener(e -> listModel.removeElement(breakpointsList.getSelectedIndex())); + + controlPanel.add(addTextField); + controlPanel.add(addButton); + controlPanel.add(removeButton); + + setLayout(new BorderLayout()); + getContentPane().add(breakpointsPanel, BorderLayout.CENTER); + getContentPane().add(controlPanel, BorderLayout.SOUTH); + + setMinimumSize(FRAME_SIZE); + setMaximumSize(FRAME_SIZE); + setPreferredSize(FRAME_SIZE); + + setLocationRelativeTo(mainWindow); + setResizable(false); + + pack(); + } +} diff --git a/src/main/java/com/loomcom/symon/ui/MemoryWindow.java b/src/main/java/com/loomcom/symon/ui/MemoryWindow.java index d73e4bb..db3e6fd 100644 --- a/src/main/java/com/loomcom/symon/ui/MemoryWindow.java +++ b/src/main/java/com/loomcom/symon/ui/MemoryWindow.java @@ -26,6 +26,8 @@ package com.loomcom.symon.ui; import com.loomcom.symon.Bus; import com.loomcom.symon.exceptions.MemoryAccessException; import com.loomcom.symon.util.HexUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -37,8 +39,7 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.EventObject; -import java.util.logging.Level; -import java.util.logging.Logger; + /** * This Frame displays the contents of a page of memory. The page number to be displayed @@ -46,6 +47,8 @@ import java.util.logging.Logger; */ public class MemoryWindow extends JFrame implements ActionListener { + private static final Logger logger = LoggerFactory.getLogger(MemoryWindow.class); + private MemoryTableModel memoryTableModel; private JTable memoryTable; private JTextField pageNumberTextField; @@ -209,9 +212,9 @@ public class MemoryWindow extends JFrame implements ActionListener { } catch (NumberFormatException ex) { // An invalid number was entered. Log the error, but otherwise // take no action. - Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Can't parse page number " + - pageNumberInput); + logger.warn("Can't parse page number {}", pageNumberInput); } + updateControls(); } }