pvbemu/app/windows/CPUWindow.js

668 lines
21 KiB
JavaScript

"use strict";
// CPU register and disassembler display
(globalThis.CPUWindow = class CPUWindow extends Toolkit.Window {
// Static initializer
static initializer() {
// System register IDs
this.ADTRE = 25;
this.CHCW = 24;
this.ECR = 4;
this.EIPC = 0;
this.EIPSW = 1;
this.FEPC = 2;
this.FEPSW = 3;
this.PC = -1;
this.PIR = 6;
this.PSW = 5;
this.TKCW = 7;
// Program register names
this.PROGRAM = {
[ 2]: "hp",
[ 3]: "sp",
[ 4]: "gp",
[ 5]: "tp",
[31]: "lp"
};
}
// Object constructor
constructor(debug, options) {
super(debug.gui, options);
// Configure instance fields
this.address = 0xFFFFFFF0;
this.columns = [ 0, 0, 0, 0 ];
this.debug = debug;
this.pendingDasm = { mode: null };
this.pendingRegs = { mode: null };
this.rows = [];
// Configure properties
this.setProperty("sim", "");
// Configure elements
this.initDisassembler();
this.initSystemRegisters();
this.initProgramRegisters();
this.initWindow();
// Disassembler on the left
this.mainWrap.add(this.dasmWrap);
// Registers on the right
this.regs = this.newPanel({
layout: "grid",
rows : "max-content max-content auto"
});
this.regs.element.setAttribute("name", "wrap-registers");
// Splitter between disassembler and registers
this.mainSplit = this.newSplitter({
component : this.regs,
orientation: "vertical",
edge : "right",
name : "{cpu.mainSplit}"
});
this.mainSplit.element.setAttribute("name", "split-main");
this.mainSplit.element.style.width = "3px";
this.mainSplit.element.style.minWidth = "3px";
this.mainSplit.element.style.cursor = "ew-resize";
this.mainWrap.add(this.mainSplit);
// Registers on the right
this.mainWrap.add(this.regs);
// System registers on top
this.regs.add(this.sysWrap);
// Splitter between system registers and program registers
this.regsSplit = this.regs.add(this.newSplitter({
component : this.sysWrap,
orientation: "horizontal",
edge : "top",
name : "{cpu.regsSplit}"
}));
this.regsSplit.element.style.height = "3px";
this.regsSplit.element.style.minHeight = "3px";
this.regsSplit.element.style.cursor = "ns-resize";
// Program registers on the bottom
this.regs.add(this.proWrap);
}
// Initialize disassembler pane
initDisassembler() {
// Wrapping element to hide overflowing scrollbar
this.dasmWrap = this.newPanel({
layout : "grid",
overflowX: "hidden",
overflowY: "hidden"
});
this.dasmWrap.element.setAttribute("name", "wrap-disassembler");
// Main element
this.dasm = this.dasmWrap.add(this.newPanel({
focusable: true,
name : "{cpu.disassembler}",
overflowX: "auto",
overflowY: "hidden"
}));
this.dasm.element.setAttribute("name", "disassembler");
this.dasm.addResizeListener(b=>this.onresize(b));
this.dasm.element.addEventListener("keydown", e=>this.onkeydasm(e));
this.dasm.element.addEventListener("wheel" , e=>this.onwheel (e));
this.rows.push(this.dasm.add(new CPUWindow.Row(this.dasm)));
}
// Initialize program registers pane
initProgramRegisters() {
// Wrapping element to hide overflowing scrollbar
this.proWrap = this.newPanel({
layout : "grid",
overflow: "hidden"
});
this.proWrap.element.setAttribute("name", "wrap-program-registers");
// Main element
this.proRegs = this.proWrap.add(this.newPanel({
overflowX: "auto",
overflowY: "scroll"
}));
this.proRegs.element.setAttribute("name", "program-registers");
// List of registers
this.proRegs.registers = {};
for (let x = 0; x <= 31; x++)
this.addRegister(false, x, CPUWindow.PROGRAM[x] || "r" + x);
}
// Initialize system registers pane
initSystemRegisters() {
// Wrapping element to hide overflowing scrollbar
this.sysWrap = this.newPanel({
layout : "grid",
overflow: "hidden"
});
this.sysWrap.element.setAttribute("name", "wrap-system-registers");
// Main element
this.sysRegs = this.sysWrap.add(this.newPanel({
overflowX: "auto",
overflowY: "scroll"
}));
this.sysRegs.element.setAttribute("name", "system-registers");
// List of registers
this.sysRegs.registers = {};
this.addRegister(true, CPUWindow.PC , "PC" );
this.addRegister(true, CPUWindow.PSW , "PSW" );
this.addRegister(true, CPUWindow.ADTRE, "ADTRE");
this.addRegister(true, CPUWindow.CHCW , "CHCW" );
this.addRegister(true, CPUWindow.ECR , "ECR" );
this.addRegister(true, CPUWindow.EIPC , "EIPC" );
this.addRegister(true, CPUWindow.EIPSW, "EIPSW");
this.addRegister(true, CPUWindow.FEPC , "FEPC" );
this.addRegister(true, CPUWindow.FEPSW, "FEPSW");
this.addRegister(true, CPUWindow.PIR , "PIR" );
this.addRegister(true, CPUWindow.TKCW , "TKCW" );
this.addRegister(true, 29 , "29" );
this.addRegister(true, 30 , "30" );
this.addRegister(true, 31 , "31" );
this.sysRegs.registers[CPUWindow.PSW].setExpanded(true);
}
// Initialize window and client
initWindow() {
// Configure element
this.element.setAttribute("window", "cpu");
// Configure body
this.body.element.setAttribute("filter", "");
// Configure client
this.client.setLayout("grid", {
columns: "auto"
});
// Configure main wrapper
this.mainWrap = this.client.add(this.newPanel({
layout : "grid",
columns: "auto max-content max-content"
}));
this.mainWrap.element.setAttribute("name", "wrap-main");
}
///////////////////////////// Public Methods //////////////////////////////
// Update the display with current emulation data
refresh(seekToPC, dasm, regs) {
if (dasm || dasm === undefined)
this.refreshDasm(this.address, 0, !!seekToPC);
if (regs || regs === undefined)
this.refreshRegs();
}
// Specify whether the component is visible
setVisible(visible, focus) {
let prev = this.visible
visible = !!visible;
super.setVisible(visible, focus);
if (visible && !prev)
this.refresh();
}
///////////////////////////// Message Methods /////////////////////////////
// Message received
message(msg) {
switch (msg.command) {
case "GetRegisters": this.getRegisters(msg); break;
case "ReadBuffer" : this.readBuffer (msg); break;
case "SetRegister" : this.setRegister (msg); break;
case "RunNext": case "SingleStep":
this.refresh(true); break;
}
}
// Retrieved all register values
getRegisters(msg) {
// Update controls
this.sysRegs.registers[CPUWindow.PC ]
.setValue(msg.pc, msg.pcFrom, msg.pcTo);
this.sysRegs.registers[CPUWindow.PSW ].setValue(msg.psw );
this.sysRegs.registers[CPUWindow.ADTRE].setValue(msg.adtre);
this.sysRegs.registers[CPUWindow.CHCW ].setValue(msg.chcw );
this.sysRegs.registers[CPUWindow.ECR ].setValue(msg.ecr );
this.sysRegs.registers[CPUWindow.EIPC ].setValue(msg.eipc );
this.sysRegs.registers[CPUWindow.EIPSW].setValue(msg.eipsw);
this.sysRegs.registers[CPUWindow.FEPC ].setValue(msg.fepc );
this.sysRegs.registers[CPUWindow.FEPSW].setValue(msg.fepsw);
this.sysRegs.registers[CPUWindow.PIR ].setValue(msg.pir );
this.sysRegs.registers[CPUWindow.TKCW ].setValue(msg.tkcw );
this.sysRegs.registers[29 ].setValue(msg.sr29 );
this.sysRegs.registers[30 ].setValue(msg.sr30 );
this.sysRegs.registers[31 ].setValue(msg.sr31 );
for (let x = 0; x <= 31; x++)
this.proRegs.registers[x].setValue(msg.program[x]);
// Check for pending display updates
let mode = this.pendingDasm.mode;
this.pendingRegs.mode = null;
switch (mode) {
case "first":
case null :
return;
case "refresh":
this.refreshRegs();
}
}
// Retrieved data for disassembly
readBuffer(msg) {
let lines = Math.min(msg.lines, this.rows.length);
// Disassemble the visible instructions
let dasm = Disassembler.disassemble(
new Uint8Array(msg.buffer), 0, msg.address, msg.target,
msg.pc, msg.line, lines);
// Ensure PC is visible if requested
let reseeking = false;
if (msg.seekToPC) {
let visible = this.lines(true);
let count = Math.min(msg.lines, visible);
let x;
// Ensure PC is visible in the disassembly
for (x = 0; x < count; x++)
if (dasm[x].address == msg.pc)
break;
// Seek to display PC in the view
if (x == count) {
reseeking = true;
this.seek(msg.pc, Math.floor(visible / 3));
}
}
// Not seeking to PC
if (!reseeking) {
// Configure instance fields
this.address = dasm[0].address;
// Configure elements
for (let x = 0; x < lines; x++)
this.rows[x].update(dasm[x], this.columns, msg.pc);
for (let x = 0; x < lines; x++)
this.rows[x].setWidths(this.columns);
}
// Check for pending display updates
let address = this.pendingDasm.address === null ?
this.address : this.pendingDasm.address;
let line = this.pendingDasm.line === null ?
0 : this.pendingDasm.line ;
let mode = this.pendingDasm.mode;
this.pendingDasm.mode = null;
switch (mode) {
case "first":
case null :
return;
case "refresh":
case "scroll" :
case "seek" :
this.refreshDasm(address, line);
}
}
// Modified a register value
setRegister(msg) {
(msg.type == "program" ? this.proRegs : this.sysRegs)
.registers[msg.id].setValue(msg.value);
this.refreshDasm(this.address, 0);
}
///////////////////////////// Private Methods /////////////////////////////
// Insert a register control to a register list
addRegister(system, id, name) {
let list = system ? this.sysRegs : this.proRegs;
let reg = new CPUWindow.Register(this.debug, list, system, id, name);
list.registers[id] = reg;
list.add(reg);
if (reg.expands)
list.add(reg.expansion);
}
// The window is being displayed for the first time
firstShow() {
super.firstShow();
this.center();
this.mainSplit.measure();
this.regsSplit.measure();
this.seek(this.address, Math.floor(this.lines(true) / 3));
}
// Determine the height in pixels of one row of output
lineHeight() {
return Math.max(10, this.rows[0].address.getBounds().height);
}
// Determine the number of rows of output
lines(fullyVisible) {
let gridHeight = this.dasm.getBounds().height;
let lineHeight = this.lineHeight();
let ret = gridHeight / lineHeight;
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
return Math.max(1, ret);
}
// Key down event handler
onkeydasm(e) {
// Control is pressed
if (e.ctrlKey) switch (e.key) {
// Auto-fit
case "f": case "F":
for (let x = 0; x < this.columns.length; x++)
this.columns[x] = 0;
for (let row of this.rows)
row.setWidths(this.columns);
for (let row of this.rows)
row.measure(this.columns);
for (let row of this.rows)
row.setWidths(this.columns);
break;
// Goto
case "g": case "G":
let addr = prompt(this.application.translate("{app.goto_}"));
if (addr === null)
break;
this.seek(
(parseInt(addr, 16) & 0xFFFFFFFE) >>> 0,
Math.floor(this.lines(true) / 3)
);
break;
default: return;
}
// Processing by key
else switch (e.key) {
case "ArrowDown": this.scroll( 1); break;
case "ArrowUp" : this.scroll(-1); break;
case "PageDown" : this.scroll( this.lines(true)); break;
case "PageUp" : this.scroll(-this.lines(true)); break;
default: return;
}
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Key down event handler
onkeydown(e) {
// Processing by key
switch (e.key) {
// Run next
case "F10":
this.debug.core.postMessage({
command : "RunNext",
dbgwnd : "CPU",
sim : this.debug.sim,
seekToPC: true
});
break;
// Single step
case "F11":
this.debug.core.postMessage({
command : "SingleStep",
dbgwnd : "CPU",
sim : this.debug.sim,
seekToPC: true
});
break;
default: return super.onkeydown(e);
}
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Resize event handler
onresize(bounds) {
// Update Splitters
this.mainSplit.measure();
this.regsSplit.measure();
// Configure disassembler elements
let lines = this.lines(false);
for (let y = this.rows.length; y < lines; y++)
this.rows[y] = this.dasm.add(new CPUWindow.Row(this.dasm));
for (let y = lines; y < this.rows.length; y++)
this.dasm.remove(this.rows[y]);
if (this.rows.length > lines)
this.rows.splice(lines, this.rows.length - lines);
this.refreshDasm();
}
// Mouse wheel event handler
onwheel(e) {
let sign = Math.sign(e.deltaY);
let mag = Math.abs (e.deltaY);
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL)
mag = Math.max(1, Math.floor(mag / this.lineHeight()));
// Configure element
e.preventDefault();
e.stopPropagation();
// Configure display
this.scroll(sign * mag);
}
// Update the disassembler with current emulation data
refreshDasm(address, line, seekToPC) {
// Do nothing while closed or already waiting to refresh
if (!this.isVisible() || this.pendingDasm.mode != null)
return;
// Working variables
address = address !== undefined ?
(address & 0xFFFFFFFE) >>> 0 : this.address;
line = line || 0;
let lines = this.lines(false);
let start = -10 - Math.max(0, line);
let end = lines - Math.min(0, line);
// Configure pending state
this.pendingDasm.mode = "first";
this.pendingDasm.address = null;
this.pendingDasm.line = null;
// Request bus data from the WebAssembly core
this.debug.core.postMessage({
command : "ReadBuffer",
sim : this.debug.sim,
dbgwnd : "CPU",
address : (address + start * 4 & 0xFFFFFFFE) >>> 0,
line : line,
lines : lines,
target : address,
seekToPC: seekToPC,
size : (end - start + 1) * 4
});
}
// Update the register list with current emulation data
refreshRegs() {
// Schedule another refresh
if (this.pendingRegs.mode != null)
this.pendingRegs.mode = "refresh";
// Do nothing while closed or already waiting to refresh
if (!this.isVisible() || this.pendingRegs.mode != null)
return;
// Configure pending state
this.pendingRegs.mode = "first";
this.pendingRegs.address = null;
this.pendingRegs.line = null;
// Request bus data from the WebAssembly core
this.debug.core.postMessage({
command: "GetRegisters",
dbgwnd : "CPU",
sim : this.debug.sim
});
}
// Move to a new address relative to the current address
scroll(lines) {
switch (this.pendingDasm.mode) {
case "first" :
case "refresh":
this.pendingDasm.mode = "scroll";
this.pendingDasm.line = -lines;
break;
case "seek" :
case "scroll":
this.pendingDasm.mode = "scroll";
this.pendingDasm.line -= lines;
break;
case null:
this.refreshDasm(this.address, -lines);
}
}
// Move to a new address positioned at a particular row of output
seek(address, line) {
switch (this.pendingDasm.mode) {
case "first" :
case "refresh":
this.pendingDasm.mode = "seek";
this.pendingDasm.address = address;
this.pendingDasm.line = line;
break;
case "seek" :
case "scroll":
this.pendingDasm.mode = "seek";
this.pendingDasm.address = address;
this.pendingDasm.line += line;
break;
case null:
this.refreshDasm(address, line);
}
}
}).initializer();
// One row of disassembly
CPUWindow.Row = class Row extends Toolkit.Panel {
// Object constructor
constructor(parent) {
super(parent.application, {
layout : "grid",
columns : "repeat(4, max-content)",
hollow : false,
overflowX: "visible",
overflowY: "visible"
});
// Configure element
this.element.style.justifyContent = "start";
this.element.setAttribute("name", "row");
// Address column
this.address = this.add(parent.newLabel({ text: "\u00a0" }));
this.address.element.setAttribute("name", "address");
// Bytes column
this.bytes = this.add(parent.newLabel({ text: "\u00a0" }));
this.bytes.element.setAttribute("name", "bytes");
// Mnemonic column
this.mnemonic = this.add(parent.newLabel({ text: "\u00a0" }));
this.mnemonic.element.setAttribute("name", "mnemonic");
// Operands column
this.operands = this.add(parent.newLabel({ text: "\u00a0" }));
this.operands.element.setAttribute("name", "operands");
}
///////////////////////////// Package Methods /////////////////////////////
// Measure the content widths of each column of output
measure(columns) {
columns[0] = Math.max(columns[0], this.address .getBounds().width);
columns[1] = Math.max(columns[1], this.bytes .getBounds().width);
columns[2] = Math.max(columns[2], this.mnemonic.getBounds().width);
columns[3] = Math.max(columns[3], this.operands.getBounds().width);
}
// Specify the column widths
setWidths(columns) {
this.address .element.style.minWidth = columns[0] + "px";
this.bytes .element.style.minWidth = columns[1] + "px";
this.mnemonic.element.style.minWidth = columns[2] + "px";
this.operands.element.style.minWidth = columns[3] + "px";
}
// Update the output labels with emulation state content
update(line, columns, pc) {
if (pc == line.address)
this.element.setAttribute("pc", "");
else this.element.removeAttribute("pc");
this.address.setText(
("0000000" + line.address.toString(16).toUpperCase()).slice(-8));
let bytes = new Array(line.bytes.length);
for (let x = 0; x < bytes.length; x++)
bytes[x] =
("0" + line.bytes[x].toString(16).toUpperCase()).slice(-2);
this.bytes.setText(bytes.join(" "));
this.mnemonic.setText(line.mnemonic);
this.operands.setText(line.operands);
this.measure(columns);
}
};