493 lines
16 KiB
Java
493 lines
16 KiB
Java
package app;
|
|
|
|
// Java imports
|
|
import java.awt.*;
|
|
import java.awt.image.*;
|
|
import java.io.*;
|
|
import javax.swing.*;
|
|
import javax.swing.filechooser.*;
|
|
|
|
// Project imports
|
|
import util.*;
|
|
import vue.*;
|
|
|
|
// Main application window
|
|
class MainWindow extends JFrame {
|
|
|
|
// Instance fields
|
|
App app; // Containing application
|
|
Breakpoint brkStep; // Single step internal breakpoint
|
|
byte[][] chrs; // Decoded pixel patterns
|
|
Color[][][] palettes; // Raster palettes
|
|
byte[] vram; // Snapshot of VIP memory
|
|
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 UPanel client; // Common client container
|
|
private JDesktopPane desktop; // Container for child windows
|
|
private JMenu mnuDebug; // Debug menu
|
|
private UPanel video; // Video output
|
|
private JMenuItem mnuFileDebugMode; // File -> Debug mode
|
|
private JMenuItem mnuFileGameMode; // File -> Game mode
|
|
|
|
// Child windows
|
|
private BreakpointsWindow breakpoints;
|
|
private CharactersWindow characters;
|
|
private ConsoleWindow console;
|
|
private CPUWindow cpu;
|
|
private MemoryWindow memory;
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Constants //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Palette indexes
|
|
static final int GENERIC = 0;
|
|
static final int GPLT0 = 1;
|
|
static final int GPLT1 = 2;
|
|
static final int GPLT2 = 3;
|
|
static final int GPLT3 = 4;
|
|
static final int JPLT0 = 5;
|
|
static final int JPLT1 = 6;
|
|
static final int JPLT2 = 7;
|
|
static final int JPLT3 = 8;
|
|
static final int LEFT = 0;
|
|
static final int RIGHT = 1;
|
|
static final int RED = 2;
|
|
|
|
// Application icon
|
|
private static final BufferedImage APPICON;
|
|
|
|
// Static initializer
|
|
static {
|
|
APPICON = Util.imageRead("images/app_icon.png");
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Constructors //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Default constructor
|
|
MainWindow(App app) {
|
|
super();
|
|
|
|
// Configure instance fields
|
|
this.app = app;
|
|
chrs = new byte[2048][64];
|
|
palettes = new Color[9][3][4];
|
|
pwd = Util.PWD;
|
|
vram = new byte[0x40000];
|
|
vue = Vue.create(app.getUseNative());
|
|
System.out.println("Native: " +
|
|
(vue.isNative() ? Vue.getNativeID() : "No"));
|
|
|
|
// Initialize palettes
|
|
var invis = new Color(0, true);
|
|
for (int x = 0; x < 9; x++)
|
|
for (int y = 0; y < 3; y++)
|
|
for (int z = 0; z < 4; z++)
|
|
palettes[x][y][z] = invis;
|
|
|
|
// Configure video pane
|
|
video = new UPanel();
|
|
video.setPreferredSize(new Dimension(384, 224));
|
|
video.setFocusable(true);
|
|
video.addPaintListener((g,w,h)->onPaintVideo(g, w, h));
|
|
|
|
// Configure client area
|
|
client = new UPanel(new BorderLayout());
|
|
client.add(video, BorderLayout.CENTER);
|
|
|
|
// Configure window
|
|
addWindowListener(Util.onClose(e->onClose()));
|
|
setContentPane(client);
|
|
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
|
|
setIconImage(APPICON);
|
|
setJMenuBar(initMenus());
|
|
app.localizer.add(this, "app.title.default");
|
|
|
|
// Configure child windows
|
|
desktop = new JDesktopPane();
|
|
desktop.setBackground(SystemColor.controlShadow);
|
|
desktop.add(breakpoints = new BreakpointsWindow(this));
|
|
desktop.add(characters = new CharactersWindow (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();
|
|
brkStep.setRead(true);
|
|
|
|
// Display window
|
|
refreshDebug(true);
|
|
pack();
|
|
setLocationRelativeTo(null);
|
|
setVisible(true);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Menu Constructors //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Produce the window's menu bar
|
|
private JMenuBar initMenus() {
|
|
var bar = new JMenuBar();
|
|
var loc = app.localizer;
|
|
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 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);
|
|
|
|
mnuDebug.addSeparator();
|
|
|
|
var mnuDebugBackgrounds = new JMenuItem();
|
|
mnuDebugBackgrounds.setEnabled(false);
|
|
loc.add(mnuDebugBackgrounds, "app.debug.backgrounds");
|
|
mnuDebug.add(mnuDebugBackgrounds);
|
|
|
|
var mnuDebugCharacters = new JMenuItem();
|
|
loc.add(mnuDebugCharacters, "app.debug.characters");
|
|
mnuDebugCharacters.addActionListener(e->characters.setVisible(true));
|
|
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;
|
|
}
|
|
|
|
// Initialize the File menu
|
|
private JMenu initMenuFile(Localizer loc) {
|
|
var mnuFile = new JMenu();
|
|
loc.add(mnuFile, "app.file.(menu)");
|
|
|
|
var mnuFileLoadRom = new JMenuItem();
|
|
loc.add(mnuFileLoadRom, "app.file.load_rom");
|
|
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();
|
|
loc.add(mnuFileNewWindow, "app.file.new_window");
|
|
mnuFileNewWindow.addActionListener(e->onNewWindow());
|
|
mnuFile.add(mnuFileNewWindow);
|
|
|
|
var mnuFileExit = new JMenuItem();
|
|
loc.add(mnuFileExit, "app.file.exit");
|
|
mnuFileExit.addActionListener(e->onClose());
|
|
mnuFile.add(mnuFileExit);
|
|
|
|
return mnuFile;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Package Methods //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Encode a character graphic
|
|
void encode(int index) {
|
|
int dest = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
|
|
var pix = chrs[index];
|
|
for (int y = 0, src = 0; y < 8; y++)
|
|
for (int b = 0, bits = 0; b < 2; b++, vram[dest++] = (byte) bits)
|
|
for (int x = 0; x < 4; x++, src++)
|
|
bits = bits >> 2 | pix[src] << 6;
|
|
vue.writeBytes(dest - 16, pix, dest - 16, 16);
|
|
refreshDebugLite(false);
|
|
}
|
|
|
|
// Refresh all debug views
|
|
void refreshDebug(boolean seekToPC) {
|
|
vue.readBytes(0x00000000, vram, 0, vram.length);
|
|
refreshCharacters();
|
|
refreshPalettes();
|
|
refreshDebugLite(seekToPC);
|
|
}
|
|
|
|
// Refresh all debug views without retrieving video memory
|
|
void refreshDebugLite(boolean seekToPC) {
|
|
breakpoints.refresh();
|
|
characters .refresh();
|
|
cpu .refresh(seekToPC);
|
|
memory .refresh();
|
|
}
|
|
|
|
// A window has been added to or removed from the program state
|
|
void windowsChanged(int number, boolean only) {
|
|
this.number = number;
|
|
this.only = only;
|
|
app.localizer.put(this, "ctrl.number", "" + number);
|
|
updateTitle();
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Event Handlers //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Window close, File -> Exit
|
|
private void onClose() {
|
|
app.removeWindow(this);
|
|
cpu.dispose();
|
|
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
|
|
private void onLoadROM() {
|
|
var loc = app.localizer;
|
|
|
|
// Prompt the user to select a file
|
|
var dlgFile = new JFileChooser(pwd);
|
|
dlgFile.addChoosableFileFilter(new FileNameExtensionFilter(
|
|
loc.get("dialog.ext_vb"), "vb"));
|
|
dlgFile.addChoosableFileFilter(new FileNameExtensionFilter(
|
|
loc.get("dialog.ext_isx"), "isx"));
|
|
dlgFile.setAcceptAllFileFilterUsed(true);
|
|
dlgFile.setDialogTitle(loc.get("dialog.load_rom"));
|
|
int option = dlgFile.showDialog(this, loc.get("dialog.load"));
|
|
|
|
// The user did not select a file
|
|
var file = dlgFile.getSelectedFile();
|
|
if (option != JFileChooser.APPROVE_OPTION || file == null)
|
|
return;
|
|
|
|
// Update the current directory
|
|
pwd = file.getParentFile();
|
|
|
|
// Read the file
|
|
var data = Util.fileRead(file);
|
|
if (data == null) {
|
|
JOptionPane.showMessageDialog(this,
|
|
loc.get("dialog.load_rom_error"),
|
|
loc.get("dialog.load_rom"),
|
|
JOptionPane.ERROR_MESSAGE
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Process the ROM
|
|
ROM rom = null;
|
|
try { rom = new ROM(data); }
|
|
catch (Exception e) {
|
|
JOptionPane.showMessageDialog(this,
|
|
loc.get("dialog.load_rom_notvb"),
|
|
loc.get("dialog.load_rom"),
|
|
JOptionPane.ERROR_MESSAGE
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Update instance fields
|
|
this.rom = rom;
|
|
romFile = file;
|
|
loc.put(this, "ctrl.filename", file.getName());
|
|
updateTitle();
|
|
|
|
// Update the emulation state
|
|
var bytes = rom.toByteArray();
|
|
// Pause emulation
|
|
vue.setROM(bytes, 0, bytes.length);
|
|
vue.reset();
|
|
refreshDebug(true);
|
|
// Resume emulation
|
|
}
|
|
|
|
// File -> New window
|
|
private void onNewWindow() {
|
|
app.addWindow();
|
|
}
|
|
|
|
// Video paint
|
|
private void onPaintVideo(Graphics2D g, int width, int height) {
|
|
int scale = Math.max(1, Math.min(width / 384, height / 224));
|
|
|
|
g.translate(
|
|
Math.max(0, (width - scale * 384) / 2),
|
|
Math.max(0, (height - scale * 224) / 2)
|
|
);
|
|
g.scale(scale, scale);
|
|
|
|
g.setColor(Color.black);
|
|
g.fillRect(0, 0, 384, 224);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Private Methods //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Update the character patterns
|
|
private void refreshCharacters() {
|
|
for (int index = 0; index < 2048; index++) {
|
|
var pix = chrs[index];
|
|
int src = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
|
|
for (int y = 0, dest = 0; y < 8; y++)
|
|
for (int b = 0; b < 2; b++, src++)
|
|
for (int x = 0, bits = vram[src]; x < 4; x++, dest++, bits >>= 2)
|
|
pix[dest] = (byte) (bits & 3);
|
|
}
|
|
}
|
|
|
|
// Update the palette composites
|
|
private void refreshPalettes() {
|
|
|
|
// Process brightness levels
|
|
int[] brt = { 0,
|
|
vue.read(0x0005F824, Vue.U8),
|
|
vue.read(0x0005F826, Vue.U8),
|
|
vue.read(0x0005F828, Vue.U8)
|
|
};
|
|
brt[3] += brt[0] + brt[1];
|
|
for (int x = 1; x < 4; x++)
|
|
brt[x] = (Math.min(127, brt[x]) * 510 + 127) / 254 << 1;
|
|
|
|
// Process all palettes
|
|
var pal = new int[4];
|
|
for (int x = 0; x < 9; x++) {
|
|
|
|
// Generic palette
|
|
if (x == GENERIC) {
|
|
pal[1] = 0x55 << 1;
|
|
pal[2] = 0xAA << 1;
|
|
pal[3] = 0xFF << 1;
|
|
}
|
|
|
|
// Palette from emulation state
|
|
else {
|
|
int bits = vue.read(0x0005F860 + (x - 1 << 1), Vue.U8);
|
|
pal[1] = brt[bits >> 2 & 3];
|
|
pal[2] = brt[bits >> 4 & 3];
|
|
pal[3] = brt[bits >> 6 ];
|
|
}
|
|
|
|
// Process colors
|
|
for (int y = 0; y < 3; y++) {
|
|
var base = app.rgbBase[y];
|
|
var dest = palettes[x][y];
|
|
for (int z = 1; z < 4; z++) {
|
|
int argb = 0xFF000000;
|
|
for (int w = 0, bits = 16; w < 3; w++, bits -= 8)
|
|
argb |= (pal[z] * base[w] + 255) / 510 << bits;
|
|
dest[z] = new Color(argb);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Separate the RGB components of a color
|
|
private static int[] split(int rgb) {
|
|
return new int[] { rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF };
|
|
}
|
|
|
|
// Update the window title
|
|
private void updateTitle() {
|
|
app.localizer.add(this,
|
|
only ? romFile != null ?
|
|
"app.title.rom" :
|
|
"app.title.default"
|
|
: romFile != null ?
|
|
"app.title.mixed" :
|
|
"app.title.number"
|
|
);
|
|
}
|
|
|
|
}
|