import { Core } from /**/"./core/Core.js"; import { Debugger } from /**/"./debugger/Debugger.js"; import { Disassembler } from /**/"./core/Disassembler.js"; import { Toolkit } from /**/"./toolkit/Toolkit.js"; // Front-end emulator application class App extends Toolkit.App { ///////////////////////// Initialization Methods ////////////////////////// constructor(bundle) { super({ style: { display : "grid", gridTemplateRows: "max-content auto" }, visibility: true, visible : false }); // Configure instance fields this.bundle = bundle; this.debugMode = true; this.dualMode = false; this.text = null; } async init() { // Theme Object.assign(document.body.style, { margin:"0", overflow:"hidden" }); this.stylesheet(/**/"web/theme/kiosk.css", false); this.stylesheet(/**/"web/theme/vbemu.css", false); this._theme = "auto"; this.themes = { dark : this.stylesheet(/**/"web/theme/dark.css" ), light : this.stylesheet(/**/"web/theme/light.css" ), virtual: this.stylesheet(/**/"web/theme/virtual.css") }; // Watch for dark mode preference changes this.isDark = window.matchMedia("(prefers-color-scheme: dark)"); this.isDark.addEventListener("change", e=>this.onDark()); this.onDark(); // Locales await this.addLocale(/**/"web/locale/en-US.json"); for (let id of [].concat(navigator.languages, ["en-US"])) { if (this.setLocale(id)) break; } this.setTitle("{app.title}", true); // Element document.body.append(this.element); window.addEventListener("resize", e=>{ this.element.style.height = window.innerHeight + "px"; this.element.style.width = window.innerWidth + "px"; }); window.dispatchEvent(new Event("resize")); this.addEventListener("keydown", e=>this.onKeyDown(e)); // Menus this.menuBar = new Toolkit.MenuBar(this); this.menuBar.setLabel("{menu._}", true); this.add(this.menuBar); this.initFileMenu (); this.initEmulationMenu(); this.initDebugMenu (0, this.debugMode); this.initDebugMenu (1, this.debugMode && this.dualMode); this.initThemeMenu (); // Fallback for bubbled key events document.body.addEventListener("focusout", e=>this.onBlur(e)); window .addEventListener("keydown" , e=>this.onKey (e)); window .addEventListener("keyup" , e=>this.onKey (e)); // Temporary: Faux game mode display this.display = new Toolkit.Component(this, { class : "tk display", style : { position: "relative" }, visible: !this.debugMode }); this.image1 = new Toolkit.Component(this, { style: { background: "#000000", position : "absolute" }}); this.display.add(this.image1); this.image2 = new Toolkit.Component(this, { style: { background: "#000000", position : "absolute" }}); this.display.add(this.image2); this.display.addEventListener("resize", e=>this.onDisplay()); this.add(this.display); // Temporary: Faux debug mode display this.desktop = new Toolkit.Desktop(this, { visible: this.debugMode }); this.add(this.desktop); // Emulation core this.core = await new Core().init(); let sims = (await this.core.create(2)).sims; this.core.onsubscription = (k,m)=>this.onSubscription(k, m); // Debugging managers this.dasm = new Disassembler(); this.debug = new Array(sims.length); for (let x = 0; x < sims.length; x++) { let dbg = this.debug[x] = new Debugger(this, sims[x], x); if (x == 0 && !this.dualMode) { dbg.cpu .substitute("#", ""); dbg.memory.substitute("#", ""); } this.desktop.add(dbg.cpu); this.desktop.add(dbg.memory); } // Reveal the application this.visible = true; this.restoreFocus(); console.log( "CPU window shortcuts:\n" + " F11 Single step\n" + " F10 Run to next\n" + " Ctrl+B Toggle bytes column\n" + " Ctrl+F Fit columns\n" + " Ctrl+G Goto" ); } // Initialize File menu initFileMenu() { let bar = this.menuBar; let item = bar.file = new Toolkit.MenuItem(this); item.setText("{menu.file._}"); bar.add(item); let menu = item.menu = new Toolkit.Menu(this); item = bar.file.loadROM0 = new Toolkit.MenuItem(this); item.setText("{menu.file.loadROM}"); item.substitute("#", this.dualMode ? " 1" : "", false); item.addEventListener("action", e=>this.onLoadROM(0)); menu.add(item); item = bar.file.loadROM1 = new Toolkit.MenuItem(this, { visible: this.dualMode }); item.setText("{menu.file.loadROM}"); item.substitute("#", " 2", false); item.addEventListener("action", e=>this.onLoadROM(1)); menu.add(item); item = bar.file.dualMode = new Toolkit.MenuItem(this, { checked: this.dualMode, type: "checkbox" }); item.setText("{menu.file.dualMode}"); item.addEventListener("action", e=>this.onDualMode()); menu.add(item); item = bar.file.debugMode = new Toolkit.MenuItem(this, { checked: this.debugMode, disabled: true, type: "checkbox" }); item.setText("{menu.file.debugMode}"); item.addEventListener("action", e=>this.onDebugMode()); menu.add(item); menu.addSeparator(); item = new Toolkit.MenuItem(this); item.setText("Export source...", false); item.addEventListener("action", ()=>this.bundle.save()); menu.add(item); } // Initialize Emulation menu initEmulationMenu() { let bar = this.menuBar; let item = bar.emulation = new Toolkit.MenuItem(this); item.setText("{menu.emulation._}"); bar.add(item); let menu = item.menu = new Toolkit.Menu(this); item = bar.emulation.run = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.emulation.run}"); menu.add(item); item = bar.emulation.reset0 = new Toolkit.MenuItem(this); item.setText("{menu.emulation.reset}"); item.substitute("#", this.dualMode ? " 1" : "", false); item.addEventListener("action", e=>this.onReset(0)); menu.add(item); item = bar.emulation.reset1 = new Toolkit.MenuItem(this, { visible: this.dualMode }); item.setText("{menu.emulation.reset}"); item.substitute("#", " 2", false); item.addEventListener("action", e=>this.onReset(1)); menu.add(item); item = bar.emulation.linkSims = new Toolkit.MenuItem(this, { disabled: true, type: "checkbox", visible: this.dualMode }); item.setText("{menu.emulation.linkSims}"); menu.add(item); } // Initialize Debug menu initDebugMenu(index, visible) { let bar = this.menuBar; let item = bar["debug" + index] = new Toolkit.MenuItem(this, { visible: visible }), top = item; item.setText("{menu.debug._}"); item.substitute("#", index == 0 ? this.dualMode ? "1" : "" : " 2", false); bar.add(item); let menu = item.menu = new Toolkit.Menu(this); item = top.console = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.console}"); menu.add(item); item = top.memory = new Toolkit.MenuItem(this); item.setText("{menu.debug.memory}"); item.addEventListener("action", e=>this.showWindow(this.debug[index].memory)); menu.add(item); item = top.cpu = new Toolkit.MenuItem(this); item.setText("{menu.debug.cpu}"); item.addEventListener("action", e=>this.showWindow(this.debug[index].cpu)); menu.add(item); item=top.breakpoints = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.breakpoints}"); menu.add(item); menu.addSeparator(); item = top.palettes = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.palettes}"); menu.add(item); item = top.characters = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.characters}"); menu.add(item); item = top.bgMaps = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.bgMaps}"); menu.add(item); item = top.backgrounds = new Toolkit.MenuItem(this, { disabled:true }); item.setText("{menu.debug.backgrounds}"); menu.add(item); item = top.objects = new Toolkit.MenuItem(this, { disabled: true }); item.setText("{menu.debug.objects}"); menu.add(item); item = top.frameBuffers = new Toolkit.MenuItem(this, {disabled: true}); item.setText("{menu.debug.frameBuffers}"); menu.add(item); } // Initialize Theme menu initThemeMenu() { let bar = this.menuBar; let item = bar.theme = new Toolkit.MenuItem(this); item.setText("{menu.theme._}"); bar.add(item); let menu = item.menu = new Toolkit.Menu(this); item = bar.theme.auto = new Toolkit.MenuItem(this, { checked: true, type: "checkbox" }); item.setText("{menu.theme.auto}"); item.theme = "auto"; item.addEventListener("action", e=>this.theme = "auto"); menu.add(item); item = bar.theme.light = new Toolkit.MenuItem(this, { checked: false, type: "checkbox" }); item.setText("{menu.theme.light}"); item.theme = "light"; item.addEventListener("action", e=>this.theme = "light"); menu.add(item); item = bar.theme.dark = new Toolkit.MenuItem(this, { checked: false, type: "checkbox" }); item.setText("{menu.theme.dark}"); item.theme = "dark"; item.addEventListener("action", e=>this.theme = "dark"); menu.add(item); item = bar.theme.light = new Toolkit.MenuItem(this, { checked: false, type: "checkbox" }); item.setText("{menu.theme.virtual}"); item.theme = "virtual"; item.addEventListener("action", e=>this.theme = "virtual"); menu.add(item); } ///////////////////////////// Event Handlers ////////////////////////////// // All elements have lost focus onBlur(e) { if ( e.relatedTarget == null || e.relatedTarget == document.body ) this.restoreFocus(); } // Dark mode preference changed onDark() { if (this._theme != "auto") return; let isDark = this.isDark.matches; this.themes.light.disabled = isDark; this.themes.dark .disabled = !isDark; } // Game mode display resized onDisplay() { let bounds = this.display.element.getBoundingClientRect(); let width = Math.max(1, bounds.width); let height = Math.max(1, bounds.height); let scale, x1, y1, x2, y2; // Single mode if (!this.dualMode) { this.image2.visible = false; scale = Math.max(1, Math.min( Math.floor(width / 384), Math.floor(height / 224) )); x1 = Math.max(0, Math.floor((width - 384 * scale) / 2)); y1 = Math.max(0, Math.floor((height - 224 * scale) / 2)); x2 = y2 = 0; } // Dual mode else { this.image2.visible = true; // Horizontal orientation if (true) { scale = Math.max(1, Math.min( Math.floor(width / 768), Math.floor(height / 224) )); let gap = Math.max(0, width - 768 * scale); gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0); x1 = gap; x2 = Math.max(384 * scale, width - 384 * scale - gap); y1 = y2 = Math.max(0, Math.floor((height - 224 * scale) / 2)); } // Vertical orientation else { scale = Math.max(1, Math.min( Math.floor(width / 384), Math.floor(height / 448) )); let gap = Math.max(0, height - 448 * scale); gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0); x1 = x2 = Math.max(0, Math.floor((width - 384 * scale) / 2)); y1 = gap; y2 = Math.max(224 * scale, height - 224 * scale - gap); } } width = 384 * scale + "px"; height = 224 * scale + "px"; Object.assign(this.image1.element.style, { left: x1+"px", top: y1+"px", width: width, height: height }); Object.assign(this.image2.element.style, { left: x2+"px", top: y2+"px", width: width, height: height }); } // File -> Debug mode onDebugMode() { this.debugMode =!this.debugMode; this.display.visible =!this.debugMode; this.desktop.visible = this.debugMode; this.configMenus(); this.onDisplay(); } // Emulation -> Dual mode onDualMode() { this.setDualMode(!this.dualMode); this.configMenus(); this.onDisplay(); } // Key press onKeyDown(e) { // Take no action if (!e.altKey || e.key != "F10" || this.menuBar.contains(document.activeElement)) return; // Move focus into the menu bar this.menuBar.focus(); Toolkit.handle(e); } // File -> Load ROM async onLoadROM(index) { // Add a file picker to the document let file = document.createElement("input"); file.type = "file"; file.style.position = "absolute"; file.style.visibility = "hidden"; document.body.appendChild(file); // Prompt the user to select a file await new Promise(resolve=>{ file.addEventListener("input", resolve); file.click(); }); file.remove(); // No file was selected file = file.files[0]; if (!file) return; // Load the file let data = null; try { data = new Uint8Array(await file.arrayBuffer()); } catch { alert(this.localize("{menu.file.loadROMError}")); return; } // Attempt to process the file as an ISX binary try { data = Debugger.isx(data).toROM(); } catch { } // Error checking if ( data.length < 1024 || data.length > 0x1000000 || (data.length & data.length - 1) ) { alert(this.localize("{menu.file.loadROMInvalid}")); return; } // Load the ROM into simulation memory let rep = await this.core.setROM(this.debug[index].sim, data, { refresh: true }); if (!rep.success) { alert(this.localize("{menu.file.loadROMError}")); return; } this.debug[index].followPC(0xFFFFFFF0); } // Emulation -> Reset async onReset(index) { await this.core.reset(this.debug[index].sim, { refresh: true }); this.debug[index].followPC(0xFFFFFFF0); } // Core subscription onSubscription(key, msg) { let target = this.debug; // Handler object for (let x = 1; x < key.length - 1; x++) target = target[key[x]]; target[key[key.length - 1]].call(target, msg); } ///////////////////////////// Public Methods ////////////////////////////// // Specify document title setTitle(title, localize) { this.setString("text", title, localize); } // Specify the color theme get theme() { return this._theme; } set theme(theme) { switch (theme) { case "light": case "dark": case "virtual": this._theme = theme; for (let entry of Object.entries(this.themes)) entry[1].disabled = entry[0] != theme; break; default: this._theme = "auto"; this.themes["virtual"].disabled = true; this.onDark(); } for (let item of this.menuBar.theme.menu.children) item.checked = item.theme == theme; } ///////////////////////////// Package Methods ///////////////////////////// // Configure components for automatic localization, or localize a message localize(a, b) { // Default behavior if (a && a != this) return super.localize(a, b); // Update localization strings if (this.text != null) { let text = this.text; document.title = !text[1] ? text[0] : this.localize(text[0], this.substitutions); } } // Return focus to the most recent focused element restoreFocus() { // Focus was successfully restored if (super.restoreFocus()) return true; // Select the foremost visible window let wnd = this.desktop.getActiveWindow(); if (wnd) { wnd.focus(); return true; } // Select the menu bar this.menuBar.focus(); return true; } // Perform a Run Next command on one of the simulations runToNext(index, options) { let debugs = [ this.debug[index] ]; if (this.dualMode) debugs.push(this.debug[index ^ 1]); let ret = this.core.runToNext(debugs.map(d=>d.sim), options); if (ret instanceof Promise) ret.then(msg=>{ for (let x = 0; x < debugs.length; x++) debugs[x].followPC(msg.pcs[x]); }); } // Perform a Single Step command on one of the simulations singleStep(index, options) { let debugs = [ this.debug[index] ]; if (this.dualMode) debugs.push(this.debug[index ^ 1]); let ret = this.core.singleStep(debugs.map(d=>d.sim), options); if (ret instanceof Promise) ret.then(msg=>{ for (let x = 0; x < debugs.length; x++) debugs[x].followPC(msg.pcs[x]); }); } ///////////////////////////// Private Methods ///////////////////////////// // Configure menu item visibility configMenus() { let bar = this.menuBar; bar.file.debugMode .checked = this.debugMode; bar.file.loadROM1 .visible = this.dualMode; bar.emulation.reset1 .visible = this.dualMode; bar.emulation.linkSims.visible = this.dualMode; bar.debug0 .visible = this.debugMode; bar.debug1 .visible = this.debugMode && this.dualMode; } // Specify whether dual mode is active setDualMode(dualMode) { // Update state if (dualMode == this.dualMode) return; this.dualMode = dualMode; // Working variables let index = dualMode ? " 1" : ""; // Update menus let bar = this.menuBar; bar.file.loadROM0 .substitute("#", index, false); bar.debug0 .substitute("#", index, false); bar.emulation.reset0.substitute("#", index, false); bar.file.dualMode .checked = this.dualMode; // Update sim 1 debug windows let dbg = this.debug[0]; dbg.cpu .substitute("#", index); dbg.memory.substitute("#", index); // Re-show any sim 2 debug windows that were previously visible if (dualMode) { for (let wnd of this.desktop.children) { if (wnd.index == 1 && wnd.wasVisible) wnd.visible = true; } } // Hide any visible sim 2 debug windows else for (let wnd of this.desktop.children) { if (wnd.index == 0) continue; wnd.wasVisible = wnd.visible; wnd.visible = false; } } // Ensure a debugger window is visible showWindow(wnd) { let adjust = false; // The window is already visible if (wnd.visible) { // The window is already in the foreground if (wnd == this.desktop.getActiveWindow()) ;//adjust = true; // Bring the window to the foreground else this.desktop.bringToFront(wnd); } // The window is not visible else { adjust = !wnd.shown; if (adjust && wnd.firstShow) wnd.firstShow(); wnd.visible = true; this.desktop.bringToFront(wnd); } // Adjust the window position if (adjust) { let bounds = this.desktop.element.getBoundingClientRect(); wnd.left = Math.max(0, (bounds.width - wnd.outerWidth ) / 2); wnd.top = Math.max(0, (bounds.height - wnd.outerHeight) / 2); } // Transfer focus into the window wnd.focus(); } // Install a stylesheet. Returns the resulting element stylesheet(filename, disabled = true) { let ret = document.createElement("link"); ret.href = filename; ret.rel = "stylesheet"; ret.type = "text/css"; ret.disabled = disabled; document.head.append(ret); return ret; } ///////////////////////////// Program Methods ///////////////////////////// // Program entry point static main(bundle) { new App(bundle).init(); } } export { App };