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