410 lines
13 KiB
Java
410 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 JPanel 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 JPanel() {
|
|
public void paintComponent(Graphics g) {
|
|
super.paintComponent(g);
|
|
onPaint((Graphics2D) g, getWidth(), getHeight());
|
|
}
|
|
};
|
|
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));
|
|
|
|
// 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);
|
|
}
|
|
|
|
}
|
|
|
|
}
|