pvbemu/web/debugger/Memory.js

562 lines
18 KiB
JavaScript

import { Toolkit } from /**/"../toolkit/Toolkit.js";
let register = Debugger => Debugger.Memory =
// Debugger memory window
class Memory extends Toolkit.Window {
///////////////////////// 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 = [
{
editAddress: 0x05000000,
viewAddress: 0x05000000
}
];
this.bus = this.buses[0];
// 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[0]);
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("focusout", e=>this.commit ( ));
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.setViewAddress(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.setViewAddress(this.bus.viewAddress - 16);
this.fetch();
Toolkit.handle(e);
return;
case "PageDown":
this.setViewAddress(this.bus.viewAddress + this.tall(true)*16);
this.fetch();
Toolkit.handle(e);
return;
case "PageUp":
this.setViewAddress(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.toAddress(this.bus.viewAddress + y * 16 + x);
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 components
let lineHeight = this.sizer.element.getBoundingClientRect().height;
this.scrHex.hscroll.unitIncrement = lineHeight;
this.hexEditor.element.style.gridAutoRows = lineHeight + "px";
// 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.setViewAddress(this.bus.viewAddress + scr.lines * 16);
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.toAddress(this.bus.editAddress-this.dataAddress);
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.toAddress(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.toAddress(this.bus.viewAddress + y * 16);
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 = this.toAddress(address-this.dataAddress+x);
// 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 = this.toAddress(address & ~15);
let scr = this.scrHex.scrollLeft;
let tall = this.tall(true, 0);
// Ensure the data row is fully visible
if (this.toAddress(row - this.bus.viewAddress) >= tall * 16) {
if (!auto) {
this.setViewAddress(
this.toAddress(this.bus.viewAddress - row) <=
this.toAddress(row - (this.bus.viewAddress + tall * 16))
? row : this.toAddress(row - (tall - 1) * 16));
} else this.setViewAddress(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 = this.toAddress(address);
this.refresh();
}
// Specify the address of the hex editor's view
setViewAddress(address) {
this.bus.viewAddress = this.toAddress(address);
}
// 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;
}
// Ensure an address is in the proper range
toAddress(address) {
return address >>> 0;
}
}
export { register };