diff --git a/locale/en-US.txt b/locale/en-US.txt index 0178e3d..b23ec23 100644 --- a/locale/en-US.txt +++ b/locale/en-US.txt @@ -8,26 +8,62 @@ locale { app { debug { - (menu) Debug - console Console - cpu CPU - memory Memory + (menu) Debug + breakpoints Breakpoints + console Console + cpu CPU + memory Memory + # VIP section + backgrounds Backgrounds + characters Characters + frame_buffers Frame buffers + objects Objects + worlds Worlds } file { - (menu) File - debug_mode Debug mode - exit Exit - game_mode Game mode - load_rom Load ROM... - new_window New window + (menu) File + debug_mode Debug mode + exit Exit + game_mode Game mode + load_rom Load ROM... + new_window New window } title { - default PVB Emulator - mixed {ctrl.number} {ctrl.filename} - {app.title.default} - number {ctrl.number} {app.title.default} - rom {ctrl.filename} - {app.title.default} + default PVB Emulator + mixed {ctrl.number} {ctrl.filename} - {app.title.default} + number {ctrl.number} {app.title.default} + rom {ctrl.filename} - {app.title.default} + } + +} + +# Breakpoints window +breakpoints { + address Address + condition Condition + default_name New breakpoint + delete Delete + enabled Enabled + exception Exception + execute Execute + name Name + new New + read Read + title Breakpoints + type Type + write Write + + error { + badliteral_a Error:{err.position}: Unable to process address "{err.text}" + badliteral_c Error:{err.position}: Unable to process literal "{err.text}" + badoperand Error:{err.position}: Invalid operand to operator "{err.text}" + badtoken Error:{err.position}: Unrecognized token "{err.text}" + earlyeof Error:{err.position}: Unexpected end of input + invalid Error:{err.position}: Token "{err.text}" cannot appear here + nesting Error:{err.position}: Expected "{err.other}", but found "{err.text}" + unexpected Error:{err.position}: Unexpected "{err.text}" } } diff --git a/src/core/cpu.c b/src/core/cpu.c index 2d50e6e..8601f1c 100644 --- a/src/core/cpu.c +++ b/src/core/cpu.c @@ -1126,6 +1126,10 @@ static vbool cpuExecute(Vue *vue) { vue->cpu.exception = 0xFF90; } + /* An application break was requested */ + if (vue->breakCode != 0) + return VUE_TRUE; + /* Common processing */ if (vue->cpu.exception == 0) { vue->cpu.cycles += CYCLES[vue->cpu.inst.id]; diff --git a/src/desktop/app/BreakpointsWindow.java b/src/desktop/app/BreakpointsWindow.java new file mode 100644 index 0000000..92abfe7 --- /dev/null +++ b/src/desktop/app/BreakpointsWindow.java @@ -0,0 +1,618 @@ +package app; + +// Java imports +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import javax.swing.*; + +// Project imports +import util.*; +import vue.*; + +// Memory viewer and hex editor window +class BreakpointsWindow extends ChildWindow { + + // Private fields + private Font font; // Display font + private ArrayList items; // List items + private int selectedIndex; // Selected list item + + // UI components + private JButton btnDelete; // Delete button + private JButton btnNew; // New button + private JCheckBox chkEnabled; // Enabled check box + private JCheckBox chkException; // Exception check box + private JCheckBox chkExecute; // Execute check box + private JCheckBox chkRead; // Read check box + private JCheckBox chkWrite; // Write check box + private JPanel client; // Client area + private JLabel errAddress; // Address error text + private JLabel errCondition; // Condition error text + private JPanel lstBreakpoints; // Breakpoint list + private JPanel spacer; // List termination spacer + private JTextField txtAddress; // Address text box + private JTextField txtCondition; // Condition text box + private JTextField txtName; // Name text box + + + + /////////////////////////////////////////////////////////////////////////// + // Constants // + /////////////////////////////////////////////////////////////////////////// + + // Address error keys + private static final String[] KEYS_ADDRESS = { + null , // NONE + "breakpoints.error.badliteral_a", // BADLITERAL + null , // BADOPERAND + null , // BADTOKEN + "breakpoints.error.earlyeof" , // EARLYEOF + null , // INVALID + null , // NESTING + "breakpoints.error.unexpected" // UNEXPECTED + }; + + // Condition error keys + private static final String[] KEYS_CONDITION = { + null , // NONE + "breakpoints.error.badliteral_c", // BADLITERAL + "breakpoints.error.badoperand" , // BADOPERAND + "breakpoints.error.badtoken" , // BADTOKEN + "breakpoints.error.earlyeof" , // EARLYEOF + "breakpoints.error.invalid" , // INVALID + "breakpoints.error.nesting" , // NESTING + "breakpoints.error.unexpected" // UNEXPECTED + }; + + // List constraints + private static final GridBagConstraints ENABLED; + private static final GridBagConstraints NAME; + private static final GridBagConstraints SPACER; + private static final GridBagConstraints STATUS; + + // Static initializer + static { + ENABLED = new GridBagConstraints(); + ENABLED.anchor = GridBagConstraints.CENTER; + ENABLED.insets = new Insets(1, 2, 1, 2); + NAME = new GridBagConstraints(); + NAME.fill = GridBagConstraints.HORIZONTAL; + NAME.gridwidth = GridBagConstraints.REMAINDER; + NAME.insets = new Insets(1, 0, 1, 2); + NAME.weightx = 1; + SPACER = new GridBagConstraints(); + SPACER.fill = GridBagConstraints.BOTH; + SPACER.gridheight = GridBagConstraints.REMAINDER; + SPACER.gridwidth = GridBagConstraints.REMAINDER; + SPACER.weighty = 1; + STATUS = new GridBagConstraints(); + STATUS.fill = GridBagConstraints.BOTH; + STATUS.insets = new Insets(0, 0, 0, 2); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Classes // + /////////////////////////////////////////////////////////////////////////// + + // Text box commit + private interface Commit { + void run(); + } + + // List item + private class Item { + Breakpoint breakpoint; + JCheckBox chkEnabled; + JLabel lblName; + JLabel lblStatus; + + // Constructor + Item(Breakpoint brk) { + var that = this; + breakpoint = brk; + brk.setFetch(false); + chkEnabled = new JCheckBox(); + chkEnabled.setBorder(null); + chkEnabled.setFocusable(false); + chkEnabled.addActionListener(e->onEnabled(that)); + parent.app.localizer.add(chkEnabled,"null","breakpoints.enabled"); + lblName = new JLabel(brk.getName()); + lblStatus = new JLabel("", SwingConstants.CENTER); + refresh(null, null); + } + + // Update the display + void refresh(Instruction inst, Access acc) { + if ( + breakpoint.getAddressError ().code != Breakpoint.NONE || + breakpoint.getConditionError().code != Breakpoint.NONE + ) lblStatus.setText("\u00d7"); // Times symbol + else if (breakpoint.any() && breakpoint.evaluate(inst, acc)) + lblStatus.setText("\u2605"); // Star + else lblStatus.setText(" "); + int lineHeight = lblStatus.getPreferredSize().height; + lblStatus.setPreferredSize(new Dimension(lineHeight, lineHeight)); + } + + // Specify whether or not the item has focus + void setFocused(boolean focused) { + var color = focused ? SystemColor.textHighlightText : + SystemColor.windowText; + lblStatus.setForeground(color); + lblName .setForeground(color); + } + + } + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + BreakpointsWindow(MainWindow parent) { + super(parent, "breakpoints.title"); + + // Configure instance fields + font = new Font(Util.fontFamily(new String[] + { "Consolas", Font.MONOSPACED } ), Font.PLAIN, 14); + items = new ArrayList(); + selectedIndex = -1; + + // Configure client area + client = new JPanel(new BorderLayout()); + client.setBackground(SystemColor.control); + client.setFocusable(true); + client.setPreferredSize(new Dimension(320, 240)); + + // Configure breakpoint list + lstBreakpoints = new JPanel(new GridBagLayout()) { + public void paintComponent(Graphics g) { + super.paintComponent(g); + onPaint((Graphics2D) g, getWidth(), getHeight()); + } + }; + lstBreakpoints.setFocusable(true); + lstBreakpoints.setBackground(SystemColor.window); + lstBreakpoints.addFocusListener(Util.onFocus( + e->lstBreakpoints.repaint(), e->lstBreakpoints.repaint())); + lstBreakpoints.addMouseListener(Util.onMouse(e->onMouseDown(e), null)); + var scr = new JScrollPane(lstBreakpoints); + client.add(scr, BorderLayout.CENTER); + spacer = new JPanel(null); + spacer.setOpaque(false); + lstBreakpoints.add(spacer, SPACER); + + // Configure properties pane + var props = new JPanel(new GridBagLayout()); + props.setBackground(SystemColor.control); + props.addMouseListener( + Util.onMouse(e->client.requestFocus(), null)); + client.add(props, BorderLayout.SOUTH); + + // Configure properties + label(props, "breakpoints.name", 2); + txtName = textBox(props, false, 2, ()->onName()); + //label(props, "breakpoints.enabled", 0); + //chkEnabled = checkBox(props, null, true); + chkEnabled = new JCheckBox(); + chkEnabled.addActionListener(e->onEnabled(null)); + label(props, "breakpoints.type", 0); + chkExecute = checkBox(props, "breakpoints.execute" , false); + chkExecute .addActionListener(e->onType(chkExecute )); + chkRead = checkBox(props, "breakpoints.read" , false); + chkRead .addActionListener(e->onType(chkRead )); + chkWrite = checkBox(props, "breakpoints.write" , false); + chkWrite .addActionListener(e->onType(chkWrite )); + chkException = checkBox(props, "breakpoints.exception", true ); + chkException.addActionListener(e->onType(chkException)); + label(props, "breakpoints.address", 0); + txtAddress = textBox(props, true, 0, ()->onAddress()); + errAddress = error(props); + label(props, "breakpoints.condition", 0); + txtCondition = textBox(props, true, 0, ()->onCondition()); + errCondition = error(props); + + // Configure buttons + var buttons = new JPanel(new GridBagLayout()); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = GridBagConstraints.REMAINDER; + props.add(buttons, gbc); + var fill = new JPanel(null); + fill.setOpaque(false); + fill.addMouseListener(Util.onMouse(e->onDebug(e), null)); + gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1; + buttons.add(fill, gbc); + btnDelete = button(buttons, "breakpoints.delete", false); + btnDelete.setEnabled(false); + btnDelete.addActionListener(e->onDelete()); + btnNew = button(buttons, "breakpoints.new", true); + btnNew.addActionListener(e->onNew()); + + // Configure component + setContentPane(client); + pack(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Package Methods // + /////////////////////////////////////////////////////////////////////////// + + // Update the display + void refresh() { + var inst = new Instruction(parent.vue.read( + parent.vue.getRegister(Vue.PC, true), Vue.S32)); + var acc = inst.access(parent.vue); + for (var item : items) + item.refresh(inst, acc); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Event Handlers // + /////////////////////////////////////////////////////////////////////////// + + // Address text box commit + private void onAddress() { + if (selectedIndex == -1) + return; + var item = items.get(selectedIndex); + item.breakpoint.setAddresses(txtAddress.getText()); + checkErrors(item); + item.refresh(null, null); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + // Condition text box commit + private void onCondition() { + if (selectedIndex == -1) + return; + var item = items.get(selectedIndex); + item.breakpoint.setCondition(txtCondition.getText()); + checkErrors(item); + item.refresh(null, null); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + // Debug interaction + private void onDebug(MouseEvent e) { + client.requestFocus(); + if ( + selectedIndex == -1 || + e.getButton() != MouseEvent.BUTTON1 || + (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) == 0 + ) return; + var brk = items.get(selectedIndex).breakpoint; + System.out.println("Address"); + System.out.println(brk.getAddresses()); + System.out.println(brk.debugRanges()); + System.out.println("Condition"); + System.out.println(brk.getCondition()); + System.out.println(brk.debugTokens()); + System.out.println(); + } + + // Delete button click + private void onDelete() { + var item = items.remove(selectedIndex); + parent.vue.remove(item.breakpoint); + lstBreakpoints.remove(item.chkEnabled); + lstBreakpoints.remove(item.lblName); + lstBreakpoints.remove(item.lblStatus); + lstBreakpoints.revalidate(); + selectedIndex = -1; + select(-1); + } + + // Enabled check box toggle + private void onEnabled(Item item) { + boolean enabled; + + // Clicked from the properties pane + if (item == null) { + enabled = chkEnabled.isSelected(); + item = items.get(selectedIndex); + item.chkEnabled.setSelected(enabled); + client.requestFocus(); + } + + // Clicked from the breakpoints list + else { + enabled = item.chkEnabled.isSelected(); + if (items.indexOf(item) == selectedIndex) + chkEnabled.setSelected(enabled); + lstBreakpoints.requestFocus(); + } + + // Common processing + item.breakpoint.setEnabled(enabled); + item.refresh(null, null); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + // List mouse press + private void onMouseDown(MouseEvent e) { + int count = items.size(); + + // Left clicking + if (count > 0 && e.getButton() == MouseEvent.BUTTON1) { + int newIndex = e.getY() / items.get(0).lblStatus.getHeight(); + if (newIndex < count) + select(newIndex); + } + + // Update layout + lstBreakpoints.requestFocus(); + lstBreakpoints.repaint(); + } + + // Name text box commit + private void onName() { + if (selectedIndex == -1) + return; + var item = items.get(selectedIndex); + item.lblName.setText(item.breakpoint.setName(txtName.getText())); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + item.refresh(null, null); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + // New button click + private void onNew() { + + var brk = parent.vue.breakpoint(); + brk.setEnabled(true); + brk.setName (parent.app.localizer.get("breakpoints.default_name")); + + var item = new Item(brk); + item.chkEnabled.setSelected(true); + item.lblName .setText(brk.getName()); + items.add(item); + + int count = items.size(); + boolean first = count == 1; + lstBreakpoints.remove(spacer); + lstBreakpoints.add(item.chkEnabled, ENABLED); + lstBreakpoints.add(item.lblStatus , STATUS ); + lstBreakpoints.add(item.lblName , NAME ); + lstBreakpoints.add(spacer , SPACER ); + + select(count - 1); + lstBreakpoints.requestFocus(); + lstBreakpoints.revalidate(); + } + + // List paint + private void onPaint(Graphics2D g, int width, int height) { + + // There is no selected item + if (selectedIndex == -1) + return; + + // Select the display colors + var item = items.get(selectedIndex); + if (lstBreakpoints.isFocusOwner()) { + g.setColor(SystemColor.textHighlight); + item.setFocused(true); + } else { + g.setColor(SystemColor.control); + item.setFocused(false); + } + + // Draw the selection background + height = item.lblStatus.getHeight(); + g.fillRect(0, selectedIndex * height, width, height); + } + + // Type check box toggle + private void onType(JCheckBox chk) { + var item = items.get(selectedIndex); + var brk = item.breakpoint; + boolean selected = chk.isSelected(); + + // Configure controls + if (chk == chkException) + brk.setException(selected); + else if (chk == chkExecute ) + brk.setExecute (selected); + else if (chk == chkRead ) + brk.setRead (selected); + else if (chk == chkWrite ) + brk.setWrite (selected); + + // Common processing + item.refresh(null, null); + client.requestFocus(); + lstBreakpoints.revalidate(); + lstBreakpoints.repaint(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Private Methods // + /////////////////////////////////////////////////////////////////////////// + + // Add a button to the form + private JButton button(JPanel panel, String key, boolean last) { + var btn = new JButton(); + if (key != null) + parent.app.localizer.add(btn, key); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(0, 0, 2, 2); + if (last) + gbc.gridwidth = GridBagConstraints.REMAINDER; + panel.add(btn, gbc); + return btn; + } + + // Add a check box to the form + private JCheckBox checkBox(JPanel panel, String key, boolean last) { + var chk = new JCheckBox(); + if (key != null) + parent.app.localizer.add(chk, key); + chk.setBorder(null); + chk.setEnabled(false); + chk.setFocusable(false); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(0, 0, 2, last ? 2 : 6); + if (last) + gbc.gridwidth = GridBagConstraints.REMAINDER; + panel.add(chk, gbc); + return chk; + } + + // Check for errors in the selected breakpoint + private void checkErrors(Item item) { + var brk = item.breakpoint; + var loc = parent.app.localizer; + + // Check for address errors + var err = brk.getAddressError(); + if (err.code != Breakpoint.NONE) { + loc.add(errAddress, KEYS_ADDRESS[err.code]); + loc.put(errAddress, "err.position", + Integer.toString(err.position)); + loc.put(errAddress, "err.text", err.text); + errAddress.setVisible(true); + } else errAddress.setVisible(false); + + // Check for condition errors + err = brk.getConditionError(); + if (err.code != Breakpoint.NONE) { + loc.add(errCondition, KEYS_CONDITION[err.code]); + loc.put(errCondition, "err.position", + Integer.toString(err.position)); + loc.put(errCondition, "err.text", err.text); + if (err.code == Breakpoint.NESTING) + loc.put(errCondition, "err.other", + err.text.equals(")") ? "]" : ")"); + errCondition.setVisible(true); + } else errCondition.setVisible(false); + + } + + // Add an error label to the form + private JLabel error(JPanel panel) { + var lbl = new JLabel(); + lbl.setForeground(Color.red); + lbl.setVisible(false); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.CENTER; + gbc.fill = GridBagConstraints.BOTH; + gbc.gridx = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 0, 2, 2); + panel.add(lbl, gbc); + return lbl; + } + + // Add a label to the form + private void label(JPanel panel, String key, int top) { + var lbl = new JLabel(); + if (key != null) + parent.app.localizer.add(lbl, key); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(top, 2, 2, 4); + panel.add(lbl, gbc); + } + + // Select a particular list item + private void select(int newIndex) { + boolean enabled = newIndex != -1; + + // The existing selection was de-selected + if (selectedIndex != -1 && newIndex != selectedIndex) + items.get(selectedIndex).setFocused(false); + + // Selecting a new list item + if (newIndex != -1) + items.get(newIndex).setFocused(true); + + // Configure instance fields + selectedIndex = newIndex; + + // Update control properties + if (enabled) { + var brk = items.get(selectedIndex).breakpoint; + chkEnabled .setSelected(brk.isEnabled ()); + chkExecute .setSelected(brk.getExecute ()); + chkRead .setSelected(brk.getRead ()); + chkWrite .setSelected(brk.getWrite ()); + chkException.setSelected(brk.getException()); + txtAddress .setText (brk.getAddresses()); + txtCondition.setText (brk.getCondition()); + txtName .setText (brk.getName ()); + } + + // Clear control properties + else { + chkEnabled .setSelected(false); + chkExecute .setSelected(false); + chkRead .setSelected(false); + chkWrite .setSelected(false); + chkException.setSelected(false); + errAddress .setText(""); errAddress .setVisible(false); + errCondition.setText(""); errCondition.setVisible(false); + txtAddress .setText(""); + txtCondition.setText(""); + txtName .setText(""); + } + + // Enable or disable controls + btnDelete .setEnabled(enabled); + chkEnabled .setEnabled(enabled); + chkExecute .setEnabled(enabled); + chkException.setEnabled(enabled); + chkRead .setEnabled(enabled); + chkWrite .setEnabled(enabled); + txtAddress .setEnabled(enabled); + txtCondition.setEnabled(enabled); + txtName .setEnabled(enabled); + + // Common processing + if (selectedIndex != -1) + checkErrors(items.get(selectedIndex)); + lstBreakpoints.repaint(); + } + + // Add a text box to the form + private JTextField textBox(JPanel panel, boolean mono, int top, + Commit handler) { + var txt = new JTextField(); + txt.setEnabled(false); + if (mono) + txt.setFont(font); + txt.addActionListener(e->client.requestFocus()); + txt.addFocusListener (Util.onFocus(null, e->handler.run())); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.CENTER; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(top, 0, 2, 2); + gbc.weightx = 1; + panel.add(txt, gbc); + return txt; + } + +} diff --git a/src/desktop/app/MainWindow.java b/src/desktop/app/MainWindow.java index 5f693eb..dd133b8 100644 --- a/src/desktop/app/MainWindow.java +++ b/src/desktop/app/MainWindow.java @@ -28,13 +28,14 @@ class MainWindow extends JFrame { private File romFile; // Currently loaded ROM file // UI components - private JPanel client; // Common client container - private ConsoleWindow console; // Console window - private CPUWindow cpu; // CPU window - private JDesktopPane desktop; // Container for child windows - private MemoryWindow memory; // Memory window - private JMenu mnuDebug; // Debug menu - private JPanel video; // Video output + private BreakpointsWindow breakpoints; // Breakpoints window + private JPanel client; // Common client container + private ConsoleWindow console; // Console window + private CPUWindow cpu; // CPU window + private JDesktopPane desktop; // Container for child windows + private MemoryWindow memory; // Memory window + private JMenu mnuDebug; // Debug menu + private JPanel video; // Video output private JMenuItem mnuFileDebugMode; // File -> Debug mode private JMenuItem mnuFileGameMode; // File -> Game mode @@ -94,9 +95,10 @@ class MainWindow extends JFrame { // Configure child windows desktop = new JDesktopPane(); desktop.setBackground(SystemColor.controlShadow); - desktop.add(console = new ConsoleWindow(this)); - desktop.add(cpu = new CPUWindow (this)); - desktop.add(memory = new MemoryWindow (this)); + desktop.add(breakpoints = new BreakpointsWindow(this)); + desktop.add(console = new ConsoleWindow (this)); + desktop.add(cpu = new CPUWindow (this)); + desktop.add(memory = new MemoryWindow (this)); // Configure internal breakpoints brkStep = vue.breakpoint(); @@ -130,20 +132,52 @@ class MainWindow extends JFrame { loc.add(mnuDebug, "app.debug.(menu)"); mnuDebug.setVisible(false); + var mnuDebugBreakpoints = new JMenuItem(); + loc.add(mnuDebugBreakpoints, "app.debug.breakpoints"); + mnuDebugBreakpoints.addActionListener(e->breakpoints.setVisible(true)); + mnuDebug.add(mnuDebugBreakpoints); + var mnuDebugConsole = new JMenuItem(); loc.add(mnuDebugConsole, "app.debug.console"); mnuDebugConsole.addActionListener(e->console.setVisible(true)); mnuDebug.add(mnuDebugConsole); + var mnuDebugCPU = new JMenuItem(); + loc.add(mnuDebugCPU, "app.debug.cpu"); + mnuDebugCPU.addActionListener(e->cpu.setVisible(true)); + mnuDebug.add(mnuDebugCPU); + var mnuDebugMemory = new JMenuItem(); loc.add(mnuDebugMemory, "app.debug.memory"); mnuDebugMemory.addActionListener(e->memory.setVisible(true)); mnuDebug.add(mnuDebugMemory); - var mnuDebugCPU = new JMenuItem(); - loc.add(mnuDebugCPU, "app.debug.cpu"); - mnuDebugCPU.addActionListener(e->cpu.setVisible(true)); - mnuDebug.add(mnuDebugCPU); + mnuDebug.addSeparator(); + + var mnuDebugBackgrounds = new JMenuItem(); + mnuDebugBackgrounds.setEnabled(false); + loc.add(mnuDebugBackgrounds, "app.debug.backgrounds"); + mnuDebug.add(mnuDebugBackgrounds); + + var mnuDebugCharacters = new JMenuItem(); + mnuDebugCharacters.setEnabled(false); + loc.add(mnuDebugCharacters, "app.debug.characters"); + mnuDebug.add(mnuDebugCharacters); + + var mnuDebugFrameBuffers = new JMenuItem(); + mnuDebugFrameBuffers.setEnabled(false); + loc.add(mnuDebugFrameBuffers, "app.debug.frame_buffers"); + mnuDebug.add(mnuDebugFrameBuffers); + + var mnuDebugObjects = new JMenuItem(); + mnuDebugObjects.setEnabled(false); + loc.add(mnuDebugObjects, "app.debug.objects"); + mnuDebug.add(mnuDebugObjects); + + var mnuDebugWorlds = new JMenuItem(); + mnuDebugWorlds.setEnabled(false); + loc.add(mnuDebugWorlds, "app.debug.worlds"); + mnuDebug.add(mnuDebugWorlds); return mnuDebug; } @@ -192,8 +226,9 @@ class MainWindow extends JFrame { // Refresh all debug views void refreshDebug(boolean seekToPC) { - cpu .refresh(seekToPC); - memory.refresh(); + breakpoints.refresh(); + cpu .refresh(seekToPC); + memory .refresh(); } // A window has been added to or removed from the program state diff --git a/src/desktop/app/Register.java b/src/desktop/app/Register.java index 500cdef..3d7f3c8 100644 --- a/src/desktop/app/Register.java +++ b/src/desktop/app/Register.java @@ -101,7 +101,8 @@ class Register { parent.add(gbc, txtValue); // Value changed - txtValue.addActionListener(e->{ + txtValue.addActionListener(e->parent.requestFocus()); + txtValue.addFocusListener(Util.onFocus(null, e->{ String text = txtValue.getText(); int val = value; try { switch (mode) { @@ -112,7 +113,7 @@ class Register { Float.floatToIntBits(Float.parseFloat(text) ); break; }} catch (Exception x) { } setValue(val); - }); + })); // Expansion controls switch (type) { @@ -208,10 +209,9 @@ class Register { // Value text box var txt = new JTextField(); - txt.addActionListener(e->{ - txt.setText((String) txt.getClientProperty("text")); - parent.requestFocus(); - }); + txt.addActionListener(e->parent.requestFocus()); + txt.addFocusListener(Util.onFocus(null, + e->txt.setText((String) txt.getClientProperty("text")))); txt.putClientProperty("index", x == 0 ? Vue.JUMP_FROM : Vue.JUMP_TO); txt.setBorder(null); @@ -479,13 +479,13 @@ class Register { controls.add(ctrl); // Event handlers - ctrl.addActionListener(e->{ - parent.requestFocus(); + ctrl.addActionListener(e->parent.requestFocus()); + ctrl.addFocusListener(Util.onFocus(null, e->{ int val = value >> bit & mask; try { val = Integer.parseInt(ctrl.getText(), hex ? 16 : 10); } catch (Exception x) { } setValue(value & ~(mask << bit) | (val & mask) << bit); - }); + })); // Configure expansion area var label = new JLabel(name); diff --git a/src/desktop/util/Localizer.java b/src/desktop/util/Localizer.java index 789ec6e..d7ff355 100644 --- a/src/desktop/util/Localizer.java +++ b/src/desktop/util/Localizer.java @@ -265,6 +265,7 @@ public class Localizer { "Required key not found: 'locale.id'"); if (!ret.containsKey("locale.name")) throw new RuntimeException( "Required key not found: 'locale.name'"); + ret.put("null", ""); return new Locale(ret); } diff --git a/src/desktop/vue/Breakpoint.c b/src/desktop/vue/Breakpoint.c index 2187e5d..762eb64 100644 --- a/src/desktop/vue/Breakpoint.c +++ b/src/desktop/vue/Breakpoint.c @@ -533,10 +533,11 @@ static int32_t brkOnBreakpoint(Vue *vue, int32_t breakType) { } // Check all breakpoints + int32_t fetch = breakType == BRK_READ && vue->cpu.fetch != -1 ? 1 : 0; for (int x = 0; x < core->numBreakpoints; x++) { Breakpoint *brk = &core->breakpoints[x]; if ( - brk->enabled && + brk->enabled && (brk->fetch || !fetch) && (breakType == 0 || (breakType & brk->hooks) != 0) && (!ranged || brkInRange(brk, start, end)) && (brk->numTokens == 0 || brkEvaluate(core, brk) != 0) diff --git a/src/desktop/vue/Breakpoint.java b/src/desktop/vue/Breakpoint.java index 1c77f29..fc7b700 100644 --- a/src/desktop/vue/Breakpoint.java +++ b/src/desktop/vue/Breakpoint.java @@ -13,6 +13,7 @@ public class Breakpoint { private Error conditionError; // Condition parsing error private int dataType; // Condition evaluation data type private int depth; // Stack size for condition evaluation + private boolean fetch; // Breakpoint traps fetch reads private int hooks; // Applied breakpoint types private boolean isEnabled; // Breakpoint is active private String name; // Display name @@ -452,6 +453,7 @@ public class Breakpoint { addresses = ""; condition = ""; conditionError = new Error(NONE, 0, ""); + fetch = true; name = ""; ranges = new int[0][]; tokens = new Token[0]; @@ -464,36 +466,54 @@ public class Breakpoint { // Public Methods // /////////////////////////////////////////////////////////////////////////// + // Determine whether the breakpoint applies to any break scenarios + public boolean any() { + return hooks != 0; + } + // Evaluate the condition against the emulation context public boolean evaluate() { + return evaluate(null, null); + } + + // Evaluate the condition against the emulation context + public boolean evaluate(Instruction inst, Access acc) { // Error checking - if (vue == null) + if (vue == null || !isEnabled || hooks == 0) return false; - // Retrieve state objects - Access acc = vue.getAccess (); - int breakType = vue.getBreakType (); - Instruction inst = vue.getInstruction(); + // Working variables + boolean applies = false; + int pc = vue.getRegister(Vue.PC, true); - // Error checking - if (!appliesTo(breakType)) - return false; + // Resolve state objects + if (inst == null) + inst = new Instruction(vue.read(pc, Vue.S32)); + if (acc == null) + acc = inst.access(vue); - // Check address ranges for execute - if (breakType == EXECUTE) { - int pc = vue.getRegister(Vue.PC, true); - if (!inRange(pc, pc + inst.size - 1)) - return false; + // Check whether the breakpoint applies to the current state + if ((hooks & EXCEPTION) != 0) + applies = applies || vue.getExceptionCode() != 0; + if ((hooks & EXECUTE) != 0) + applies = applies || inRange(pc, pc + inst.size - 1); + if ((hooks & (READ | WRITE)) != 0 && acc != null) switch (inst.id) { + case Vue.CAXI : + case Vue.IN_B : case Vue.IN_H : case Vue.IN_W : + case Vue.LD_B : case Vue.LD_H : case Vue.LD_W : + case Vue.OUT_B: case Vue.OUT_H: case Vue.OUT_W: + case Vue.ST_B : case Vue.ST_H : case Vue.ST_W : + applies = applies || inRange(acc.address, + acc.address + JavaVue.TYPE_SIZES[acc.type] - 1); } - - // Check address ranges for read and write - else if (breakType == READ || breakType == WRITE && - !inRange(acc.address, acc.address+JavaVue.TYPE_SIZES[acc.type]-1)) + if (!applies) return false; // Evaluate the condition - return isTrue(new int[depth], breakType, inst, acc); + if (acc == null) + acc = new Access(); + return isTrue(new int[depth], 0, inst, acc); } // Retrieve the most recent address text @@ -526,6 +546,11 @@ public class Breakpoint { return (hooks & EXECUTE) != 0; } + // Determine whether the breakpoint hooks fetch reads + public boolean getFetch() { + return fetch; + } + // Determine whether the breakpoint hooks reads public boolean getRead() { return (hooks & READ) != 0; @@ -662,7 +687,7 @@ public class Breakpoint { } // x // Unexpected end of input - if (mode == 0 && ranges.size() > 0) { + if (mode == 0 && (ranges.size() > 0 || first != null)) { addressError = new Error(EARLYEOF, x + 1, ""); if (vue != null) vue.updateRanges(this); @@ -759,9 +784,16 @@ public class Breakpoint { vue.updateState(this); } + // Specify whether the breakpoint hooks fetch reads + public void setFetch(boolean fetch) { + this.fetch = fetch; + if (vue != null) + vue.updateState(this); + } + // Specify the display name - public void setName(String name) { - this.name = name == null ? "" : name; + public String setName(String name) { + return this.name = name == null ? "" : name; } // Specify whether the breakpoint hooks reads @@ -785,8 +817,9 @@ public class Breakpoint { /////////////////////////////////////////////////////////////////////////// // Determine whether the breakpoint applies to a break scenario - boolean appliesTo(int breakType) { - return isActive() && (breakType == 0 || (breakType & hooks) != 0); + boolean appliesTo(int breakType, boolean fetch) { + return isActive() && (this.fetch || !fetch) && + (breakType == 0 || (breakType & hooks) != 0); } // Perform a typed evaluation of the condition expression @@ -1145,7 +1178,6 @@ public class Breakpoint { // Not Logical case NOT_L: - newType = BOOL; break; // Round to Nearest @@ -1289,7 +1321,7 @@ public class Breakpoint { // Select the ID of the conversion operation int cvt = 0; - switch (tok.dataType) { + switch (dataType) { case BOOL : cvt = BOOL_ ; break; case FLOAT : cvt = FLOAT_; break; case SIGNED : cvt = S32 ; break; @@ -1305,7 +1337,7 @@ public class Breakpoint { // Insert a conversion token else { var imp = new Token(UNARY, -1, ""); - switch (tok.dataType) { + switch (dataType) { case BOOL : imp.text = "bool" ; break; case FLOAT : imp.text = "float"; break; case SIGNED : imp.text = "s32" ; break; @@ -1347,7 +1379,7 @@ public class Breakpoint { int start = 0; // Locate the bounds of the innermost nested group - for (int x = 0; x < end; x++) { + for (int x = 0; x <= end; x++) { var tok = tokens.get(x); if (tok.type == OPEN) start = x + 1; @@ -1447,7 +1479,7 @@ public class Breakpoint { } // A group was not closed, or a binary operation was incomplete - if (!stack.empty() || mode == 0) { + if (!stack.empty() || mode == 0 && tokens.size() != 0) { conditionError = new Error(EARLYEOF, condition.length(), ""); return false; @@ -1927,6 +1959,10 @@ public class Breakpoint { if (conditionError.code != NONE) return "Error"; + // There is no condition + if (tokens.length == 0) + return ""; + // Determine the maximum width of the text fields int max = 0; for (var tok : tokens) diff --git a/src/desktop/vue/CPU.java b/src/desktop/vue/CPU.java index fdd62f7..8965190 100644 --- a/src/desktop/vue/CPU.java +++ b/src/desktop/vue/CPU.java @@ -443,7 +443,7 @@ class CPU { // First unit if (fetch == 0) { - inst.bits = access.value << 16; + inst.bits = access.value & 0xFFFF; if (Instruction.size(access.value >> 10 & 0x3F) == 4) { fetch = 1; return false; @@ -452,7 +452,7 @@ class CPU { // Second unit else { - inst.bits |= access.value & 0xFFFF; + inst.bits |= access.value << 16; fetch = -1; } diff --git a/src/desktop/vue/Instruction.java b/src/desktop/vue/Instruction.java index a93028f..16be026 100644 --- a/src/desktop/vue/Instruction.java +++ b/src/desktop/vue/Instruction.java @@ -82,6 +82,11 @@ public class Instruction { // Default constructor public Instruction() { } + // Decoding constructor + public Instruction(int bits) { + decode(bits); + } + // Cloning constructor Instruction(Instruction o) { this(); @@ -104,27 +109,67 @@ public class Instruction { // Public Methods // /////////////////////////////////////////////////////////////////////////// - // Decode an instruction from a byte array - public void decode(byte[] data, int offset) { - bits = (data[offset + 1] & 0xFF) << 24 | (data[offset] & 0xFF) << 16; - if (size(bits >>> 26) == 4) - bits |= (data[offset + 3] & 0xFF) << 8 | data[offset + 2] & 0xFF; - decode(bits); + // Produce an access descriptor from the current instruction state + public Access access(Vue vue) { + boolean read = false; + var ret = new Access(); + ret.address = vue.getRegister(reg1, false) + disp; + ret.type = Vue.S32; + + // Configure descriptor by ID + switch (id) { + case Vue.CAXI : read = true; break; + case Vue.IN_B : ret.type = Vue.U8 ; read = true; break; + case Vue.IN_H : ret.type = Vue.U16; read = true; break; + case Vue.IN_W : read = true; break; + case Vue.LD_B : ret.type = Vue.S8 ; read = true; break; + case Vue.LD_H : ret.type = Vue.S16; read = true; break; + case Vue.LD_W : read = true; break; + case Vue.OUT_B: ret.type = Vue.U8 ; break; + case Vue.OUT_H: ret.type = Vue.U16; break; + case Vue.OUT_W: break; + case Vue.ST_B : ret.type = Vue.S8 ; break; + case Vue.ST_H : ret.type = Vue.S16; break; + case Vue.ST_W : break; + // TODO: Bit strings + default: return null; + } + + // Read the value currently on the bus + if (read) + ret.value = vue.read(ret.address, ret.type); + + // Select the value to write to the bus + else ret.value = vue.getRegister(id == Vue.CAXI && + vue.getRegister(reg2, false) == vue.read(ret.address, Vue.S32) ? + 30 : reg2, false); + + return ret; } - // Decode an instruction from its binary encoding - public void decode(int bits) { + // Decode an instruction from a byte array + public Instruction decode(byte[] data, int offset) { + return decode( + (data[offset + 0] & 0xFF) | + (data[offset + 1] & 0xFF) << 8 | + (data[offset + 2] & 0xFF) << 16 | + (data[offset + 3] & 0xFF) << 24 + ); + } + + // Decode an instruction from its binary encoding (swapped halfwords) + public Instruction decode(int bits) { byte extend; // Sign-extend the immediate operand int x; // Working variable // Configure instance fields - this.bits = bits; - opcode = bits >> 26 & 63; - x = opcode << 1; - id = LOOKUP_OPCODE[x ]; - extend = LOOKUP_OPCODE[x + 1]; - format = extend < 0 ? -extend : extend; - size = format < 4 ? 2 : 4; + this.bits = bits = bits << 16 | bits >>> 16; + opcode = bits >> 26 & 63; + x = opcode << 1; + id = LOOKUP_OPCODE[x ]; + extend = LOOKUP_OPCODE[x + 1]; + format = extend < 0 ? -extend : extend; + size = format < 4 ? 2 : 4; // Decode by format switch (format) { @@ -169,6 +214,7 @@ public class Instruction { break; } + return this; } } diff --git a/src/desktop/vue/JavaVue.java b/src/desktop/vue/JavaVue.java index 773d196..ab2fefc 100644 --- a/src/desktop/vue/JavaVue.java +++ b/src/desktop/vue/JavaVue.java @@ -285,11 +285,12 @@ class JavaVue extends Vue { } // Check all breakpoints - int count = breakpoints.size(); + int count = breakpoints.size(); + boolean fetch = breakType == Breakpoint.READ && cpu.fetch != -1; for (int x = 0; breakCode == 0 && x < count; x++) { var brk = breakpoints.get(x); if ( - brk.appliesTo(breakType) && + brk.appliesTo(breakType, fetch) && (!ranged || brk.inRange(start, end)) && brk.isTrue(stack, breakType, cpu.inst, cpu.access) ) breakCode = x + 1; diff --git a/src/desktop/vue/NativeVue.c b/src/desktop/vue/NativeVue.c index 2710e89..d0a4a04 100644 --- a/src/desktop/vue/NativeVue.c +++ b/src/desktop/vue/NativeVue.c @@ -35,6 +35,7 @@ typedef struct { // Precompiled breakpoint typedef struct { int32_t enabled; // The breakpoint is enabled + int32_t fetch; // Breakpoint traps fetch reads int32_t hooks; // Events hooked by the breakpoint int32_t numRanges; // Number of address ranges int32_t numTokens; // Number of condition tokens @@ -106,7 +107,9 @@ JNIEXPORT void JNICALL Java_vue_NativeVue_breakpoint core->numBreakpoints++; core->breakpoints = realloc(core->breakpoints,core->numBreakpoints * sizeof (Breakpoint)); - memset(&core->breakpoints[core->numBreakpoints-1], 0,sizeof (Breakpoint)); + Breakpoint *brk = &core->breakpoints[core->numBreakpoints - 1]; + memset(brk, 0 ,sizeof (Breakpoint)); + brk->fetch = VUE_TRUE; } // Release any used resources @@ -368,10 +371,11 @@ JNIEXPORT void JNICALL Java_vue_NativeVue_updateRanges // A breakpoint's enabled/hook state has changed JNIEXPORT void JNICALL Java_vue_NativeVue_updateState (JNIEnv *env, jobject vue, jlong handle, jint index, jboolean enabled, - jint hooks) { + jboolean fetch, jint hooks) { Core *core = *(Core **)&handle; Breakpoint *brk = &core->breakpoints[index]; brk->enabled = enabled; + brk->fetch = fetch; brk->hooks = hooks; } diff --git a/src/desktop/vue/NativeVue.java b/src/desktop/vue/NativeVue.java index 4000f1d..3982969 100644 --- a/src/desktop/vue/NativeVue.java +++ b/src/desktop/vue/NativeVue.java @@ -155,13 +155,13 @@ class NativeVue extends Vue { } // A breakpoint's enabled/hook state has changed - private native void updateState(long handle, int index, boolean isEnabled, - int hooks); + private native void updateState(long handle, int index, boolean enabled, + boolean fetch, int hooks); void updateState(Breakpoint brk) { super.updateState(brk); int index = breakpoints.indexOf(brk); - updateState(handle, index, brk.isEnabled(), - brk.getHooks()); + updateState(handle, index, + brk.isEnabled(), brk.getFetch(), brk.getHooks()); } // A breakpoint's condition tokens have changed diff --git a/src/desktop/vue/Vue.java b/src/desktop/vue/Vue.java index 22ff06e..367d174 100644 --- a/src/desktop/vue/Vue.java +++ b/src/desktop/vue/Vue.java @@ -183,25 +183,20 @@ public abstract class Vue { // Evaluate an expression public Object evaluate(String expression) { - var brk = new Breakpoint(this); + var inst = new Instruction(read(getRegister(PC, true), S32)); + var brk = new Breakpoint(this); brk.setCondition(expression); brk.setEnabled (true); - return brk.evaluateTyped(new int[brk.getDepth()], 0, - getInstruction(), getAccess()); + return brk.evaluateTyped( + new int[brk.getDepth()], 0, inst, inst.access(this)); } - // Retrieve a snapshot of the current state's memory access - public abstract Access getAccess(); - // Retrieve the most recent applicaiton break code public abstract int getBreakCode(); // Retrieve the most recent exception code public abstract int getExceptionCode(); - // Retrieve a snapshot of the current state's instruction - public abstract Instruction getInstruction(); - // Retrieve a register value public abstract int getRegister(int index, boolean system);