pvbemu/app/app/App.js

449 lines
14 KiB
JavaScript

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("Memory keyboard commands:");
console.log(" Ctrl+G: Goto");
console.log("Disassembler keyboard commands:");
console.log(" Ctrl+B: Toggle bytes column");
console.log(" Ctrl+F: Fit columns");
console.log(" Ctrl+G: Goto");
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 };