518 lines
15 KiB
JavaScript
518 lines
15 KiB
JavaScript
import { Util } from /**/"./Util.js";
|
|
|
|
|
|
|
|
// Bus indexes
|
|
const MEMORY = 0;
|
|
|
|
// 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 bus = this.parent[this.parent.bus];
|
|
let address = this.parent.mask(bus.address + this.index * 16);
|
|
let data = bus.data;
|
|
let dataAddress = bus.dataAddress;
|
|
let hexCaps = this.parent.dasm.hexCaps;
|
|
let offset =
|
|
(this.parent.row(address) - this.parent.row(dataAddress)) * 16;
|
|
|
|
// 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 = "--";
|
|
lbl.classList.remove("tk-selected");
|
|
}
|
|
}
|
|
|
|
// 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) == bus.selection) {
|
|
lbl.classList.add("tk-selected");
|
|
if (this.parent.digit !== null)
|
|
text = this.parent.digit.toString(16);
|
|
}
|
|
|
|
// The byte is not the current selection
|
|
else lbl.classList.remove("tk-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.Component {
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(debug) {
|
|
super(debug.app, {
|
|
className : "tk tk-memory",
|
|
tagName : "div",
|
|
style : {
|
|
alignItems : "stretch",
|
|
display : "grid",
|
|
gridTemplateRows: "auto",
|
|
position : "relative"
|
|
}
|
|
});
|
|
|
|
// Configure instance fields
|
|
this.app = debug.app;
|
|
this.bus = MEMORY;
|
|
this.dasm = debug.disassembler;
|
|
this.debug = debug;
|
|
this.digit = null;
|
|
this.isSubscribed = false;
|
|
this.lines = [];
|
|
this.sim = debug.sim;
|
|
|
|
// Initialize bus
|
|
this[MEMORY] = {
|
|
address : 0x05000000,
|
|
data : [],
|
|
dataAddress: 0x05000000,
|
|
selection : 0x05000000
|
|
};
|
|
|
|
// Configure editor pane
|
|
this.editor = new Toolkit.ScrollPane(this.app, {
|
|
className : "tk tk-scrollpane tk-editor",
|
|
horizontal: Toolkit.ScrollPane.AS_NEEDED,
|
|
focusable : true,
|
|
tabStop : true,
|
|
tagName : "div",
|
|
vertical : Toolkit.ScrollPane.NEVER
|
|
});
|
|
this.append(this.editor);
|
|
|
|
// Configure view
|
|
this.view = document.createElement("div");
|
|
this.view.className = "tk tk-view";
|
|
Object.assign(this.view.style, {
|
|
display : "grid",
|
|
gridTemplateColumns: "repeat(17, max-content)"
|
|
});
|
|
this.editor.setView(this.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.editor.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) {
|
|
let bus = this[this.bus];
|
|
|
|
// Begin an edit
|
|
if (this.digit === null) {
|
|
this.digit = digit;
|
|
this.setSelection(bus.selection, true);
|
|
}
|
|
|
|
// Complete an edit
|
|
else {
|
|
this.digit = this.digit << 4 | digit;
|
|
this.setSelection(bus.selection + 1);
|
|
}
|
|
|
|
}
|
|
|
|
// Key press
|
|
onKeyDown(e) {
|
|
let bus = this[this.bus];
|
|
let key = e.key;
|
|
|
|
// A hex digit was entered
|
|
if (key.toUpperCase() in DIGITS) {
|
|
this.onDigit(DIGITS[key.toUpperCase()]);
|
|
key = "digit";
|
|
}
|
|
|
|
// Ctrl key is pressed
|
|
if (e.ctrlKey) switch (key) {
|
|
|
|
// Goto
|
|
case "g": case "G":
|
|
this.promptGoto();
|
|
break;
|
|
|
|
default: return;
|
|
}
|
|
|
|
// Ctrl key is not pressed
|
|
else switch (key) {
|
|
|
|
// Arrow key navigation
|
|
case "ArrowDown" : this.setSelection(bus.selection + 16); break;
|
|
case "ArrowLeft" : this.setSelection(bus.selection - 1); break;
|
|
case "ArrowRight": this.setSelection(bus.selection + 1); break;
|
|
case "ArrowUp" : this.setSelection(bus.selection - 16); break;
|
|
|
|
// Commit current edit
|
|
case "Enter":
|
|
case " ":
|
|
if (this.digit !== null)
|
|
this.setSelection(bus.selection);
|
|
break;
|
|
|
|
// Page key navigation
|
|
case "PageDown":
|
|
this.setSelection(bus.selection + this.tall(false) * 16);
|
|
break;
|
|
case "PageUp":
|
|
this.setSelection(bus.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.fetch(this[this.bus].address + offset, true);
|
|
}
|
|
|
|
// Pointer down
|
|
onPointerDown(e) {
|
|
|
|
// Common handling
|
|
this.editor.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(0, Math.ceil(this.metrics.getBounds().height));
|
|
if (lineHeight == 0)
|
|
return;
|
|
let y = Math.floor(
|
|
(e.y - this.view.getBoundingClientRect().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[this.bus].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.editor.horizontal
|
|
.setIncrement(this.metrics.getBounds().width);
|
|
|
|
// Update the display
|
|
if (fetch)
|
|
this.fetch(this[this.bus].address, true);
|
|
else this.refresh();
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// Update with memory state from the core
|
|
refresh(data) {
|
|
let bus = this[this.bus];
|
|
|
|
// Update with data from the core thread
|
|
if (data) {
|
|
bus.data = data.bytes;
|
|
bus.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[this.bus].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[this.bus].address = address = this.mask(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"
|
|
})
|
|
);
|
|
}
|
|
|
|
// Mask an address according to the current bus
|
|
mask(address) {
|
|
return Util.u32(address);
|
|
}
|
|
|
|
// 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((address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16);
|
|
|
|
// Move the selection and refresh the display
|
|
this.setSelection(address);
|
|
}
|
|
|
|
// Determine which row relative to top the selection is on
|
|
row(address) {
|
|
let row = address - this[this.bus].address & 0xFFFFFFF0;
|
|
row = Util.s32(row);
|
|
return row / 16;
|
|
}
|
|
|
|
// Specify which byte is selected
|
|
setSelection(address, noCommit) {
|
|
let bus = this[this.bus];
|
|
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
|
|
bus.selection = address = this.mask(address);
|
|
|
|
// Working variables
|
|
let row = this.row(address);
|
|
|
|
// The new address is above the top line of output
|
|
if (row < 0) {
|
|
this.fetch(bus.address + row * 16 & 0xFFFFFFF0, true);
|
|
return;
|
|
}
|
|
|
|
// The new address is below the bottom line of output
|
|
let tall = this.tall(false);
|
|
if (row >= tall) {
|
|
this.fetch(address - tall * 16 + 16 & 0xFFFFFFF0, true);
|
|
return;
|
|
}
|
|
|
|
// Update the display
|
|
if (fetch)
|
|
this.fetch(bus.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.editor.getBounds().height / lineHeight));
|
|
}
|
|
|
|
// Write a value to the core thread
|
|
write(value) {
|
|
let bus = this[this.bus];
|
|
let offset = (this.row(bus.selection) + 16) * 16;
|
|
if (offset < bus.data.length)
|
|
bus.data[offset | bus.selection & 15] = value;
|
|
this.sim.write(
|
|
bus.selection,
|
|
Uint8Array.from([ value ]), {
|
|
refresh: true
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export { Memory };
|