From 85cc0f57542404d59417387cc76c768b94ea7087 Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Mon, 6 Sep 2021 00:09:15 +0000 Subject: [PATCH] Adding disassembler to CPU window --- app/Emulator.js | 33 +-- app/_boot.js | 1 + app/theme/kiosk.css | 8 + app/toolkit/Window.js | 40 ++-- app/windows/CPUWindow.js | 353 ++++++++++++++++++++++++++++---- app/windows/Disassembler.js | 387 ++++++++++++++++++++++++++++++++++++ app/windows/MemoryWindow.js | 190 +++++++++++------- app/windows/Register.js | 2 +- core/cpu.c | 121 +++++++++++ makefile | 2 +- 10 files changed, 997 insertions(+), 140 deletions(-) create mode 100644 app/windows/Disassembler.js create mode 100644 core/cpu.c diff --git a/app/Emulator.js b/app/Emulator.js index af9cd51..831a3be 100644 --- a/app/Emulator.js +++ b/app/Emulator.js @@ -35,22 +35,22 @@ // Retrieve the values of all the CPU registers getRegisters(msg) { - msg.pc = this.core.GetProgramCounter(msg.sim, 0); - msg.pcFrom = this.core.GetProgramCounter(msg.sim, 1); - msg.pcTo = this.core.GetProgramCounter(msg.sim, 2); - msg.adtre = this.core.GetSystemRegister(msg.sim, 25); - msg.chcw = this.core.GetSystemRegister(msg.sim, 24); - msg.ecr = this.core.GetSystemRegister(msg.sim, 4); - msg.eipc = this.core.GetSystemRegister(msg.sim, 0); - msg.eipsw = this.core.GetSystemRegister(msg.sim, 1); - msg.fepc = this.core.GetSystemRegister(msg.sim, 2); - msg.fepsw = this.core.GetSystemRegister(msg.sim, 3); - msg.pir = this.core.GetSystemRegister(msg.sim, 6); - msg.psw = this.core.GetSystemRegister(msg.sim, 5); - msg.tkcw = this.core.GetSystemRegister(msg.sim, 7); - msg.sr29 = this.core.GetSystemRegister(msg.sim, 29); - msg.sr30 = this.core.GetSystemRegister(msg.sim, 30); - msg.sr31 = this.core.GetSystemRegister(msg.sim, 31); + msg.pc = this.core.GetProgramCounter(msg.sim, 0) >>> 0; + msg.pcFrom = this.core.GetProgramCounter(msg.sim, 1) >>> 0; + msg.pcTo = this.core.GetProgramCounter(msg.sim, 2) >>> 0; + msg.adtre = this.core.GetSystemRegister(msg.sim, 25) >>> 0; + msg.chcw = this.core.GetSystemRegister(msg.sim, 24) >>> 0; + msg.ecr = this.core.GetSystemRegister(msg.sim, 4) >>> 0; + msg.eipc = this.core.GetSystemRegister(msg.sim, 0) >>> 0; + msg.eipsw = this.core.GetSystemRegister(msg.sim, 1) >>> 0; + msg.fepc = this.core.GetSystemRegister(msg.sim, 2) >>> 0; + msg.fepsw = this.core.GetSystemRegister(msg.sim, 3) >>> 0; + msg.pir = this.core.GetSystemRegister(msg.sim, 6) >>> 0; + msg.psw = this.core.GetSystemRegister(msg.sim, 5) >>> 0; + msg.tkcw = this.core.GetSystemRegister(msg.sim, 7) >>> 0; + msg.sr29 = this.core.GetSystemRegister(msg.sim, 29) >>> 0; + msg.sr30 = this.core.GetSystemRegister(msg.sim, 30) >>> 0; + msg.sr31 = this.core.GetSystemRegister(msg.sim, 31) >>> 0; msg.program = new Array(32); for (let x = 0; x <= 31; x++) msg.program[x] = this.core.GetProgramRegister(msg.sim, x); @@ -79,6 +79,7 @@ msg.buffer = this.core.memory.buffer.slice( buffer.pointer, buffer.pointer + msg.size); this.free(buffer); + msg.pc = this.core.GetProgramCounter(msg.sim) >>> 0; postMessage(msg, msg.buffer); } diff --git a/app/_boot.js b/app/_boot.js index 5b7983e..5b7c836 100644 --- a/app/_boot.js +++ b/app/_boot.js @@ -316,6 +316,7 @@ let run = async function() { await Bundle.run("app/toolkit/TextBox.js"); await Bundle.run("app/toolkit/Window.js"); await Bundle.run("app/windows/CPUWindow.js"); + await Bundle.run("app/windows/Disassembler.js"); await Bundle.run("app/windows/Register.js"); await Bundle.run("app/windows/MemoryWindow.js"); await App.create(); diff --git a/app/theme/kiosk.css b/app/theme/kiosk.css index 91a63b2..7246211 100644 --- a/app/theme/kiosk.css +++ b/app/theme/kiosk.css @@ -499,3 +499,11 @@ input[type="text"] { [aria-disabled="true"] [name="check"]{ cursor: not-allowed !important; } + +[role="dialog"][window="cpu"] [name="disassembler"] { + font-family: var(--font-hex); +} + +[role="dialog"][window="cpu"] [name="disassembler"] [name="row"] { + column-gap: calc(var(--font-size) * 1.5); +} diff --git a/app/toolkit/Window.js b/app/toolkit/Window.js index 650fd38..998003e 100644 --- a/app/toolkit/Window.js +++ b/app/toolkit/Window.js @@ -5,7 +5,11 @@ Toolkit.Window = class Window extends Toolkit.Panel { // Object constructor constructor(application, options) { - super(application, options); + super(application, options ? { + height : options.height, + visible: true, + width : options.width + } : {}); options = options || {}; // Configure instance fields @@ -15,15 +19,17 @@ Toolkit.Window = class Window extends Toolkit.Panel { this.dragCursor = { x: 0, y: 0 }; this.dragEdge = null; this.dragPointer = null; - this.initialCenter = "center" in options ? !!options.center : false; this.lastFocus = this.element; - this.shown = this.visible; + this.shown = "visible" in options ? !!options.visible : true; + this.visible = this.shown; // Configure element this.setLayout("grid", { columns: "auto" }); this.setRole("dialog"); this.setLocation(0, 0); this.element.style.position = "absolute"; + if (!this.shown) + this.element.style.visibility = "hidden"; this.element.setAttribute("aria-modal", "false"); this.element.setAttribute("focus" , "false"); this.element.setAttribute("tabindex" , "0" ); @@ -90,6 +96,13 @@ Toolkit.Window = class Window extends Toolkit.Panel { this.closeListeners.push(listener); } + // Request focus on the appropriate element + focus() { + if (this.lastFocus != this) + this.lastFocus.focus(); + else this.element.focus(); + } + // Retrieve the window's title text getTitle() { return this.title.getText(); @@ -107,11 +120,15 @@ Toolkit.Window = class Window extends Toolkit.Panel { // Specify whether the component is visible setVisible(visible, focus) { - super.setVisible(visible); + this.visible = visible = !!visible; + if (!visible) { + this.element.style.visibility = "hidden"; + return; + } + this.element.style.removeProperty("visibility"); if (this.client === undefined) return; - if (visible) - this.contain(); + this.contain(); if (focus) this.focus(); if (!this.shown) @@ -126,17 +143,6 @@ Toolkit.Window = class Window extends Toolkit.Panel { - ///////////////////////////// Package Methods ///////////////////////////// - - // Request focus on the appropriate element - focus() { - if (this.lastFocus != this) - this.lastFocus.focus(); - else this.element.focus(); - } - - - ///////////////////////////// Private Methods ///////////////////////////// // Position the window in the center of the desktop diff --git a/app/windows/CPUWindow.js b/app/windows/CPUWindow.js index 58f00a2..fa08ca1 100644 --- a/app/windows/CPUWindow.js +++ b/app/windows/CPUWindow.js @@ -35,21 +35,22 @@ super(debug.gui, options); // Configure instance fields - this.address = 0xFFFFFFF0; - this.debug = debug; + 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(); - // Layout components - // Disassembler on the left this.mainWrap.add(this.dasmWrap); @@ -113,6 +114,11 @@ 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 @@ -137,7 +143,6 @@ this.proRegs.registers = {}; for (let x = 0; x <= 31; x++) this.addRegister(false, x, CPUWindow.PROGRAM[x] || "r" + x); - } // Initialize system registers pane @@ -189,7 +194,6 @@ this.client.setLayout("grid", { columns: "auto" }); - this.client.addResizeListener(b=>this.onresize(b)); // Configure main wrapper this.mainWrap = this.client.add(this.newPanel({ @@ -203,32 +207,19 @@ ///////////////////////////// Public Methods ////////////////////////////// - // The window is being displayed for the first time - firstShow() { - super.firstShow(); - this.center(); - this.mainSplit.measure(); - this.regsSplit.measure(); - this.refresh(); + // Update the display with current emulation data + refresh() { + this.refreshDasm(); + this.refreshRegs(); } - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the display with current emulation data - refresh(clientHeight, lineHeight, registers) { - if (!this.isVisible()) - return; - if (registers) { - this.getRegisters({ registers }); - return; - } - this.debug.core.postMessage({ - command: "GetRegisters", - debug : "CPU", - sim : this.debug.sim - }); + // Specify whether the component is visible + setVisible(visible, focus) { + let prev = this.visible + visible = !!visible; + super.setVisible(visible, focus); + if (visible && !prev) + this.refresh(); } @@ -239,6 +230,7 @@ message(msg) { switch (msg.command) { case "GetRegisters": this.getRegisters(msg); break; + case "ReadBuffer" : this.readBuffer (msg); break; case "SetRegister" : this.setRegister (msg); break; } } @@ -262,6 +254,54 @@ 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); + + // 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); + 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 @@ -284,13 +324,254 @@ list.add(reg.expansion); } - // Resize event handler - onresize(bounds) { - if (!this.isVisible()) - return; - //this.regs.setHeight(bounds.height); + // 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) { + 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(); + } + + // 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) { + + // 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, + debug : "CPU", + address: (address + start * 4 & 0xFFFFFFFE) >>> 0, + line : line, + lines : lines, + target : address, + 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", + debug : "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 ///////////////////////////// + + // 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) { + 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); + + 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); + } + +}; diff --git a/app/windows/Disassembler.js b/app/windows/Disassembler.js new file mode 100644 index 0000000..472aa51 --- /dev/null +++ b/app/windows/Disassembler.js @@ -0,0 +1,387 @@ +"use strict"; + +// Decode and format instruction code +(globalThis.Disassembler = class Disassembler { + + // Static initializer + static initializer() { + + // Bcond conditions + this.BCONDS = [ + "BV" , "BL" , "BZ" , "BNH", "BN", "BR" , "BLT", "BLE", + "BNV", "BNL", "BNZ", "BH" , "BP", "NOP", "BGE", "BGT" + ]; + + // Mapping for bit string instruction IDs + this.BITSTRING = [ + "SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD", + null , null , null , null , + "ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" , + "ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" + ]; + + // Mapping for floating-point/Nintendo instruction IDs + this.FLOATENDO = [ + "CMPF.S" , null , "CVT.WS" , "CVT.SW" , + "ADDF.S" , "SUBF.S" , "MULF.S" , "DIVF.S" , + "XB" , "XH" , "REV" , "TRNC.SW", + "MPYHW" + ]; + + // Opcode definitions + this.OPDEFS = [ + { format: 1, mnemonic: "MOV" }, + { format: 1, mnemonic: "ADD" }, + { format: 1, mnemonic: "SUB" }, + { format: 1, mnemonic: "CMP" }, + { format: 1, mnemonic: "SHL" }, + { format: 1, mnemonic: "SHR" }, + { format: 1, mnemonic: "JMP" }, + { format: 1, mnemonic: "SAR" }, + { format: 1, mnemonic: "MUL" }, + { format: 1, mnemonic: "DIV" }, + { format: 1, mnemonic: "MULU" }, + { format: 1, mnemonic: "DIVU" }, + { format: 1, mnemonic: "OR" }, + { format: 1, mnemonic: "AND" }, + { format: 1, mnemonic: "XOR" }, + { format: 1, mnemonic: "NOT" }, + { format: 2, mnemonic: "MOV" }, + { format: 2, mnemonic: "ADD" }, + { format: 2, mnemonic: "SETF" }, + { format: 2, mnemonic: "CMP" }, + { format: 2, mnemonic: "SHL" }, + { format: 2, mnemonic: "SHR" }, + { format: 2, mnemonic: "CLI" }, + { format: 2, mnemonic: "SAR" }, + { format: 2, mnemonic: "TRAP" }, + { format: 2, mnemonic: "RETI" }, + { format: 2, mnemonic: "HALT" }, + { format: 0, mnemonic: null }, + { format: 2, mnemonic: "LDSR" }, + { format: 2, mnemonic: "STSR" }, + { format: 2, mnemonic: "SEI" }, + { format: 2, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 3, mnemonic: null }, + { format: 5, mnemonic: "MOVEA" }, + { format: 5, mnemonic: "ADDI" }, + { format: 4, mnemonic: "JR" }, + { format: 4, mnemonic: "JAL" }, + { format: 5, mnemonic: "ORI" }, + { format: 5, mnemonic: "ANDI" }, + { format: 5, mnemonic: "XORI" }, + { format: 5, mnemonic: "MOVHI" }, + { format: 6, mnemonic: "LD.B" }, + { format: 6, mnemonic: "LD.H" }, + { format: 0, mnemonic: null }, + { format: 6, mnemonic: "LD.W" }, + { format: 6, mnemonic: "ST.B" }, + { format: 6, mnemonic: "ST.H" }, + { format: 0, mnemonic: null }, + { format: 6, mnemonic: "ST.W" }, + { format: 6, mnemonic: "IN.B" }, + { format: 6, mnemonic: "IN.H" }, + { format: 6, mnemonic: "IN.W" }, + { format: 6, mnemonic: "CAXI" }, + { format: 6, mnemonic: "OUT.B" }, + { format: 6, mnemonic: "OUT.H" }, + { format: 7, mnemonic: null }, + { format: 6, mnemonic: "OUT.W" } + ]; + + // Program register names + this.PROREGNAMES = [ + "r0" , "r1" , "hp" , "sp" , "gp" , "tp" , "r6" , "r7" , + "r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15", + "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", + "r24", "r25", "r26", "r27", "r28", "r29", "r30", "lp" + ]; + + // SETF conditions + this.SETFS = [ + "V" , "L" , "Z" , "NH", "N", "T", "LT", "LE", + "NV", "NL", "NZ", "H" , "P", "F", "GE", "GT" + ]; + + // System register names + this.SYSREGNAMES = [ + "EIPC", "EIPSW", "FEPC", "FEPSW", "ECR", "PSW", "PIR", "TKCW", + "8" , "9" , "10" , "11" , "12" , "13" , "14" , "15" , + "16" , "17" , "18" , "19" , "20" , "21" , "22" , "23" , + "CHCW", "ADTRE", "26" , "27" , "28" , "29" , "30" , "31" + ]; + } + + + + /**************************** Static Methods *****************************/ + + // Disassemble instructions as lines of text + static disassemble(buffer, offset, address, target, pc, line, lines) { + let history, hIndex; + + // Two bytes before PC to ensure PC isn't skipped + let prePC = (pc - 2 & 0xFFFFFFFF) >>> 0; + + // Prepare history buffer + if (line > 0) { + history = new Array(line); + hIndex = 0; + } + + // Locate the instruction containing the target address + for (;;) { + + // Emergency error checking + if (offset >= buffer.length) + throw "Error: Target address not in disassembly buffer"; + + // Determine the size of the current instruction + let size = address == prePC || + this.OPDEFS[buffer[offset + 1] >>> 2].format < 4 ? 2 : 4; + + // The instruction contianis the target address + if ((target - address & 0xFFFFFFFF) >>> 0 < size) + break; + + // Record the current instruction in the history + if (line > 0) { + let item = history[hIndex] = history[hIndex] || {}; + hIndex = hIndex < history.length - 1 ? hIndex + 1 : 0; + item.address = address; + item.offset = offset; + } + + // Advance to the next instruction + offset += size; + address = (address + size & 0xFFFFFFFF) >>> 0; + } + + // The target address is before the first line of output + for (; line < 0; line++) { + let size = address == prePC || + this.OPDEFS[buffer[offset + 1] >>> 2].format < 4 ? 2 : 4; + offset += size; + address = (address + size & 0xFFFFFFFF) >>> 0; + } + + // The target address is after the first line of output + if (line > 0) { + let item = history[hIndex]; + + // Emergency error checking + if (!item) + throw "Error: First output not in disassembly history"; + + // Inherit the address of the first history item + address = item.address; + offset = item.offset; + } + + // Decode the instructions of the output + let ret = new Array(lines); + for (let x = 0; x < lines; x++) { + let inst = ret[x] = this.decode(buffer, offset, address); + let size = address == prePC ? 2 : inst.size; + offset += size; + address = (address + size & 0xFFFFFFFF) >>> 0; + } + + return ret; + } + + + + /**************************** Private Methods ****************************/ + + // Retrieve bits for a 16-bit instruction + static bits(inst) { + return inst.size == 2 ? + inst.bytes[1] << 8 | inst.bytes[0] : ( + inst.bytes[1] << 24 | inst.bytes[0] << 16 | + inst.bytes[3] << 8 | inst.bytes[2] + ) >>> 0 + ; + } + + // Decode one instruction + static decode(buffer, offset, address) { + let opcode = buffer[offset + 1] >>> 2; + let opdef = this.OPDEFS[opcode]; + let size = opdef.format < 4 ? 2 : 4; + + // Emergency error checking + if (offset + size > buffer.length) + throw "Error: Insufficient disassembly data"; + + // Produce output line object + let inst = { + address : address, + bytes : new Uint8Array(buffer.buffer.slice(offset, offset+size)), + mnemonic: opdef.mnemonic, + opcode : opcode, + operands: null, + size : size + }; + + // Processing by instruction format + switch (opdef.format) { + case 1: this.decodeFormat1(inst ); break; + case 2: this.decodeFormat2(inst ); break; + case 3: this.decodeFormat3(inst, address); break; + case 4: this.decodeFormat4(inst, address); break; + case 5: this.decodeFormat5(inst ); break; + case 6: this.decodeFormat6(inst ); break; + case 7: this.decodeFormat7(inst ); break; + } + + // Illegal opcode + if (inst.mnemonic == null) + inst.mnemonic = "---"; + + return inst; + } + + // Format I + static decodeFormat1(inst) { + let bits = this.bits(inst); + let reg1 = this.PROREGNAMES[bits & 31]; + switch (inst.opcode) { + case 0b000110: // JMP + inst.operands = "[" + reg1 + "]"; + break; + case 0b001111: // NOT + inst.operands = reg1; + break; + default: // All others + inst.operands = reg1 + ", " + this.PROREGNAMES[bits >> 5 & 31]; + } + } + + // Format II + static decodeFormat2(inst) { + let bits = this.bits(inst); + let reg2 = this.PROREGNAMES[bits >> 5 & 31]; + let other = bits & 31; + switch (inst.opcode) { + case 0b010010: // SETF + inst.operands = this.SETFS[other & 15] + ", " + reg2; + break; + case 0b011001: // TRAP + inst.operands = other; + break; + case 0b011100: // LDSR + inst.operands = reg2 + ", " + this.SYSREGNAMES[other]; + break; + case 0b011101: // STSR + inst.operands = this.SYSREGNAMES[other] + ", " + reg2; + break; + case 0b011111: // Bit string + inst.mnemonic = this.BITSTRING[other]; + break; + case 0b010110: // CLI + case 0b011001: // RETI + case 0b011010: // HALT + case 0b011110: // SEI + break; + case 0b010000: // MOV + case 0b010001: // ADD + case 0b010011: // CMP + inst.operands = this.signExtend(other, 5) + ", " + reg2; + break; + default: // SHL, SHR, SAR + inst.operands = other + ", " + reg2; + } + } + + // Format III + static decodeFormat3(inst, address) { + let bits = this.bits(inst); + let disp = this.signExtend(bits & 0x1FF, 9); + let cond = bits >> 9 & 15; + inst.mnemonic = this.BCONDS[cond]; + if (cond == 13) + return; // NOP + inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0) + .toString(16).toUpperCase()).slice(-8); + } + + // Format IV + static decodeFormat4(inst, address) { + let bits = this.bits(inst); + let disp = this.signExtend(bits & 0x3FFFFFF, 26); + inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0) + .toString(16).toUpperCase()).slice(-8); + } + + // Format V + static decodeFormat5(inst) { + let bits = this.bits(inst); + let reg2 = this.PROREGNAMES[bits >> 21 & 31]; + let reg1 = this.PROREGNAMES[bits >> 16 & 31]; + let imm = bits & 0xFFFF; + switch (inst.opcode) { + case 0b101001: // ADDI + inst.operands = + this.signExtend(imm, 16) + ", " + reg1 + ", " + reg2; + break; + default: // All others + inst.operands = "0x" + ("000" + imm.toString(16).toUpperCase()) + .slice(-4) + ", " + reg1 + ", " + reg2; + } + } + + // Format VI + static decodeFormat6(inst) { + let bits = this.bits(inst); + let reg2 = this.PROREGNAMES[bits >> 21 & 31]; + let reg1 = this.PROREGNAMES[bits >> 16 & 31]; + let disp = this.signExtend(bits & 0xFFFF, 16); + disp = disp == 0 ? "" : (disp < 0 ? "-" : "") + "0x" + + Math.abs(disp).toString(16).toUpperCase(); + switch (inst.opcode) { + case 0b110000: // LD.B + case 0b110001: // LD.H + case 0b110011: // LD.W + case 0b111000: // IN.B + case 0b111001: // IN.H + case 0b111011: // IN.W + inst.operands = disp + "[" + reg1 + "], " + reg2; + break; + default: // Output and store + inst.operands = reg2 + ", " + disp + "[" + reg1 + "]"; + } + } + + // Format VII + static decodeFormat7(inst) { + let bits = this.bits(inst); + let reg2 = this.PROREGNAMES[bits >> 21 & 31]; + let reg1 = this.PROREGNAMES[bits >> 16 & 31]; + let subop = this.signExtend(bits & 0xFFFF, 16); + inst.mnemonic = this.FLOATENDO[subop]; + if (inst.mnemonic = null) + return; + switch (subop) { + case 0b001000: // XB + case 0b001001: // XH + inst.operands = reg2; + break; + default: // All others + inst.operands = reg1 + ", " + reg2; + } + } + + // Sign extend a value + static signExtend(value, bits) { + return value & 1 << bits - 1 ? value | -1 << bits : value; + } + +}).initializer(); diff --git a/app/windows/MemoryWindow.js b/app/windows/MemoryWindow.js index 865ef86..03ce5ef 100644 --- a/app/windows/MemoryWindow.js +++ b/app/windows/MemoryWindow.js @@ -10,6 +10,7 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { // Configure instance fields this.address = 0x05000000; this.debug = debug; + this.pending = { mode: null }; this.rows = []; // Configure element @@ -20,7 +21,6 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { // Configure client this.client.setLayout("grid"); - this.client.addResizeListener(b=>this.refresh(b.height)); // Wrapping element to hide overflowing scrollbar this.hexWrap = this.client.add(this.newPanel({ @@ -34,12 +34,15 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { focusable: true, layout : "block", hollow : false, + name : "{memory.hexEditor}", overflowX: "auto", overflowY: "hidden" })); this.hex.element.setAttribute("role", "grid"); this.hex.element.setAttribute("name", "hex"); - this.hex.element.addEventListener("wheel", e=>this.onwheel(e)); + this.hex.element.addEventListener("keydown", e=>this.onkeyhex(e)); + this.hex.element.addEventListener("wheel" , e=>this.onwheel (e)); + this.hex.addResizeListener(b=>this.onresize()); // Configure properties this.setProperty("sim", ""); @@ -49,44 +52,42 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { - ///////////////////////////// Package Methods ///////////////////////////// - - // Update display text with localized strings - localize() { - let hex = ""; - if (this.application) - hex = this.application.translate("{memory.hexEditor}"); - this.hex.element.setAttribute("aria-label", hex); - } + ///////////////////////////// Public Methods ////////////////////////////// // Update the display with current emulation data - refresh(gridHeight, lineHeight) { + refresh(address) { - // Do nothing while closed - if (!this.isVisible()) + // Do nothing while closed or already waiting to refresh + if (!this.isVisible() || this.pending.mode !== null) return; // Working variables - gridHeight = gridHeight || this.hex.getBounds().height; - lineHeight = lineHeight || this.lineHeight(); - let rowCount = this.lines(false, gridHeight, lineHeight); - + address = address === undefined ? this.address : address; + let lines = this.lines(false); + + // Configure pending state + this.pending.mode = "first"; + this.pending.address = null; + this.pending.line = null; + // Request bus data from the WebAssembly core this.debug.core.postMessage({ command: "ReadBuffer", - debug : "Memory", - address: this.address, sim : this.debug.sim, - size : rowCount * 16 + debug : "Memory", + address: address, + lines : lines, + size : lines * 16 }); + } - // Configure elements - for (let y = this.rows.length; y < rowCount; y++) - this.rows[y] = this.hex.add(new MemoryWindow.Row(this.hex)); - for (let y = rowCount; y < this.rows.length; y++) - this.hex.remove(this.rows[y]); - if (this.rows.length > rowCount) - this.rows.splice(rowCount, this.rows.length - rowCount); + // Specify whether the component is visible + setVisible(visible, focus) { + let prev = this.visible + visible = !!visible; + super.setVisible(visible, focus); + if (visible && !prev) + this.refresh(); } @@ -102,12 +103,35 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { // Received bytes from the bus readBuffer(msg) { - let bytes = new Uint8Array(msg.buffer); + let buffer = new Uint8Array(msg.buffer); + let lines = Math.min(msg.lines, this.rows.length); + + // Configure instance fields + this.address = msg.address; + + // Update display for ( - let x = 0, address = this.address, offset = 0; - x < this.rows.length && offset < bytes.length; + let x = 0, address = msg.address, offset = 0; + x < lines && offset < buffer.length; x++, address = (address + 16 & 0xFFFFFFF0) >>> 0, offset += 16 - ) this.rows[x].update(address, bytes, offset); + ) this.rows[x].update(address, buffer, offset); + + // Check for pending display updates + let address = this.pending.address === null ? + this.address : this.pending.address; + let line = this.pending.line === null ? + 0 : this.pending.line ; + let mode = this.pending.mode; + this.pending.mode = null; + switch (mode) { + case "first": + case null : + return; + case "refresh": + case "scroll" : + case "seek" : + this.refresh((address + line * 16 & 0xFFFFFFF0) >>> 0); + } } @@ -126,16 +150,16 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { } // Determine the number of rows of output - lines(fullyVisible, gridHeight, lineHeight) { - gridHeight = gridHeight || this.client.getBounds().height; - lineHeight = lineHeight || this.lineHeight(); - let ret = gridHeight / lineHeight; - ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret); + lines(fullyVisible) { + let gridHeight = this.hex.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 - onkeydown(e) { + onkeyhex(e) { // Control is pressed if (e.ctrlKey) switch (e.key) { @@ -147,59 +171,87 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { (parseInt(addr, 16) & 0xFFFFFFF0) >>> 0, Math.floor(this.lines(true) / 3) ); - this.refresh(); break; default: return; } // Processing by key else switch (e.key) { - case "ArrowDown": - this.address = (this.address + 16 & 0xFFFFFFF0) >>> 0; - this.refresh(); - break; - case "ArrowUp": - this.address = (this.address - 16 & 0xFFFFFFF0) >>> 0; - this.refresh(); - break; - case "PageUp": - this.address = (this.address - 16 * this.lines(true) & - 0xFFFFFFF0) >>> 0; - this.refresh(); - break; - case "PageDown": - this.address = (this.address + 16 * this.lines(true) & - 0xFFFFFFF0) >>> 0; - this.refresh(); - break; - default: return super.onkeydown(e); + 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(); } + + // Resize event handler + onresize() { + let lines = this.lines(false); + for (let y = this.rows.length; y < lines; y++) + this.rows[y] = this.hex.add(new MemoryWindow.Row(this.hex)); + for (let y = lines; y < this.rows.length; y++) + this.hex.remove(this.rows[y]); + if (this.rows.length > lines) + this.rows.splice(lines, this.rows.length - lines); + this.refresh(); + } // Mouse wheel event handler onwheel(e) { - let lineHeight = this.lineHeight(); - let sign = Math.sign(e.deltaY); - let mag = Math.abs (e.deltaY); + 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 / lineHeight)); + mag = Math.max(1, Math.floor(mag / this.lineHeight())); // Configure element e.preventDefault(); e.stopPropagation(); - // Specify the new address - this.address = (this.address + sign * mag * 16 & 0xFFFFFFF0) >>> 0; - this.refresh(null, lineHeight); + // Configure display + this.scroll(sign * mag); } - + + // Move to a new address relative to the current address + scroll(lines) { + switch (this.pending.mode) { + case "first" : + case "refresh": + this.pending.mode = "scroll"; + this.pending.line = lines; + break; + case "scroll": + case "seek" : + this.pending.mode = "scroll"; + this.pending.line += lines; + break; + case null: + this.refresh((this.address + lines * 16 & 0xFFFFFFF0) >>> 0); + } + } + // Move to a new address positioned at a particular row of output - seek(address, index) { - this.address = (address - (index || 0) * 16 & 0xFFFFFFF0) >>> 0; + seek(address, line) { + switch (this.pending.mode) { + case "first" : + case "refresh": + this.pending.mode = "seek"; + this.pending.address = address; + this.pending.line = line; + break; + case "scroll": + case "seek" : + this.pending.mode = "seek"; + this.pending.address = address; + this.pending.line += line; + break; + case null: + this.refresh((address - line * 16 & 0xFFFFFFF0) >>> 0); + } } }; diff --git a/app/windows/Register.js b/app/windows/Register.js index 567f528..e4506e2 100644 --- a/app/windows/Register.js +++ b/app/windows/Register.js @@ -61,7 +61,7 @@ this.setExpanded(this.chkExpand.isChecked())); // Value text box - this.txtValue = this.add(this.newTextBox({ text: "00000000" })); + this.txtValue = this.add(this.newTextBox({ text: "\u00a0" })); this.txtValue.element.setAttribute("name", "value"); this.txtValue.addCommitListener(e=>this.onvalue()); diff --git a/core/cpu.c b/core/cpu.c new file mode 100644 index 0000000..8a37bc0 --- /dev/null +++ b/core/cpu.c @@ -0,0 +1,121 @@ +/* This file is included into vb.c and cannot be compiled on its own. */ +#ifdef VBAPI + + + +/********************************* Constants *********************************/ + +/* Instruction IDs */ +#define CPU_ILLEGAL -1 +#define CPU_ADD_IMM 0 +#define CPU_ADD_REG 1 +#define CPU_ADDF_S 2 +#define CPU_ADDI 3 +#define CPU_AND 4 +#define CPU_ANDBSU 5 +#define CPU_ANDI 6 +#define CPU_ANDNBSU 7 +#define CPU_BCOND 8 +#define CPU_CAXI 9 +#define CPU_CLI 10 +#define CPU_CMP_IMM 11 +#define CPU_CMP_REG 12 +#define CPU_CMPF_S 13 +#define CPU_CVT_SW 14 +#define CPU_CVT_WS 15 +#define CPU_DIV 16 +#define CPU_DIVF_S 17 +#define CPU_DIVU 18 +#define CPU_HALT 19 +#define CPU_IN_B 20 +#define CPU_IN_H 21 +#define CPU_IN_W 22 +#define CPU_JAL 23 +#define CPU_JMP 24 +#define CPU_JR 25 +#define CPU_LD_B 26 +#define CPU_LD_H 27 +#define CPU_LD_W 28 +#define CPU_LDSR 29 +#define CPU_MOV_IMM 30 +#define CPU_MOV_REG 31 +#define CPU_MOVBSU 32 +#define CPU_MOVEA 33 +#define CPU_MOVHI 34 +#define CPU_MPYHW 35 +#define CPU_MUL 36 +#define CPU_MULF_S 37 +#define CPU_MULU 38 +#define CPU_NOT 39 +#define CPU_NOTBSU 40 +#define CPU_OR 41 +#define CPU_ORBSU 42 +#define CPU_ORI 43 +#define CPU_ORNBSU 44 +#define CPU_OUT_B 45 +#define CPU_OUT_H 46 +#define CPU_OUT_W 47 +#define CPU_RETI 48 +#define CPU_REV 49 +#define CPU_SAR_IMM 50 +#define CPU_SAR_REG 51 +#define CPU_SCH0BSD 52 +#define CPU_SCH0BSU 53 +#define CPU_SCH1BSD 54 +#define CPU_SCH1BSU 55 +#define CPU_SEI 56 +#define CPU_SETF 57 +#define CPU_SHL_IMM 58 +#define CPU_SHL_REG 59 +#define CPU_SHR_IMM 60 +#define CPU_SHR_REG 61 +#define CPU_ST_B 62 +#define CPU_ST_H 63 +#define CPU_ST_W 64 +#define CPU_STSR 65 +#define CPU_SUB 66 +#define CPU_SUBF_S 67 +#define CPU_TRAP 68 +#define CPU_TRNC_SW 69 +#define CPU_XB 70 +#define CPU_XH 71 +#define CPU_XOR 72 +#define CPU_XORBSU 73 +#define CPU_XORI 74 +#define CPU_XORNBSU 75 +#define CPU_BITSTRING 76 +#define CPU_FLOATENDO 77 + +/* Mapping for bit string sub-opcodes */ +static const int8_t CPU_BITSTRINGS[] = { + CPU_SCH0BSU, CPU_SCH0BSD, CPU_SCH1BSU, CPU_SCH1BSD, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ORBSU , CPU_ANDBSU , CPU_XORBSU , CPU_MOVBSU , + CPU_ORNBSU , CPU_ANDNBSU, CPU_XORNBSU, CPU_NOTBSU , + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL +}; + +/* Mapping for floating-point/Nintendo sub-opcodes */ +static const int8_t CPU_FLOATENDO[] = { + CPU_CMPF_S , CPU_ILLEGAL, CPU_CVT_WS , CPU_CVT_SW , + CPU_ADDF_S , CPU_SUBF_S , CPU_MULF_S , CPU_DIVF_S , + CPU_XB , CPU_XH , CPU_REV , CPU_TRNC_SW, + CPU_MPYHW , CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, + CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL, CPU_ILLEGAL +}; + +#endif /* VBAPI */ diff --git a/makefile b/makefile index 506715a..d832e1a 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ .PHONY: help help: @echo - @echo "Virtual Boy Emulator - September 1, 2021" + @echo "Virtual Boy Emulator - September 5, 2021" @echo @echo "Target build environment is any Debian with the following packages:" @echo " emscripten"