559 lines
17 KiB
JavaScript
559 lines
17 KiB
JavaScript
|
import { Toolkit } from /**/"../toolkit/Toolkit.js";
|
||
|
let register = Debugger => Debugger.Memory =
|
||
|
|
||
|
// Debugger memory window
|
||
|
class Memory extends Toolkit.Window {
|
||
|
|
||
|
//////////////////////////////// Constants ////////////////////////////////
|
||
|
|
||
|
// Bus indexes
|
||
|
static MEMORY = 0;
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////// Initialization Methods //////////////////////////
|
||
|
|
||
|
constructor(debug, index) {
|
||
|
super(debug.app, {
|
||
|
class: "tk window memory"
|
||
|
});
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.data = null,
|
||
|
this.dataAddress = null,
|
||
|
this.debug = debug;
|
||
|
this.delta = 0;
|
||
|
this.editDigit = null;
|
||
|
this.height = 300;
|
||
|
this.index = index;
|
||
|
this.lines = [];
|
||
|
this.pending = false;
|
||
|
this.shown = false;
|
||
|
this.subscription = [ 0, index, "memory", "refresh" ];
|
||
|
this.width = 400;
|
||
|
|
||
|
// Available buses
|
||
|
this.buses = [
|
||
|
{
|
||
|
index : Memory.MEMORY,
|
||
|
editAddress: 0x05000000,
|
||
|
viewAddress: 0x05000000
|
||
|
}
|
||
|
];
|
||
|
this.bus = this.buses[Memory.MEMORY];
|
||
|
|
||
|
// Window
|
||
|
this.setTitle("{debug.memory._}", true);
|
||
|
this.substitute("#", " " + (index + 1));
|
||
|
if (index == 1)
|
||
|
this.element.classList.add("two");
|
||
|
this.addEventListener("close" , e=>this.visible = false);
|
||
|
this.addEventListener("visibility", e=>this.onVisibility(e));
|
||
|
|
||
|
// Client area
|
||
|
Object.assign(this.client.style, {
|
||
|
display : "grid",
|
||
|
gridTemplateRows: "max-content auto"
|
||
|
});
|
||
|
|
||
|
// Bus drop-down
|
||
|
this.drpBus = new Toolkit.DropDown(debug.app);
|
||
|
this.drpBus.setLabel("{debug.memory.bus}", true);
|
||
|
this.drpBus.setTitle("{debug.memory.bus}", true);
|
||
|
this.drpBus.add("{debug.memory.busMemory}", true,
|
||
|
this.buses[Memory.MEMORY]);
|
||
|
this.drpBus.addEventListener("input", e=>this.busInput());
|
||
|
this.add(this.drpBus);
|
||
|
|
||
|
// Hex editor
|
||
|
this.hexEditor = new Toolkit.Component(debug.app, {
|
||
|
class : "tk mono hex-editor",
|
||
|
role : "application",
|
||
|
tabIndex: "0",
|
||
|
style : {
|
||
|
display : "grid",
|
||
|
gridTemplateColumns: "repeat(17, max-content)",
|
||
|
height : "100%",
|
||
|
minWidth : "100%",
|
||
|
overflow : "hidden",
|
||
|
position : "relative",
|
||
|
width : "max-content"
|
||
|
}
|
||
|
});
|
||
|
this.hexEditor.localize = ()=>{
|
||
|
this.hexEditor.localizeRoleDescription();
|
||
|
this.hexEditor.localizeLabel();
|
||
|
};
|
||
|
this.hexEditor.setLabel("{debug.memory.hexEditor}", true);
|
||
|
this.hexEditor.setRoleDescription("{debug.memory.hexEditor}", true);
|
||
|
this.hexEditor.addEventListener("keydown", e=>this.hexKeyDown(e));
|
||
|
this.hexEditor.addEventListener("resize" , e=>this.hexResize ( ));
|
||
|
this.hexEditor.addEventListener("wheel" , e=>this.hexWheel (e));
|
||
|
this.hexEditor.addEventListener(
|
||
|
"pointerdown", e=>this.hexPointerDown(e));
|
||
|
this.lastFocus = this.hexEditor;
|
||
|
|
||
|
// Label for measuring text dimensions
|
||
|
this.sizer = new Toolkit.Label(debug.app, {
|
||
|
class : "tk label mono",
|
||
|
visible : false,
|
||
|
visibility: true,
|
||
|
style: {
|
||
|
position: "absolute"
|
||
|
}
|
||
|
});
|
||
|
this.sizer.setText("\u00a0", false); //
|
||
|
this.hexEditor.append(this.sizer);
|
||
|
|
||
|
// Hex editor scroll pane
|
||
|
this.scrHex = new Toolkit.ScrollPane(debug.app, {
|
||
|
overflowX: "auto",
|
||
|
overflowY: "hidden",
|
||
|
view : this.hexEditor
|
||
|
});
|
||
|
this.add(this.scrHex);
|
||
|
|
||
|
// Hide the bus drop-down: Virtual Boy only has one bus
|
||
|
this.drpBus.visible = false;
|
||
|
this.client.style.gridTemplateRows = "auto";
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Event Handlers //////////////////////////////
|
||
|
|
||
|
// Bus drop-down selection
|
||
|
busInput() {
|
||
|
|
||
|
// An edit is in progress
|
||
|
if (this.editDigit !== null)
|
||
|
this.commit(false);
|
||
|
|
||
|
// Switch to the new bus
|
||
|
this.bus = this.drpBus.value;
|
||
|
this.fetch();
|
||
|
}
|
||
|
|
||
|
// Hex editor key press
|
||
|
hexKeyDown(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (e.altKey)
|
||
|
return;
|
||
|
|
||
|
// Processing by key, Ctrl pressed
|
||
|
if (e.ctrlKey) switch (e.key) {
|
||
|
case "g": case "G":
|
||
|
Toolkit.handle(e);
|
||
|
this.goto();
|
||
|
return;
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// Processing by key, scroll lock off
|
||
|
if (!e.getModifierState("ScrollLock")) switch (e.key) {
|
||
|
case "ArrowDown":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress + 16);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowLeft":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress - 1);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowRight":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress + 1);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowUp":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress - 16);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "PageDown":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress + this.tall(true)*16);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "PageUp":
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress - this.tall(true)*16);
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Processing by key, scroll lock on
|
||
|
else switch (e.key) {
|
||
|
case "ArrowDown":
|
||
|
this.bus.viewAddress += 16;
|
||
|
this.fetch();
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowLeft":
|
||
|
this.scrHex.scrollLeft -= this.scrHex.hscroll.unitIncrement;
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowRight":
|
||
|
this.scrHex.scrollLeft += this.scrHex.hscroll.unitIncrement;
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "ArrowUp":
|
||
|
this.bus.viewAddress -= 16;
|
||
|
this.fetch();
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "PageDown":
|
||
|
this.bus.viewAddress += this.tall(true) * 16;
|
||
|
this.fetch();
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
case "PageUp":
|
||
|
this.bus.viewAddress -= this.tall(true) * 16;
|
||
|
this.fetch();
|
||
|
Toolkit.handle(e);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Processing by key, editing
|
||
|
switch (e.key) {
|
||
|
|
||
|
case "0": case "1": case "2": case "3": case "4":
|
||
|
case "5": case "6": case "7": case "8": case "9":
|
||
|
case "a": case "A": case "b": case "B": case "c":
|
||
|
case "C": case "d": case "D": case "e": case "E":
|
||
|
case "f": case "F":
|
||
|
let digit = parseInt(e.key, 16);
|
||
|
if (this.editDigit === null) {
|
||
|
this.editDigit = digit;
|
||
|
this.setEditAddress(this.bus.editAddress);
|
||
|
} else {
|
||
|
this.editDigit = this.editDigit << 4 | digit;
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress + 1);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
// Commit the current edit
|
||
|
case "Enter":
|
||
|
if (this.editDigit === null)
|
||
|
break;
|
||
|
this.commit();
|
||
|
this.setEditAddress(this.bus.editAddress + 1);
|
||
|
break;
|
||
|
|
||
|
// Cancel the current edit
|
||
|
case "Escape":
|
||
|
if (this.editDigit === null)
|
||
|
return;
|
||
|
this.editDigit = null;
|
||
|
this.setEditAddress(this.bus.editAddress);
|
||
|
break;
|
||
|
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
Toolkit.handle(e);
|
||
|
}
|
||
|
|
||
|
// Hex editor pointer down
|
||
|
hexPointerDown(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (e.button != 0)
|
||
|
return;
|
||
|
|
||
|
// Working variables
|
||
|
let cols = this.lines[0].lblBytes.map(l=>l.getBoundingClientRect());
|
||
|
let y = Math.max(0, Math.floor((e.clientY-cols[0].y)/cols[0].height));
|
||
|
let x = 15;
|
||
|
|
||
|
// Determine which column is closest to the touch point
|
||
|
if (e.clientX < cols[15].right) {
|
||
|
for (let l = 0; l < 15; l++) {
|
||
|
if (e.clientX > (cols[l].right + cols[l + 1].x) / 2)
|
||
|
continue;
|
||
|
x = l;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Update the selection address
|
||
|
let address = this.bus.viewAddress + y * 16 + x >>> 0;
|
||
|
if (this.editDigit !== null && address != this.bus.editAddress)
|
||
|
this.commit();
|
||
|
this.setEditAddress(address);
|
||
|
}
|
||
|
|
||
|
// Hex editor resized
|
||
|
hexResize() {
|
||
|
let tall = this.tall(false);
|
||
|
let grew = this.lines.length < tall;
|
||
|
|
||
|
// Process all visible lines
|
||
|
for (let y = this.lines.length; y < tall; y++) {
|
||
|
let line = {
|
||
|
lblAddress: document.createElement("div"),
|
||
|
lblBytes : []
|
||
|
};
|
||
|
|
||
|
// Address label
|
||
|
line.lblAddress.className = "addr" + (y == 0 ? " first" : "");
|
||
|
this.hexEditor.append(line.lblAddress);
|
||
|
|
||
|
// Byte labels
|
||
|
for (let x = 0; x < 16; x++) {
|
||
|
let lbl = line.lblBytes[x] = document.createElement("div");
|
||
|
lbl.className = "byte b" + x + (y == 0 ? " first" : "");
|
||
|
this.hexEditor.append(lbl);
|
||
|
}
|
||
|
|
||
|
this.lines.push(line);
|
||
|
}
|
||
|
|
||
|
// Remove lines that are no longer visible
|
||
|
while (tall < this.lines.length) {
|
||
|
let line = this.lines[tall];
|
||
|
line.lblAddress.remove();
|
||
|
for (let lbl of line.lblBytes)
|
||
|
lbl.remove();
|
||
|
this.lines.splice(tall, 1);
|
||
|
}
|
||
|
|
||
|
// Configure scroll bar
|
||
|
this.scrHex.hscroll.unitIncrement =
|
||
|
this.sizer.element.getBoundingClientRect().height;
|
||
|
|
||
|
// Update components
|
||
|
if (grew)
|
||
|
this.fetch();
|
||
|
else this.refresh();
|
||
|
}
|
||
|
|
||
|
// Hex editor mouse wheel
|
||
|
hexWheel(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||
|
return;
|
||
|
|
||
|
// Always handle the event
|
||
|
Toolkit.handle(e);
|
||
|
|
||
|
// Determine how many full lines were scrolled
|
||
|
let scr = Debugger.linesScrolled(e,
|
||
|
this.sizer.element.getBoundingClientRect().height,
|
||
|
this.tall(true),
|
||
|
this.delta
|
||
|
);
|
||
|
this.delta = scr.delta;
|
||
|
scr.lines = Math.max(-3, Math.min(3, scr.lines));
|
||
|
|
||
|
// No lines were scrolled
|
||
|
if (scr.lines == 0)
|
||
|
return;
|
||
|
|
||
|
// Scroll the view
|
||
|
this.bus.viewAddress = this.bus.viewAddress + scr.lines * 16 >>> 0;
|
||
|
this.fetch();
|
||
|
}
|
||
|
|
||
|
// Window visibility
|
||
|
onVisibility(e) {
|
||
|
this.shown = this.shown || e.visible;
|
||
|
if (!e.visible)
|
||
|
this.debug.core.unsubscribe(this.subscription, false);
|
||
|
else this.fetch();
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Package Methods /////////////////////////////
|
||
|
|
||
|
// Prompt the user to navigate to a new editing address
|
||
|
goto() {
|
||
|
|
||
|
// Retrieve the value from the user
|
||
|
let addr = prompt(this.app.localize("{debug.memory.goto}"));
|
||
|
if (addr === null)
|
||
|
return;
|
||
|
addr = parseInt(addr.trim(), 16);
|
||
|
if (
|
||
|
!Number.isInteger(addr) ||
|
||
|
addr < 0 ||
|
||
|
addr > 4294967295
|
||
|
) return;
|
||
|
|
||
|
// Commit an outstanding edit
|
||
|
if (this.editDigit !== null && this.bus.editAddress != addr)
|
||
|
this.commit();
|
||
|
|
||
|
// Navigate to the given address
|
||
|
this.setEditAddress(addr, 1/3);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Private Methods /////////////////////////////
|
||
|
|
||
|
// Write the edited value to the simulation state
|
||
|
commit(refresh = true) {
|
||
|
|
||
|
// Error checking
|
||
|
if (this.editDigit === null)
|
||
|
return;
|
||
|
|
||
|
// The edited value is in the bus's data buffer
|
||
|
if (this.data != null) {
|
||
|
let offset = this.bus.editAddress - this.dataAddress >>> 0;
|
||
|
if (offset < this.data.length)
|
||
|
this.data[offset] = this.editDigit;
|
||
|
}
|
||
|
|
||
|
// Write one byte to the simulation state
|
||
|
let data = new Uint8Array(1);
|
||
|
data[0] = this.editDigit;
|
||
|
this.editDigit = null;
|
||
|
this.debug.core.write(this.debug.sim, this.bus.editAddress,
|
||
|
data, { refresh: refresh });
|
||
|
}
|
||
|
|
||
|
// Retrieve data from the simulation state
|
||
|
async fetch() {
|
||
|
|
||
|
// Select the parameters for the simulation fetch
|
||
|
let params = {
|
||
|
address: this.bus.viewAddress - 10 * 16,
|
||
|
length : (this.tall(false) + 20) * 16
|
||
|
};
|
||
|
|
||
|
// A communication with the core thread is already underway
|
||
|
if (this.pending) {
|
||
|
this.pending = params;
|
||
|
this.refresh();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Retrieve data from the simulation state
|
||
|
this.pending = params;
|
||
|
for (let data=null, promise=null; this.pending instanceof Object;) {
|
||
|
|
||
|
// Wait for a transaction to complete
|
||
|
if (promise != null) {
|
||
|
this.pending = true;
|
||
|
data = await promise;
|
||
|
promise = null;
|
||
|
}
|
||
|
|
||
|
// Initiate a new transaction
|
||
|
if (this.pending instanceof Object) {
|
||
|
params = this.pending;
|
||
|
let options = {};
|
||
|
if (this.isVisible())
|
||
|
options.subscription = this.subscription;
|
||
|
promise = this.debug.core.read(this.debug.sim,
|
||
|
params.address, params.length, options);
|
||
|
}
|
||
|
|
||
|
// Process the result of a transaction
|
||
|
if (data != null) {
|
||
|
this.refresh(data);
|
||
|
data = null;
|
||
|
}
|
||
|
|
||
|
};
|
||
|
this.pending = false;
|
||
|
}
|
||
|
|
||
|
// Update hex editor
|
||
|
refresh(msg = null) {
|
||
|
|
||
|
// Receiving data from the simulation state
|
||
|
if (msg != null) {
|
||
|
this.data = msg.data;
|
||
|
this.dataAddress = msg.address;
|
||
|
}
|
||
|
|
||
|
// Process all lines
|
||
|
for (let y = 0; y < this.lines.length; y++) {
|
||
|
let address = this.bus.viewAddress + y * 16 >>> 0;
|
||
|
let line = this.lines[y];
|
||
|
|
||
|
// Address label
|
||
|
line.lblAddress.innerText = this.debug.hex(address, 8, false);
|
||
|
|
||
|
// Process all bytes
|
||
|
for (let x = 0; x < 16; x++) {
|
||
|
let label = line.lblBytes[x];
|
||
|
let text = "--";
|
||
|
|
||
|
// Currently editing this byte
|
||
|
if (address+x==this.bus.editAddress && this.editDigit!==null) {
|
||
|
text = this.debug.hex(this.editDigit, 1, false);
|
||
|
}
|
||
|
|
||
|
// Bus data exists
|
||
|
else if (this.data != null) {
|
||
|
let offset = address - this.dataAddress + x >>> 0;
|
||
|
|
||
|
// The byte is contained in the bus data buffer
|
||
|
if (offset >= 0 && offset < this.data.length)
|
||
|
text = this.debug.hex(this.data[offset], 2, false);
|
||
|
}
|
||
|
|
||
|
label.innerText = text;
|
||
|
label.classList[address + x == this.bus.editAddress ?
|
||
|
"add" : "remove"]("edit");
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Specify the address of the hex editor's selection
|
||
|
setEditAddress(address, auto = false) {
|
||
|
let col = this.lines[0].lblBytes[address&15].getBoundingClientRect();
|
||
|
let port = this.scrHex.viewport.element.getBoundingClientRect();
|
||
|
let row = (address & ~15) >>> 0;
|
||
|
let scr = this.scrHex.scrollLeft;
|
||
|
let tall = this.tall(true, 0);
|
||
|
|
||
|
// Ensure the data row is fully visible
|
||
|
if (row - this.bus.viewAddress >>> 0 >= tall * 16) {
|
||
|
if (!auto) {
|
||
|
this.bus.viewAddress =
|
||
|
this.bus.viewAddress - row >>> 0 <=
|
||
|
row - (this.bus.viewAddress + tall * 16) >>> 0
|
||
|
? row : row - (tall - 1) * 16;
|
||
|
} else this.bus.viewAddress = row - Math.floor(tall * auto) * 16;
|
||
|
this.fetch();
|
||
|
}
|
||
|
|
||
|
// Ensure the column is fully visible
|
||
|
this.scrHex.scrollLeft =
|
||
|
Math.min(
|
||
|
Math.max(
|
||
|
scr,
|
||
|
scr + col.right - port.right
|
||
|
),
|
||
|
scr - port.x + col.x
|
||
|
)
|
||
|
;
|
||
|
|
||
|
// Refresh the display;
|
||
|
this.bus.editAddress = address;
|
||
|
this.refresh();
|
||
|
}
|
||
|
|
||
|
// Measure the number of lines visible in the view
|
||
|
tall(fully = null, plus = 1) {
|
||
|
return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"](
|
||
|
this.scrHex.viewport.element.getBoundingClientRect().height /
|
||
|
this.sizer .element.getBoundingClientRect().height
|
||
|
)) + plus;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
export { register };
|