From c6d53328281fc3b8b0ffd04e25d7cefb1688b7e0 Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Sat, 26 Dec 2020 20:26:12 -0600 Subject: [PATCH] Introducing BG Maps window, performance adjustments for Characters window --- locale/en-US.txt | 18 +- makefile | 2 +- src/desktop/app/App.java | 8 +- src/desktop/app/BGMapsWindow.java | 512 ++++++++++++++++++++++++++ src/desktop/app/CharactersWindow.java | 225 +++++------ src/desktop/app/MainWindow.java | 75 ++-- src/desktop/util/UComboBox.java | 95 +++++ 7 files changed, 763 insertions(+), 172 deletions(-) create mode 100644 src/desktop/app/BGMapsWindow.java create mode 100644 src/desktop/util/UComboBox.java diff --git a/locale/en-US.txt b/locale/en-US.txt index dc27c95..59c3043 100644 --- a/locale/en-US.txt +++ b/locale/en-US.txt @@ -14,7 +14,7 @@ app { cpu CPU memory Memory # VIP section - backgrounds Backgrounds + bg_maps BG Maps characters Characters frame_buffers Frame buffers objects Objects @@ -39,6 +39,22 @@ app { } +# BG Maps window +bg_maps { + address Address + cell Cell + character Character + generic Generic + grid Grid + hflip H-Flip + index Index + map Map + palette Palette + scale Scale + title BG Maps + vflip V-Flip +} + # Breakpoints window breakpoints { address Address diff --git a/makefile b/makefile index 6613981..ea8540a 100644 --- a/makefile +++ b/makefile @@ -9,7 +9,7 @@ default: @echo $(include_linux) @echo "Planet Virtual Boy Emulator" @echo " https://www.planetvb.com/" - @echo " December 25, 2020" + @echo " December 26, 2020" @echo @echo "Intended build environment: Debian i386 or amd64" @echo " gcc-multilib" diff --git a/src/desktop/app/App.java b/src/desktop/app/App.java index df9b4c2..128c923 100644 --- a/src/desktop/app/App.java +++ b/src/desktop/app/App.java @@ -55,8 +55,12 @@ public class App { fntMono = new Util.Font(new Font(Util.fontFamily(new String[] { "Consolas", Font.MONOSPACED } ), Font.PLAIN, 14)); hexDigitWidth = hexDigitWidth(); - rgbBase = new int[][] { GREEN, MAGENTA, RED }; - rgbClear = 0x003050; + rgbBase = new int[][] { + Arrays.copyOf(GREEN , 4), + Arrays.copyOf(MAGENTA, 4), + Arrays.copyOf(RED , 4) + }; + rgbClear = 0x555555; // Additional processing setUseNative(useNative); diff --git a/src/desktop/app/BGMapsWindow.java b/src/desktop/app/BGMapsWindow.java new file mode 100644 index 0000000..aa26540 --- /dev/null +++ b/src/desktop/app/BGMapsWindow.java @@ -0,0 +1,512 @@ +package app; + +// Java imports +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.util.*; +import javax.swing.*; +import javax.swing.border.*; + +// Project imports +import util.*; +import vue.*; + +// VIP backgrounds window +class BGMapsWindow extends ChildWindow { + + // Instance fields + private int cellIndex; // Current cell index + private int character; // Cell character index + private Point dragging; // Most recent pattern mouse position + private boolean generic; // Use the generic palette + private boolean grid; // Draw a grid around characters + private boolean hFlip; // Cell is flipped horizontally + private int mapIndex; // Current BG map index + private int palette; // Palette index + private int[] pix; // Image buffer + private int scale; // Display scale + private boolean vFlip; // Cell is flipped vertically + private BufferedImage image; // BG map graphic + + // UI components + private JCheckBox chkGeneric; // Generic colors check box + private JCheckBox chkGrid; // Grid check box + private JCheckBox chkHFlip; // H-flip check box + private JCheckBox chkVFlip; // V-flip check box + private UComboBox cmbPalette; // Palette drop-down + private UPanel client; // Client area + private UPanel panCell; // Cell panel + private UPanel panMap; // BG map panel + private JScrollPane scrControls; // Controls panel + private JScrollPane scrMap; // BG map container + private JSlider sldScale; // Scale slider + private JSpinner spnCellIndex; // Cell index spinner + private JSpinner spnCharacter; // Character spinner + private JSpinner spnMapIndex; // BG map index spinner + private JTextField txtCellAddress; // Cell address text box + private JTextField txtMapAddress; // BG map address text box + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + BGMapsWindow(MainWindow parent) { + super(parent, "bg_maps.title"); + + // Configure instance fields + generic = false; + image = new BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB); + pix = new int[512 * 512]; + scale = 2; + + // Configure client area + client = new UPanel(new BorderLayout()); + client.setBackground(SystemColor.control); + client.setFocusable(true); + client.setPreferredSize(new Dimension(480, 360)); + client.addComponentListener(Util.onResize(e->onResize())); + + // Configure controls panel + var ctrls = new UPanel(new GridBagLayout()); + ctrls.setBackground(SystemColor.control); + ctrls.addMouseListener( + Util.onMouse(e->client.requestFocus(), null)); + scrControls = new JScrollPane(ctrls, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scrControls.setBorder(null); + scrControls.getVerticalScrollBar().setUnitIncrement(20); + client.add(scrControls, BorderLayout.WEST); + + // BG Map controls + label(ctrls, "bg_maps.map", true); + spnMapIndex = spinner(ctrls, 0, 15, 0, true); + spnMapIndex.addChangeListener(e-> + setMap((Integer) spnMapIndex.getValue())); + label(ctrls, "bg_maps.address", false); + txtMapAddress = textBox(ctrls); + txtMapAddress.addFocusListener(Util.onFocus(null, + e->onAddress(txtMapAddress))); + + // Cell controls container + var cell = new UPanel(new GridBagLayout()); + cell.setBorder(new TitledBorder("")); + parent.app.localizer.add(cell, "bg_maps.cell"); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 2, 2, 2); + ctrls.add(cell, gbc); + + // Cell controls + label(cell, "bg_maps.index", true); + spnCellIndex = spinner(cell, 0, 4095, 0, true); + spnCellIndex.addChangeListener(e-> + setCell((Integer) spnCellIndex.getValue())); + label(cell, "bg_maps.address", false); + txtCellAddress = textBox(cell); + txtCellAddress.addFocusListener(Util.onFocus(null, + e->onAddress(txtCellAddress))); + label(cell, "bg_maps.character", false); + spnCharacter = spinner(cell, 0, 2047, 0, false); + spnCharacter.addChangeListener(e->onEdit()); + label(cell, "bg_maps.palette", false); + cmbPalette = select(cell, new String[] { + "palette.gplt0", "palette.gplt1", "palette.gplt2", "palette.gplt3" + }); + cmbPalette.addActionListener(e->onEdit()); + label(cell, "bg_maps.hflip", false); + chkHFlip = checkBox(cell); + chkHFlip.addActionListener(e->onEdit()); + label(cell, "bg_maps.vflip", false); + chkVFlip = checkBox(cell); + chkVFlip.addActionListener(e->onEdit()); + + // Cell panel + panCell = new UPanel(); + panCell.setOpaque(false); + panCell.setPreferredSize(new Dimension(96, 96)); + panCell.addPaintListener((g,w,h)->onPaintCell(g, w, h)); + gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.CENTER; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(2, 2, 2, 2); + cell.add(panCell, gbc); + terminator(cell); + + // Fill the extra space above the view controls + var spacer = new UPanel(); + spacer.setOpaque(false); + gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.weighty = 1; + ctrls.add(spacer, gbc); + + // View controls + label(ctrls, "bg_maps.grid", false); + chkGrid = checkBox(ctrls); + chkGrid.addActionListener(e->{ + grid = chkGrid.isSelected(); onView(); }); + label(ctrls, "bg_maps.generic", false); + chkGeneric = checkBox(ctrls); + chkGeneric.setSelected(generic); + chkGeneric.addActionListener(e->{ + generic = chkGeneric.isSelected(); refresh(); }); + label(ctrls, "bg_maps.scale", false); + sldScale = slider(ctrls, 1, 10, scale); + sldScale.addChangeListener(e->{ + scale = sldScale.getValue(); onView(); }); + terminator(ctrls); + + // Configure BG map panel + panMap = new UPanel(); + panMap.setBackground(SystemColor.control); + panMap.addMouseListener( + Util.onMouse(e->onMouse(e), e->onMouse(e))); + panMap.addMouseMotionListener( + Util.onMouseMove(null, e->onMouse(e))); + panMap.addPaintListener((g,w,h)->onPaintMap(g, w, h)); + scrMap = new JScrollPane(panMap); + client.add(scrMap, BorderLayout.CENTER); + + // Configure component + setContentPane(client); + setCell(0); + onView(); + pack(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Package Methods // + /////////////////////////////////////////////////////////////////////////// + + // Update the display + void refresh() { + + // Update BG map graphic + int src = 0x00020000 | mapIndex << 13; + for (int index = 0; index < 4096; index++, src += 2) { + int bits = parent.vram[src] & 0xFF | parent.vram[src + 1] << 8; + var pal = parent.palettes[generic ? MainWindow.GENERIC : + MainWindow.GPLT0 + (bits >> 14 & 3)][MainWindow.RED]; + parent.drawCharacter( + bits & 0x07FF, (bits & 0x2000) != 0, (bits & 0x1000) != 0, pal, + pix, index >> 6 << 12 | (index & 63) << 3, 512 + ); + } + image.setRGB(0, 0, 512, 512, pix, 0, 512); + + // Update display; + panCell.repaint(); + panMap .repaint(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Event Handlers // + /////////////////////////////////////////////////////////////////////////// + + // Address text boxes commit + private void onAddress(JTextField src) { + int address; + + // Parse the given address + try { address = (int) Long.parseLong(src.getText(), 16); } + catch (Exception e) { + setMap(mapIndex); + return; + } + + // Restrict address range + if ((address >> 24 & 7) != 0) { + setMap(mapIndex); + return; + } + address &= 0x0007FFFF; + + // Check if the address is in BG map memory + if (address < 0x00020000 || address >= 0x00040000) { + setMap(mapIndex); + return; + } + + // Update the map and character indexes + address -= 0x00020000; + mapIndex = address >> 13; + setCell((address & 0x00001FFE) >> 1); + } + + // A cell value has changed + private void onEdit() { + + // Process cell properties + character = (Integer) spnCharacter.getValue(); + hFlip = chkHFlip.isSelected(); + palette = cmbPalette.getSelectedIndex(); + vFlip = chkVFlip.isSelected(); + + // Update VRAM state + parent.vue.write( + 0x00020000 | mapIndex << 13 | cellIndex << 1, Vue.U16, + palette << 14 | (hFlip ? 0x2000 : 0) | + (vFlip ? 0x1000 : 0) | character + ); + parent.refreshDebug(false); + } + + // BG map panel mouse + private void onMouse(MouseEvent e) { + int id = e.getID(); + + // Mouse release + if (id == MouseEvent.MOUSE_RELEASED) { + if (e.getButton() == MouseEvent.BUTTON1) + dragging = null; + return; + } + + // Mouse press + if (id == MouseEvent.MOUSE_PRESSED) { + client.requestFocus(); + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = e.getPoint(); + } + + // Not dragging + if (dragging == null) + return; + + // Working variables + int grid = this.grid ? 1 : 0; + int size = 8 * scale + grid; + int col = e.getX() / size; + int row = e.getY() / size; + + // The pixel is not within a cell + if (col >= 64 || row >= 64) + return; + + // Calculate the index of the selected cell + setCell(row << 6 | col); + } + + // Cell panel paint + private void onPaintCell(Graphics2D g, int width, int height) { + int scale = Math.max(1, Math.min(width >> 3, height >> 3)); + int x = (width - (scale << 3) + 1) >> 1; + int y = (height - (scale << 3) + 1) >> 1; + int ix = (cellIndex & 63) << 3; + int iy = cellIndex >> 6 << 3; + g.drawImage(image, + x, y, x + scale * 8, y + scale * 8, + ix, iy, ix + 8 , iy + 8 , + null); + } + + // BG map panel paint + private void onPaintMap(Graphics2D g, int width, int height) { + + // Drawing with a grid + if (grid) { + + int scale = 8 * this.scale; + for (int y = 0, z = 0; y < 64; y++) + for (int x = 0; x < 64; x++, z++) { + int ix = (z & 63) << 3; + int iy = z >> 6 << 3; + int ox = x * (scale + 1); + int oy = y * (scale + 1); + g.drawImage(image, + ox, oy, ox + scale, oy + scale, + ix, iy, ix + 8 , iy + 8 , + null); + } + } + + // Not drawing with a grid + else g.drawImage(image, 0, 0, 512 * scale, 512 * scale, null); + + // Highlight the selected cell + int size = 8 * scale + (grid ? 1 : 0); + int X = (cellIndex & 63) * size; + int Y = (cellIndex >> 6) * size; + int light = SystemColor.textHighlight.getRGB() & 0x00FFFFFF; + g.setColor(new Color(0xD0000000 | light, true)); + g.drawRect(X , Y , 8 * scale - 1, 8 * scale - 1); + g.setColor(new Color(0x90000000 | light, true)); + g.drawRect(X + 1, Y + 1, 8 * scale - 3, 8 * scale - 3); + g.setColor(new Color(0x50000000 | light, true)); + g.fillRect(X + 2, Y + 2, 8 * scale - 4, 8 * scale - 4); + + } + + // Window resize + private void onResize() { + var viewport = scrControls.getViewport(); + int inner = viewport.getView().getPreferredSize().width; + int outer = viewport.getExtentSize().width; + + // Size the controls container to match the inner component + if (inner != outer) { + scrControls.setPreferredSize(new Dimension( + scrControls.getPreferredSize().width + inner - outer, 0)); + scrControls.revalidate(); + scrControls.repaint(); + } + + } + + // View control changed + private void onView() { + int grid = this.grid ? 1 : 0; + int size = 8 * scale + grid; + int pref = size * 64 - grid; + scrMap.getVerticalScrollBar().setUnitIncrement(8 * scale + grid); + panMap.setPreferredSize(new Dimension(pref, pref)); + panMap.revalidate(); + panMap.repaint(); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Private Methods // + /////////////////////////////////////////////////////////////////////////// + + // Add a check box to the controls panel + private JCheckBox checkBox(UPanel panel) { + var chk = new JCheckBox(); + chk.setBorder(null); + chk.setFocusable(false); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 0, 2, 2); + panel.add(chk, gbc); + return chk; + } + + // Add a label to the controls panel + private void label(UPanel panel, String key, boolean top) { + var lbl = new JLabel(); + parent.app.localizer.add(lbl, key); + var gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(top ? 2 : 0, 2, 2, 2); + panel.add(lbl, gbc); + } + + // Update controls + private void refreshLite() { + int mapAddr = mapIndex << 13 | 0x00020000; + int cellAddr = mapAddr | cellIndex << 1; + + int bits = parent.vram[cellAddr] & 0xFF | parent.vram[cellAddr+1] << 8; + character = bits & 0x07FF; + hFlip = (bits & 0x2000) != 0; + palette = bits >> 14 & 3; + vFlip = (bits & 0x1000) != 0; + + chkHFlip .setSelected(hFlip); + chkVFlip .setSelected(vFlip); + cmbPalette .setSelectedIndex(palette); + spnCellIndex .setValue(cellIndex); + spnCharacter .setValue(character); + spnMapIndex .setValue(mapIndex); + txtMapAddress .setText(String.format("%08X", mapAddr )); + txtCellAddress.setText(String.format("%08X", cellAddr)); + + panCell.repaint(); + panMap .repaint(); + } + + // Add a combo box to the controls panel + private UComboBox select(UPanel panel, String[] options) { + var cmb = new UComboBox(); + parent.app.localizer.add(cmb, options); + cmb.setSelectedIndex(0); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 0, 2, 2); + panel.add(cmb, gbc); + return cmb; + } + + // Specify the current cell index + private void setCell(int index) { + cellIndex = index; + refreshLite(); + } + + // Specify the current BG map index + private void setMap(int index) { + mapIndex = index; + refreshLite(); + } + + // Add a slider to the controls panel + private JSlider slider(UPanel panel, int min, int max, int value) { + var sld = new JSlider(min, max, value); + sld.setFocusable(false); + sld.setPreferredSize(new Dimension(0, sld.getPreferredSize().height)); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 0, 1, 2); + panel.add(sld, gbc); + return sld; + } + + // Add a spinner to the controls panel + private JSpinner spinner(UPanel panel, int min, int max, int value, + boolean top) { + var spn = new JSpinner(new SpinnerNumberModel(value, min, max, 1)); + var txt = new JSpinner.NumberEditor(spn, "#"); + txt.getTextField().addActionListener(e->client.requestFocus()); + spn.setEditor(txt); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(top ? 2 : 0, 0, 2, 2); + gbc.weightx = 1; + panel.add(spn, gbc); + return spn; + } + + // Terminate a controls container + private void terminator(UPanel panel) { + var spacer = new UPanel(); + spacer.setOpaque(false); + spacer.setPreferredSize(new Dimension(0, 0)); + var gbc = new GridBagConstraints(); + gbc.gridheight = GridBagConstraints.REMAINDER; + gbc.gridwidth = GridBagConstraints.REMAINDER; + panel.add(spacer, gbc); + } + + // Add a text box to the controls panel + private JTextField textBox(UPanel panel) { + var txt = new JTextField(); + txt.setFont(parent.app.fntMono); + txt.addActionListener(e->client.requestFocus()); + var size = txt.getPreferredSize(); + txt.setPreferredSize(new Dimension( + parent.app.hexDigitWidth * 8 + 2 + size.width, size.height)); + var gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(0, 0, 2, 2); + panel.add(txt, gbc); + return txt; + } + +} diff --git a/src/desktop/app/CharactersWindow.java b/src/desktop/app/CharactersWindow.java index bb7af27..d49026d 100644 --- a/src/desktop/app/CharactersWindow.java +++ b/src/desktop/app/CharactersWindow.java @@ -3,6 +3,7 @@ package app; // Java imports import java.awt.*; import java.awt.event.*; +import java.awt.image.*; import java.util.*; import javax.swing.*; @@ -18,12 +19,15 @@ class CharactersWindow extends ChildWindow { private int index; // Current character index private boolean grid; // Draw a grid around characters private int palette; // Palette index + private int[] pix; // Image buffer private int scale; // Display scale private int wide; // Number of characters per row + private BufferedImage image; // Character graphics // UI components private JCheckBox chkGrid; // Grid check box private UPanel client; // Client area + private UComboBox cmbPalette; // Palette drop-down private UPanel panCharacters; // Characters panel private UPanel panPalette; // Palette panel private UPanel panPattern; // Pattern panel @@ -35,7 +39,6 @@ class CharactersWindow extends ChildWindow { private JSpinner spnWide; // Wide spinner private JTextField txtAddress; // Address text box private JTextField txtMirror; // Mirror text box - private JComboBox cmbPalette; // Palette drop-down @@ -50,6 +53,8 @@ class CharactersWindow extends ChildWindow { // Configure instance fields color = 0; grid = true; + image = new BufferedImage(256, 512, BufferedImage.TYPE_INT_RGB); + pix = new int[256 * 512]; scale = 3; // Template scroll bar to check control width in the current LAF @@ -77,8 +82,8 @@ class CharactersWindow extends ChildWindow { // Character controls label(ctrls, "characters.index", true); spnIndex = spinner(ctrls, 0, 2047, 0, true); - spnIndex.addChangeListener(e->{ - setIndex((Integer) spnIndex.getValue()); }); + spnIndex.addChangeListener(e-> + setIndex((Integer) spnIndex.getValue())); label(ctrls, "characters.address", false); txtAddress = textBox(ctrls); txtAddress.addFocusListener(Util.onFocus(null, @@ -159,7 +164,9 @@ class CharactersWindow extends ChildWindow { panCharacters = new UPanel(); panCharacters.setBackground(SystemColor.control); panCharacters.addMouseListener( - Util.onMouse(e->onMouseDownCharacters(e), null)); + Util.onMouse(e->onMouseCharacters(e), e->onMouseCharacters(e))); + panCharacters.addMouseMotionListener( + Util.onMouseMove(null, e->onMouseCharacters(e))); panCharacters.addPaintListener((g,w,h)->onPaintCharacters(g, w, h)); scrCharacters = new JScrollPane(panCharacters); client.add(scrCharacters, BorderLayout.CENTER); @@ -167,6 +174,7 @@ class CharactersWindow extends ChildWindow { // Configure component setContentPane(client); setIndex(0); + onView(); pack(); } @@ -178,7 +186,17 @@ class CharactersWindow extends ChildWindow { // Update the display void refresh() { - repaint(); + + // Update characters graphic + var pal = parent.palettes[palette][MainWindow.RED]; + for (int index = 0; index < 2048; index++) + parent.drawCharacter(index, false, false, pal, pix, + index >> 5 << 11 | (index & 31) << 3, 256); + image.setRGB(0, 0, 256, 512, pix, 0, 256); + + // Update display; + panCharacters.repaint(); + panPattern .repaint(); } @@ -217,14 +235,27 @@ class CharactersWindow extends ChildWindow { setIndex(index); } - // Characters mouse button press - private void onMouseDownCharacters(MouseEvent e) { + // Characters mouse + private void onMouseCharacters(MouseEvent e) { + int id = e.getID(); - // Common processing - client.requestFocus(); + // Mouse release + if (id == MouseEvent.MOUSE_RELEASED) { + if (e.getButton() == MouseEvent.BUTTON1) + dragging = null; + return; + } - // Only consider left clicks - if (e.getButton() != MouseEvent.BUTTON1) + // Mouse press + if (id == MouseEvent.MOUSE_PRESSED) { + client.requestFocus(); + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = e.getPoint(); + } + + // Not dragging + if (dragging == null) return; // Working variables @@ -278,7 +309,11 @@ class CharactersWindow extends ChildWindow { // Pattern mouse private void onMousePattern(MouseEvent e) { - int id = e.getID(); + int id = e.getID(); + int scale = Math.max(1, Math.min( + panPattern.getWidth () >> 3, + panPattern.getHeight() >> 3 + )); // Mouse release if (id == MouseEvent.MOUSE_RELEASED) { @@ -289,109 +324,60 @@ class CharactersWindow extends ChildWindow { // Mouse press if (id == MouseEvent.MOUSE_PRESSED) { + client.requestFocus(); if (e.getButton() != MouseEvent.BUTTON1) return; - dragging = e.getPoint(); + dragging = e.getPoint(); + dragging.x = dragging.x / scale - (dragging.x < 0 ? 1 : 0); + dragging.y = dragging.y / scale - (dragging.y < 0 ? 1 : 0); } // Not dragging if (dragging == null) return; - // Configure working variables - var pix = parent.chrs[index]; - var pos = e.getPoint(); - int scale = Math.max(1, Math.min( - panPattern.getWidth() / 8, panPattern.getHeight() / 8)); + // Retrieve the current point + var pos = e.getPoint(); + pos.x = pos.x / scale - (pos.x < 0 ? 1 : 0); + pos.y = pos.y / scale - (pos.y < 0 ? 1 : 0); - // Determine the bounds of the line segment - int left = Math.min(dragging.x, pos.x) / scale; - int right = (Math.max(dragging.x, pos.x) - 1) / scale; - int top = Math.min(dragging.y, pos.y) / scale; - int bottom = (Math.max(dragging.y, pos.y) - 1) / scale; - if (left >= 8 || right < 0 || top >= 8 || bottom < 0) { - dragging = pos; - return; + // Draw a line segment (adapted from Wikipedia) + int dx = Math.abs(pos.x - dragging.x); + int sx = dragging.x < pos.x ? 1 : -1; + int dy = -Math.abs(pos.y - dragging.y); + int sy = dragging.y < pos.y ? 1 : -1; + int err = dx + dy; + int addr = index >> 9 << 15 | 0x00006000 | (index & 511) << 4; + for (;;) { + + // Draw the current pixel + if ((dragging.x&7) == dragging.x && (dragging.y&7) == dragging.y) { + int b = addr | dragging.y << 1 | dragging.x >> 2; + int bit = (dragging.x & 3) << 1; + parent.vram[b] = (byte) + (parent.vram[b] & ~(3 << bit) | color << bit); + } + + // The last pixel has been drawn + if (dragging.x == pos.x && dragging.y == pos.y) + break; + + // Advance to the next pixel + int e2 = err << 1; + if (e2 >= dy) + { err += dy; dragging.x += sx; } + if (e2 <= dx) + { err += dx; dragging.y += sy; } } - // The line segment occupies a single column of pixels - if (left == right) { - top = Math.max(0, top ); - bottom = Math.min(7, bottom); - - // Draw the column - for ( - int y = top, dest = left + top * 8; - y <= bottom; - y++, dest += 8 - ) pix[dest] = (byte) color; - - // Update the current VRAM state - parent.encode(index); - dragging = pos; - return; - } - - // Calculate and order the vertex coordinates - float v0x = (float) dragging.x / scale; - float v0y = (float) dragging.y / scale; - float v1x = (float) pos .x / scale; - float v1y = (float) pos .y / scale; - if (v0x > v1x) { - float t = v0x; v0x = v1x; v1x = t; - t = v0y; v0y = v1y; v1y = t; - } - - // Determine the starting position of the line segment - float slope = (v1y - v0y) / (v1x - v0x); - float cur, next; - if (v0x < 0) { - left = 0; - cur = v0y - slope * v0x; - next = cur + slope; - } else { - cur = v0y; - next = v0y + slope * (1 - v0x % 1); - } - - // Draw all columns of pixels - int last = Math.min(7, right); - for (int x = left; x <= last; x++) { - - // The column is the final column in the line segment - if (x == right) - next = v1y; - - // Determine the top and bottom rows of pixels - v0y = Math.max(cur, next); - if (v0y % 1 == 0) - v0y--; - top = Math.max(0, (int) Math.floor(Math.min(cur, next))); - bottom = Math.min(7, (int) Math.floor(v0y )); - - // Draw the column - for ( - int y = top, dest = x + top * 8; - y <= bottom; - y++, dest += 8 - ) pix[dest] = (byte) color; - - // Advance to the next column - cur = next; - next += slope; - } - - // Update the current VRAM state - parent.encode(index); - dragging = pos; + // Update VRAM state + parent.vue.writeBytes(addr, parent.vram, addr, 16); + parent.refreshDebug(false); } // Characters paint private void onPaintCharacters(Graphics2D g, int width, int height) { - var clear = new Color(parent.app.rgbClear); int grid = this.grid ? 1 : 0; - var pal = parent.palettes[palette][MainWindow.RED]; - var pix = new byte[64]; int size = scale * 8 + grid; int wide = this.wide != 0 ? this.wide : Math.max(1, (width + grid) / size); @@ -402,9 +388,12 @@ class CharactersWindow extends ChildWindow { for (int X = 0, x = 0; x < wide; x++, z++, X += size) { if (z == 2048) break; - g.setColor(clear); - g.fillRect(X, Y, size - grid, size - grid); - drawCharacter(g, X, Y, scale, z, pal); + int ix = (z & 31) << 3; + int iy = z >> 5 << 3; + g.drawImage(image, + X, Y, X + size - grid, Y + size - grid, + ix, iy, ix + 8 , iy + 8 , + null); } // Highlight the selected character @@ -438,7 +427,7 @@ class CharactersWindow extends ChildWindow { } // Draw the color area - g.setColor(x == 0 ? new Color(parent.app.rgbClear) : pal[x]); + g.setColor(new Color(pal[x])); g.fillRect(left + 3, top + 3, size , size ); } @@ -449,10 +438,12 @@ class CharactersWindow extends ChildWindow { int scale = Math.max(1, Math.min(width >> 3, height >> 3)); int x = (width - (scale << 3) + 1) >> 1; int y = (height - (scale << 3) + 1) >> 1; - g.setColor(new Color(parent.app.rgbClear)); - g.fillRect(x, y, scale * 8, scale * 8); - drawCharacter(g, x, y, scale, index, - parent.palettes[palette][MainWindow.RED]); + int ix = (index & 31) << 3; + int iy = index >> 5 << 3; + g.drawImage(image, + x, y, x + scale * 8, y + scale * 8, + ix, iy, ix + 8 , iy + 8 , + null); } // Window resize @@ -515,6 +506,7 @@ class CharactersWindow extends ChildWindow { wide * (scale * 8 + grid) - grid, (2047 + wide) / wide * (scale * 8 + grid) - grid )); + scrCharacters.getVerticalScrollBar().setUnitIncrement(8*scale+grid); panCharacters.revalidate(); panCharacters.repaint(); } @@ -538,19 +530,6 @@ class CharactersWindow extends ChildWindow { return chk; } - // Draw a character - private void drawCharacter(Graphics2D g, int X, int Y, int scale, - int index, Color[] pal) { - var pix = parent.chrs[index]; - for (int y = 0, src = 0; y < 8; y++) - for (int x = 0; x < 8; x++, src++) { - if (pix[src] == 0) - continue; - g.setColor(pal[pix[src]]); - g.fillRect(X + x * scale, Y + y * scale, scale, scale); - } - } - // Add a label to the controls panel private void label(UPanel panel, String key, boolean top) { var lbl = new JLabel(); @@ -562,14 +541,14 @@ class CharactersWindow extends ChildWindow { } // Add a combo box to the controls panel - private JComboBox select(UPanel panel, String[] options) { - var cmb = new JComboBox(); + private UComboBox select(UPanel panel, String[] options) { + var cmb = new UComboBox(); parent.app.localizer.add(cmb, options); cmb.setSelectedIndex(0); var gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.insets = new Insets(0, 0, 1, 2); + gbc.insets = new Insets(0, 0, 2, 2); panel.add(cmb, gbc); return cmb; } diff --git a/src/desktop/app/MainWindow.java b/src/desktop/app/MainWindow.java index e1e3c9a..dd51f8e 100644 --- a/src/desktop/app/MainWindow.java +++ b/src/desktop/app/MainWindow.java @@ -15,12 +15,11 @@ import vue.*; 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 + App app; // Containing application + Breakpoint brkStep; // Single step internal breakpoint + int[][][] palettes; // Raster palettes + byte[] vram; // Snapshot of VIP memory + Vue vue; // Emulation core context // Private fields private boolean debugMode; // Window is in debug mode @@ -39,6 +38,7 @@ class MainWindow extends JFrame { private JMenuItem mnuFileGameMode; // File -> Game mode // Child windows + private BGMapsWindow bgMaps; private BreakpointsWindow breakpoints; private CharactersWindow characters; private ConsoleWindow console; @@ -85,21 +85,13 @@ class MainWindow extends JFrame { // Configure instance fields this.app = app; - chrs = new byte[2048][64]; - palettes = new Color[9][3][4]; + palettes = new int[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)); @@ -121,6 +113,7 @@ class MainWindow extends JFrame { // Configure child windows desktop = new JDesktopPane(); desktop.setBackground(SystemColor.controlShadow); + desktop.add(bgMaps = new BGMapsWindow (this)); desktop.add(breakpoints = new BreakpointsWindow(this)); desktop.add(characters = new CharactersWindow (this)); desktop.add(console = new ConsoleWindow (this)); @@ -181,10 +174,10 @@ class MainWindow extends JFrame { mnuDebug.addSeparator(); - var mnuDebugBackgrounds = new JMenuItem(); - mnuDebugBackgrounds.setEnabled(false); - loc.add(mnuDebugBackgrounds, "app.debug.backgrounds"); - mnuDebug.add(mnuDebugBackgrounds); + var mnuDebugBGMaps = new JMenuItem(); + loc.add(mnuDebugBGMaps, "app.debug.bg_maps"); + mnuDebugBGMaps.addActionListener(e->bgMaps.setVisible(true)); + mnuDebug.add(mnuDebugBGMaps); var mnuDebugCharacters = new JMenuItem(); loc.add(mnuDebugCharacters, "app.debug.characters"); @@ -251,28 +244,32 @@ class MainWindow extends JFrame { // 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); + // Draw a character into a pixel buffer + void drawCharacter(int index, boolean hFlip, boolean vFlip, int[] palette, + int[] buffer, int offset, int stride) { + int addr = index >> 9 << 15 | 0x00006000 | (index & 511) << 4; + + for (int y = 0; y < 8; y++, offset += stride - 8) + for (int x = 0; x < 8; x++, offset += 1 ) { + int ix = hFlip ? 7 - x : x; + int iy = vFlip ? 7 - y : y; + int b = addr | iy << 1 | ix >> 2; + int bit = (ix & 3) << 1; + buffer[offset] = palette[vram[b] >> bit & 3]; + } + } // 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) { + bgMaps .refresh(); breakpoints.refresh(); characters .refresh(); cpu .refresh(seekToPC); @@ -412,18 +409,6 @@ class MainWindow extends JFrame { // 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() { @@ -460,11 +445,11 @@ class MainWindow extends JFrame { for (int y = 0; y < 3; y++) { var base = app.rgbBase[y]; var dest = palettes[x][y]; + dest[0] = 0xFF000000 | app.rgbClear; for (int z = 1; z < 4; z++) { - int argb = 0xFF000000; + dest[z] = 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); + dest[z] |= (pal[z] * base[w] + 255) / 510 << bits; } } diff --git a/src/desktop/util/UComboBox.java b/src/desktop/util/UComboBox.java new file mode 100644 index 0000000..011d87b --- /dev/null +++ b/src/desktop/util/UComboBox.java @@ -0,0 +1,95 @@ +package util; + +// Java imports +import java.awt.event.*; +import java.util.*; +import javax.swing.*; + +// Wrapper around JComboBox to enforce desired behaviors +public class UComboBox extends JComboBox { + + // Instance fields + private HashMap actionListeners; + + + + /////////////////////////////////////////////////////////////////////////// + // Constants // + /////////////////////////////////////////////////////////////////////////// + + // Empty item set + private static final String[] EMPTY = new String[0]; + + + + /////////////////////////////////////////////////////////////////////////// + // Constructors // + /////////////////////////////////////////////////////////////////////////// + + // Default constructor + public UComboBox() { + this(null); + } + + // List constructor + public UComboBox(String[] items) { + super(); + actionListeners = new HashMap(); + setItems(items); + } + + + + /////////////////////////////////////////////////////////////////////////// + // Public Methods // + /////////////////////////////////////////////////////////////////////////// + + // Add a listener to receive action events + public void addActionListener(ActionListener l) { + + // Error checking + if (l == null) + return; + + // Wrap the event listener in a guard + ActionListener L = e->{ + + // Search the stack trace for an invocation not from Java itself + var trace = Thread.currentThread().getStackTrace(); + for (int x = 2; x < trace.length; x++) + if (trace[x].getModuleName() == null) + return; + + // Call the event handler + l.actionPerformed(e); + }; + + // Manage the collections + actionListeners.put(l, L); + super.addActionListener(L); + } + + // Retrieve a list of the current action listeners + public ActionListener[] getActionListeners() { + return actionListeners.keySet().toArray( + new ActionListener[actionListeners.size()]); + } + + // Remove an action listener from the collection + public void removeActionListener(ActionListener l) { + var L = actionListeners.get(l); + if (L == null) + return; + actionListeners.remove(l); + super.removeActionListener(L); + } + + // Update the items in the list + public void setItems(String[] items) { + int index = getSelectedIndex(); + setModel(new DefaultComboBoxModel( + items != null ? items : EMPTY)); + setSelectedIndex(index); + } + +}