449 lines
14 KiB
JavaScript
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 };
|