Introducing debug mode, Console and Memory windows

This commit is contained in:
Guy Perfect 2020-08-05 14:33:04 -05:00
parent 952605a8a4
commit 61bee38e3d
9 changed files with 550 additions and 35 deletions

View File

@ -7,6 +7,21 @@ locale {
# Main window # Main window
app { 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 { title {
default PVB Emulator default PVB Emulator
mixed {ctrl.number} {ctrl.filename} - {app.title.default} mixed {ctrl.number} {ctrl.filename} - {app.title.default}
@ -14,13 +29,16 @@ app {
rom {ctrl.filename} - {app.title.default} 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 # Emulation core

View File

@ -10,7 +10,7 @@ default:
@echo @echo
@echo "Planet Virtual Boy Emulator" @echo "Planet Virtual Boy Emulator"
@echo " https://www.planetvb.com/" @echo " https://www.planetvb.com/"
@echo " August 4, 2020" @echo " August 5, 2020"
@echo @echo
@echo "Intended build environment: Debian i386 or amd64" @echo "Intended build environment: Debian i386 or amd64"
@echo " gcc-multilib" @echo " gcc-multilib"
@ -84,8 +84,8 @@ native:
pack: pack:
$(eval jarname = "pvbemu_`date +%Y%m%d`.jar") $(eval jarname = "pvbemu_`date +%Y%m%d`.jar")
@echo " Bundling into $(jarname)" @echo " Bundling into $(jarname)"
@jar -cfe $(jarname) Main *.class \ @jar -cfe $(jarname) Main *.class license.txt \
app images locale native src util vue makefile license.txt app images locale native util vue
# Performs a full build and packages it into a .jar # Performs a full build and packages it into a .jar
.PHONY: release .PHONY: release

View File

@ -42,8 +42,14 @@ public class Main {
catch (Error e) { } 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 // Begin application operations
new App(); new App(useNative);
} }
} }

View File

@ -11,10 +11,10 @@ import vue.*;
public class App { public class App {
// Instance fields // Instance fields
private Localizer.Locale[] locales; // Language translations private Localizer localizer; // UI localization manager
private Localizer localizer; // UI localization manager private boolean useNative; // Produce native core contexts
private boolean useNative; // Produce native core contexts private Localizer.Locale[] locales; // Language translations
private ArrayList<Window> windows; // Application windows private ArrayList<MainWindow> windows; // Application windows
@ -23,14 +23,14 @@ public class App {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Default constructor // Default constructor
public App() { public App(boolean useNative) {
// Instance fields // Instance fields
localizer = new Localizer(); localizer = new Localizer();
windows = new ArrayList<Window>(); windows = new ArrayList<MainWindow>();
// Additional processing // Additional processing
setUseNative(true); setUseNative(useNative);
initLocales(); initLocales();
addWindow(); addWindow();
} }
@ -43,7 +43,7 @@ public class App {
// Add a new program window // Add a new program window
void addWindow() { void addWindow() {
windows.add(new Window(this)); windows.add(new MainWindow(this));
windowsChanged(); windowsChanged();
} }
@ -70,12 +70,12 @@ public class App {
} }
// Determine the number of a window in the collection // Determine the number of a window in the collection
int numberOf(Window window) { int numberOf(MainWindow window) {
return windows.indexOf(window) + 1; return windows.indexOf(window) + 1;
} }
// Remove a program window // Remove a program window
void removeWindow(Window window) { void removeWindow(MainWindow window) {
windows.remove(window); windows.remove(window);
windowsChanged(); windowsChanged();
} }
@ -87,7 +87,9 @@ public class App {
// Specify whether using the native module // Specify whether using the native module
boolean setUseNative(boolean useNative) { 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;
} }

View File

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

View File

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

View File

@ -12,19 +12,29 @@ import util.*;
import vue.*; import vue.*;
// Main application window // Main application window
class Window extends JFrame { class MainWindow extends JFrame {
// Instance fields // Instance fields
private App app; // Containing application App app; // Containing application
private int number; // Window number within application VUE vue; // Emulation core context
private boolean only; // This is the only application window
private ROM rom; // Currently loaded ROM // Private fields
private File romFile; // Currently loaded ROM file private boolean debugMode; // Window is in debug mode
private VUE vue; // Emulation core context 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 // UI components
private File pwd; // Most recent working directory private JPanel client; // Common client container
private JPanel video; // Video output 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 // Default constructor
Window(App app) { MainWindow(App app) {
super(); super();
// Configure instance fields // Configure instance fields
@ -56,22 +66,33 @@ class Window extends JFrame {
vue = VUE.create(app.getUseNative()); vue = VUE.create(app.getUseNative());
// Configure video pane // Configure video pane
video = new JPanel() { video = new JPanel(null) {
public void paintComponent(Graphics g) { public void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
onPaintVideo((Graphics2D) g, getWidth(), getHeight()); onPaintVideo((Graphics2D) g, getWidth(), getHeight());
} }
}; };
video.setPreferredSize(new Dimension(384, 224)); video.setPreferredSize(new Dimension(384, 224));
video.setFocusable(true);
// Configure client area
client = new JPanel(new BorderLayout());
client.add(video, BorderLayout.CENTER);
// Configure window // Configure window
addWindowListener(Util.onClose(e->onClose())); addWindowListener(Util.onClose(e->onClose()));
setContentPane(video); setContentPane(client);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
setIconImage(APPICON); setIconImage(APPICON);
setJMenuBar(initMenus()); setJMenuBar(initMenus());
app.getLocalizer().add(this, "app.title.default"); 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 // Display window
pack(); pack();
setLocationRelativeTo(null); setLocationRelativeTo(null);
@ -89,10 +110,30 @@ class Window extends JFrame {
var bar = new JMenuBar(); var bar = new JMenuBar();
var loc = app.getLocalizer(); var loc = app.getLocalizer();
bar.setBorder(null); bar.setBorder(null);
bar.add(initMenuFile(loc)); bar.add(initMenuFile (loc));
bar.add(initMenuDebug(loc));
return bar; 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 // Initialize the File menu
private JMenu initMenuFile(Localizer loc) { private JMenu initMenuFile(Localizer loc) {
var mnuFile = new JMenu(); var mnuFile = new JMenu();
@ -103,6 +144,17 @@ class Window extends JFrame {
mnuFileLoadRom.addActionListener(e->onLoadROM()); mnuFileLoadRom.addActionListener(e->onLoadROM());
mnuFile.add(mnuFileLoadRom); 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(); mnuFile.addSeparator();
var mnuFileNewWindow = new JMenuItem(); var mnuFileNewWindow = new JMenuItem();
@ -124,6 +176,11 @@ class Window extends JFrame {
// Package Methods // // Package Methods //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Refresh all debug views
void refreshDebug() {
memory.refresh();
}
// A window has been added to or removed from the program state // A window has been added to or removed from the program state
void windowsChanged(int number, boolean only) { void windowsChanged(int number, boolean only) {
this.number = number; this.number = number;
@ -145,6 +202,32 @@ class Window extends JFrame {
vue.dispose(); 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 // File -> Load ROM
private void onLoadROM() { private void onLoadROM() {
var loc = app.getLocalizer(); var loc = app.getLocalizer();
@ -200,6 +283,7 @@ class Window extends JFrame {
var bytes = rom.toByteArray(); var bytes = rom.toByteArray();
// Pause emulation // Pause emulation
vue.setROM(bytes, 0, bytes.length); vue.setROM(bytes, 0, bytes.length);
refreshDebug();
// Resume emulation // Resume emulation
} }

274
src/desktop/app/Memory.java Normal file
View File

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

View File

@ -58,11 +58,22 @@ public final class Util {
new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE), 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) // Filesystem state manager for the current .jar (if any)
private static final FileSystem JARFS; private static final FileSystem JARFS;
// Static initializer // Static initializer
static { static {
// Font families
var fonts = GraphicsEnvironment
.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
Arrays.sort(fonts, String.CASE_INSENSITIVE_ORDER);
FONTS = fonts;
// .jar filesystem
FileSystem fs = null; FileSystem fs = null;
try { try {
fs = FileSystems.newFileSystem( fs = FileSystems.newFileSystem(
@ -153,6 +164,15 @@ public final class Util {
return data; 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 // Read an image file as an icon
public static Icon iconRead(String filename) { public static Icon iconRead(String filename) {
BufferedImage img = imageRead(filename); BufferedImage img = imageRead(filename);
@ -200,6 +220,13 @@ public final class Util {
return null; 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 // Produce a WindowListener with a functional interface
public static WindowListener onClose(OnClose close) { public static WindowListener onClose(OnClose close) {
return new WindowListener() { return new WindowListener() {