pvbemu/app/app/Memory.js

468 lines
13 KiB
JavaScript

import { Util } from /**/"./Util.js";
// Text to hex digit conversion
const DIGITS = {
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
"8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
};
///////////////////////////////////////////////////////////////////////////////
// Line //
///////////////////////////////////////////////////////////////////////////////
// One line of output
class Line {
///////////////////////// Initialization Methods //////////////////////////
constructor(parent, index) {
// Configure instance fields
this.index = index;
this.parent = parent;
// Address label
this.lblAddress = document.createElement("div");
this.lblAddress.className = "tk tk-address";
parent.view.appendChild(this.lblAddress);
// Byte labels
this.lblBytes = new Array(16);
for (let x = 0; x < 16; x++) {
let lbl = this.lblBytes[x] = document.createElement("div");
lbl.className = "tk tk-byte tk-" + x.toString(16);
parent.view.appendChild(lbl);
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the elements' display
refresh() {
let address = Util.u32(this.parent.address + this.index * 16);
let data = this.parent.data;
let dataAddress = this.parent.dataAddress;
let hexCaps = this.parent.dasm.hexCaps;
let offset = Util.s32(address - dataAddress);
// Format the line's address
let text = address.toString(16).padStart(8, "0");
if (hexCaps)
text = text.toUpperCase();
this.lblAddress.innerText = text;
// The line's data is not available
if (offset < 0 || offset >= data.length)
for (let lbl of this.lblBytes)
lbl.innerText = "--";
// The line's data is available
else for (let x = 0; x < 16; x++, offset++) {
let lbl = this.lblBytes[x];
text = data[offset].toString(16).padStart(2, "0");
// The byte is the current selection
if (Util.u32(address + x) == this.parent.selection) {
lbl.classList.add("selected");
if (this.parent.digit !== null)
text = this.parent.digit.toString(16);
}
// The byte is not the current selection
else lbl.classList.remove("selected");
// Update the label's text
if (hexCaps)
text = text.toUpperCase();
lbl.innerText = text;
}
}
// Specify whether the elements on this line are visible
setVisible(visible) {
visible = visible ? "block" : "none";
this.lblAddress.style.display = visible;
for (let lbl of this.lblBytes)
lbl.style.display = visible;
}
}
///////////////////////////////////////////////////////////////////////////////
// Memory //
///////////////////////////////////////////////////////////////////////////////
// Memory hex editor
class Memory extends Toolkit.ScrollPane {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug) {
super(debug.app, {
className : "tk tk-scrollpane tk-memory",
horizontal: Toolkit.ScrollPane.AS_NEEDED,
focusable : true,
tabStop : true,
tagName : "div",
vertical : Toolkit.ScrollPane.NEVER
});
// Configure instance fields
this.address = 0x05000000;
this.app = debug.app;
this.dasm = debug.disassembler;
this.data = [];
this.dataAddress = this.address;
this.debug = debug;
this.digit = null;
this.isSubscribed = false;
this.lines = [];
this.selection = this.address;
this.sim = debug.sim;
// Configure view
let view = document.createElement("div");
view.className = "tk tk-view";
Object.assign(view.style, {
display : "grid",
gridTemplateColumns: "repeat(17, max-content)"
});
this.setView(view);
// Font-measuring element
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-metrics tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = "X";
this.append(this.metrics.element);
// Configure event handlers
Toolkit.addResizeListener(this.viewport, e=>this.onResize(e));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("wheel" , e=>this.onMouseWheel (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Typed a digit
onDigit(digit) {
// Begin an edit
if (this.digit === null) {
this.digit = digit;
this.setSelection(this.selection, true);
}
// Complete an edit
else {
this.digit = this.digit << 4 | digit;
this.setSelection(this.selection + 1);
}
}
// Key press
onKeyDown(e) {
let key = e.key;
// A hex digit was entered
if (key.toUpperCase() in DIGITS) {
this.onDigit(DIGITS[key.toUpperCase()]);
key = "digit";
}
// Processing by key
switch (key) {
// Arrow key navigation
case "ArrowDown" : this.setSelection(this.selection + 16); break;
case "ArrowLeft" : this.setSelection(this.selection - 1); break;
case "ArrowRight": this.setSelection(this.selection + 1); break;
case "ArrowUp" : this.setSelection(this.selection - 16); break;
// Commit current edit
case "Enter":
case " ":
if (this.digit !== null)
this.setSelection(this.selection);
break;
// Goto
case "g": case "G":
if (!e.ctrlKey)
return;
this.promptGoto();
break;
// Page key navigation
case "PageDown":
this.setSelection(this.selection + this.tall(false) * 16);
break;
case "PageUp":
this.setSelection(this.selection - this.tall(false) * 16);
break;
// Hex digit: already processed
case "digit": break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 48;
if (offset == 0)
return;
// Update the display address
this.address = Util.u32(this.address + offset);
this.fetch(this.address, true);
}
// Pointer down
onPointerDown(e) {
// Common handling
this.focus();
e.stopPropagation();
e.preventDefault();
// Not a click action
if (e.button != 0)
return;
// Determine the row that was clicked on
let lineHeight = !this.metrics ? 0 :
Math.max(1, Math.ceil(this.metrics.getBounds().height));
let y = Math.floor((e.y - this.getBounds().top) / lineHeight);
// Determine the column that was clicked on
let columns = this.lines[0].lblBytes;
let bndCur = columns[0].getBoundingClientRect();
if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) {
let bndNext = x == 15 ? null :
columns[x + 1].getBoundingClientRect();
// The current column was clicked: update the selection
if (e.x < (x == 15 ? bndCur.right :
bndCur.right + (bndNext.left - bndCur.right) / 2)) {
this.setSelection(this.address + y * 16 + x);
return;
}
// Advance to the next column
bndCur = bndNext;
}
}
// Viewport resized
onResize(e) {
let fetch = false;
let tall = this.tall(true);
// Add additional lines to the output
for (let x = 0; x < tall; x++) {
if (x >= this.lines.length) {
fetch = true;
this.lines.push(new Line(this, x));
}
this.lines[x].setVisible(true);
}
// Remove extra lines from the output
for (let x = tall; x < this.lines.length; x++)
this.lines[x].setVisible(false);
// Configure horizontal scroll bar
if (this.metrics)
this.horizontal.setIncrement(this.metrics.getBounds().width);
// Update the display
if (fetch)
this.fetch(this.address, true);
else this.refresh();
}
///////////////////////////// Public Methods //////////////////////////////
// Update with memory state from the core
refresh(data) {
// Update with data from the core thread
if (data) {
this.data = data.bytes;
this.dataAddress = data.address;
}
// Update elements
for (let y = 0, tall = this.tall(true); y < tall; y++)
this.lines[y].refresh();
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch(this.address);
// Unsubscribe from core updates
else this.sim.unsubscribe("memory");
}
///////////////////////////// Package Methods /////////////////////////////
// The disassembler configuration has changed
dasmChanged() {
this.refresh();
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve memory data from the core
async fetch(address, prefresh) {
// Configure instance fields
this.address = address;
// Update the view immediately
if (prefresh)
this.refresh();
// Retrieve data from the core
this.refresh(
await this.sim.read(
address - 16 * 16,
(this.tall(true) + 32) * 16, {
subscribe: this.isSubscribed && "memory"
})
);
}
// Prompt the user to specify a new address
promptGoto() {
// Receive input from the user
let address = prompt(this.app.translate("common.gotoPrompt"));
if (address == null)
return;
// Process the input as an address in hexadecimal
address = parseInt(address, 16);
if (isNaN(address))
return;
// The address is not currently visible in the output
let tall = this.tall(false);
if (Util.u32(address - this.address) >= tall * 16) {
this.fetch(Util.u32(
(address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16));
}
// Move the selection and refresh the display
this.setSelection(Util.u32(address));
}
// Specify which byte is selected
setSelection(address, noCommit) {
let fetch = false;
// Commit a pending data entry
if (!noCommit && this.digit !== null) {
this.write(this.digit);
this.digit = null;
fetch = true;
}
// Configure instance fields
this.selection = address = Util.u32(address);
// Working variables
let row = Util.s32(address - this.address & 0xFFFFFFF0) / 16;
// The new address is above the top line of output
if (row < 0) {
this.fetch(Util.u32(this.address + row * 16), true);
return;
}
// The new address is below the bottom line of output
let tall = this.tall(false);
if (row >= tall) {
this.fetch(Util.u32(address - tall * 16 + 16 & 0xFFFFFFF0), true);
return;
}
// Update the display
if (fetch)
this.fetch(this.address, true);
else this.refresh();
}
// Measure how many rows of output are visible
tall(partial) {
let lineHeight = !this.metrics ? 0 :
Math.ceil(this.metrics.getBounds().height);
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
this.viewport.getBoundingClientRect().height / lineHeight));
}
// Write a value to the core thread
write(value) {
this.data[Util.s32(this.selection - this.dataAddress)] = value;
this.sim.write(
this.selection,
Uint8Array.from([ value ]), {
refresh: true
});
}
}
export { Memory };