pvbemu/src/desktop/app/DisassemblerPane.java

406 lines
13 KiB
Java

package app;
// Java imports
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
// Project imports
import util.*;
import vue.*;
// Disassembler UI
class DisassemblerPane extends JScrollPane {
// Instance fields
private int address; // Address of top row of output
private CPUWindow parent; // Containing CPU window
private boolean showBytes; // Display the bytes column
private boolean shown; // Component has been shown
private int[] widths; // Column widths
// UI components
private UPanel client; // Client area
private ArrayList<Row> rows; // Disassembler output
///////////////////////////////////////////////////////////////////////////
// Classes //
///////////////////////////////////////////////////////////////////////////
// One row of disassembler output
private class Row extends Disassembler.Row {
Instruction inst; // Decoded instruction
JLabel[] labels; // Display text
}
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
DisassemblerPane(CPUWindow parent) {
super(VERTICAL_SCROLLBAR_NEVER, HORIZONTAL_SCROLLBAR_AS_NEEDED);
// Configure instance fields
this.parent = parent;
rows = new ArrayList<Row>();
showBytes = true;
widths = new int[5];
// Configure client area
client = new UPanel();
client.setBackground(SystemColor.window);
client.setFocusable(true);
client.addFocusListener(
Util.onFocus(e->client.repaint(), e->client.repaint()));
client.addKeyListener(Util.onKey(e->onKeyDown(e), null));
client.addMouseListener(Util.onMouse(e->client.requestFocus(), null));
client.addMouseWheelListener(e->onMouseWheel(e));
client.addPaintListener((g,w,h)->onPaint(g, w, h));
// Configure component
setViewportView(client);
}
///////////////////////////////////////////////////////////////////////////
// Package Methods //
///////////////////////////////////////////////////////////////////////////
// Update the display
void refresh(boolean seekToPC) {
if (seekToPC) {
int pc = parent.parent.vue.getRegister(Vue.PC, true);
if (!isVisible(pc))
seek(pc, tall(false) / 3);
}
client.repaint();
}
///////////////////////////////////////////////////////////////////////////
// Event Handlers //
///////////////////////////////////////////////////////////////////////////
// Key down
private void onKeyDown(KeyEvent e) {
int code = e.getKeyCode();
int count = tall(false);
int mods = e.getModifiersEx();
var vue = parent.parent.vue;
boolean alt = (mods & InputEvent.ALT_DOWN_MASK ) != 0;
boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0;
// No Alt combinations
if (alt) return;
// Goto
if (ctrl && code == KeyEvent.VK_G) {
String text = JOptionPane.showInputDialog(
this, "Goto:", "Goto", JOptionPane.PLAIN_MESSAGE);
Object eval = vue.evaluate(text);
int addr = 0;
if (eval instanceof Integer)
addr = (Integer) eval;
else if (eval instanceof Long)
addr = (int) (long) (Long) eval;
else return;
if (!isVisible(addr))
seek(addr, count / 3);
return;
}
// Processing by key code
int pc = vue.getRegister(Vue.PC, true);
var step = parent.parent.brkStep;
switch (code) {
case KeyEvent.VK_UP : seek(address, 1); break;
case KeyEvent.VK_DOWN : seek(address, -1); break;
case KeyEvent.VK_PAGE_UP : seek(address, count); break;
case KeyEvent.VK_PAGE_DOWN: seek(address, -count); break;
// Single Step
case KeyEvent.VK_F11:
step.setCondition("!fetch&&pc!=" + pc);
step.setEnabled(true);
vue.emulate(20000000);
if (step.evaluate())
step.setEnabled(false);
parent.parent.refreshDebug(true);
break;
// Run to Next
case KeyEvent.VK_F12:
step.setCondition("!fetch&&pc==" +
(pc + Instruction.size(vue.read(pc, Vue.U16) >> 10)));
step.setEnabled(true);
vue.emulate(20000000);
if (step.evaluate())
step.setEnabled(false);
parent.parent.refreshDebug(true);
break;
}
}
// Mouse wheel
private void onMouseWheel(MouseWheelEvent e) {
seek(address, -e.getUnitsToScroll());
}
// Client paint
private void onPaint(Graphics2D g, int width, int height) {
var vue = parent.parent.vue;
int pc = vue.getRegister(Vue.PC, true);
// The view is being shown for the first time
if (!shown) {
shown = true;
seek(pc, tall(false) / 3);
return;
}
// Configure working variables
int address = this.address;
int count = tall(true);
var data = new byte[count * 4];
int lineHeight = parent.parent.app.fntMono.metrics.getHeight();
int offset = 0;
// Disassemble from the current address
vue.readBytes(address, data, 0, data.length);
widths[3] = widths[4] = 0;
for (int y = 0; y < count; y++) {
var row = y < rows.size() ? rows.get(y) : addRow();
var color = SystemColor.windowText;
// Disassemble the instruction
row.inst.decode(data, offset);
Disassembler.disassemble(address, row.inst, row);
updateRow(row);
// Highlight PC
if (address == pc) {
Color bg = SystemColor.control;
if (client.isFocusOwner()) {
bg = SystemColor.textHighlight;
color = SystemColor.textHighlightText;
}
g.setColor(bg);
g.fillRect(
0, y * lineHeight,
width, lineHeight
);
}
// Configure label text color
for (var label : row.labels)
label.setForeground(color);
// Advance to the next instruction
int size = address + 2 == pc ? 2 : row.inst.size;
address += size;
offset += size;
}
// Configure all rows
for (int y = 0; y < rows.size(); y++) {
var row = rows.get(y);
int top = y * lineHeight;
// Configure all labels
for (int z = 0, x = 0; z < 5; z++) {
var label = row.labels[z];
// The label is not part of the output
if (y >= count || widths[z] == 0) {
label.setVisible(false);
continue;
}
// Configure the label
label.setLocation(x, top);
label.setSize(label.getPreferredSize());
x += widths[z] + lineHeight;
}
}
// Update the client's size
var size = client.getPreferredSize();
width = -lineHeight + 1;
for (int x = 0; x < 5; x++)
if (widths[x] != 0 && (x != 2 || showBytes))
width += lineHeight + widths[x];
if (width == size.width)
return;
client.setPreferredSize(new Dimension(width, 0));
revalidate();
}
///////////////////////////////////////////////////////////////////////////
// Private Methods //
///////////////////////////////////////////////////////////////////////////
// Create a new row object
private Row addRow() {
var row = new Row();
row.inst = new Instruction();
row.labels = new JLabel[5];
// Initialize columns
for (int x = 0; x < 5; x++) {
var label = row.labels[x] = new JLabel();
if (x != 4)
label.setFont(parent.parent.app.fntMono);
client.add(label);
}
rows.add(row);
return row;
}
// Determine whether the instruction at an address is in the client view
private boolean isVisible(int target) {
int count = Math.max(1, tall(false));
var data = new byte[count * 4];
int offset = 0;
var vue = parent.parent.vue;
int pc = vue.getRegister(Vue.PC, true);
target &= 0xFFFFFFFE;
// Load enough bytes to represent every fully visible instruction
vue.readBytes(address, data, 0, data.length);
// Iterate through instructions
for (int x = 0, address = this.address; x < count; x++) {
// Determine the instruction's size
int size = address + 2 == pc ? 2 :
Instruction.size(data[offset + 1] >> 2 & 0x3F);
// The current instruction is the target
if (address == target || size == 4 && address + 2 == target)
return true;
// Advance to the next instruction
address += size;
offset += size;
}
// The instruction is not visible in the view
return false;
}
// Determine the address of the top row of output
private void seek(int target, int row) {
var data = new byte[(Math.abs(row) + 9) * 4];
int offset = 0;
var vue = parent.parent.vue;
int pc = vue.getRegister(Vue.PC, true);
target &= 0xFFFFFFFE;
// Scrolling down
if (row <= 0) {
// Load bytes starting up to 8 instructions prior to the target
address = target - 32;
vue.readBytes(address, data, 0, data.length);
// Iterate through instructions
for (boolean found = false;;) {
// Determine the instruction's size
int size = address + 2 == pc ? 2 :
Instruction.size(data[offset + 1] >> 2 & 0x3F);
// The current instruction is the target
if (address == target || size == 4 && address + 2 == target)
found = true;
if (found && row++ == 0)
break;
// Advance to the next instruction
address += size;
offset += size;
}
}
// Scrolling up
else {
// Load bytes starting up to 8 instructions prior to the top row
address = target - data.length + 4;
vue.readBytes(address, data, 0, data.length);
// Iterate through instructions
var addresses = new int[row]; // Circular buffer
for (int index = 0;;) {
// Determine the instruction's size
int size = pc == address + 2 ? 2 :
Instruction.size(data[offset + 1] >> 2 & 63);
// The current instruction is the target
if (address == target || size == 4 && address + 2 == target) {
address = addresses[index];
break;
}
// Advance to the next instruction
addresses[index] = address;
if (++index == row)
index = 0;
address += size;
offset += size;
}
}
// Common processing
client.repaint();
}
// Determine how many rows of output are visible
private int tall(boolean partial) {
int lineHeight = parent.parent.app.fntMono.metrics.getHeight();
return Math.max(1, (client.getHeight() +
(partial ? lineHeight - 1 : 0)
) / lineHeight);
}
// Update a row with its text and measure the column widths
private void updateRow(Row row) {
for (int x = 0; x < 5; x++) {
var label = row.labels[x];
String text = null;
switch (x) {
case 0: text = row.address ; break;
case 1: text = row.bytes ; break;
case 2: text = row.mnemonic; break;
case 3: text = row.operands; break;
}
if (text != null) {
label.setText(text);
label.setVisible(x == 1 ? showBytes : true);
widths[x] = Math.max(widths[x],label.getPreferredSize().width);
} else label.setVisible(false);
}
}
}