diff --git a/app/App.js b/app/App.js deleted file mode 100644 index 5f7cd5c..0000000 --- a/app/App.js +++ /dev/null @@ -1,184 +0,0 @@ -"use strict"; - -// Top-level state and UI manager -globalThis.App = class App { - - // Produce a new class instance - static async create() { - let ret = new App(); - await ret.init(); - return ret; - } - - // Perform initial tasks - async init() { - - // Initialization tasks - this.initEnv(); - this.initMenus(); - - // WebAssembly core module - this.core = new Worker(Bundle.get("app/Emulator.js").toDataURL()); - await new Promise((resolve,reject)=>{ - this.core.onmessage = e=>{ - this.core.onmessage = e=>this.onmessage(e.data); - resolve(); - }; - this.core.postMessage({ - command: "Init", - wasm : Bundle.get("core.wasm").data.buffer - }); - }); - - // Desktop pane - this.desktop = this.gui.newPanel({ layout: "desktop" }); - this.desktop.setRole("group"); - this.desktop.element.setAttribute("desktop", ""); - this.gui.add(this.desktop); - - // Debugging windows - this.debuggers = [ - new Debugger(this, 0), - new Debugger(this, 1) - ]; - - } - - // Initialize environment settings - initEnv() { - - // Configure themes - Bundle.get("app/theme/kiosk.css").style(); - this.themes = { - dark : Bundle.get("app/theme/dark.css" ).style(false), - light : Bundle.get("app/theme/light.css" ).style(true ), - virtual: Bundle.get("app/theme/virtual.css").style(false) - }; - this.theme = this.themes["light"]; - - // Produce toolkit instance - this.gui = new Toolkit.Application({ - layout: "grid", - rows : "max-content auto" - }); - document.body.appendChild(this.gui.element); - window.addEventListener("resize", ()=>{ - this.gui.setSize(window.innerWidth+"px", window.innerHeight+"px"); - }); - window.dispatchEvent(new Event("resize")); - - // Configure locales - this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString()); - this.gui.setLocale(navigator.language); - } - - // Initialize main menu bar - initMenus() { - - // Menu bar - this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" }); - this.gui.add(this.mainMenu); - this.gui.addPropagationListener(e=>this.mainMenu.restoreFocus()); - - // File menu - let menu = this.mainMenu.newMenu({ text: "{menu.file._}"}); - let item = menu.newMenuItem({ text: "{menu.file.loadROM}"}); - item.addClickListener(()=>this.loadROM()); - - // Debug menu - menu = this.mainMenu.newMenu({ text: "{menu.debug._}" }); - item = menu.newMenuItem({ text: "{memory._}" }); - item.addClickListener( - ()=>this.debuggers[0].memory.setVisible(true, true)); - item = menu.newMenuItem({ text: "{cpu._}" }); - item.addClickListener( - ()=>this.debuggers[0].cpu.setVisible(true, true)); - - // Theme menu - menu = this.mainMenu.newMenu({ text: "{menu.theme._}" }); - item = menu.newMenuItem({ text: "{menu.theme.light}" }); - item.addClickListener(()=>this.setTheme("light")); - item = menu.newMenuItem({ text: "{menu.theme.dark}" }); - item.addClickListener(()=>this.setTheme("dark")); - item = menu.newMenuItem({ text: "{menu.theme.virtual}" }); - item.addClickListener(()=>this.setTheme("virtual")); - } - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - onmessage(msg) { - if ("dbgwnd" in msg) { - this.debuggers[msg.sim].message(msg); - return; - } - switch (msg.command) { - case "SetROM": this.setROM(msg); break; - } - } - - // ROM buffer has been configured - setROM(msg) { - let dbg = this.debuggers[msg.sim]; - dbg.refresh(true); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Prompt the user to select a ROM file - loadROM() { - let file = document.createElement("input"); - file.type = "file"; - file.addEventListener("input", ()=>this.selectROM(file.files[0])); - file.click(); - } - - // Specify a ROM file - async selectROM(file) { - - // No file is specified (perhaps the user canceled) - if (file == null) - return; - - // Check the file's size - if ( - file.size < 1024 || - file.size > 0x1000000 || - (file.size - 1 & file.size) != 0 - ) { - alert(this.gui.translate("{app.romNotVB}")); - return; - } - - // Load the file data into a byte buffer - let filename = file.name; - try { file = await file.arrayBuffer(); } - catch { - alert(this.gui.translate("{app.readFileError}")); - return; - } - - // Send the ROM to the WebAssembly core module - this.core.postMessage({ - command: "SetROM", - reset : true, - rom : file, - sim : 0 - }, file); - } - - // Specify the current color theme - setTheme(key) { - let theme = this.themes[key]; - if (theme == this.theme) - return; - let old = this.theme; - this.theme = theme; - theme.setEnabled(true); - old.setEnabled(false); - } - -}; diff --git a/app/Bundle.java b/app/Bundle.java index abef5dc..efa3aa9 100644 --- a/app/Bundle.java +++ b/app/Bundle.java @@ -98,12 +98,12 @@ public class Bundle { var manifest = new StringBuilder(); manifest.append("\"use strict\";\nlet manifest=["); for (var file : values) { - manifest.append("{" + - "name:\"" + file.filename + "\"," + - "size:" + file.data.length + - "},"); + manifest.append( + "[\"" + file.filename + "\"," + file.data.length + "]"); + if (file != values[values.length - 1]) + manifest.append(","); } - manifest.append("];\nlet bundleName=\"" + bundleName + "\";\n"); + manifest.append("],bundleName=\"" + bundleName + "\";"); // Prepend the manifest to _boot.js var boot = files.get("app/_boot.js"); diff --git a/app/Debugger.js b/app/Debugger.js deleted file mode 100644 index 2b917b7..0000000 --- a/app/Debugger.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; - -// Debugging UI manager -globalThis.Debugger = class Debugger { - - // Object constructor - constructor(app, sim) { - - // Configure instance fields - this.app = app; - this.core = app.core; - this.gui = app.gui; - this.sim = sim; - - // Memory window - this.memory = new MemoryWindow(this, { - title : "{sim}{memory._}", - height : 300, - visible: false, - width : 400 - }); - this.memory.addCloseListener(e=>this.memory.setVisible(false)); - app.desktop.add(this.memory); - - // CPU window - this.cpu = new CPUWindow(this, { - title : "{sim}{cpu._}", - height : 300, - visible: false, - width : 400 - }); - this.cpu.addCloseListener(e=>this.cpu.setVisible(false)); - app.desktop.add(this.cpu); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Message received from emulation thread - message(msg) { - switch (msg.dbgwnd) { - case "CPU" : this.cpu .message(msg); break; - case "Memory": this.memory.message(msg); break; - } - } - - // Reload all output - refresh(seekToPC) { - this.cpu .refresh(seekToPC); - this.memory.refresh(); - } - -}; diff --git a/app/Emulator.js b/app/Emulator.js deleted file mode 100644 index d1c35fb..0000000 --- a/app/Emulator.js +++ /dev/null @@ -1,161 +0,0 @@ -"use strict"; - -// Worker that manages a WebAssembly instance of the C core library -(globalThis.Emulator = class Emulator { - - // Static initializer - static initializer() { - new Emulator(); - } - - // Object constructor - constructor() { - - // Configure instance fields - this.buffers = {}; - - // Configure message port - onmessage = e=>this.onmessage(e.data); - } - - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - onmessage(msg) { - switch (msg.command) { - case "GetRegisters": this.getRegisters(msg); break; - case "Init" : this.init (msg); break; - case "ReadBuffer" : this.readBuffer (msg); break; - case "RunNext" : this.runNext (msg); break; - case "SetRegister" : this.setRegister (msg); break; - case "SetROM" : this.setROM (msg); break; - case "SingleStep" : this.singleStep (msg); break; - case "Write" : this.write (msg); break; - } - } - - // Retrieve the values of all the CPU registers - getRegisters(msg) { - 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); - postMessage(msg); - } - - // Initialize the WebAssembly core module - async init(msg) { - - // Load and instantiate the WebAssembly module - this.wasm = await WebAssembly.instantiate(msg.wasm, - { env: { emscripten_notify_memory_growth: ()=>this.onmemory() }}); - this.wasm.instance.exports.Init(); - - // Configure instance fields - this.core = this.wasm.instance.exports; - - postMessage({ command: "Init" }); - } - - // Read multiple data units from the bus - readBuffer(msg) { - let buffer = this.malloc(Uint8Array, msg.size); - this.core.ReadBuffer(msg.sim, buffer.pointer, - msg.address, msg.size, msg.debug ? 1 : 0); - 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); - } - - // Attempt to advance to the next instruction - runNext(msg) { - this.core.RunNext(msg.sim); - postMessage(msg); - } - - // Specify a new value for a register - setRegister(msg) { - switch (msg.type) { - case "pc" : msg.value = - this.core.SetProgramCounter (msg.sim, msg.value); - break; - case "program": msg.value = - this.core.SetProgramRegister(msg.sim, msg.id, msg.value); - break; - case "system" : msg.value = - this.core.SetSystemRegister (msg.sim, msg.id, msg.value); - } - postMessage(msg); - } - - // Supply a ROM buffer - setROM(msg) { - let rom = new Uint8Array(msg.rom); - let buffer = this.malloc(Uint8Array, rom.length); - for (let x = 0; x < rom.length; x++) - buffer.data[x] = rom[x]; - msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length); - delete msg.rom; - if (msg.reset) - this.core.Reset(msg.sim); - postMessage(msg); - } - - // Execute the current instruction - singleStep(msg) { - this.core.SingleStep(msg.sim); - postMessage(msg); - } - - // Write a single value into the bus - write(msg) { - this.core.Write(msg.sim, msg.address, msg.type, msg.value, - msg.debug ? 1 : 0); - postMessage(msg); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Delete a previously-allocated memory buffer - free(buffer) { - this.core.Free(buffer.pointer); - delete this.buffers[buffer.pointer]; - } - - // Allocate a typed array in WebAssembly core memory - malloc(type, size) { - let pointer = this.core.Malloc(size); - let data = new type(this.core.memory.buffer, pointer, size); - return this.buffers[pointer] = { data: data, pointer: pointer }; - } - - // WebAssembly memory has grown - onmemory() { - for (let buffer of Object.values(this.buffers)) { - buffer.data = new buffer.data.constructor( - this.core.memory.buffer, buffer.pointer); - } - } - -}).initializer(); diff --git a/app/_boot.js b/app/_boot.js index 0453cf8..5e93351 100644 --- a/app/_boot.js +++ b/app/_boot.js @@ -1,10 +1,8 @@ -// Produce an async function from a source string -if (!globalThis.AsyncFunction) - globalThis.AsyncFunction = - Object.getPrototypeOf(async function(){}).constructor; - -// Read scripts from files on disk to aid with debugging -let debug = location.hash == "#debug"; +/* + The Bundle.java utility prepends a file manifest to this script before + execution is started. This script runs within the context of an async + function. +*/ @@ -12,55 +10,145 @@ let debug = location.hash == "#debug"; // Bundle // /////////////////////////////////////////////////////////////////////////////// -// Resource asset manager -globalThis.Bundle = class BundledFile { +// Bundled file manager +let Bundle = globalThis.Bundle = new class Bundle extends Array { + constructor() { + super(); + // Configure instance fields + this.debug = + location.protocol != "file:" && location.hash == "#debug"; + this.decoder = new TextDecoder(); + this.encoder = new TextEncoder(); + this.moduleCall = (... a)=>this.module (... a); + this.resourceCall = (... a)=>this.resource(... a); - ///////////////////////////// Static Methods ////////////////////////////// + // Generate the CRC32 lookup table + this.crcLookup = new Uint32Array(256); + for (let x = 0; x <= 255; x++) { + let l = x; + for (let j = 7; j >= 0; j--) + l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); + this.crcLookup[x] = l; + } - // Adds a bundled file from loaded file data - static add(name, data) { - return Bundle.files[name] = new Bundle(name, data); - } - - // Retrieve the file given its filename - static get(name) { - return Bundle.files[name]; - } - - // Run a file as a JavaScript source file - static async run(name) { - await Bundle.files[name].run(); - } - - // Resolve a URL for a source file - static source(name) { - return debug ? name : Bundle.files[name].toDataURL(); } - ///////////////////////// Initialization Methods ////////////////////////// + ///////////////////////////// Public Methods ////////////////////////////// + + // Add a file to the bundle + add(name, data) { + this.push(this[name] = new BundledFile(name, data)); + } + + // Export all bundled resources to a .ZIP file + save() { + let centrals = new Array(this.length); + let locals = new Array(this.length); + let offset = 0; + let size = 0; + + // Encode file and directory entries + for (let x = 0; x < this.length; x++) { + let file = this[x]; + let sum = this.crc32(file.data); + locals [x] = file.toZipHeader(sum); + centrals[x] = file.toZipHeader(sum, offset); + offset += locals [x].length; + size += centrals[x].length; + } + + // Encode end of central directory + let end = []; + this.writeInt(end, 4, 0x06054B50); // Signature + this.writeInt(end, 2, 0); // Disk number + this.writeInt(end, 2, 0); // Central dir start disk + this.writeInt(end, 2, this.length); // # central dir this disk + this.writeInt(end, 2, this.length); // # central dir total + this.writeInt(end, 4, size); // Size of central dir + this.writeInt(end, 4, offset); // Offset of central dir + this.writeInt(end, 2, 0); // .ZIP comment length + + // Prompt the user to save the resulting file + let a = document.createElement("a"); + a.download = bundleName + ".zip"; + a.href = URL.createObjectURL(new Blob( + locals.concat(centrals).concat([Uint8Array.from(end)]), + { type: "application/zip" } + )); + a.style.visibility = "hidden"; + document.body.appendChild(a); + a.click(); + a.remove(); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Write a byte array into an output buffer + writeBytes(data, bytes) { + //data.push(... bytes); + for (let b of bytes) + data.push(b); + } + + // Write an integer into an output buffer + writeInt(data, size, value) { + for (; size > 0; size--, value >>= 8) + data.push(value & 0xFF); + } + + // Write a string of text as bytes into an output buffer + writeString(data, text) { + data.push(... this.encoder.encode(text)); + } + + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Calculate the CRC32 checksum for a byte array + crc32(data) { + let c = 0xFFFFFFFF; + for (let x = 0; x < data.length; x++) + c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]); + return ~c & 0xFFFFFFFF; + } + +}(); + + + +/////////////////////////////////////////////////////////////////////////////// +// BundledFile // +/////////////////////////////////////////////////////////////////////////////// + +// Individual bundled file +class BundledFile { - // Object constructor constructor(name, data) { // Configure instance fields this.data = data; this.name = name; - // Detect the MIME type + // Resolve the MIME type this.mime = - name.endsWith(".css" ) ? "text/css;charset=UTF-8" : - name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" : - name.endsWith(".png" ) ? "image/png" : - name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" : - name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".wasm" ) ? "application/wasm" : - name.endsWith(".woff2") ? "font/woff2" : + name.endsWith(".css" ) ? "text/css;charset=UTF-8" : + name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" : + name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" : + name.endsWith(".json" ) ? "application/json;charset=UTF-8" : + name.endsWith(".png" ) ? "image/png" : + name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" : + name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" : + name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" : + name.endsWith(".wasm" ) ? "application/wasm" : + name.endsWith(".woff2") ? "font/woff2" : "application/octet-stream" ; @@ -70,255 +158,151 @@ globalThis.Bundle = class BundledFile { ///////////////////////////// Public Methods ////////////////////////////// - // Execute the file as a JavaScript source file - async run() { - - // Not running in debug mode - if (!debug) { - await new AsyncFunction(this.toString())(); - return; + // Install a font from a bundled resource + async installFont(name) { + if (name === undefined) { + name = "/" + this.name; + name = name.substring(name.lastIndexOf("/") + 1); } - - // Running in debug mode - await new Promise((resolve,reject)=>{ - let script = document.createElement("script"); - document.head.appendChild(script); - script.addEventListener("load", ()=>resolve()); - script.src = this.name; - }); - + let ret = new FontFace(name, "url('"+ + (Bundle.debug ? this.name : this.toDataURL()) + "'"); + await ret.load(); + document.fonts.add(ret); + return ret; } - // Register the file as a CSS stylesheet - style(enabled) { - let link = document.createElement("link"); - link.href = debug ? this.name : this.toDataURL(); - link.rel = "stylesheet"; - link.type = "text/css"; - link.setEnabled = enabled=>{ + // Install an image as a CSS icon from a bundled resource + installImage(name, filename) { + document.documentElement.style.setProperty("--" + + name || this.name.replaceAll(/\/\./, "_"), + "url('" + (Bundle.debug ? + filename || this.name : this.toDataURL()) + "')"); + return name; + } + + // Install a stylesheet from a bundled resource + installStylesheet(enabled) { + let ret = document.createElement("link"); + ret.href = Bundle.debug ? this.name : this.toDataURL(); + ret.rel = "stylesheet"; + ret.type = "text/css"; + ret.setEnabled = enabled=>{ if (enabled) - link.removeAttribute("disabled"); - else link.setAttribute("disabled", null); + ret.removeAttribute("disabled"); + else ret.setAttribute("disabled", ""); }; - link.setEnabled(enabled === undefined || !!enabled); - document.head.appendChild(link); - return link; - } - - // Produce a blob from the file data - toBlob() { - return new Blob(this.data, { type: this.mime }); - } - - // Produce a blob URL for the file data - toBlobURL() { - return URL.createObjectURL(this.toBlob()); + ret.setEnabled(!!enabled); + document.head.appendChild(ret); + return ret; } // Encode the file data as a data URL toDataURL() { - return "data:" + this.mime + ";base64," + btoa(this.toString()); + return "data:" + this.mime + ";base64," + + btoa(String.fromCharCode(...this.data)); + } + + // Interpret the file's contents as bundled script source data URL + toScript() { + + // Process all URL strings prefixed with /**/ + let parts = this.toString().split("/**/"); + let src = parts.shift(); + for (let part of parts) { + let quote = part.indexOf("\"", 1); + + // Begin with the path of the current file + let path = this.name.split("/"); + path.pop(); + + // Navigate to the path of the target file + let file = part.substring(1, quote).split("/"); + while (file.length > 0) { + let sub = file.shift(); + switch (sub) { + case "..": path.pop(); // Fallthrough + case "." : break; + default : path.push(sub); + } + } + + // Append the file as a data URL + file = Bundle[path.join("/")]; + src += "\"" + file[ + file.mime.startsWith("text/javascript") ? + "toScript" : "toDataURL" + ]() + "\"" + part.substring(quote + 1); + } + + // Encode the transformed source as a data URL + return "data:" + this.mime + ";base64," + btoa(src); } // Decode the file data as a UTF-8 string toString() { - return new TextDecoder().decode(this.data); + return Bundle.decoder.decode(this.data); } -}; -Bundle.files = []; + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Produce a .ZIP header for export + toZipHeader(crc32, offset) { + let central = offset !== undefined; + let ret = []; + if (central) { + Bundle.writeInt (ret, 4, 0x02014B50); // Signature + Bundle.writeInt (ret, 2, 20); // Version created by + } else + Bundle.writeInt (ret, 4, 0x04034B50); // Signature + Bundle.writeInt (ret, 2, 20); // Version required + Bundle.writeInt (ret, 2, 0); // Bit flags + Bundle.writeInt (ret, 2, 0); // Compression method + Bundle.writeInt (ret, 2, 0); // Modified time + Bundle.writeInt (ret, 2, 0); // Modified date + Bundle.writeInt (ret, 4, crc32); // Checksum + Bundle.writeInt (ret, 4, this.data.length); // Compressed size + Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size + Bundle.writeInt (ret, 2, this.name.length); // Filename length + Bundle.writeInt (ret, 2, 0); // Extra field length + if (central) { + Bundle.writeInt (ret, 2, 0); // File comment length + Bundle.writeInt (ret, 2, 0); // Disk number start + Bundle.writeInt (ret, 2, 0); // Internal attributes + Bundle.writeInt (ret, 4, 0); // External attributes + Bundle.writeInt (ret, 4, offset); // Relative offset + } + Bundle.writeString (ret, this.name); // Filename + if (!central) + Bundle.writeBytes(ret, this.data); // File data + return Uint8Array.from(ret); + } + +} /////////////////////////////////////////////////////////////////////////////// -// ZIP Bundler // +// Boot Program // /////////////////////////////////////////////////////////////////////////////// -// Data buffer utility processor -class Bin { - - // Object constructor - constructor() { - this.data = []; - this.offset = 0; - } - - ///////////////////////////// Public Methods ////////////////////////////// - - // Convert the data contents to a byte array - toByteArray() { - return Uint8Array.from(this.data); - } - - // Encode a byte array - writeBytes(data) { - this.data = this.data.concat(Array.from(data)); - } - - // Encode a sized integer - writeInt(length, value) { - for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8) - this.data.push(value & 0xFF); - } - - // Encode a string as UTF-8 with prepended length - writeString(value) { - this.writeBytes(new TextEncoder().encode(value)); - } - -} - -// Generate the CRC32 lookup table -let crcLookup = new Uint32Array(256); -for (let x = 0; x <= 255; x++) { - let l = x; - for (let j = 7; j >= 0; j--) - l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); - crcLookup[x] = l; -} - -// Calculate the CRC32 checksum for a byte array -function crc32(data) { - let c = 0xFFFFFFFF; - for (let x = 0; x < data.length; x++) - c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]); - return ~c & 0xFFFFFFFF; -} - -// Produce a .ZIP header from a bundled file -function toZipHeader(file, crc32, offset) { - let central = offset || offset === 0; - let ret = new Bin(); - if (central) { - ret.writeInt (4, 0x02014B50); // Signature - ret.writeInt (2, 20); // Version created by - } else - ret.writeInt (4, 0x04034B50); // Signature - ret.writeInt (2, 20); // Version required - ret.writeInt (2, 0); // Bit flags - ret.writeInt (2, 0); // Compression method - ret.writeInt (2, 0); // Modified time - ret.writeInt (2, 0); // Modified date - ret.writeInt (4, crc32); // Checksum - ret.writeInt (4, file.data.length); // Compressed size - ret.writeInt (4, file.data.length); // Uncompressed size - ret.writeInt (2, file.name.length); // Filename length - ret.writeInt (2, 0); // Extra field length - if (central) { - ret.writeInt (2, 0); // File comment length - ret.writeInt (2, 0); // Disk number start - ret.writeInt (2, 0); // Internal attributes - ret.writeInt (4, 0); // External attributes - ret.writeInt (4, offset); // Relative offset - } - ret.writeString (file.name, true); // Filename - if (!central) - ret.writeBytes(file.data); // File data - return ret.toByteArray(); -} - -// Package all bundled files into a .zip file for download -Bundle.save = function() { - let centrals = new Array(manifest.length); - let locals = new Array(manifest.length); - let offset = 0; - let size = 0; - - // Encode file and directory entries - let keys = Object.keys(Bundle.files); - for (let x = 0; x < keys.length; x++) { - let file = Bundle.get(keys[x]); - let sum = crc32(file.data); - locals [x] = toZipHeader(file, sum); - centrals[x] = toZipHeader(file, sum, offset); - offset += locals [x].length; - size += centrals[x].length; - } - - // Encode end of central directory - let end = new Bin(); - end.writeInt(4, 0x06054B50); // Signature - end.writeInt(2, 0); // Disk number - end.writeInt(2, 0); // Central dir start disk - end.writeInt(2, centrals.length); // # central dir this disk - end.writeInt(2, centrals.length); // # central dir total - end.writeInt(4, size); // Size of central dir - end.writeInt(4, offset); // Offset of central dir - end.writeInt(2, 0); // .ZIP comment length - - // Prompt the user to save the resulting file - let a = document.createElement("a"); - a.download = bundleName + ".zip"; - a.href = URL.createObjectURL(new Blob( - locals.concat(centrals).concat([end.toByteArray()]), - { type: "application/zip" } - )); - a.click(); -} - - - - -/////////////////////////////////////////////////////////////////////////////// -// Boot Loader // -/////////////////////////////////////////////////////////////////////////////// - -/* - The Bundle.java utility prepends a manifest object to _boot.js that is an - array of file infos, each of which has a name and size property. -*/ +// De-register the boot function +delete globalThis.a; // Remove the bundle image element from the document +Bundle.src = arguments[0].src; arguments[0].remove(); -// Process all files from the bundle blob -let blob = arguments[1]; -let offset = arguments[2] - manifest[0].size * 4; -for (let file of manifest) { - let data = new Uint8Array(file.size); - for (let x = 0; x < file.size; x++, offset += 4) - data[x] = blob[offset]; - if (file == manifest[0]) - offset += 4; - Bundle.add(file.name, data); +// Convert the file manifest into BundledFile objects +let buffer = arguments[1]; +let offset = arguments[2] - manifest[0][1]; +for (let entry of manifest) { + Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1])); + offset += entry[1]; + if (Bundle.length == 1) + offset++; // Skip null delimiter } -// Program startup -let run = async function() { - - // Fonts - for (let file of Object.values(Bundle.files)) { - if (!file.name.endsWith(".woff2")) - continue; - let family = "/" + file.name; - family = family.substring(family.lastIndexOf("/")+1, family.length-6); - let font = new FontFace(family, file.data); - await font.load(); - document.fonts.add(font); - } - - // Scripts - await Bundle.run("app/App.js"); - await Bundle.run("app/Debugger.js"); - await Bundle.run("app/toolkit/Toolkit.js"); - await Bundle.run("app/toolkit/Component.js"); - await Bundle.run("app/toolkit/Panel.js"); - await Bundle.run("app/toolkit/Application.js"); - await Bundle.run("app/toolkit/Button.js"); - await Bundle.run("app/toolkit/ButtonGroup.js"); - await Bundle.run("app/toolkit/CheckBox.js"); - await Bundle.run("app/toolkit/Label.js"); - await Bundle.run("app/toolkit/MenuBar.js"); - await Bundle.run("app/toolkit/MenuItem.js"); - await Bundle.run("app/toolkit/Menu.js"); - await Bundle.run("app/toolkit/RadioButton.js"); - await Bundle.run("app/toolkit/Splitter.js"); - 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(); -}; -run(); +// Begin program operations +import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript()); diff --git a/app/app/App.js b/app/app/App.js new file mode 100644 index 0000000..8d2f8b0 --- /dev/null +++ b/app/app/App.js @@ -0,0 +1,444 @@ +import { Debugger } from /**/"./Debugger.js"; + + + +/////////////////////////////////////////////////////////////////////////////// +// App // +/////////////////////////////////////////////////////////////////////////////// + +// Web-based emulator application +class App extends Toolkit { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(options) { + super({ + className: "tk tk-app", + label : "app.title", + role : "application", + tagName : "div", + style : { + display : "flex", + flexDirection: "column" + } + }); + + // Configure instance fields + options = options || {}; + this.debugMode = true; + this.dualSims = false; + this.core = options.core; + this.linkSims = true; + this.locales = {}; + this.themes = {}; + this.Toolkit = Toolkit; + + // Configure themes + if ("themes" in options) + for (let theme of Object.entries(options.themes)) + this.addTheme(theme[0], theme[1]); + if ("theme" in options) + this.setTheme(options.theme); + + // Configure locales + if ("locales" in options) + for (let locale of options.locales) + this.addLocale(locale); + if ("locale" in options) + this.setLocale(options.locale); + + // Configure widget + this.localize(this); + + // Not presenting a standalone application + if (!options.standalone) + return; + + // Set up standalone widgets + this.initMenuBar(); + this.desktop = new Toolkit.Desktop(this, + { style: { flexGrow: 1 } }); + this.add(this.desktop); + + // Configure document for presentation + document.body.className = "tk tk-body"; + window.addEventListener("resize", e=> + this.element.style.height = window.innerHeight + "px"); + window.dispatchEvent(new Event("resize")); + document.body.appendChild(this.element); + + // Configure debugger components + this[0] = new Debugger(this, 0, this.core[0]); + this[1] = new Debugger(this, 1, this.core[1]); + + // Configure subscription handling + this.subscriptions = { + [this.core[0].sim]: this[0], + [this.core[1].sim]: this[1] + }; + this.core.onsubscriptions = e=>this.onSubscriptions(e); + + // Temporary config debugging + console.log("Disassembler keyboard commands:"); + console.log(" Ctrl+G: Goto (also works in Memory window)"); + console.log(" F10: Run to next"); + console.log(" F11: Single step"); + console.log("Call dasm(\"key\", value) in the console " + + "to configure the disassembler:"); + console.log(this[0].getDasmConfig()); + window.dasm = (key, value)=>{ + let config = this[0].getDasmConfig(); + if (!key in config || typeof value != typeof config[key]) + return; + if (typeof value == "number" && value != 1 && value != 0) + return; + config[key] = value; + this[0].setDasmConfig(config); + this[1].setDasmConfig(config); + return this[0].getDasmConfig(); + }; + } + + // Configure file menu + initFileMenu(menuBar) { + let menu, item; + + // Menu + menuBar.add(menu = menuBar.file = new Toolkit.MenuItem(this, + { text: "app.menu.file._" })); + + // Load ROM + menu.add(item = menu.loadROM0 = new Toolkit.MenuItem(this, { + text: "app.menu.file.loadROM" + })); + item.setSubstitution("sim", ""); + item.addEventListener("action", + ()=>this.promptFile(f=>this.loadROM(0, f))); + menu.add(item = menu.loadROM1 = new Toolkit.MenuItem(this, { + text : "app.menu.file.loadROM", + visible: false + })); + item.setSubstitution("sim", " 2"); + item.addEventListener("action", + ()=>this.promptFile(f=>this.loadROM(1, f))); + + // Debug Mode + menu.add(item = menu.debugMode = new Toolkit.MenuItem(this, { + checked: this.debugMode, + enabled: false, + text : "app.menu.file.debugMode", + type : "check" + })); + item.addEventListener("action", e=>e.component.setChecked(true)); + } + + // Configure Emulation menu + initEmulationMenu(menuBar) { + let menu, item; + + menuBar.add(menu = menuBar.emulation = new Toolkit.MenuItem(this, + { text: "app.menu.emulation._" })); + + menu.add(item = menu.runPause = new Toolkit.MenuItem(this, { + enabled: false, + text : "app.menu.emulation.run" + })); + + menu.add(item = menu.reset = new Toolkit.MenuItem(this, { + enabled: false, + text : "app.menu.emulation.reset" + })); + + menu.add(item = menu.dualSims = new Toolkit.MenuItem(this, { + checked: this.dualSims, + text : "app.menu.emulation.dualSims", + type : "check" + })); + item.addEventListener("action", + e=>this.setDualSims(e.component.isChecked)); + + menu.add(item = menu.linkSims = new Toolkit.MenuItem(this, { + checked: this.linkSims, + text : "app.menu.emulation.linkSims", + type : "check", + visible: this.dualSims + })); + item.addEventListener("action", + e=>this.setLinkSims(e.component.isChecked)); + } + + // Configure Debug menus + initDebugMenu(menuBar, sim) { + let menu, item; + + menuBar.add(menu = menuBar["debug" + sim] = + new Toolkit.MenuItem(this, { + text : "app.menu.debug._", + visible: sim == 0 || this.dualSims + })); + menu.setSubstitution("sim", + sim == 1 || this.dualSims ? " " + (sim + 1) : ""); + + menu.add(item = menu.console = new Toolkit.MenuItem(this, + { text: "app.menu.debug.console", enabled: false })); + + menu.add(item = menu.memory = new Toolkit.MenuItem(this, + { text: "app.menu.debug.memory" })); + item.addEventListener("action", + ()=>this.showWindow(this[sim].memoryWindow)); + + menu.add(item = menu.cpu = new Toolkit.MenuItem(this, + { text: "app.menu.debug.cpu" })); + item.addEventListener("action", + ()=>this.showWindow(this[sim].cpuWindow)); + + menu.add(item = menu.breakpoints = new Toolkit.MenuItem(this, + { text: "app.menu.debug.breakpoints", enabled: false })); + menu.addSeparator(); + menu.add(item = menu.palettes = new Toolkit.MenuItem(this, + { text: "app.menu.debug.palettes", enabled: false })); + menu.add(item = menu.characters = new Toolkit.MenuItem(this, + { text: "app.menu.debug.characters", enabled: false })); + menu.add(item = menu.bgMaps = new Toolkit.MenuItem(this, + { text: "app.menu.debug.bgMaps", enabled: false })); + menu.add(item = menu.objects = new Toolkit.MenuItem(this, + { text: "app.menu.debug.objects", enabled: false })); + menu.add(item = menu.worlds = new Toolkit.MenuItem(this, + { text: "app.menu.debug.worlds", enabled: false })); + menu.add(item = menu.frameBuffers = new Toolkit.MenuItem(this, + { text: "app.menu.debug.frameBuffers", enabled: false })); + + } + + // Configure Theme menu + initThemeMenu(menuBar) { + let menu, item; + + menuBar.add(menu = menuBar.theme = new Toolkit.MenuItem(this, + { text: "app.menu.theme._" })); + + menu.add(item = menu.light = new Toolkit.MenuItem(this, + { text: "app.menu.theme.light" })); + item.addEventListener("action", e=>this.setTheme("light")); + + menu.add(item = menu.dark = new Toolkit.MenuItem(this, + { text: "app.menu.theme.dark" })); + item.addEventListener("action", e=>this.setTheme("dark")); + + menu.add(item = menu.virtual = new Toolkit.MenuItem(this, + { text: "app.menu.theme.virtual" })); + item.addEventListener("action", e=>this.setTheme("virtual")); + } + + // Set up the menu bar + initMenuBar() { + let menuBar = this.menuBar = new Toolkit.MenuBar(this, + { label: "app.menu._" }); + this.initFileMenu (menuBar); + this.initEmulationMenu(menuBar); + this.initDebugMenu (menuBar, 0); + this.initDebugMenu (menuBar, 1); + this.initThemeMenu (menuBar); + this.add(menuBar); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Subscriptions arrived from the core thread + onSubscriptions(subscriptions) { + for (let sim of Object.entries(subscriptions)) { + let dbg = this.subscriptions[sim[0]]; + for (let sub of Object.entries(sim[1])) switch (sub[0]) { + case "proregs": dbg.programRegisters.refresh(sub[1]); break; + case "sysregs": dbg.systemRegisters .refresh(sub[1]); break; + case "dasm" : dbg.disassembler .refresh(sub[1]); break; + case "memory" : dbg.memory .refresh(sub[1]); break; + } + } + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Register a locale JSON + addLocale(locale) { + if (!("id" in locale)) + throw "No id field in locale"; + this.locales[locale.id] = Toolkit.flatten(locale); + } + + // Register a theme stylesheet + addTheme(id, stylesheet) { + this.themes[id] = stylesheet; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Specify the language for localization management + setLocale(id) { + if (!(id in this.locales)) { + let lang = id.substring(0, 2); + id = "en-US"; + for (let key of Object.keys(this.locales)) { + if (key.substring(0, 2) == lang) { + id = key; + break; + } + } + } + super.setLocale(this.locales[id]); + } + + // Specify the active color theme + setTheme(key) { + if (!(key in this.themes)) + return; + for (let tkey of Object.keys(this.themes)) + this.themes[tkey].setEnabled(tkey == key); + } + + // Regenerate localized display text + translate() { + if (arguments.length != 0) + return super.translate.apply(this, arguments); + document.title = super.translate("app.title", this); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Load a ROM for a simulation + async loadROM(index, file) { + + // No file was given + if (!file) + return; + + // Load the file into memory + try { file = new Uint8Array(await file.arrayBuffer()); } + catch { + alert(this.translate("error.fileRead")); + return; + } + + // Validate file size + if ( + file.length < 1024 || + file.length > 0x1000000 || + (file.length - 1 & file.length) != 0 + ) { + alert(this.translate("error.romNotVB")); + return; + } + + // Load the ROM into the simulation + if (!(await this[index].sim.setROM(file, { refresh: true }))) { + alert(this.translate("error.romNotVB")); + return; + } + + // Seek the disassembler to PC + this[index].disassembler.seek(0xFFFFFFF0, true); + } + + // Prompt the user to select a file + promptFile(then) { + let file = document.createElement("input"); + file.type = "file"; + file.addEventListener("input", + e=>file.files[0] && then(file.files[0])); + file.click(); + } + + // Attempt to run until the next instruction + async runNext(index) { + let two = this.dualSims && this.linkSims; + + // Perform the operation + let data = await this.core.runNext( + this[index].sim.sim, + two ? this[index ^ 1].sim.sim : 0, { + refresh: true + }); + + // Update the disassemblers + this[index].disassembler.pc = data.pc[0]; + this[index].disassembler.seek(data.pc[0]); + if (two) { + this[index ^ 1].disassembler.pc = data.pc[1]; + this[index ^ 1].disassembler.seek(data.pc[1]); + } + } + + // Specify whether dual sims mode is active + setDualSims(dualSims) { + let sub = dualSims ? " 1" : ""; + + // Configure instance fields + this.dualSims = dualSims = !!dualSims; + + // Configure menus + this.menuBar.emulation.dualSims.setChecked(dualSims); + this.menuBar.emulation.linkSims.setVisible(dualSims); + this.menuBar.file.loadROM0.setSubstitution("sim", sub); + this.menuBar.file.loadROM1.setVisible(dualSims); + this.menuBar.debug0.setSubstitution("sim", sub); + this.menuBar.debug1.setVisible(dualSims); + + // Configure debuggers + this[0].setDualSims(dualSims); + this[1].setDualSims(dualSims); + this.core.connect(this[0].sim.sim, + dualSims && this.linkSims ? this[1].sim.sim : 0); + } + + // Specify whether the sims are connected for communicatinos + setLinkSims(linked) { + linked = !!linked; + + // State is not changing + if (linked == this.linkSims) + return; + + // Link or un-link the sims + if (this.dualSims) + this.core.connect(this[0].sim.sim, linked ? this[1].sim.sim : 0); + } + + // Display a window + showWindow(wnd) { + wnd.setVisible(true); + wnd.focus() + } + + // Execute one instruction + async singleStep(index) { + let two = this.dualSims && this.linkSims; + + // Perform the operation + let data = await this.core.singleStep( + this[index].sim.sim, + two ? this[index ^ 1].sim.sim : 0, { + refresh: true + }); + + // Update the disassemblers + this[index].disassembler.pc = data.pc[0]; + this[index].disassembler.seek(data.pc[0]); + if (two) { + this[index ^ 1].disassembler.pc = data.pc[1]; + this[index ^ 1].disassembler.seek(data.pc[1]); + } + } + +} + + + +export { App }; diff --git a/app/app/CPU.js b/app/app/CPU.js new file mode 100644 index 0000000..f3ec1dd --- /dev/null +++ b/app/app/CPU.js @@ -0,0 +1,106 @@ +import { Disassembler } from /**/"./Disassembler.js"; + + + + + + + + +/////////////////////////////////////////////////////////////////////////////// +// CPU // +/////////////////////////////////////////////////////////////////////////////// + +// CPU register editor and disassembler +class CPU extends Toolkit.SplitPane { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, sim) { + super(app, { + className: "tk tk-splitpane tk-cpu", + edge : Toolkit.SplitPane.RIGHT, + style : { + position: "relative" + } + }); + + this.app = app; + this.sim = sim; + this.initDasm(); + + this.metrics = new Toolkit.Component(this.app, { + className: "tk tk-mono", + tagName : "div", + style : { + position : "absolute", + visibility: "hidden" + } + }); + let text = ""; + for (let x = 0; x < 16; x++) { + if (x) text += "\n"; + let digit = x.toString(16); + text += digit + "\n" + digit.toUpperCase(); + } + this.metrics.element.innerText = text; + this.splitter.append(this.metrics.element); + + this.setView(1, this.regs = new Toolkit.SplitPane(app, { + className: "tk tk-splitpane", + edge : Toolkit.SplitPane.TOP + })); + + this.regs.setView(0, this.sysregs = new RegisterList(this, true )); + this.regs.setView(1, this.proregs = new RegisterList(this, false)); + + // Adjust split panes to the initial size of the System Registers pane + let resize; + let preshow = e=>this.onPreshow(resize); + resize = new ResizeObserver(preshow); + resize.observe(this.sysregs.viewport); + resize.observe(this.metrics.element); + + this.metrics.addEventListener("resize", e=>this.metricsResize()); + } + + initDasm() { + this.dasm = new Disassembler(this.app, this.sim); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + + + // Resize handler prior to first visibility + onPreshow(resize) { + this.metricsResize(); + + // Once the list of registers is visible, stop listening + if (this.isVisible()) { + resize.disconnect(); + this.sysregs.view.element.style.display = "grid"; + return; + } + + // Update the split panes + let sys = this.sysregs.view.element; + let pro = this.proregs.view.element; + this.setValue( + Math.max(sys.scrollWidth, pro.scrollWidth) + + this.sysregs.vertical.getBounds().width + ); + this.regs.setValue( + this.sysregs[PSW].expansion.getBounds().bottom - + sys.getBoundingClientRect().top + ); + } + + +} + + + +export { CPU }; diff --git a/app/app/Debugger.js b/app/app/Debugger.js new file mode 100644 index 0000000..48490f0 --- /dev/null +++ b/app/app/Debugger.js @@ -0,0 +1,187 @@ +import { Disassembler } from /**/"./Disassembler.js"; +import { Memory } from /**/"./Memory.js"; +import { RegisterList } from /**/"./RegisterList.js"; + + + +class Debugger { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, index, sim) { + + // Configure instance fields + this.app = app; + this.index = index; + this.sim = sim; + + // Configure components + this.disassembler = new Disassembler(this); + this.memory = new Memory (this); + this.programRegisters = new RegisterList(this, false); + this.systemRegisters = new RegisterList(this, true ); + + // Configure windows + this.initCPUWindow (); + this.initMemoryWindow(); + } + + // Set up the CPU window + initCPUWindow() { + let app = this.app; + + // Produce the window + let wnd = this.cpuWindow = new app.Toolkit.Window(app, { + width : 400, + height : 300, + className : "tk tk-window tk-cpu" + (this.index==0?"":" two"), + closeToolTip: "common.close", + title : "cpu._", + visible : false + }); + wnd.setSubstitution("sim", this.index == 1 ? " 2" : ""); + wnd.addEventListener("close", ()=>wnd.setVisible(false)); + app.desktop.add(wnd); + + // Visibility override + let that = this; + let setVisible = wnd.setVisible; + wnd.setVisible = function(visible) { + that.disassembler .setSubscribed(visible); + that.systemRegisters .setSubscribed(visible); + that.programRegisters.setSubscribed(visible); + setVisible.apply(wnd, arguments); + }; + + // Auto-seek the initial view + let onSeek = ()=>this.disassembler.seek(this.disassembler.pc, true); + Toolkit.addResizeListener(this.disassembler.element, onSeek); + wnd.addEventListener("firstshow", ()=>{ + app.desktop.center(wnd); + Toolkit.removeResizeListener(this.disassembler.element, onSeek); + }); + + // Window splitters + let sptMain = new Toolkit.SplitPane(this.app, { + className: "tk tk-splitpane tk-main", + edge : Toolkit.SplitPane.RIGHT + }); + let sptRegs = new Toolkit.SplitPane(this.app, { + className: "tk tk-splitpane tk-registers", + edge : Toolkit.SplitPane.TOP + }); + + // Configure window splitter initial size + let resize = new ResizeObserver(()=>{ + + // Measure register lists + let sys = this.systemRegisters .getPreferredSize(); + let pro = this.programRegisters.getPreferredSize(); + let height = Math.ceil(Math.max(sys.height, pro.height)); + let width = Math.ceil(Math.max(sys.width , pro.width )) + + this.systemRegisters.vertical.getBounds().width; + + // Configure splitters + if (sptMain.getValue() != width) + sptMain.setValue(width); + if (sptRegs.getValue() != height) + sptRegs.setValue(height); + }); + resize.observe(this.programRegisters.view.element); + resize.observe(this.systemRegisters .view.element); + + // Stop monitoring splitter size when something receives focus + let onFocus = e=>{ + resize.disconnect(); + wnd.removeEventListener("focus", onFocus, true); + }; + sptMain.addEventListener("focus", onFocus, true); + + // Configure window layout + sptMain.setView(0, this.disassembler); + sptMain.setView(1, sptRegs); + sptRegs.setView(0, this.systemRegisters); + sptRegs.setView(1, this.programRegisters); + wnd.append(sptMain); + } + + // Set up the Memory window + initMemoryWindow() { + let app = this.app; + + // Produce the window + let wnd = this.memoryWindow = new app.Toolkit.Window(app, { + width : 400, + height : 300, + className : "tk tk-window" + (this.index == 0 ? "" : " two"), + closeToolTip: "common.close", + title : "memory._", + visible : false + }); + wnd.setSubstitution("sim", this.index == 1 ? " 2" : ""); + wnd.addEventListener("close" , ()=>wnd.setVisible(false)); + wnd.addEventListener("firstshow", ()=>app.desktop.center(wnd)); + app.desktop.add(wnd); + + // Visibility override + let that = this; + let setVisible = wnd.setVisible; + wnd.setVisible = function(visible) { + that.memory.setSubscribed(visible); + setVisible.apply(wnd, arguments); + }; + + // Configure window layout + wnd.append(this.memory); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Retrieve the disassembler configuraiton + getDasmConfig() { + return this.disassembler.getConfig(); + } + + // Attempt to run until the next instruction + runNext() { + this.app.runNext(this.index); + } + + // Update the disassembler configuration + setDasmConfig(config) { + this.disassembler .setConfig(config); + this.memory .dasmChanged(); + this.programRegisters.dasmChanged(); + this.systemRegisters .dasmChanged(); + } + + // Specify whether dual sims mode is active + setDualSims(dualSims) { + + // Update substitutions for sim 1 + if (this.index == 0) { + let sub = dualSims ? " 1" : ""; + this.cpuWindow .setSubstitution("sim", sub); + this.memoryWindow.setSubstitution("sim", sub); + } + + // Hide windows for sim 2 + else if (!dualSims) { + this.cpuWindow .close(false); + this.memoryWindow.close(false); + } + + } + + // Execute one instruction + singleStep() { + this.app.singleStep(this.index); + } + +} + + + +export { Debugger }; diff --git a/app/app/Disassembler.js b/app/app/Disassembler.js new file mode 100644 index 0000000..140b9b5 --- /dev/null +++ b/app/app/Disassembler.js @@ -0,0 +1,892 @@ +import { Util } from /**/"../app/Util.js"; + + + +// Opcode definition +class Opdef { + constructor(format, mnemonic, signExtend) { + this.format = format; + this.mnemonic = mnemonic; + this.signExtend = !!signExtend; + } +} + +// Top-level opcode definition lookup table by opcode +let OPDEFS = [ + new Opdef(1, "MOV" ), new Opdef(1, "ADD" ), new Opdef(1, "SUB" ), + new Opdef(1, "CMP" ), new Opdef(1, "SHL" ), new Opdef(1, "SHR" ), + new Opdef(1, "JMP" ), new Opdef(1, "SAR" ), new Opdef(1, "MUL" ), + new Opdef(1, "DIV" ), new Opdef(1, "MULU" ), new Opdef(1, "DIVU" ), + new Opdef(1, "OR" ), new Opdef(1, "AND" ), new Opdef(1, "XOR" ), + new Opdef(1, "NOT" ), new Opdef(2, "MOV" ,1), new Opdef(2, "ADD",1), + new Opdef(2, "SETF" ), new Opdef(2, "CMP" ,1), new Opdef(2, "SHL" ), + new Opdef(2, "SHR" ), new Opdef(2, "CLI" ), new Opdef(2, "SAR" ), + new Opdef(2, "TRAP" ), new Opdef(2, "RETI" ), new Opdef(2, "HALT" ), + new Opdef(0, null ), new Opdef(2, "LDSR" ), new Opdef(2, "STSR" ), + new Opdef(2, "SEI" ), new Opdef(2, null ), new Opdef(3, "Bcond"), + new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"), + new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"), + new Opdef(3, "Bcond"), new Opdef(5,"MOVEA",1), new Opdef(5,"ADDI",1), + new Opdef(4, "JR" ), new Opdef(4, "JAL" ), new Opdef(5, "ORI" ), + new Opdef(5, "ANDI" ), new Opdef(5, "XORI" ), new Opdef(5, "MOVHI"), + new Opdef(6, "LD.B" ), new Opdef(6, "LD.H" ), new Opdef(0, null ), + new Opdef(6, "LD.W" ), new Opdef(6, "ST.B" ), new Opdef(6, "ST.H" ), + new Opdef(0, null ), new Opdef(6, "ST.W" ), new Opdef(6, "IN.B" ), + new Opdef(6, "IN.H" ), new Opdef(6, "CAXI" ), new Opdef(6, "IN.W" ), + new Opdef(6, "OUT.B"), new Opdef(6, "OUT.H" ), new Opdef(7, null ), + new Opdef(6, "OUT.W") +]; + +// Bit string mnemonic lookup table by sub-opcode +let BITSTRINGS = [ + "SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD", + null , null , null , null , + "ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" , + "ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" +]; + +// Floating-point/Nintendo mnemonic lookup table by sub-opcode +let FLOATENDOS = [ + "CMPF.S", null , "CVT.WS", "CVT.SW" , + "ADDF.S", "SUBF.S", "MULF.S", "DIVF.S" , + "XB" , "XH" , "REV" , "TRNC.SW", + "MPYHW" +]; + +// Program register names +let PROREGS = { 2: "hp", 3: "sp", 4: "gp", 5: "tp", 31: "lp" }; + +// System register names +let SYSREGS = [ + "EIPC", "EIPSW", "FEPC", "FEPSW", + "ECR" , "PSW" , "PIR" , "TKCW" , + null , null , null , null , + null , null , null , null , + null , null , null , null , + null , null , null , null , + "CHCW", "ADTRE", null , null , + null , null , null , null +]; + +// Condition mnemonics +let CONDS = [ + "V" , ["C" , "L" ], ["E" , "Z" ], "NH", + "N" , "T" , "LT" , "LE", + "NV", ["NC", "NL"], ["NE", "NZ"], "H" , + "P" , "F" , "GE" , "GT" +]; + +// Output setting keys +const SETTINGS = [ + "bcondMerged", "branchAddress", "condCase", "condCL", "condEZ", + "condNames", "hexCaps", "hexDollar", "hexSuffix", "imm5OtherHex", + "imm5ShiftHex", "imm5TrapHex", "imm16AddiLargeHex", "imm16AddiSmallHex", + "imm16MoveHex", "imm16OtherHex", "jmpBrackets", "memoryLargeHex", + "memorySmallHex", "memoryInside", "mnemonicCaps", "operandReverse", + "proregCaps", "proregNames", "setfMerged", "sysregCaps", "sysregNames" +]; + + + +/////////////////////////////////////////////////////////////////////////////// +// Line // +/////////////////////////////////////////////////////////////////////////////// + +// One line of output +class Line { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(parent, first) { + + // Configure instance fields + this.parent = parent; + + // Configure labels + this.lblAddress = this.label("tk-address" , first); + this.lblBytes = this.label("tk-bytes" , first); + this.lblMnemonic = this.label("tk-mnemonic", first); + this.lblOperands = this.label("tk-operands", false); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update the elements' display + refresh(row, isPC) { + + // The row is not available + if (!row) { + this.lblAddress .innerText = "--------"; + this.lblBytes .innerText = ""; + this.lblMnemonic.innerText = "---"; + this.lblOperands.innerText = ""; + } + + // Update labels with the disassembled row's contents + else { + this.lblAddress .innerText = row.address; + this.lblBytes .innerText = row.bytes; + this.lblMnemonic.innerText = row.mnemonic; + this.lblOperands.innerText = row.operands; + } + + // Update style according to selection + let method = isPC ? "add" : "remove"; + this.lblAddress .classList[method]("tk-selected"); + this.lblBytes .classList[method]("tk-selected"); + this.lblMnemonic.classList[method]("tk-selected"); + this.lblOperands.classList[method]("tk-selected"); + } + + // Specify whether the elements on this line are visible + setVisible(visible) { + visible = visible ? "block" : "none"; + this.lblAddress .style.display = visible; + this.lblBytes .style.display = visible; + this.lblMnemonic.style.display = visible; + this.lblOperands.style.display = visible; + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Create a display label + label(className, first) { + + // Create the label element + let label = document.createElement("div"); + label.className = "tk " + className; + + // The label is part of the first row of output + let element = label; + if (first) { + + // Create a container element + element = document.createElement("div"); + element.append(label); + element.max = 0; + + // Ensure the container can always fit the column contents + Toolkit.addResizeListener(element, ()=>{ + let width = Math.ceil(label.getBoundingClientRect().width); + if (width <= element.max) + return; + element.max = width; + element.style.minWidth = width + "px"; + }); + + } + + // Configure elements + this.parent.view.append(element); + return label; + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// +// Disassembler // +/////////////////////////////////////////////////////////////////////////////// + +// Text disassembler for NVC +class Disassembler extends Toolkit.ScrollPane { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(debug) { + super(debug.app, { + className : "tk tk-scrollpane tk-disassembler", + horizontal: Toolkit.ScrollPane.AS_NEEDED, + focusable : true, + tabStop : true, + tagName : "div", + vertical : Toolkit.ScrollPane.NEVER + }); + + // Configure instance fields + this.address = Util.u32(0xFFFFFFF0); + this.app = debug.app; + this.columns = [ 0, 0, 0, 0 ]; + this.data = []; + this.debug = debug; + this.isSubscribed = false; + this.lines = null; + this.pc = this.address; + this.pending = []; + this.rows = []; + this.scroll = 0; + this.sim = debug.sim; + + // Default output settings + this.setConfig({ + bcondMerged : true, + branchAddress : true, + condCase : false, + condCL : 1, + condEZ : 1, + condNames : true, + hexCaps : true, + hexDollar : false, + hexSuffix : false, + imm5OtherHex : false, + imm5ShiftHex : false, + imm5TrapHex : false, + imm16AddiLargeHex: true, + imm16AddiSmallHex: false, + imm16MoveHex : true, + imm16OtherHex : true, + jmpBrackets : true, + memoryLargeHex : true, + memorySmallHex : false, + memoryInside : false, + mnemonicCaps : true, + operandReverse : false, + proregCaps : false, + proregNames : true, + setfMerged : false, + sysregCaps : true, + sysregNames : true + }); + + // Configure viewport + this.viewport.classList.add("tk-mono"); + + // Configure view + let view = document.createElement("div"); + view.className = "tk tk-view"; + Object.assign(view.style, { + display : "grid", + gridTemplateColumns: "repeat(3, max-content) auto" + }); + 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); + + // First row always exists + this.lines = [ new Line(this, true) ]; + + // Configure event handlers + Toolkit.addResizeListener(this.viewport, e=>this.onResize(e)); + this.addEventListener("keydown", e=>this.onKeyDown (e)); + this.addEventListener("wheel" , e=>this.onMouseWheel(e)); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Key press + onKeyDown(e) { + let tall = this.tall(false); + + // Processing by key + switch (e.key) { + + // Navigation + case "ArrowDown" : this.fetch(+1 , true); break; + case "ArrowUp" : this.fetch(-1 , true); break; + case "PageDown" : this.fetch(+tall, true); break; + case "PageUp" : this.fetch(-tall, true); break; + + // View control + case "ArrowLeft" : this.horizontal.setValue( + this.horizontal.value - this.horizontal.increment); break; + case "ArrowRight": this.horizontal.setValue( + this.horizontal.value + this.horizontal.increment); break; + + // Goto + case "g": case "G": + if (!e.ctrlKey) + return; + this.promptGoto(); + break; + + // Single step + case "F10": + this.debug.runNext(); + break; + + // Single step + case "F11": + this.debug.singleStep(); + 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) * 3; + if (offset == 0) + return; + + // Update the display address + this.fetch(offset, true); + } + + // 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)); + } + 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(0, true); + else this.refresh(); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Produce disassembly text + disassemble(rows) { + + // Produce a deep copy of the input list + let copy = new Array(rows.length); + for (let x = 0; x < rows.length; x++) { + copy[x] = {}; + Object.assign(copy[x], rows[x]); + } + rows = copy; + + // Process all rows + for (let row of rows) { + row.operands = []; + + // Read instruction bits from the bus + let bits0 = row.bytes[1] << 8 | row.bytes[0]; + let bits1; + if (row.bytes.length == 4) + bits1 = row.bytes[3] << 8 | row.bytes[2]; + + // Working variables + let opcode = bits0 >> 10; + let opdef = OPDEFS[opcode]; + + // Sub-opcode mnemonics + if (row.opcode == 0b011111) + row.mnemonic = BITSTRINGS[bits0 & 31] || "---"; + else if (row.opcode == 0b111110) + row.mnemonic = FLOATENDOS[bits1 >> 10 & 63] || "---"; + else row.mnemonic = opdef.mnemonic; + + // Processing by format + switch (opdef.format) { + case 1: this.formatI (row, bits0 ); break; + case 3: this.formatIII(row, bits0 ); break; + case 4: this.formatIV (row, bits0, bits1); break; + case 6: this.formatVI (row, bits0, bits1); break; + case 7: this.formatVII(row, bits0 ); break; + case 2: + this.formatII(row, bits0, opdef.signExtend); break; + case 5: + this.formatV (row, bits0, bits1, opdef.signExtend); + } + + // Format bytes + let text = []; + for (let x = 0; x < row.bytes.length; x++) + text.push(row.bytes[x].toString(16).padStart(2, "0")); + row.bytes = text.join(" "); + + // Post-processing + row.address = row.address.toString(16).padStart(8, "0"); + if (this.hexCaps) { + row.address = row.address.toUpperCase(); + row.bytes = row.bytes .toUpperCase(); + } + if (!this.mnemonicCaps) + row.mnemonic = row.mnemonic.toLowerCase(); + if (this.operandReverse) + row.operands.reverse(); + row.operands = row.operands.join(", "); + } + + return rows; + } + + // Retrieve all output settings in an object + getConfig() { + let ret = {}; + for (let key of SETTINGS) + ret[key] = this[key]; + return ret; + } + + // Update with disassembly state from the core + refresh(data = 0) { + let bias; + + // Scrolling prefresh + if (typeof data == "number") + bias = 16 + data; + + // Received data from the core thread + else { + this.data = data.rows; + this.pc = data.pc; + if (this.data.length == 0) + return; + this.address = this.data[0].address; + this.rows = this.disassemble(this.data); + bias = 16 + + (data.scroll === null ? 0 : this.scroll - data.scroll); + } + + // Update elements + let count = Math.min(this.tall(true), this.data.length); + for (let y = 0; y < count; y++) { + let index = bias + y; + let line = this.data[index]; + let row = this.rows[index]; + this.lines[y].refresh(row, line && line.address == this.pc); + } + + // Refesh scroll pane + this.update(); + } + + // Bring an address into view + seek(address, force) { + + // Check if the address is already in the view + if (!force) { + let bias = 16; + let tall = this.tall(false); + let count = Math.min(tall, this.data.length); + + // The address is currently visible in the output + for (let y = 0; y < count; y++) { + let row = this.data[bias + y]; + if (!row || Util.u32(address - row.address) >= row.size) + continue; + + // The address is on this row + this.refresh(); + return; + } + + } + + // Place the address at a particular position in the view + this.address = address; + this.fetch(null); + } + + // Update output settings + setConfig(config) { + + // Update settings + for (let key of SETTINGS) + if (key in config) + this[key] = config[key]; + + // Regenerate output + this.refresh({ + pc : this.pc, + rows : this.data, + scroll: null + }); + } + + // 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(0); + + // Unsubscribe from core updates + else this.sim.unsubscribe("dasm"); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Select a condition's name + cond(cond) { + let ret = CONDS[cond]; + switch (cond) { + case 1: case 9: return CONDS[cond][this.condCL]; + case 2: case 10: return CONDS[cond][this.condEZ]; + } + return CONDS[cond]; + } + + // Retrieve disassembly data from the core + async fetch(scroll, prefresh) { + let row; + + // Scrolling relative to the current view + if (scroll) { + if (prefresh) + this.refresh(scroll); + this.scroll = Util.s32(this.scroll + scroll); + row = -scroll; + } + + // Jumping to an address directly + else row = scroll === null ? Math.floor(this.tall(false) / 3) + 16 : 0; + + // Retrieve data from the core + this.refresh( + await this.sim.disassemble( + this.address, + row, + this.tall(true) + 32, + scroll === null ? null : this.scroll, { + subscribe: this.isSubscribed && "dasm" + }) + ); + } + + // Represent a hexadecimal value + hex(value, digits) { + let sign = Util.s32(value) < 0 ? "-" : ""; + let ret = Math.abs(Util.u32(value)).toString(16).padStart(digits,"0"); + if (this.hexCaps) + ret = ret.toUpperCase(); + if (this.hexSuffix) + ret = ("abcdefABCDEF".indexOf(ret[0]) == -1 ? "" : "0") + + ret + "h"; + else ret = (this.hexDollar ? "$" : "0x") + ret; + return sign + ret; + } + + // 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; + + // Move the selection and refresh the display + this.seek(Util.u32(address)); + } + + // Select a program register name + proreg(index) { + let ret = this.proregNames && PROREGS[index] || "r" + index; + return this.proregCaps ? ret.toUpperCase() : ret; + } + + // 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)); + } + + + + //////////////////////////// Decoding Methods ///////////////////////////// + + // Disassemble a Format I instruction + formatI(row, bits0) { + let reg1 = this.proreg(bits0 & 31); + + // JMP + if (row.mnemonic == "JMP") { + if (this.jmpBrackets) + reg1 = "[" + reg1 + "]"; + row.operands.push(reg1); + } + + // Other instructions + else { + let reg2 = this.proreg(bits0 >> 5 & 31); + row.operands.push(reg1, reg2); + } + + } + + // Disassemble a Format II instruction + formatII(row, bits0, signExtend) { + + // Bit-string instructions are zero-operand + if (bits0 >> 10 == 0b011111) + return; + + // Processing by mnemonic + switch (row.mnemonic) { + + // Zero-operand + case "---" : // Fallthrough + case "CLI" : // Fallthrough + case "HALT": // Fallthrough + case "RETI": // Fallthrough + case "SEI" : return; + + // Distinct notation + case "LDSR": return this.ldstsr(row, bits0, true ); + case "SETF": return this.setf (row, bits0 ); + case "STSR": return this.ldstsr(row, bits0, false); + } + + // Retrieve immediate operand + let imm = bits0 & 31; + if (signExtend) + imm = Util.signExtend(bits0, 5); + + // TRAP instruction is one-operand + if (row.mnemonic == "TRAP") { + row.operands.push(this.trapHex ? + this.hex(imm, 1) : imm.toString()); + return; + } + + // Processing by mnemonic + let hex = this.imm5OtherHex; + switch (row.mnemonic) { + case "SAR": // Fallthrough + case "SHL": // Fallthrough + case "SHR": hex = this.imm5ShiftHex; + } + imm = hex ? this.hex(imm, 1) : imm.toString(); + + // Two-operand instruction + let reg2 = this.proreg(bits0 >> 5 & 31); + row.operands.push(imm, reg2); + } + + // Disassemble a Format III instruction + formatIII(row, bits0) { + let cond = this.cond(bits0 >> 9 & 15); + let disp = Util.signExtend(bits0 & 0x1FF, 9); + + // Condition merged with mnemonic + if (this.bcondMerged) { + switch (cond) { + case "F": row.mnemonic = "NOP"; return; + case "T": row.mnemonic = "BR" ; break; + default : row.mnemonic = "B" + cond; + } + } + + // Condition as operand + else { + if (!this.condCaps) + cond = cond.toLowerCase(); + row.operands.push(cond); + } + + // Operand as destination address + if (this.branchAddress) { + disp = Util.u32(row.address + disp & 0xFFFFFFFE) + .toString(16).padStart(8, "0"); + if (this.hexCaps) + disp = disp.toUpperCase(); + row.operands.push(disp); + } + + // Operand as displacement + else { + let sign = disp < 0 ? "-" : disp > 0 ? "+" : ""; + let rel = this.hex(Math.abs(disp), 1); + row.operands.push(sign + rel); + } + + } + + // Disassemble a Format IV instruction + formatIV(row, bits0, bits1) { + let disp = Util.signExtend(bits0 << 16 | bits1, 26); + + // Operand as destination address + if (this.branchAddress) { + disp = Util.u32(row.address + disp & 0xFFFFFFFE) + .toString(16).padStart(8, "0"); + if (this.hexCaps) + disp = disp.toUpperCase(); + row.operands.push(disp); + } + + // Operand as displacement + else { + let sign = disp < 0 ? "-" : disp > 0 ? "+" : ""; + let rel = this.hex(Math.abs(disp), 1); + row.operands.push(sign + rel); + } + + } + + // Disassemble a Format V instruction + formatV(row, bits0, bits1, signExtend) { + let imm = signExtend ? Util.signExtend(bits1) : bits1; + let reg1 = this.proreg(bits0 & 31); + let reg2 = this.proreg(bits0 >> 5 & 31); + + if ( + row.mnemonic == "ADDI" ? + Math.abs(imm) <= 256 ? + this.imm16AddiSmallHex : + this.imm16AddiLargeHex + : row.mnemonic == "MOVEA" || row.mnemonic == "MOVHI" ? + this.imm16MoveHex + : + this.imm16OtherHex + ) imm = this.hex(imm, 4); + + row.operands.push(imm, reg1, reg2); + } + + // Disassemble a Format VI instruction + formatVI(row, bits0, bits1) { + let disp = Util.signExtend(bits1); + let reg1 = this.proreg(bits0 & 31); + let reg2 = this.proreg(bits0 >> 5 & 31); + let sign = + disp < 0 ? "-" : + disp == 0 || !this.memoryInside ? "" : + "+"; + + // Displacement is hexadecimal + disp = Math.abs(disp); + if (disp == 0) + disp = "" + else if (disp <= 256 ? this.memorySmallHex : this.memoryLargeHex) + disp = this.hex(disp, 1); + + // Format the displacement figure according to its presentation + disp = this.memoryInside ? + sign == "" ? "" : " " + sign + " " + disp : + sign + disp + ; + + // Apply operands + row.operands.push(this.memoryInside ? + "[" + reg1 + disp + "]" : + disp + "[" + reg1 + "]", + reg2); + + // Swap operands for output and store instructions + switch (row.mnemonic) { + case "OUT.B": case "OUT.H": case "OUT.W": + case "ST.B" : case "ST.H" : case "ST.W" : + row.operands.reverse(); + } + + } + + // Disassemble a Format VII instruction + formatVII(row, bits0) { + let reg1 = this.proreg(bits0 & 31); + let reg2 = this.proreg(bits0 >> 5 & 31); + + // Invalid sub-opcode is zero-operand + if (row.mnemonic == "---") + return; + + // Processing by mnemonic + switch (row.mnemonic) { + case "XB": // Fallthrough + case "XH": break; + default : row.operands.push(reg1); + } + + row.operands.push(reg2); + } + + // Format an LDSR or STSR instruction + ldstsr(row, bits0, reverse) { + + // System register + let sysreg = bits0 & 31; + sysreg = this.sysregNames && SYSREGS[sysreg] || sysreg.toString(); + if (!this.sysregCaps) + sysreg = sysreg.toLowerCase(); + + // Program register + let reg2 = this.proreg(bits0 >> 5 & 31); + + // Operands + row.operands.push(sysreg, reg2); + if (reverse) + row.operands.reverse(); + } + + // Format a SETF instruction + setf(row, bits0) { + let cond = this.cond (bits0 & 15); + let reg2 = this.proreg(bits0 >> 5 & 31); + + // Condition merged with mnemonic + if (!this.bcondMerged) { + row.mnemonic += cond; + } + + // Condition as operand + else { + if (!this.condCaps) + cond = cond.toLowerCase(); + row.operands.push(cond); + } + + row.operands.push(reg2); + } + +} + + + +export { Disassembler }; diff --git a/app/app/Memory.js b/app/app/Memory.js new file mode 100644 index 0000000..be6603c --- /dev/null +++ b/app/app/Memory.js @@ -0,0 +1,467 @@ +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 }; diff --git a/app/app/RegisterList.js b/app/app/RegisterList.js new file mode 100644 index 0000000..2dfee16 --- /dev/null +++ b/app/app/RegisterList.js @@ -0,0 +1,863 @@ +import { Util } from /**/"./Util.js"; + + + +// Value types +const HEX = 0; +const SIGNED = 1; +const UNSIGNED = 2; +const FLOAT = 3; + +// System register indexes +const ADTRE = 25; +const CHCW = 24; +const ECR = 4; +const EIPC = 0; +const EIPSW = 1; +const FEPC = 2; +const FEPSW = 3; +const PC = -1; +const PIR = 6; +const PSW = 5; +const TKCW = 7; + +// Program register names +const PROREGS = { + [ 2]: "hp", + [ 3]: "sp", + [ 4]: "gp", + [ 5]: "tp", + [31]: "lp" +}; + +// System register names +const SYSREGS = { + [ADTRE]: "ADTRE", + [CHCW ]: "CHCW", + [ECR ]: "ECR", + [EIPC ]: "EIPC", + [EIPSW]: "EIPSW", + [FEPC ]: "FEPC", + [FEPSW]: "FEPSW", + [PC ]: "PC", + [PIR ]: "PIR", + [PSW ]: "PSW", + [TKCW ]: "TKCW", + [29 ]: "29", + [30 ]: "30", + [31 ]: "31" +}; + +// Expansion control types +const BIT = 0; +const INT = 1; + +// Produce a template object for register expansion controls +function ctrl(name, shift, size, disabled) { + return { + disabled: !!disabled, + name : name, + shift : shift, + size : size + }; +} + +// Program register epansion controls +const EXP_PROGRAM = [ + ctrl("cpu.hex" , true , HEX ), + ctrl("cpu.signed" , false, SIGNED ), + ctrl("cpu.unsigned", false, UNSIGNED), + ctrl("cpu.float" , false, FLOAT ) +]; + +// CHCW expansion controls +const EXP_CHCW = [ + ctrl("ICE", 1, 1) +]; + +// ECR expansion controls +const EXP_ECR = [ + ctrl("FECC", 16, 16), + ctrl("EICC", 0, 16) +]; + +// PIR expansion controls +const EXP_PIR = [ + ctrl("PT", 0, 16, true) +]; + +// PSW expansion controls +const EXP_PSW = [ + ctrl("CY", 3, 1), ctrl("FRO", 9, 1), + ctrl("OV", 2, 1), ctrl("FIV", 8, 1), + ctrl("S" , 1, 1), ctrl("FZD", 7, 1), + ctrl("Z" , 0, 1), ctrl("FOV", 6, 1), + ctrl("NP", 15, 1), ctrl("FUD", 5, 1), + ctrl("EP", 14, 1), ctrl("FPR", 4, 1), + ctrl("ID", 12, 1), ctrl("I" , 16, 4), + ctrl("AE", 13, 1) +]; + +// TKCW expansion controls +const EXP_TKCW = [ + ctrl("FIT", 7, 1, true), ctrl("FUT", 4, 1, true), + ctrl("FZT", 6, 1, true), ctrl("FPT", 3, 1, true), + ctrl("FVT", 5, 1, true), ctrl("OTM", 8, 1, true), + ctrl("RDI", 2, 1, true), ctrl("RD" , 0, 2, true) +]; + + + +/////////////////////////////////////////////////////////////////////////////// +// Register // +/////////////////////////////////////////////////////////////////////////////// + +// One register within a register list +class Register { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(list, index, andMask, orMask) { + + // Configure instance fields + this.andMask = andMask; + this.app = list.app; + this.controls = []; + this.dasm = list.dasm; + this.index = index; + this.isExpanded = null; + this.list = list; + this.orMask = orMask; + this.sim = list.sim; + this.system = list.system; + this.type = HEX; + this.value = 0x00000000; + + // Configure main controls + this.contents = new Toolkit.Component(this.app, { + className: "tk tk-contents", + tagName : "div", + style : { + display : "grid", + gridTemplateColumns: "max-content auto max-content" + } + }); + + // Processing by type + this[this.system ? "initSystem" : "initProgram"](); + + // Expansion button + this.btnExpand = new Toolkit.Component(this.app, { + className: "tk tk-expand tk-mono", + tagName : "div" + }); + this.list.view.append(this.btnExpand); + + // Name label + this.lblName = document.createElement("div"); + Object.assign(this.lblName, { + className: "tk tk-name", + id : Toolkit.id(), + innerText: this.dasm.sysregCaps?this.name:this.name.toLowerCase() + }); + this.lblName.style.userSelect = "none"; + this.list.view.append(this.lblName); + + // Value text box + this.txtValue = new Toolkit.TextBox(this.app, { + className: "tk tk-textbox tk-mono", + maxLength: 8 + }); + this.txtValue.setAttribute("aria-labelledby", this.lblName.id); + this.txtValue.setAttribute("digits", "8"); + this.txtValue.addEventListener("action", e=>this.onValue()); + this.list.view.append(this.txtValue); + + // Expansion area + if (this.expansion != null) + this.list.view.append(this.expansion); + + // Enable expansion function + if (this.expansion != null) { + let key = e=>this.expandKeyDown (e); + let pointer = e=>this.expandPointerDown(e); + this.btnExpand.setAttribute("aria-controls", this.expansion.id); + this.btnExpand.setAttribute("aria-labelledby", this.lblName.id); + this.btnExpand.setAttribute("role", "button"); + this.btnExpand.setAttribute("tabindex", "0"); + this.btnExpand.addEventListener("keydown" , key ); + this.btnExpand.addEventListener("pointerdown", pointer); + this.lblName .addEventListener("pointerdown", pointer); + this.setExpanded(this.system && this.index == PSW); + } + + // Expansion function is unavailable + else this.btnExpand.setAttribute("aria-hidden", "true"); + + } + + // Set up a program register + initProgram() { + this.name = PROREGS[this.index] || "r" + this.index.toString(); + this.initExpansion(EXP_PROGRAM); + } + + // Set up a system register + initSystem() { + this.name = SYSREGS[this.index] || this.index.toString(); + + switch (this.index) { + case CHCW : + this.initExpansion(EXP_CHCW); break; + case ECR : + this.initExpansion(EXP_ECR ); break; + case EIPSW: case FEPSW: case PSW: + this.initExpansion(EXP_PSW ); break; + case PIR : + this.initExpansion(EXP_PIR ); break; + case TKCW : + this.initExpansion(EXP_TKCW); break; + } + + } + + // Initialize expansion controls + initExpansion(controls) { + let two = this.index == ECR || this.index == PIR; + + // Establish expansion element + let exp = this.expansion = new Toolkit.Component(this.app, { + className: "tk tk-expansion", + id : Toolkit.id(), + tagName : "div", + style : { + display : "none", + gridColumnEnd : "span 3", + gridTemplateColumns: + this.system ? "repeat(2, max-content)" : "max-content" + } + }); + + // Produce program register controls + if (!this.system) { + let group = new Toolkit.Group(); + exp.append(group); + + // Process all controls + for (let template of controls) { + + // Create control + let ctrl = new Toolkit.Radio(this.app, { + group : group, + selected: template.shift, + text : template.name + }); + ctrl.format = template.size; + + // Configure event handler + ctrl.addEventListener("action", + e=>this.setType(e.component.format)); + + // Add the control to the element + let box = document.createElement("div"); + box.append(ctrl.element); + exp.append(box); + } + + return; + } + + // Process all control templates + for (let template of controls) { + let box, ctrl; + + // Not using an inner two-column layout + if (!two) + exp.append(box = document.createElement("div")); + + // Bit check box + if (template.size == 1) { + box.classList.add("tk-bit"); + + // Create control + ctrl = new Toolkit.CheckBox(this.app, { + text : "name", + substitutions: { name: template.name } + }); + ctrl.mask = 1 << template.shift; + box.append(ctrl.element); + + // Disable control + if (template.disabled) + ctrl.setEnabled(false); + + // Configure event handler + ctrl.addEventListener("action", e=>this.onBit(e.component)); + } + + // Number text box + else { + if (!two) + box.classList.add("tk-number"); + + // Create label + let label = document.createElement("label"); + Object.assign(label, { + className: "tk tk-label", + innerText: template.name, + }); + if (!two) Object.assign(box.style, { + columnGap : "2px", + display : "grid", + gridTemplateColumns: "max-content auto" + }); + (two ? exp : box).append(label); + + // Create control + ctrl = new Toolkit.TextBox(this.app, { + id : Toolkit.id(), + style: { height: "1em" } + }); + label.htmlFor = ctrl.id; + (two ? exp : box).append(ctrl.element); + + // Control is a hex field + if (template.size == 16) { + ctrl.element.classList.add("tk-mono"); + ctrl.setAttribute("digits", 4); + ctrl.setMaxLength(4); + } + + // Disable control + if (template.disabled) { + ctrl.setEnabled(false); + (two ? label : box).setAttribute("disabled", "true"); + } + + // Configure event handler + ctrl.addEventListener("action", e=>this.onNumber(e.component)); + } + + Object.assign(ctrl, template); + this.controls.push(ctrl); + } + + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Expand button key press + expandKeyDown(e) { + + // Processing by key + switch (e.key) { + case "Enter": + case " ": + this.setExpanded(!this.isExpanded); + break; + default: return; + } + + // Configure event + e.stopPropagation(); + e.preventDefault(); + } + + // Expand button pointer down + expandPointerDown(e) { + + // Focus management + this.btnExpand.focus(); + + // Error checking + if (e.button != 0) + return; + + // Configure event + e.stopPropagation(); + e.preventDefault(); + + // Configure expansion area + this.setExpanded(!this.isExpanded); + } + + // Expansion bit check box + onBit(ctrl) { + this.setValue(ctrl.isSelected ? + this.value | ctrl.mask : + this.value & Util.u32(~ctrl.mask) + ); + } + + // Expansion number text box + onNumber(ctrl) { + let mask = (1 << ctrl.size) - 1 << ctrl.shift; + let value = parseInt(ctrl.getText(), ctrl.size == 16 ? 16 : 10); + this.setValue(isNaN(value) ? this.value : + this.value & Util.u32(~mask) | value << ctrl.shift & mask); + } + + // Register value + onValue() { + let text = this.txtValue.getText(); + let value; + + // Processing by type + switch (this.type) { + + // Unsigned hexadecimal + case HEX: + value = parseInt(text, 16); + break; + + // Decimal + case SIGNED: + case UNSIGNED: + value = parseInt(text); + break; + + // Float + case FLOAT: + value = parseFloat(text); + if (isNaN(value)) + break; + value = Util.fromF32(value); + break; + } + + // Assign the new value + this.setValue(isNaN(value) ? this.value : value); + } + + // Specify whether the expansion area is visible + setExpanded(expanded) { + expanded = !!expanded; + + // Error checking + if (this.expansion == null || expanded === this.isExpanded) + return; + + // Configure instance fields + this.isExpanded = expanded; + + // Configure elements + let key = expanded ? "common.collapse" : "common.expand"; + this.btnExpand.setAttribute("aria-expanded", expanded); + this.btnExpand.setToolTip(key); + this.expansion.element.style.display = + expanded ? "inline-grid" : "none"; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Disassembler settings have been updated + dasmChanged() { + let dasm = this.list.dasm; + let name = this.name; + + // Program register name + if (!this.system) { + if (!dasm.proregNames) + name = "r" + this.index.toString(); + if (dasm.proregCaps) + name = name.toUpperCase(); + } + + // System register name + else { + if (!dasm.sysregCaps) + name = name.toLowerCase(); + } + + // Common processing + this.lblName.innerText = name; + this.refresh(this.value); + } + + // Update the value returned from the core + refresh(value) { + let text; + + // Configure instance fields + this.value = value; + + // Value text box + if (this.type == HEX) { + this.txtValue.element.classList.add("tk-mono"); + this.txtValue.setMaxLength(8); + } else { + this.txtValue.element.classList.remove("tk-mono"); + this.txtValue.setMaxLength(null); + } + switch (this.type) { + + // Unsigned hexadecimal + case HEX: + text = value.toString(16).padStart(8, "0"); + if (this.dasm.hexCaps) + text = text.toUpperCase(); + break; + + // Signed decimal + case SIGNED: + text = Util.s32(value).toString(); + break; + + // Unsigned decial + case UNSIGNED: + text = Util.u32(value).toString(); + break; + + // Float + case FLOAT: + if ((value & 0x7F800000) != 0x7F800000) { + text = Util.toF32(value).toFixed(5).replace(/0+$/, ""); + if (text.endsWith(".")) + text += "0"; + } else text = "NaN"; + break; + } + this.txtValue.setText(text); + + // No further processing for program registers + if (!this.system) + return; + + // Process all expansion controls + for (let ctrl of this.controls) { + + // Bit check box + if (ctrl.size == 1) { + ctrl.setSelected(value & ctrl.mask); + continue; + } + + // Integer text box + text = value >> ctrl.shift & (1 << ctrl.size) - 1; + text = ctrl.size != 16 ? text.toString() : + text.toString(16).padStart(4, "0"); + if (this.dasm.hexCaps) + text = text.toUpperCase(); + ctrl.setText(text); + } + + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Specify the formatting type of the register value + setType(type) { + if (type == this.type) + return; + this.type = type; + this.refresh(this.value); + } + + // Specify a new value for the register + async setValue(value) { + + // Update the display with the new value immediately + value = Util.u32(value & this.andMask | this.orMask); + let matched = value == this.value; + this.refresh(value); + if (matched) + return; + + // Update the new value in the core + let options = { refresh: true }; + this.refresh(await ( + !this.system ? + this.sim.setProgramRegister(this.index, value, options) : + this.index == PC ? + this.sim.setProgramCounter ( value, options) : + this.sim.setSystemRegister (this.index, value, options) + )); + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// +// RegisterList // +/////////////////////////////////////////////////////////////////////////////// + +// Scrolling list of registers +class RegisterList extends Toolkit.ScrollPane { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(debug, system) { + super(debug.app, { + className: "tk tk-scrollpane tk-reglist " + + (system ? "tk-system" : "tk-program"), + vertical : Toolkit.ScrollPane.ALWAYS + }); + + // Configure instance fields + this.app = debug.app; + this.dasm = debug.disassembler; + this.method = system?"getSystemRegisters":"getProgramRegisters"; + this.sim = debug.sim; + this.subscription = system ? "sysregs" : "proregs"; + this.system = system; + + // Configure view element + this.setView(new Toolkit.Component(debug.app, { + className: "tk tk-list", + tagName : "div", + style : { + display : "grid", + gridTemplateColumns: "max-content auto max-content" + } + })); + + // Font-measuring element + let text = ""; + for (let x = 0; x < 16; x++) { + if (x != 0) text += "\n"; + let digit = x.toString(16); + text += digit + "\n" + digit.toUpperCase(); + } + this.metrics = new Toolkit.Component(this.app, { + className: "tk tk-mono", + tagName : "div", + style : { + position : "absolute", + visibility: "hidden" + } + }); + this.metrics.element.innerText = text; + this.metrics.addEventListener("resize", e=>this.onMetrics()); + this.viewport.append(this.metrics.element); + + // Processing by type + this[system ? "initSystem" : "initProgram"](); + + // Configure component + this.addEventListener("keydown", e=>this.onKeyDown (e)); + this.addEventListener("wheel" , e=>this.onMouseWheel(e)); + } + + // Initialize a list of program registers + initProgram() { + this[0] = new Register(this, 0, 0x00000000, 0x00000000); + for (let x = 1; x < 32; x++) + this[x] = new Register(this, x, 0xFFFFFFFF, 0x00000000); + } + + // Initialie a list of system registers + initSystem() { + this[PC ] = new Register(this, PC , 0xFFFFFFFE, 0x00000000); + this[PSW ] = new Register(this, PSW , 0x000FF3FF, 0x00000000); + this[ADTRE] = new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000); + this[CHCW ] = new Register(this, CHCW , 0x00000002, 0x00000000); + this[ECR ] = new Register(this, ECR , 0xFFFFFFFF, 0x00000000); + this[EIPC ] = new Register(this, EIPC , 0xFFFFFFFE, 0x00000000); + this[EIPSW] = new Register(this, EIPSW, 0x000FF3FF, 0x00000000); + this[FEPC ] = new Register(this, FEPC , 0xFFFFFFFE, 0x00000000); + this[FEPSW] = new Register(this, FEPSW, 0x000FF3FF, 0x00000000); + this[PIR ] = new Register(this, PIR , 0x00000000, 0x00005346); + this[TKCW ] = new Register(this, TKCW , 0x00000000, 0x000000E0); + this[29 ] = new Register(this, 29 , 0xFFFFFFFF, 0x00000000); + this[30 ] = new Register(this, 30 , 0x00000000, 0x00000004); + this[31 ] = new Register(this, 31 , 0xFFFFFFFF, 0x00000000); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Key press + onKeyDown(e) { + + // Processing by key + switch (e.key) { + case "ArrowDown": + this.vertical.setValue(this.vertical.value + + this.vertical.increment); + break; + case "ArrowLeft": + this.horizontal.setValue(this.horizontal.value - + this.horizontal.increment); + break; + case "ArrowRight": + this.horizontal.setValue(this.horizontal.value + + this.horizontal.increment); + break; + case "ArrowUp": + this.vertical.setValue(this.vertical.value - + this.vertical.increment); + break; + case "PageDown": + this.vertical.setValue(this.vertical.value + + this.vertical.extent); + break; + case "PageUp": + this.vertical.setValue(this.vertical.value - + this.vertical.extent); + break; + default: return; + } + + // Configure event + e.stopPropagation(); + e.preventDefault(); + } + + // Metrics element resized + onMetrics() { + + // Error checking + if (!this.metrics) + return; + + // Measure the dimensions of one hex character + let bounds = this.metrics.getBounds(); + if (bounds.height <= 0) + return; + let width = Math.ceil(bounds.width); + let height = Math.ceil(bounds.height / 32); + + // Resize all monospaced text boxes + for (let box of this.element + .querySelectorAll(".tk-textbox[digits]")) { + Object.assign(box.style, { + height: height + "px", + width : (parseInt(box.getAttribute("digits")) * width) + "px" + }); + } + + // Update scroll bars + this.horizontal.setIncrement(height); + this.vertical .setIncrement(height); + } + + // Mouse wheel + onMouseWheel(e) { + + // User agent scaling action + if (e.ctrlKey) + return; + + // No rotation has occurred + let offset = Math.sign(e.deltaY) * 3; + if (offset == 0) + return; + + // Update the display address + this.vertical.setValue(this.vertical.value + + this.vertical.increment * offset); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Update with CPU state from the core + refresh(registers) { + + // System registers + if (this.system) { + this[ADTRE].refresh(registers.adtre); + this[CHCW ].refresh(registers.chcw ); + this[ECR ].refresh(registers.ecr ); + this[EIPC ].refresh(registers.eipc ); + this[EIPSW].refresh(registers.eipsw); + this[FEPC ].refresh(registers.fepc ); + this[FEPSW].refresh(registers.fepsw); + this[PC ].refresh(registers.pc ); + this[PIR ].refresh(registers.pir ); + this[PSW ].refresh(registers.psw ); + this[TKCW ].refresh(registers.tkcw ); + this[29 ].refresh(registers[29] ); + this[30 ].refresh(registers[30] ); + this[31 ].refresh(registers[31] ); + } + + // Program registers + else for (let x = 0; x < 32; x++) + this[x].refresh(registers[x]); + } + + // 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(); + + // Unsubscribe from core updates + else this.sim.unsubscribe(this.subscription); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Disassembler settings have been updated + dasmChanged() { + if (this.system) { + for (let key of Object.keys(SYSREGS)) + this[key].dasmChanged(); + } else { + for (let key = 0; key < 32; key++) + this[key].dasmChanged(); + } + } + + // Determine the initial size of the register list + getPreferredSize() { + let ret = { + height: 0, + width : 0 + }; + + // Error checking + if (!this.view) + return ret; + + // Measure the view element + ret.width = this.view.element.scrollWidth; + + // Locate the bottom of PSW + if (this.system && this[PSW].expansion) { + ret.height = this[PSW].expansion.getBounds().bottom - + this.view.getBounds().top; + } + + return ret; + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Retrieve CPU state from the core + async fetch() { + this.refresh( + await this.sim[this.method]({ + subscribe: this.isSubscribed && this.subscription + }) + ); + } + +} + + + +export { RegisterList }; diff --git a/app/app/Util.js b/app/app/Util.js new file mode 100644 index 0000000..243ef03 --- /dev/null +++ b/app/app/Util.js @@ -0,0 +1,44 @@ +let F32 = new Float32Array( 1); +let S32 = new Int32Array (F32.buffer, 0, 1); +let U32 = new Uint32Array (F32.buffer, 0, 1); + +// Interpret a floating short as a 32-bit integer +function fromF32(x) { + F32[0] = x; + return S32[0]; +} + +// Interpret a 32-bit integer as a floating short +function toF32(x) { + S32[0] = x; + return F32[0]; +} + +// Represent a value as a signed 32-bit integer +function s32(x) { + S32[0] = x; + return S32[0]; +} + +// Sign-extend a value with a given number of bits +function signExtend(value, bits) { + bits = 32 - bits; + S32[0] = value << bits; + return S32[0] >> bits; +} + +// Represent a value as an unsigned 32-bit integer +function u32(x) { + U32[0] = x; + return U32[0]; +} + + + +export let Util = { + fromF32 : fromF32, + toF32 : toF32, + s32 : s32, + signExtend: signExtend, + u32 : u32 +}; diff --git a/app/core/Core.js b/app/core/Core.js new file mode 100644 index 0000000..b56a341 --- /dev/null +++ b/app/core/Core.js @@ -0,0 +1,196 @@ +import { Sim } from /**/"./Sim.js"; + +let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString(); + +let RESTRICT = {}; +let WASM_URL = url(/**/"./core.wasm" ); +let WORKER_URL = url(/**/"./CoreWorker.js"); + + + +/////////////////////////////////////////////////////////////////////////////// +// Core // +/////////////////////////////////////////////////////////////////////////////// + +// Environment manager for simulated Virtual Boys +class Core { + + //////////////////////////////// Constants //////////////////////////////// + + // States + static IDLE = 0; + static RUNNING = 1; + + + + ///////////////////////////// Static Methods ////////////////////////////// + + // Create a new instance of Core + static create(options) { + return new Core(RESTRICT).init(options); + } + + + + ///////////////////////// Initialization Methods ////////////////////////// + + // Stub constructor + constructor(restrict) { + if (restrict != RESTRICT) { + throw "Cannot instantiate Core directly. " + + "Use Core.create() instead."; + } + } + + // Substitute constructor + async init(options = {}) { + + // Configure instance fields + this.length = 0; + this.onsubscriptions = null; + this.resolutions = []; + this.state = Core.IDLE; + this.worker = new Worker(WORKER_URL); + this.worker.onmessage = e=>this.onMessage(e.data); + + // Issue a create command + if ("sims" in options) + await this.create(options.sims, WASM_URL); + + // Only initialize the WebAssembly module + else this.send("init", false, { wasm: WASM_URL }); + + return this; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Worker message received + onMessage(data) { + + // Process a promised response + if ("response" in data) + this.resolutions.shift()(data.response); + + // Process subscriptions + if (this.onsubscriptions && data.subscriptions) + this.onsubscriptions(data.subscriptions); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Associate two simulations as peers, or remove an association + connect(a, b, options = {}) { + return this.send({ + command: "connect", + respond: !("respond" in options) || !!options.respond, + sims : [ a, b ] + }); + } + + // Create and initialize new simulations + async create(sims, wasm) { + let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0); + + // Execute the command in the core thread + let response = await this.send({ + command: "create", + sims : numSims, + wasm : wasm + }); + + // Process the core thread's response + let ret = []; + for (let x = 0; x < numSims; x++, this.length++) + ret.push(this[this.length] = + new Sim(this, response[x], this.length)); + return sims === undefined ? ret[0] : ret; + } + + // Delete a simulation + destroy(sim, options = {}) { + + // Configure simulation + sim = this[sim] || sim; + if (sim.core != this) + return; + let ptr = sim.destroy(); + + // State management + for (let x = sim.index + 1; x < this.length; x++) + (this[x - 1] = this[x]).index--; + delete this[--this.length]; + + // Execute the command on the core thread + return this.send({ + command: "destroy", + respond: !("respond" in options) || !!options.respond, + sim : ptr + }); + } + + // Attempt to run until the next instruction + runNext(a, b, options = {}) { + return this.send({ + command: "runNext", + refresh: !!options.refresh, + respond: !("respond" in options) || !!options.respond, + sims : [ a, b ] + }); + } + + // Execute one instruction + singleStep(a, b, options = {}) { + return this.send({ + command: "singleStep", + refresh: !!options.refresh, + respond: !("respond" in options) || !!options.respond, + sims : [ a, b ] + }); + } + + // Unsubscribe from frame data + unsubscribe(key, sim = 0) { + this.send({ + command: "unsubscribe", + key : key, + respond: false, + sim : sim + }); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Send a message to the Worker + send(data = {}, transfers = []) { + + // Create the message object + Object.assign(data, { + respond: !("respond" in data) || !!data.respond, + run : !("run" in data) || !!data.run + }); + + // Do not wait on a response + if (!data.respond) + this.worker.postMessage(data, transfers); + + // Wait for the response to come back + else return new Promise((resolve, reject)=>{ + this.resolutions.push(response=>resolve(response)); + this.worker.postMessage(data, transfers); + }); + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// + +export { Core }; diff --git a/app/core/CoreWorker.js b/app/core/CoreWorker.js new file mode 100644 index 0000000..b8989e4 --- /dev/null +++ b/app/core/CoreWorker.js @@ -0,0 +1,378 @@ +"use strict"; + + + +// Un-sign a 32-bit integer +// Emscripten is sign-extending uint32_t and Firefox can't import in Workers +let u32 = (()=>{ + let U32 = new Uint32Array(1); + return x=>{ U32[0] = x; return U32[0]; }; +})(); + + + +/////////////////////////////////////////////////////////////////////////////// +// CoreWorker // +/////////////////////////////////////////////////////////////////////////////// + +// Thread manager for Core commands +new class CoreWorker { + + ///////////////////////// Initialization Methods ////////////////////////// + + // Stub constructor + constructor() { + onmessage = async e=>{ + await this.init(e.data.wasm); + onmessage = e=>this.onCommand(e.data, false); + onmessage(e); + }; + } + + // Substitute constructor + async init(wasm) { + + // Load the WebAssembly module + let imports = { + env: { emscripten_notify_memory_growth: ()=>this.onMemory() } + }; + this.wasm = await (typeof wasm == "string" ? + WebAssembly.instantiateStreaming(fetch(wasm), imports) : + WebAssembly.instantiate ( wasm , imports) + ); + + // Configure instance fields + this.api = this.wasm.instance.exports; + this.frameData = null; + this.isRunning = false; + this.memory = this.api.memory.buffer; + this.ptrSize = this.api.PointerSize(); + this.ptrType = this.ptrSize == 4 ? Uint32Array : Uint64Array; + this.subscriptions = {}; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Message from audio thread + onAudio(frames) { + } + + // Message from main thread + onCommand(data) { + + // Subscribe to the command + if (data.subscribe) { + let sub = data.sim || 0; + sub = this.subscriptions[sub] || (this.subscriptions[sub] = {}); + sub = sub[data.subscribe] = {}; + Object.assign(sub, data); + delete sub.promised; + delete sub.run; + delete sub.subscribe; + } + + // Execute the command + if (data.run) + this[data.command](data); + + // Process all subscriptions to refresh any debugging interfaces + if (data.refresh) + this.doSubscriptions(data.sim ? [ data.sim ] : data.sims); + + // Reply to the main thread + if (data.respond) { + postMessage({ + response: data.response + }, data.transfers); + } + } + + // Memory growth + onMemory() { + this.memory = this.api.memory.buffer; + } + + + + //////////////////////////////// Commands ///////////////////////////////// + + // Associate two simulations as peers, or remove an association + connect(data) { + this.api.vbConnect(data.sims[0], data.sims[1]); + } + + // Allocate and initialize a new simulation + create(data) { + let ptr = this.api.Create(data.sims); + data.response = new this.ptrType(this.memory, ptr, data.sims).slice(); + data.transfers = [ data.response.buffer ]; + this.api.Free(ptr); + } + + // Delete a simulation + destroy(data) { + this.api.Destroy(data.sim); + } + + // Locate instructions for disassembly + disassemble(data) { + let decode; // Address of next row + let index; // Index in list of next row + let rows = new Array(data.rows); + let pc = u32(this.api.vbGetProgramCounter(data.sim)); + let row; // Located output row + + // The target address is before or on the first row of output + if (data.row <= 0) { + decode = u32(data.target - 4 * Math.max(0, data.row + 10)); + + // Locate the target row + for (;;) { + row = this.dasmRow(data.sim, decode, pc); + if (u32(data.target - decode) < row.size) + break; + decode = u32(decode + row.size); + } + + // Locate the first row of output + for (index = data.row; index < 0; index++) { + decode = u32(decode + row.size); + row = this.dasmRow(data.sim, decode, pc); + } + + // Prepare to process remaining rows + decode = u32(decode + row.size); + rows[0] = row; + index = 1; + } + + // The target address is after the first row of output + else { + let circle = new Array(data.row + 1); + let count = Math.min(data.row + 1, data.rows); + let src = 0; + decode = u32(data.target - 4 * (data.row + 10)); + + // Locate the target row + for (;;) { + row = circle[src] = this.dasmRow(data.sim, decode, pc); + decode = u32(decode + row.size); + if (u32(data.target - row.address) < row.size) + break; + src = (src + 1) % circle.length; + } + + // Copy entries from the circular buffer to the output list + for (index = 0; index < count; index++) { + src = (src + 1) % circle.length; + rows[index] = circle[src]; + } + + } + + // Locate any remaining rows + for (; index < data.rows; index++) { + let row = rows[index] = this.dasmRow(data.sim, decode, pc); + decode = u32(decode + row.size); + } + + // Respond to main thread + data.response = { + pc : pc, + rows : rows, + scroll: data.scroll + }; + } + + // Retrieve all CPU program registers + getProgramRegisters(data) { + let ret = data.response = new Uint32Array(32); + for (let x = 0; x < 32; x++) + ret[x] = this.api.vbGetProgramRegister(data.sim, x); + data.transfers = [ ret.buffer ]; + } + + // Retrieve the value of a system register + getSystemRegister(data) { + data.response = u32(this.api.vbGetSystemRegister(data.sim, data.id)); + } + + // Retrieve all CPU system registers (including PC) + getSystemRegisters(data) { + data.response = { + adtre: u32(this.api.vbGetSystemRegister(data.sim, 25)), + chcw : u32(this.api.vbGetSystemRegister(data.sim, 24)), + ecr : u32(this.api.vbGetSystemRegister(data.sim, 4)), + eipc : u32(this.api.vbGetSystemRegister(data.sim, 0)), + eipsw: u32(this.api.vbGetSystemRegister(data.sim, 1)), + fepc : u32(this.api.vbGetSystemRegister(data.sim, 2)), + fepsw: u32(this.api.vbGetSystemRegister(data.sim, 3)), + pc : u32(this.api.vbGetProgramCounter(data.sim )), + pir : u32(this.api.vbGetSystemRegister(data.sim, 6)), + psw : u32(this.api.vbGetSystemRegister(data.sim, 5)), + tkcw : u32(this.api.vbGetSystemRegister(data.sim, 7)), + [29] : u32(this.api.vbGetSystemRegister(data.sim, 29)), + [30] : u32(this.api.vbGetSystemRegister(data.sim, 30)), + [31] : u32(this.api.vbGetSystemRegister(data.sim, 31)) + }; + } + + // Read bytes from the simulation + read(data) { + let ptr = this.api.Malloc(data.length); + this.api.ReadBuffer(data.sim, ptr, data.address, data.length); + let buffer = new Uint8Array(this.memory, ptr, data.length).slice(); + this.api.Free(ptr); + data.response = { + address: data.address, + bytes : buffer + }; + data.transfers = [ buffer.buffer ]; + } + + // Attempt to execute until the following instruction + runNext(data) { + this.api.RunNext(data.sims[0], data.sims[1]); + let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ]; + if (data.sims[1]) + pc.push(u32(this.api.vbGetProgramCounter(data.sims[1]))); + data.response = { + pc: pc + } + } + + // Specify a new value for PC + setProgramCounter(data) { + data.response = + u32(this.api.vbSetProgramCounter(data.sim, data.value)); + } + + // Specify a new value for a program register + setProgramRegister(data) { + data.response = this.api.vbSetProgramRegister + (data.sim, data.id, data.value); + } + + // Specify a ROM buffer + setROM(data) { + let ptr = this.api.Malloc(data.rom.length); + let buffer = new Uint8Array(this.memory, ptr, data.rom.length); + for (let x = 0; x < data.rom.length; x++) + buffer[x] = data.rom[x]; + data.response = !!this.api.SetROM(data.sim, ptr, data.rom.length); + } + + // Specify a new value for a system register + setSystemRegister(data) { + data.response = u32(this.api.vbSetSystemRegister + (data.sim, data.id, data.value)); + } + + // Execute one instruction + singleStep(data) { + this.api.SingleStep(data.sims[0], data.sims[1]); + let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ]; + if (data.sims[1]) + pc.push(u32(this.api.vbGetProgramCounter(data.sims[1]))); + data.response = { + pc: pc + } + } + + // Unsubscribe from frame data + unsubscribe(data) { + let sim = data.sim || 0; + if (sim in this.subscriptions) { + let subs = this.subscriptions[sim]; + delete subs[data.key]; + if (Object.keys(subs).length == 0) + delete this.subscriptions[sim]; + } + } + + // Write bytes to the simulation + write(data) { + let ptr = this.api.Malloc(data.bytes.length); + let buffer = new Uint8Array(this.memory, ptr, data.bytes.length); + for (let x = 0; x < data.bytes.length; x++) + buffer[x] = data.bytes[x]; + this.api.WriteBuffer(data.sim, ptr, data.address, data.bytes.length); + this.api.Free(ptr); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Retrieve basic information for a row of disassembly + dasmRow(sim, address, pc) { + let bits = this.api.vbRead(sim, address, 3 /* VB_U16 */); + let opcode = bits >> 10 & 63; + let size = ( + opcode < 0b101000 || // Formats I through III + opcode == 0b110010 || // Illegal + opcode == 0b110110 // Illegal + ) ? 2 : 4; + + // Establish row information + let row = { + address: address, + bytes : [ bits & 0xFF, bits >> 8 ], + size : u32(address + 2) == pc ? 2 : size + //size : Math.min(u32(pc - address), size) + }; + + // Read additional bytes + if (size == 4) { + bits = this.api.vbRead(sim, address + 2, 3 /* VB_U16 */); + row.bytes.push(bits & 0xFF, bits >> 8); + } + + return row; + } + + // Process subscriptions and send a message to the main thread + doSubscriptions(sims) { + let message = { subscriptions: {} }; + let transfers = []; + + // Process all simulations + for (let sim of sims) { + + // There are no subscriptions for this sim + if (!(sim in this.subscriptions)) + continue; + + // Working variables + let subs = message.subscriptions[sim] = {}; + + // Process all subscriptions + for (let sub of Object.entries(this.subscriptions[sim])) { + + // Run the command + this[sub[1].command](sub[1]); + + // Add the response to the message + if (sub[1].response) { + subs[sub[0]] = sub[1].response; + delete sub[1].response; + } + + // Add the transferable objects to the message + if (sub[1].transfers) { + transfers.push(... sub[1].transfers); + delete sub[1].transfers; + } + + } + + } + + // Send the message to the main thread + if (Object.keys(message).length != 0) + postMessage(message, transfers); + } + +}(); diff --git a/app/core/Sim.js b/app/core/Sim.js new file mode 100644 index 0000000..f7d6928 --- /dev/null +++ b/app/core/Sim.js @@ -0,0 +1,151 @@ +/////////////////////////////////////////////////////////////////////////////// +// Sim // +/////////////////////////////////////////////////////////////////////////////// + +// One simulated Virtual Boy +class Sim { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(core, sim, index) { + this.core = core; + this.index = index; + this.sim = sim; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Locate CPU instructions + disassemble(target, row, rows, scroll, options = {}) { + return this.core.send({ + command : "disassemble", + row : row, + rows : rows, + scroll : scroll, + sim : this.sim, + subscribe: options.subscribe, + target : target + }); + } + + // Retrieve all CPU program registers + getProgramRegisters(options = {}) { + return this.core.send({ + command : "getProgramRegisters", + sim : this.sim, + subscribe: options.subscribe + }); + } + + // Retrieve the value of a system register + getSystemRegister(id, options = {}) { + return this.core.send({ + command : "getSystemRegister", + id : id, + sim : this.sim, + subscribe: options.subscribe + }); + } + + // Retrieve all CPU system registers (including PC) + getSystemRegisters(options = {}) { + return this.core.send({ + command : "getSystemRegisters", + sim : this.sim, + subscribe: options.subscribe + }); + } + + // Read multiple bytes from the bus + read(address, length, options = {}) { + return this.core.send({ + address : address, + command : "read", + debug : !("debug" in options) || !!options.debug, + length : length, + sim : this.sim, + subscribe: options.subscribe + }); + } + + // Specify a new value for PC + setProgramCounter(value, options = {}) { + return this.core.send({ + command: "setProgramCounter", + refresh: !!options.refresh, + sim : this.sim, + value : value + }); + } + + // Specify a new value for a program register + setProgramRegister(id, value, options = {}) { + return this.core.send({ + command: "setProgramRegister", + id : id, + refresh: !!options.refresh, + sim : this.sim, + value : value + }); + } + + // Specify the current ROM buffer + setROM(rom, options = {}) { + return this.core.send({ + command: "setROM", + rom : rom, + refresh: !!options.refresh, + sim : this.sim + }, [ rom.buffer ]); + } + + // Specify a new value for a system register + setSystemRegister(id, value, options = {}) { + return this.core.send({ + command: "setSystemRegister", + id : id, + refresh: !!options.refresh, + sim : this.sim, + value : value + }); + } + + // Ubsubscribe from frame data + unsubscribe(key) { + return this.core.unsubscribe(key, this.sim); + } + + // Write multiple bytes to the bus + write(address, bytes, options = {}) { + return this.core.send({ + address : address, + command : "write", + bytes : bytes, + debug : !("debug" in options) || !!options.debug, + refresh : !!options.refresh, + sim : this.sim, + subscribe: options.subscribe + }, + bytes instanceof ArrayBuffer ? [ bytes ] : + bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] : + undefined + ); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // The simulation has been destroyed + destroy() { + let sim = this.sim; + this.core = null; + this.sim = 0; + return sim; + } + +} + +export { Sim }; diff --git a/app/locale/en-US.js b/app/locale/en-US.js deleted file mode 100644 index 7cac528..0000000 --- a/app/locale/en-US.js +++ /dev/null @@ -1,44 +0,0 @@ -{ - key : "en-US", - name: "English (United States)", - app : { - close : "Close", - console : "Console", - goto_ : "Enter the address to seek to:", - romLoaded : "Successfully loaded file \"{filename}\" ({size})", - romNotVB : "The selected file is not a Virtual Boy ROM.", - readFileError: "Unable to read the selected file." - }, - cpu: { - _ : "CPU", - disassembler: "Disassembler", - float_ : "Float", - hex : "Hex", - pcFrom : "From", - pcTo : "To", - mainSplit : "Disassembler and registers splitter", - regsSplit : "System registers and program registers splitter", - signed : "Signed", - unsigned : "Unsigned" - }, - memory: { - _ : "Memory", - hexEditor: "Hex viewer" - }, - menu: { - _ : "Main application menu", - debug: { - _: "Debug", - }, - file : { - _ : "File", - loadROM: "Load ROM..." - }, - theme: { - _ : "Theme", - dark : "Dark", - light : "Light", - virtual: "Virtual" - } - } -} diff --git a/app/locale/en-US.json b/app/locale/en-US.json new file mode 100644 index 0000000..ab06e98 --- /dev/null +++ b/app/locale/en-US.json @@ -0,0 +1,74 @@ +{ + "id": "en-US", + + "common": { + "close" : "Close", + "collapse" : "Collapse", + "expand" : "Expand", + "gotoPrompt": "Enter the address to go to:" + }, + + "error": { + "fileRead": "An error occurred when reading the file.", + "romNotVB": "The selected file is not a Virtual Boy ROM." + }, + + "app": { + "title": "Virtual Boy Emulator", + + "menu": { + "_": "Application menu bar", + + "file": { + "_" : "File", + "loadROM" : "Load ROM{sim}...", + "debugMode": "Debug mode" + }, + + "emulation": { + "_" : "Emulation", + "run" : "Run", + "reset" : "Reset", + "dualSims": "Dual sims", + "linkSims": "Link sims" + }, + + "debug": { + "_" : "Debug{sim}", + "console" : "Console", + "memory" : "Memory", + "cpu" : "CPU", + "breakpoints" : "Breakpoints", + "palettes" : "Palettes", + "characters" : "Characters", + "bgMaps" : "BG maps", + "objects" : "Objects", + "worlds" : "Worlds", + "frameBuffers": "Frame buffers" + }, + + "theme": { + "_" : "Theme", + "light" : "Light", + "dark" : "Dark", + "virtual": "Virtual" + } + + } + + }, + + "cpu": { + "_" : "CPU{sim}", + "float" : "Float", + "hex" : "Hex", + "signed" : "Signed", + "splitHorizontal": "Program and system registers splitter", + "splitVertical" : "Disassembler and registers splitter", + "unsigned" : "Unsigned" + }, + + "memory": { + "_": "Memory{sim}" + } +} diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..55ea892 --- /dev/null +++ b/app/main.js @@ -0,0 +1,34 @@ +// Global theme assets +Bundle["app/theme/kiosk.css"].installStylesheet(true); +await Bundle["app/theme/inconsolata.woff2"].installFont( + "Inconsolata SemiExpanded Medium"); +await Bundle["app/theme/roboto.woff2"].installFont("Roboto"); +Bundle["app/theme/check.svg" ].installImage("tk-check" , "check.svg" ); +Bundle["app/theme/close.svg" ].installImage("tk-close" , "close.svg" ); +Bundle["app/theme/collapse.svg"].installImage("tk-collapse", "collapse.svg"); +Bundle["app/theme/expand.svg" ].installImage("tk-expand" , "expand.svg" ); +Bundle["app/theme/radio.svg" ].installImage("tk-radio" , "radio.svg" ); +Bundle["app/theme/scroll.svg" ].installImage("tk-scroll" , "scroll.svg" ); + +// Module imports +import { Core } from /**/"./core/Core.js"; +import { Toolkit } from /**/"./toolkit/Toolkit.js"; +import { App } from /**/"./app/App.js"; + +// Begin application +let dark = matchMedia("(prefers-color-scheme: dark)").matches; +let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString(); +new App({ + core : await Core.create({ sims: 2 }), + locale : navigator.language, + standalone: true, + theme : dark ? "dark" : "light", + locales : [ + await (await fetch(url(/**/"./locale/en-US.json"))).json() + ], + themes : { + dark : Bundle["app/theme/dark.css" ].installStylesheet( dark), + light : Bundle["app/theme/light.css" ].installStylesheet(!dark), + virtual: Bundle["app/theme/virtual.css"].installStylesheet(false) + } +}); diff --git a/app/template.html b/app/template.html index 285484d..93fd934 100644 --- a/app/template.html +++ b/app/template.html @@ -3,8 +3,10 @@