From 61bee38e3d8bf711fae846dbce43fd8c2f73ab01 Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Wed, 5 Aug 2020 14:33:04 -0500 Subject: [PATCH] Introducing debug mode, Console and Memory windows --- locale/en-US.txt | 30 +- makefile | 6 +- src/desktop/Main.java | 8 +- src/desktop/app/App.java | 26 +- src/desktop/app/ChildWindow.java | 66 +++++ src/desktop/app/Console.java | 38 +++ .../app/{Window.java => MainWindow.java} | 110 ++++++- src/desktop/app/Memory.java | 274 ++++++++++++++++++ src/desktop/util/Util.java | 27 ++ 9 files changed, 550 insertions(+), 35 deletions(-) create mode 100644 src/desktop/app/ChildWindow.java create mode 100644 src/desktop/app/Console.java rename src/desktop/app/{Window.java => MainWindow.java} (66%) create mode 100644 src/desktop/app/Memory.java diff --git a/locale/en-US.txt b/locale/en-US.txt index 26e8d02..97e0486 100644 --- a/locale/en-US.txt +++ b/locale/en-US.txt @@ -7,6 +7,21 @@ locale { # Main window app { + debug { + (menu) Debug + console Console + memory Memory + } + + file { + (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} @@ -14,13 +29,16 @@ app { rom {ctrl.filename} - {app.title.default} } - file { - (menu) File - load_rom Load ROM... - new_window New window - exit Exit - } +} +# Console window +console { + title Console +} + +# Memory window +memory { + title Memory } # Emulation core diff --git a/makefile b/makefile index 2830f0c..d5c9951 100644 --- a/makefile +++ b/makefile @@ -10,7 +10,7 @@ default: @echo @echo "Planet Virtual Boy Emulator" @echo " https://www.planetvb.com/" - @echo " August 4, 2020" + @echo " August 5, 2020" @echo @echo "Intended build environment: Debian i386 or amd64" @echo " gcc-multilib" @@ -84,8 +84,8 @@ native: pack: $(eval jarname = "pvbemu_`date +%Y%m%d`.jar") @echo " Bundling into $(jarname)" - @jar -cfe $(jarname) Main *.class \ - app images locale native src util vue makefile license.txt + @jar -cfe $(jarname) Main *.class license.txt \ + app images locale native util vue # Performs a full build and packages it into a .jar .PHONY: release diff --git a/src/desktop/Main.java b/src/desktop/Main.java index ca5d6a7..bdc90e4 100644 --- a/src/desktop/Main.java +++ b/src/desktop/Main.java @@ -42,8 +42,14 @@ public class Main { catch (Error e) { } } + // Use the native module + boolean useNative = false; + for (String arg : args) + if (arg.trim().toLowerCase().equals("native")) + useNative = true; + // Begin application operations - new App(); + new App(useNative); } } diff --git a/src/desktop/app/App.java b/src/desktop/app/App.java index 192673a..316991e 100644 --- a/src/desktop/app/App.java +++ b/src/desktop/app/App.java @@ -11,10 +11,10 @@ import vue.*; public class App { // Instance fields - private Localizer.Locale[] locales; // Language translations - private Localizer localizer; // UI localization manager - private boolean useNative; // Produce native core contexts - private ArrayList windows; // Application windows + private Localizer localizer; // UI localization manager + private boolean useNative; // Produce native core contexts + private Localizer.Locale[] locales; // Language translations + private ArrayList windows; // Application windows @@ -23,14 +23,14 @@ public class App { /////////////////////////////////////////////////////////////////////////// // Default constructor - public App() { + public App(boolean useNative) { // Instance fields - localizer = new Localizer(); - windows = new ArrayList(); + localizer = new Localizer(); + windows = new ArrayList(); // Additional processing - setUseNative(true); + setUseNative(useNative); initLocales(); addWindow(); } @@ -43,7 +43,7 @@ public class App { // Add a new program window void addWindow() { - windows.add(new Window(this)); + windows.add(new MainWindow(this)); windowsChanged(); } @@ -70,12 +70,12 @@ public class App { } // Determine the number of a window in the collection - int numberOf(Window window) { + int numberOf(MainWindow window) { return windows.indexOf(window) + 1; } // Remove a program window - void removeWindow(Window window) { + void removeWindow(MainWindow window) { windows.remove(window); windowsChanged(); } @@ -87,7 +87,9 @@ public class App { // Specify whether using the native module boolean setUseNative(boolean useNative) { - return this.useNative = useNative && VUE.isNativeLoaded(); + this.useNative = useNative && VUE.isNativeLoaded(); + System.out.println("Native: " + (this.useNative ? "Yes" : "No")); + return this.useNative; } diff --git a/src/desktop/app/ChildWindow.java b/src/desktop/app/ChildWindow.java new file mode 100644 index 0000000..2dfd246 --- /dev/null +++ b/src/desktop/app/ChildWindow.java @@ -0,0 +1,66 @@ +package app; + +// Java imports +import java.awt.*; +import javax.swing.*; + +// Project imports +import util.*; + +// Child window +class ChildWindow extends JInternalFrame { + + // Instance fields + MainWindow parent; // Parent application window + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + ChildWindow(MainWindow parent, String key) { + super(); + + // Configure instanece fields + this.parent = parent; + + // Configure component + parent.app.getLocalizer().add(this, key); + addInternalFrameListener(Util.onClose2(e->setVisible(false))); + setClosable(true); + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + setResizable(true); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Public Methods // + /////////////////////////////////////////////////////////////////////////// + + // Show or hide the component + public void setVisible​(boolean visible) { + + // Making visible + if (visible) { + + // Not currently visible: center in parent + if (!isVisible()) { + var parent = getParent().getParent(); + setLocation( + Math.max(0, (parent.getWidth () - getWidth ()) / 2), + Math.max(0, (parent.getHeight() - getHeight()) / 2) + ); + } + + // Already visible: bring to front + else moveToFront(); + } + + // Change visibility + super.setVisible(visible); + } + +} diff --git a/src/desktop/app/Console.java b/src/desktop/app/Console.java new file mode 100644 index 0000000..65b11e2 --- /dev/null +++ b/src/desktop/app/Console.java @@ -0,0 +1,38 @@ +package app; + +// Java imports +import java.awt.*; +import javax.swing.*; + +// Console window +class Console extends ChildWindow { + + // Instance fields + private boolean shown; // Component has been visible + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + Console(MainWindow parent) { + super(parent, "console.title"); + getContentPane().setPreferredSize(new Dimension(384, 224)); + pack(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Package Methods // + /////////////////////////////////////////////////////////////////////////// + + // Show the component on the first transition to debug mode + void firstShow() { + if (!shown) + setVisible(shown = true); + } + +} diff --git a/src/desktop/app/Window.java b/src/desktop/app/MainWindow.java similarity index 66% rename from src/desktop/app/Window.java rename to src/desktop/app/MainWindow.java index 26e246f..d30d9b5 100644 --- a/src/desktop/app/Window.java +++ b/src/desktop/app/MainWindow.java @@ -12,19 +12,29 @@ import util.*; import vue.*; // Main application window -class Window extends JFrame { +class MainWindow extends JFrame { // Instance fields - private App app; // Containing application - private int number; // Window number within application - private boolean only; // This is the only application window - private ROM rom; // Currently loaded ROM - private File romFile; // Currently loaded ROM file - private VUE vue; // Emulation core context + App app; // Containing application + VUE vue; // Emulation core context + + // Private fields + private boolean debugMode; // Window is in debug mode + private int number; // Window number within application + private boolean only; // This is the only application window + private File pwd; // Most recent working directory + private ROM rom; // Currently loaded ROM + private File romFile; // Currently loaded ROM file // UI components - private File pwd; // Most recent working directory - private JPanel video; // Video output + private JPanel client; // Common client container + private Console console; // Console window + private JDesktopPane desktop; // Container for child windows + private Memory memory; // Memory window + private JPanel video; // Video output + private JMenu mnuDebug; // Debug menu + private JMenuItem mnuFileDebugMode; // File -> Debug mode + private JMenuItem mnuFileGameMode; // File -> Game mode @@ -47,7 +57,7 @@ class Window extends JFrame { /////////////////////////////////////////////////////////////////////////// // Default constructor - Window(App app) { + MainWindow(App app) { super(); // Configure instance fields @@ -56,22 +66,33 @@ class Window extends JFrame { vue = VUE.create(app.getUseNative()); // Configure video pane - video = new JPanel() { + video = new JPanel(null) { public void paintComponent(Graphics g) { super.paintComponent(g); onPaintVideo((Graphics2D) g, getWidth(), getHeight()); } }; video.setPreferredSize(new Dimension(384, 224)); + video.setFocusable(true); + + // Configure client area + client = new JPanel(new BorderLayout()); + client.add(video, BorderLayout.CENTER); // Configure window addWindowListener(Util.onClose(e->onClose())); - setContentPane(video); + setContentPane(client); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); setIconImage(APPICON); setJMenuBar(initMenus()); app.getLocalizer().add(this, "app.title.default"); + // Configure child windows + desktop = new JDesktopPane(); + desktop.setBackground(SystemColor.controlShadow); + desktop.add(console = new Console(this)); + desktop.add(memory = new Memory (this)); + // Display window pack(); setLocationRelativeTo(null); @@ -89,10 +110,30 @@ class Window extends JFrame { var bar = new JMenuBar(); var loc = app.getLocalizer(); bar.setBorder(null); - bar.add(initMenuFile(loc)); + bar.add(initMenuFile (loc)); + bar.add(initMenuDebug(loc)); return bar; } + // Initialize the Debug menu + private JMenu initMenuDebug(Localizer loc) { + mnuDebug = new JMenu(); + loc.add(mnuDebug, "app.debug.(menu)"); + mnuDebug.setVisible(false); + + var mnuDebugConsole = new JMenuItem(); + loc.add(mnuDebugConsole, "app.debug.console"); + mnuDebugConsole.addActionListener(e->console.setVisible(true)); + mnuDebug.add(mnuDebugConsole); + + var mnuDebugMemory = new JMenuItem(); + loc.add(mnuDebugMemory, "app.debug.memory"); + mnuDebugMemory.addActionListener(e->memory.setVisible(true)); + mnuDebug.add(mnuDebugMemory); + + return mnuDebug; + } + // Initialize the File menu private JMenu initMenuFile(Localizer loc) { var mnuFile = new JMenu(); @@ -103,6 +144,17 @@ class Window extends JFrame { mnuFileLoadRom.addActionListener(e->onLoadROM()); mnuFile.add(mnuFileLoadRom); + mnuFileDebugMode = new JMenuItem(); + loc.add(mnuFileDebugMode, "app.file.debug_mode"); + mnuFileDebugMode.addActionListener(e->onDebugMode(true)); + mnuFile.add(mnuFileDebugMode); + + mnuFileGameMode = new JMenuItem(); + loc.add(mnuFileGameMode, "app.file.game_mode"); + mnuFileGameMode.addActionListener(e->onDebugMode(false)); + mnuFileGameMode.setVisible(false); + mnuFile.add(mnuFileGameMode); + mnuFile.addSeparator(); var mnuFileNewWindow = new JMenuItem(); @@ -124,6 +176,11 @@ class Window extends JFrame { // Package Methods // /////////////////////////////////////////////////////////////////////////// + // Refresh all debug views + void refreshDebug() { + memory.refresh(); + } + // A window has been added to or removed from the program state void windowsChanged(int number, boolean only) { this.number = number; @@ -145,6 +202,32 @@ class Window extends JFrame { vue.dispose(); } + // File -> Debug mode, File -> Game mode + private void onDebugMode(boolean debugMode) { + this.debugMode = debugMode; + mnuFileDebugMode.setVisible(!debugMode); + mnuFileGameMode .setVisible( debugMode); + + // Transition to debug mode + if (debugMode) { + client.remove(video); + client.add(desktop, BorderLayout.CENTER); + console.setContentPane(video); + console.firstShow(); + mnuDebug.setVisible(true); + } + + // Transition to game mode + else { + client.remove(desktop); + client.add(video, BorderLayout.CENTER); + mnuDebug.setVisible(false); + } + + client.revalidate(); + client.repaint(); + } + // File -> Load ROM private void onLoadROM() { var loc = app.getLocalizer(); @@ -200,6 +283,7 @@ class Window extends JFrame { var bytes = rom.toByteArray(); // Pause emulation vue.setROM(bytes, 0, bytes.length); + refreshDebug(); // Resume emulation } diff --git a/src/desktop/app/Memory.java b/src/desktop/app/Memory.java new file mode 100644 index 0000000..80319d1 --- /dev/null +++ b/src/desktop/app/Memory.java @@ -0,0 +1,274 @@ +package app; + +// Java imports +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import javax.swing.*; + +// Project imports +import util.*; + +// Memory viewer and hex editor window +class Memory extends ChildWindow { + + // Private fields + private int address; // Address of top row + private Font font; // Display font + + // UI components + private JPanel client; // Client area + private JLabel fontHeight; // Font height proxy + private ArrayList rows; // Rows of text + + + + /////////////////////////////////////////////////////////////////////////// + // Classes // + /////////////////////////////////////////////////////////////////////////// + + // One row of output + private class Row { + JLabel address; // Address (row header) + JLabel[] bytes; // Hexadecimal bytes + } + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + Memory(MainWindow parent) { + super(parent, "memory.title"); + + // Configure instance fields + address = 0x00000000; + font = new Font(Util.fontFamily(new String[] + { "Consolas", Font.MONOSPACED } ), Font.PLAIN, 14); + fontHeight = new JLabel("."); + rows = new ArrayList(); + + // Configure client area + client = new JPanel(null); + client.addComponentListener(Util.onResize(e->onResize())); + client.addKeyListener(Util.onKey(e->onKeyDown(e), null)); + client.addMouseWheelListener(e->onWheel(e)); + client.setFocusable(true); + client.setPreferredSize(new Dimension(640, 480)); + client.setBackground(SystemColor.window); + + // Configure component + setContentPane(client); + setFont2(font); + pack(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Package Methods // + /////////////////////////////////////////////////////////////////////////// + + // Update the display + void refresh() { + + // The element is not ready + if (client == null) + return; + + // Configure working variables + int height = client.getHeight(); + int lineHeight = fontHeight.getPreferredSize().height; + int count = (height + lineHeight - 1) / lineHeight; + var data = new byte[count * 16]; + var widths = new int[2]; + + // Retrieve all visible bytes from the emulation context + parent.vue.read(address, data, 0, data.length); + + // Update visible rows + for (int x = 0; x < count; x++) { + Row row; + + // Retrieve the row if it exists, make a new one otherwise + if (x < rows.size()) + row = rows.get(x); + else row = createRow(); + + // Configure the row + update(row, address + x * 16, data, x * 16); + setVisible(row, true); + measure(row, widths); + } + + // Hide any rows that are not visible + for (int x = count; x < rows.size(); x++) + setVisible(rows.get(x), false); + + // Position components + for (int x = 0; x < rows.size(); x++) + arrange(rows.get(x), x * lineHeight, widths, lineHeight); + + // Finalize layout + client.revalidate(); + client.repaint(); + } + + // Specify a new font + void setFont2(Font font) { + this.font = font; + fontHeight.setFont(font); + for (var row : rows) { + row.address.setFont(font); + for (var label : row.bytes) + label.setFont(font); + } + onResize(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Event Handlers // + /////////////////////////////////////////////////////////////////////////// + + // Key down + private void onKeyDown(KeyEvent e) { + int code = e.getKeyCode(); + int mods = e.getModifiersEx(); + boolean alt = (mods & InputEvent.ALT_DOWN_MASK ) != 0; + boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0; + int tall = Math.max(1, tall(false)); + + // No Alt combinations + if (alt) return; + + // Goto + if (ctrl && code == KeyEvent.VK_G) { + String addr = JOptionPane.showInputDialog( + this, "Goto:", "Goto", JOptionPane.PLAIN_MESSAGE); + if (addr != null && addr.trim().length() != 0) { + try { setAddress((int) Long.parseLong(addr, 16)); } + catch (Exception x) { } + } + return; + } + + // Seek + switch (code) { + case KeyEvent.VK_UP : setAddress(address - 16); break; + case KeyEvent.VK_DOWN : setAddress(address + 16); break; + case KeyEvent.VK_PAGE_UP : setAddress(address - tall * 16); break; + case KeyEvent.VK_PAGE_DOWN: setAddress(address + tall * 16); break; + } + + } + + // Client resize + private void onResize() { + refresh(); + } + + // Mouse wheel + private void onWheel(MouseWheelEvent e) { + int amount = e.getUnitsToScroll(); + int mods = e.getModifiersEx(); + boolean alt = (mods & InputEvent.ALT_DOWN_MASK ) != 0; + boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0; + + // No Alt or Ctrl combinations + if (amount == 0 || alt || ctrl) + return; + + // Seek + setAddress(address + 16 * amount); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Private Methods // + /////////////////////////////////////////////////////////////////////////// + + // Position the elements of a row + private void arrange(Row row, int y, int[] widths, int height) { + int spacing = (height + 1) / 2; + + // Size and position the address header + row.address.setSize(row.address.getPreferredSize()); + row.address.setLocation(0, y); + + // Size and position the byte labels + for (int z = 0, x = widths[0] + height; z < 16; z++) { + var label = row.bytes[z]; + label.setBounds(x, y, widths[1], height); + x += widths[1] + (z == 7 ? height : spacing); + } + + } + + // Add a new row of output + private Row createRow() { + var row = new Row(); + row.address = + + // Address label + row.address = new JLabel(); + row.address.setFont(font); + row.address.setForeground(SystemColor.windowText); + row.address.setVisible(false); + client.add(row.address); + + // Byte labels + row.bytes = new JLabel[16]; + for (int x = 0; x < row.bytes.length; x++) { + var label = row.bytes[x] = new JLabel(); + label.setFont(font); + label.setForeground(SystemColor.windowText); + label.setVisible(false); + client.add(label); + } + + // Add the row to the collection + rows.add(row); + return row; + } + + // Measure the minimum column widths for a row + private void measure(Row row, int[] widths) { + widths[0] = Math.max(widths[0], row.address.getPreferredSize().width); + for (int x = 0; x < 16; x++) + widths[1] = Math.max(widths[1], + row.bytes[x].getPreferredSize().width); + } + + // Specify the address of the top row of output + private void setAddress(int address) { + this.address = address & 0xFFFFFFF0; + refresh(); + } + + // Show or hide a row + private void setVisible(Row row, boolean visible) { + row.address.setVisible(visible); + for (var label : row.bytes) + label.setVisible(visible); + } + + // Determine how many rows of output are visible + private int tall(boolean partial) { + int lineHeight = fontHeight.getPreferredSize().height; + return lineHeight == 0 ? 0 : + (client.getHeight() + (partial ? lineHeight + 1 : 0)) / lineHeight; + } + + // Update the text of a row + private void update(Row row, int address, byte[] data, int offset) { + row.address.setText(String.format("%08X", address)); + for (var label : row.bytes) + label.setText(String.format("%02X", data[offset++] & 0xFF)); + } + +} diff --git a/src/desktop/util/Util.java b/src/desktop/util/Util.java index 5e9263b..ccbaadc 100644 --- a/src/desktop/util/Util.java +++ b/src/desktop/util/Util.java @@ -58,11 +58,22 @@ public final class Util { new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE), }; + // Available font families + private static final String[] FONTS; + // Filesystem state manager for the current .jar (if any) private static final FileSystem JARFS; // Static initializer static { + + // Font families + var fonts = GraphicsEnvironment + .getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + Arrays.sort(fonts, String.CASE_INSENSITIVE_ORDER); + FONTS = fonts; + + // .jar filesystem FileSystem fs = null; try { fs = FileSystems.newFileSystem( @@ -153,6 +164,15 @@ public final class Util { return data; } + // Select a font family from a list, if avaiable + public static String fontFamily(String[] families) { + for (String family : families) + if (Arrays.binarySearch(FONTS, family, + String.CASE_INSENSITIVE_ORDER) >= 0) + return family; + return null; + } + // Read an image file as an icon public static Icon iconRead(String filename) { BufferedImage img = imageRead(filename); @@ -200,6 +220,13 @@ public final class Util { return null; } + // Produce a list of available font family names + public static String[] listFonts() { + var ret = new String[FONTS.length]; + System.arraycopy(FONTS, 0, ret, 0, FONTS.length); + return ret; + } + // Produce a WindowListener with a functional interface public static WindowListener onClose(OnClose close) { return new WindowListener() {