Compare commits

..

No commits in common. "master" and "old-web" have entirely different histories.

42 changed files with 11563 additions and 2178 deletions

214
app/Bundle.java Normal file
View File

@ -0,0 +1,214 @@
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.imageio.*;
public class Bundle {
// File loaded from disk
static class File2 implements Comparable<File2> {
// Instance fields
byte[] data; // Contents
String filename; // Full path relative to root
// Comparator
public int compareTo(File2 o) {
if (filename.equals("app/_boot.js"))
return -1;
if (o.filename.equals("app/_boot.js"))
return 1;
return filename.compareTo(o.filename);
}
}
// Load all files in directory tree into memory
static HashMap<String, File2> readFiles(String bundleTitle) {
var dirs = new ArrayDeque<File>();
var root = new File(".");
var subs = new ArrayDeque<String>();
var files = new HashMap<String, File2>();
// Process all subdirectories
dirs.add(root);
while (!dirs.isEmpty()) {
var dir = dirs.remove();
// Add all subdirectories
for (var sub : dir.listFiles(f->f.isDirectory())) {
// Exclusions
if (dir == root && sub.getName().equals(".git"))
continue;
// Add the directory for bundling
dirs.add(sub);
}
// Add all files
for (var file : dir.listFiles(f->f.isFile())) {
var file2 = new File2();
// Read the file into memory
try {
var stream = new FileInputStream(file);
file2.data = stream.readAllBytes();
stream.close();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
// Determine the file's full pathname
subs.clear();
subs.addFirst(file.getName());
for (;;) {
file = file.getParentFile();
if (file.equals(root))
break;
subs.addFirst(file.getName());
}
file2.filename = String.join("/", subs);
// Exclusions
if (
file2.filename.startsWith(".git" ) ||
file2.filename.startsWith(bundleTitle + "_") &&
file2.filename.endsWith (".html" )
) continue;
// Add the file to the output
files.put(file2.filename, file2);
}
}
return files;
}
// Prepend manifest object to _boot.js
static void manifest(HashMap<String, File2> files, String bundleName) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build a file manifest
var manifest = new StringBuilder();
manifest.append("\"use strict\";\nlet manifest=[");
for (var file : values) {
manifest.append(
"[\"" + file.filename + "\"," + file.data.length + "]");
if (file != values[values.length - 1])
manifest.append(",");
}
manifest.append("],bundleName=\"" + bundleName + "\";");
// Prepend the manifest to _boot.js
var boot = files.get("app/_boot.js");
boot.data = (
manifest.toString() +
new String(boot.data, StandardCharsets.UTF_8) +
"\u0000"
).getBytes(StandardCharsets.UTF_8);
}
// Construct bundled blob
static byte[] blob(HashMap<String, File2> files) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build the blob
var blob = new ByteArrayOutputStream();
for (var file : values) try {
blob.write(file.data);
} catch (Exception e) { }
return blob.toByteArray();
}
// Encode bundled blob as a .png
static byte[] png(byte[] blob) {
// Calculate the dimensions of the image
int width = (int) Math.ceil(Math.sqrt(blob.length));
int height = (int) Math.ceil((double) blob.length / width);
// Prepare the pixel data
var pixels = new int[width * height];
for (int x = 0; x < blob.length; x++) {
int l = blob[x] & 0xFF;
pixels[x] = 0xFF000000 | l << 16 | l << 8 | l;
}
// Produce a BufferedImage containing the pixels
var img = new BufferedImage(width, height,
BufferedImage.TYPE_BYTE_GRAY);
img.getRaster().setPixels(0, 0, width, height, pixels);
// Encode the image as a PNG byte array
var png = new ByteArrayOutputStream();
try { ImageIO.write(img, "png", png); }
catch (Exception e) { }
return png.toByteArray();
}
// Embed bundle .png into template.html as a data URL
static void template(byte[] png, String bundleName) {
// Encode the PNG as a data URL
String url = "data:image/png;base64," +
Base64.getMimeEncoder().encodeToString(png)
.replaceAll("\\r\\n", "");
try {
// Read template.html into memory
var inStream = new FileInputStream("app/template.html");
String template =
new String(inStream.readAllBytes(), StandardCharsets.UTF_8)
.replace("src=\"\"", "src=\"" + url + "\"")
;
inStream.close();
// Write the output HTML file
var outStream = new FileOutputStream(bundleName + ".html");
outStream.write(template.getBytes(StandardCharsets.UTF_8));
outStream.close();
} catch (Exception e) { throw new RuntimeException(e.getMessage()); }
}
// Determine the filename of the bundle
static String bundleName(String name) {
var calendar = Calendar.getInstance();
return String.format("%s_%04d%02d%02d",
name,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
);
}
// Program entry point
public static void main(String[] args) {
// Error checking
if (args.length != 1) {
System.err.println("Usage: Bundle <name>");
return;
}
// Program tasks
String bundleName = bundleName(args[0]);
var files = readFiles(args[0]);
manifest(files, bundleName);
var blob = blob(files);
var png = png(blob);
template(png, bundleName);
}
}

308
app/_boot.js Normal file
View File

@ -0,0 +1,308 @@
/*
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.
*/
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// 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);
// 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;
}
}
///////////////////////////// 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 {
constructor(name, data) {
// Configure instance fields
this.data = data;
this.name = name;
// 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(".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"
;
}
///////////////////////////// Public Methods //////////////////////////////
// Install a font from a bundled resource
async installFont(name) {
if (name === undefined) {
name = "/" + this.name;
name = name.substring(name.lastIndexOf("/") + 1);
}
let ret = new FontFace(name, "url('"+
(Bundle.debug ? this.name : this.toDataURL()) + "'");
await ret.load();
document.fonts.add(ret);
return ret;
}
// 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)
ret.removeAttribute("disabled");
else ret.setAttribute("disabled", "");
};
ret.setEnabled(!!enabled);
document.head.appendChild(ret);
return ret;
}
// Encode the file data as a data URL
toDataURL() {
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 Bundle.decoder.decode(this.data);
}
///////////////////////////// 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);
}
}
///////////////////////////////////////////////////////////////////////////////
// Boot Program //
///////////////////////////////////////////////////////////////////////////////
// De-register the boot function
delete globalThis.a;
// Remove the bundle image element from the document
Bundle.src = arguments[0].src;
arguments[0].remove();
// 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
}
// Begin program operations
import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());

448
app/app/App.js Normal file
View File

@ -0,0 +1,448 @@
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 };

106
app/app/CPU.js Normal file
View File

@ -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 };

187
app/app/Debugger.js Normal file
View File

@ -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 };

958
app/app/Disassembler.js Normal file
View File

@ -0,0 +1,958 @@
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.first = first;
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 = row && 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) {
// Column elements
let columns = [
this.lblAddress,
this.lblBytes,
this.lblMnemonic,
this.lblOperands
];
// Column elements on the first row
if (this.first) {
columns[0] = columns[0].parentNode; // Address
columns[1] = columns[1].parentNode; // Bytes
columns[2] = columns[2].parentNode; // Mnemonic
}
// Column visibility
visible = [
visible, // Address
visible && this.parent.hasBytes, // Bytes
visible, // Mnemonic
visible // Operands
];
// Configure elements
for (let x = 0; x < 4; x++)
columns[x].style.display = visible[x] ? "block" : "none";
}
///////////////////////////// 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.hasBytes = true;
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);
// Ctrl key is pressed
if (e.ctrlKey) switch (e.key) {
// Toggle bytes column
case "b": case "B":
this.showBytes(!this.hasBytes);
break;
// Fit columns
case "f": case "F":
this.fitColumns();
break;
// Goto
case "g": case "G":
this.promptGoto();
break;
default: return;
}
// Ctrl key is not pressed
else 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;
// 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"
})
);
}
// Shrink all columns to their minimum size
fitColumns() {
let line = this.lines[0];
for (let column of [ "lblAddress", "lblBytes", "lblMnemonic" ] ) {
let element = line[column].parentNode;
element.max = 0;
element.style.removeProperty("min-width");
}
}
// 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;
}
// Specify whether or not to show the bytes column
showBytes(show) {
let tall = this.tall(true);
// Configure instance fields
this.hasBytes = show;
// Configure elements
this.view.style.gridTemplateColumns =
"repeat(" + (show ? 3 : 2) + ", max-content) auto";
for (let x = 0; x < tall; x++)
this.lines[x].setVisible(true);
// Measure scroll pane
this.update();
}
// 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.getBounds().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 };

517
app/app/Memory.js Normal file
View File

@ -0,0 +1,517 @@
import { Util } from /**/"./Util.js";
// Bus indexes
const MEMORY = 0;
// Text to hex digit conversion
const DIGITS = {
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
"8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
};
///////////////////////////////////////////////////////////////////////////////
// Line //
///////////////////////////////////////////////////////////////////////////////
// One line of output
class Line {
///////////////////////// Initialization Methods //////////////////////////
constructor(parent, index) {
// Configure instance fields
this.index = index;
this.parent = parent;
// Address label
this.lblAddress = document.createElement("div");
this.lblAddress.className = "tk tk-address";
parent.view.appendChild(this.lblAddress);
// Byte labels
this.lblBytes = new Array(16);
for (let x = 0; x < 16; x++) {
let lbl = this.lblBytes[x] = document.createElement("div");
lbl.className = "tk tk-byte tk-" + x.toString(16);
parent.view.appendChild(lbl);
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the elements' display
refresh() {
let bus = this.parent[this.parent.bus];
let address = this.parent.mask(bus.address + this.index * 16);
let data = bus.data;
let dataAddress = bus.dataAddress;
let hexCaps = this.parent.dasm.hexCaps;
let offset =
(this.parent.row(address) - this.parent.row(dataAddress)) * 16;
// Format the line's address
let text = address.toString(16).padStart(8, "0");
if (hexCaps)
text = text.toUpperCase();
this.lblAddress.innerText = text;
// The line's data is not available
if (offset < 0 || offset >= data.length) {
for (let lbl of this.lblBytes) {
lbl.innerText = "--";
lbl.classList.remove("tk-selected");
}
}
// The line's data is available
else for (let x = 0; x < 16; x++, offset++) {
let lbl = this.lblBytes[x];
text = data[offset].toString(16).padStart(2, "0");
// The byte is the current selection
if (Util.u32(address + x) == bus.selection) {
lbl.classList.add("tk-selected");
if (this.parent.digit !== null)
text = this.parent.digit.toString(16);
}
// The byte is not the current selection
else lbl.classList.remove("tk-selected");
// Update the label's text
if (hexCaps)
text = text.toUpperCase();
lbl.innerText = text;
}
}
// Specify whether the elements on this line are visible
setVisible(visible) {
visible = visible ? "block" : "none";
this.lblAddress.style.display = visible;
for (let lbl of this.lblBytes)
lbl.style.display = visible;
}
}
///////////////////////////////////////////////////////////////////////////////
// Memory //
///////////////////////////////////////////////////////////////////////////////
// Memory hex editor
class Memory extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug) {
super(debug.app, {
className : "tk tk-memory",
tagName : "div",
style : {
alignItems : "stretch",
display : "grid",
gridTemplateRows: "auto",
position : "relative"
}
});
// Configure instance fields
this.app = debug.app;
this.bus = MEMORY;
this.dasm = debug.disassembler;
this.debug = debug;
this.digit = null;
this.isSubscribed = false;
this.lines = [];
this.sim = debug.sim;
// Initialize bus
this[MEMORY] = {
address : 0x05000000,
data : [],
dataAddress: 0x05000000,
selection : 0x05000000
};
// Configure editor pane
this.editor = new Toolkit.ScrollPane(this.app, {
className : "tk tk-scrollpane tk-editor",
horizontal: Toolkit.ScrollPane.AS_NEEDED,
focusable : true,
tabStop : true,
tagName : "div",
vertical : Toolkit.ScrollPane.NEVER
});
this.append(this.editor);
// Configure view
this.view = document.createElement("div");
this.view.className = "tk tk-view";
Object.assign(this.view.style, {
display : "grid",
gridTemplateColumns: "repeat(17, max-content)"
});
this.editor.setView(this.view);
// Font-measuring element
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-metrics tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = "X";
this.append(this.metrics.element);
// Configure event handlers
Toolkit.addResizeListener(this.editor.viewport, e=>this.onResize(e));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("wheel" , e=>this.onMouseWheel (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Typed a digit
onDigit(digit) {
let bus = this[this.bus];
// Begin an edit
if (this.digit === null) {
this.digit = digit;
this.setSelection(bus.selection, true);
}
// Complete an edit
else {
this.digit = this.digit << 4 | digit;
this.setSelection(bus.selection + 1);
}
}
// Key press
onKeyDown(e) {
let bus = this[this.bus];
let key = e.key;
// A hex digit was entered
if (key.toUpperCase() in DIGITS) {
this.onDigit(DIGITS[key.toUpperCase()]);
key = "digit";
}
// Ctrl key is pressed
if (e.ctrlKey) switch (key) {
// Goto
case "g": case "G":
this.promptGoto();
break;
default: return;
}
// Ctrl key is not pressed
else switch (key) {
// Arrow key navigation
case "ArrowDown" : this.setSelection(bus.selection + 16); break;
case "ArrowLeft" : this.setSelection(bus.selection - 1); break;
case "ArrowRight": this.setSelection(bus.selection + 1); break;
case "ArrowUp" : this.setSelection(bus.selection - 16); break;
// Commit current edit
case "Enter":
case " ":
if (this.digit !== null)
this.setSelection(bus.selection);
break;
// Page key navigation
case "PageDown":
this.setSelection(bus.selection + this.tall(false) * 16);
break;
case "PageUp":
this.setSelection(bus.selection - this.tall(false) * 16);
break;
// Hex digit: already processed
case "digit": break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 48;
if (offset == 0)
return;
// Update the display address
this.fetch(this[this.bus].address + offset, true);
}
// Pointer down
onPointerDown(e) {
// Common handling
this.editor.focus();
e.stopPropagation();
e.preventDefault();
// Not a click action
if (e.button != 0)
return;
// Determine the row that was clicked on
let lineHeight = !this.metrics ? 0 :
Math.max(0, Math.ceil(this.metrics.getBounds().height));
if (lineHeight == 0)
return;
let y = Math.floor(
(e.y - this.view.getBoundingClientRect().top) / lineHeight);
// Determine the column that was clicked on
let columns = this.lines[0].lblBytes;
let bndCur = columns[0].getBoundingClientRect();
if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) {
let bndNext = x == 15 ? null :
columns[x + 1].getBoundingClientRect();
// The current column was clicked: update the selection
if (e.x < (x == 15 ? bndCur.right :
bndCur.right + (bndNext.left - bndCur.right) / 2)) {
this.setSelection(this[this.bus].address + y * 16 + x);
return;
}
// Advance to the next column
bndCur = bndNext;
}
}
// Viewport resized
onResize(e) {
let fetch = false;
let tall = this.tall(true);
// Add additional lines to the output
for (let x = 0; x < tall; x++) {
if (x >= this.lines.length) {
fetch = true;
this.lines.push(new Line(this, x));
}
this.lines[x].setVisible(true);
}
// Remove extra lines from the output
for (let x = tall; x < this.lines.length; x++)
this.lines[x].setVisible(false);
// Configure horizontal scroll bar
if (this.metrics) this.editor.horizontal
.setIncrement(this.metrics.getBounds().width);
// Update the display
if (fetch)
this.fetch(this[this.bus].address, true);
else this.refresh();
}
///////////////////////////// Public Methods //////////////////////////////
// Update with memory state from the core
refresh(data) {
let bus = this[this.bus];
// Update with data from the core thread
if (data) {
bus.data = data.bytes;
bus.dataAddress = data.address;
}
// Update elements
for (let y = 0, tall = this.tall(true); y < tall; y++)
this.lines[y].refresh();
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch(this[this.bus].address);
// Unsubscribe from core updates
else this.sim.unsubscribe("memory");
}
///////////////////////////// Package Methods /////////////////////////////
// The disassembler configuration has changed
dasmChanged() {
this.refresh();
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve memory data from the core
async fetch(address, prefresh) {
// Configure instance fields
this[this.bus].address = address = this.mask(address);
// Update the view immediately
if (prefresh)
this.refresh();
// Retrieve data from the core
this.refresh(
await this.sim.read(
address - 16 * 16,
(this.tall(true) + 32) * 16, {
subscribe: this.isSubscribed && "memory"
})
);
}
// Mask an address according to the current bus
mask(address) {
return Util.u32(address);
}
// Prompt the user to specify a new address
promptGoto() {
// Receive input from the user
let address = prompt(this.app.translate("common.gotoPrompt"));
if (address == null)
return;
// Process the input as an address in hexadecimal
address = parseInt(address, 16);
if (isNaN(address))
return;
// The address is not currently visible in the output
let tall = this.tall(false);
if (Util.u32(address - this.address) >= tall * 16)
this.fetch((address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16);
// Move the selection and refresh the display
this.setSelection(address);
}
// Determine which row relative to top the selection is on
row(address) {
let row = address - this[this.bus].address & 0xFFFFFFF0;
row = Util.s32(row);
return row / 16;
}
// Specify which byte is selected
setSelection(address, noCommit) {
let bus = this[this.bus];
let fetch = false;
// Commit a pending data entry
if (!noCommit && this.digit !== null) {
this.write(this.digit);
this.digit = null;
fetch = true;
}
// Configure instance fields
bus.selection = address = this.mask(address);
// Working variables
let row = this.row(address);
// The new address is above the top line of output
if (row < 0) {
this.fetch(bus.address + row * 16 & 0xFFFFFFF0, true);
return;
}
// The new address is below the bottom line of output
let tall = this.tall(false);
if (row >= tall) {
this.fetch(address - tall * 16 + 16 & 0xFFFFFFF0, true);
return;
}
// Update the display
if (fetch)
this.fetch(bus.address, true);
else this.refresh();
}
// Measure how many rows of output are visible
tall(partial) {
let lineHeight = !this.metrics ? 0 :
Math.ceil(this.metrics.getBounds().height);
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
this.editor.getBounds().height / lineHeight));
}
// Write a value to the core thread
write(value) {
let bus = this[this.bus];
let offset = (this.row(bus.selection) + 16) * 16;
if (offset < bus.data.length)
bus.data[offset | bus.selection & 15] = value;
this.sim.write(
bus.selection,
Uint8Array.from([ value ]), {
refresh: true
});
}
}
export { Memory };

889
app/app/RegisterList.js Normal file
View File

@ -0,0 +1,889 @@
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.format = HEX;
this.index = index;
this.isExpanded = null;
this.list = list;
this.metrics = { width: 0, height: 0 };
this.orMask = orMask;
this.sim = list.sim;
this.system = list.system;
this.value = 0x00000000;
// Establish elements
let row = document.createElement("tr");
let cell;
list.view.append(row);
// 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"
});
row .append(cell = document.createElement("td"));
cell.className = "tk";
cell.style.width = "1px";
cell.append(this.btnExpand.element);
// 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";
row .append(cell = document.createElement("td"));
cell.className = "tk";
cell.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());
row .append(cell = document.createElement("td"));
Object.assign(cell.style, {
textAlign: "right",
width : "1px"
});
cell.className = "tk";
cell.append(this.txtValue.element);
// 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 = document.createElement("tr");
exp.contents = new Toolkit.Component(this.app, {
className: "tk tk-expansion",
id : Toolkit.id(),
tagName : "div",
style : {
display : "grid",
gridTemplateColumns:
this.system ? "repeat(2, max-content)" : "max-content"
}
});
let cell = document.createElement("td");
cell.className = "tk";
cell.colSpan = "3";
cell.append(exp.contents.element);
exp.append(cell);
exp = exp.contents;
// 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.setFormat(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.format) {
// 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);
}
///////////////////////////// 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 = Util.u32(value);
// Value text box
switch (this.format) {
// 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);
}
}
// 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.style.display =
expanded ? "table-row" : "none";
}
// Specify the font metrics
setMetrics(width, height) {
// Configure instance fields
this.metrics = { width: width, height: height };
// Height
height += "px";
this.txtValue.element.style.height = height;
for (let ctrl of this.controls.filter(c=>c.size > 1))
ctrl.element.style.height = height;
// Hexadecimal formatting
if (this.format == HEX) {
this.txtValue.element.style.width = (width * 8) + "px";
this.txtValue.setMaxLength(8);
}
// Decimal formatting
else {
this.txtValue.element.style.removeProperty("width");
this.txtValue.setMaxLength(null);
}
// Expansion text boxes
for (let box of this.controls.filter(c=>c.size > 1)) {
box.element.style.height = height;
if (box.size == 16)
box.element.style.width = (width * 4) + "px";
}
}
///////////////////////////// Private Methods /////////////////////////////
// Specify the formatting type of the register value
setFormat(format) {
if (format == this.format)
return;
this.format = format;
this.txtValue.element
.classList[format == HEX ? "add" : "remove"]("tk-mono");
this.setMetrics(this.metrics.width, this.metrics.height);
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.registers = [];
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 : "table",
style : {
width: "100%"
}
}));
// 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.add(new Register(this, 0, 0x00000000, 0x00000000));
for (let x = 1; x < 32; x++)
this.add(new Register(this, x, 0xFFFFFFFF, 0x00000000));
}
// Initialie a list of system registers
initSystem() {
this.add(new Register(this, PC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, PSW , 0x000FF3FF, 0x00000000));
this.add(new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, CHCW , 0x00000002, 0x00000000));
this.add(new Register(this, ECR , 0xFFFFFFFF, 0x00000000));
this.add(new Register(this, EIPC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, EIPSW, 0x000FF3FF, 0x00000000));
this.add(new Register(this, FEPC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, FEPSW, 0x000FF3FF, 0x00000000));
this.add(new Register(this, PIR , 0x00000000, 0x00005346));
this.add(new Register(this, TKCW , 0x00000000, 0x000000E0));
this.add(new Register(this, 29 , 0xFFFFFFFF, 0x00000000));
this.add(new Register(this, 30 , 0x00000000, 0x00000004));
this.add(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 text boxes
for (let reg of this.registers)
reg.setMetrics(width, height);
// 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) {
for (let reg of Object.entries(SYSREGS))
this[reg[0]].refresh(registers[reg[1].toLowerCase()]);
}
// 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() {
for (let reg of this.registers)
reg.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.getBoundingClientRect().bottom -
this.view.getBounds().top;
}
return ret;
}
///////////////////////////// Private Methods /////////////////////////////
// Add a register to the list
add(reg) {
this[reg.index] = reg;
this.registers.push(reg);
}
// Retrieve CPU state from the core
async fetch() {
this.refresh(
await this.sim[this.method]({
subscribe: this.isSubscribed && this.subscription
})
);
}
}
export { RegisterList };

44
app/app/Util.js Normal file
View File

@ -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
};

196
app/core/Core.js Normal file
View File

@ -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 };

377
app/core/CoreWorker.js Normal file
View File

@ -0,0 +1,377 @@
"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
};
// 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);
}
}();

151
app/core/Sim.js Normal file
View File

@ -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 };

74
app/locale/en-US.json Normal file
View File

@ -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}"
}
}

34
app/main.js Normal file
View File

@ -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)
}
});

12
app/template.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Virtual Boy Emulator</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<script>window.a=async(b,c,d,e)=>{e=document.createElement('canvas');e.width=c;e.height=d;e=e.getContext('2d');e.drawImage(b,0,0);c=e.getImageData(0,0,c,d).data.filter((z,y)=>!(y&3));d=c.indexOf(0);Object.getPrototypeOf(a).constructor(String.fromCharCode(...c.slice(0,d)))(b,c,d)}</script>
</head>
<body>
<img alt="" style="display: none;" onload="a(this,width,height)" src="">
</body>
</html>

6
app/theme/check.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 459 B

6
app/theme/close.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 0.52916666,0.52916665 0.396875,0 0.52916664,0.52916665 0.5291667,-0.52916665 0.396875,0 0,0.396875 L 1.8520833,1.4552083 2.38125,1.984375 l 0,0.396875 -0.396875,0 L 1.4552083,1.8520834 0.92604166,2.38125 l -0.396875,0 0,-0.396875 L 1.0583333,1.4552083 0.52916666,0.92604165 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 652 B

6
app/theme/collapse.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
<g>
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

26
app/theme/dark.css Normal file
View File

@ -0,0 +1,26 @@
:root {
--tk-control : #333333;
--tk-control-active : #555555;
--tk-control-border : #cccccc;
--tk-control-highlight : #444444;
--tk-control-shadow : #9b9b9b;
--tk-control-text : #cccccc;
--tk-desktop : #111111;
--tk-selected : #008542;
--tk-selected-blur : #325342;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #ffffff99;
--tk-window : #222222;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text: #eeeeee;
--tk-window-blur-title : #9fafb9;
--tk-window-blur-title2 : #c0b2ab;
--tk-window-blur-title-text: #444444;
--tk-window-close : #ee9999;
--tk-window-close-text : #ffffff;
--tk-window-text : #cccccc;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

7
app/theme/expand.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
<g>
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.3229167" y="0.79375005" />
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 522 B

BIN
app/theme/inconsolata.woff2 Normal file

Binary file not shown.

761
app/theme/kiosk.css Normal file
View File

@ -0,0 +1,761 @@
:root {
--tk-font-dialog: "Roboto", sans-serif;
--tk-font-mono : "Inconsolata SemiExpanded Medium", monospace;
--tk-font-size : 12px;
}
.tk {
font-family: var(--tk-font-dialog);
font-size : var(--tk-font-size);
line-height: 1em;
margin : 0;
outline : none;
padding : 0;
}
table.tk {
border : none;
border-spacing: 0;
}
.tk-body {
overflow: hidden;
}
.tk-app {
/* Height managed through resize listener */
width: 100%;
}
.tk-mono {
font-family: var(--tk-font-mono);
}
/********************************** Button ***********************************/
.tk-button > * {
background: var(--tk-control);
border : 1px solid var(--tk-control-shadow);
box-shadow: 1px 1px 0 0 var(--tk-control-shadow);
color : var(--tk-control-text);
margin : 0 1px 1px 0;
padding : 3px;
text-align: center;
}
.tk-button:focus > * {
background: var(--tk-control-active);
}
.tk-button.active > * {
box-shadow: none;
margin : 1px 0 0 1px;
}
.tk-button[aria-disabled="true"] > * {
color: var(--tk-control-shadow);
}
/********************************* Check Box *********************************/
.tk-checkbox {
align-items: center;
column-gap : 2px;
}
.tk-checkbox .tk-icon {
align-items: center;
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
box-sizing : border-box;
display : flex;
height : 12px;
width : 12px;
}
.tk-checkbox .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-check);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-check);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-checkbox[aria-checked="true"] .tk-icon:before {
background: var(--tk-window-text);
}
.tk-checkbox:focus .tk-icon {
background: var(--tk-control-active);
}
.tk-checkbox[aria-checked="true"]:focus .tk-icon:before {
background: var(--tk-control-text);
}
.tk-checkbox.active:focus .tk-icon:before {
background: var(--tk-control-shadow);
}
/****************************** Drop-Down List *******************************/
.tk-dropdown {
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
border-radius: 0;
color : var(--tk-window-text);
margin : 0;
padding : 1px;
}
/********************************* Menu Bar **********************************/
.tk-menu-bar {
background : var(--tk-control);
border-bottom: 1px solid var(--tk-control-border);
color : var(--tk-control-text);
column-gap : 1px;
display : flex;
flex-wrap : wrap;
padding : 2px;
user-select : none;
white-space : nowrap;
}
.tk-menu {
background : var(--tk-control);
border : 1px solid var(--tk-control-border);
box-shadow : 1px 1px 0 0 var(--tk-control-border);
display : flex;
flex-direction: column;
padding : 3px;
row-gap : 1px;
}
.tk-menu-item > * {
background: var(--tk-control);
border : 1px solid transparent;
column-gap: 4px;
display : flex;
margin : 0 1px 1px 0;
padding : 3px;
}
.tk-menu-item > * > .tk-icon {
box-sizing: border-box;
display : none;
height : 1em;
width : 1em;
}
.tk-menu-item > * > .tk-text {
flex-grow: 1;
}
.tk-menu-item[aria-disabled="true"] > * > .tk-text {
color: var(--tk-control-shadow);
}
.tk-menu-item:not(.active, [aria-disabled="true"]):hover > *,
.tk-menu-item:not(.active):focus > * {
border-color: var(--tk-control-shadow);
box-shadow : 1px 1px 0 0 var(--tk-control-shadow);
}
.tk-menu.icons > .tk-menu-item > * > .tk-icon {
display: block;
}
.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon {
border: 1px solid var(--tk-control-border);
}
.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-check);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-check);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-menu-item[role="menuitemcheckbox"][aria-checked="true"]
> * > .tk-icon:before {
background: var(--tk-control-text);
}
.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"]
> * > .tk-icon {
border: 1px solid var(--tk-control-shadow);
}
.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"]
> * > .tk-icon:before {
background: var(--tk-control-shadow);
}
.tk-menu-item:not(.active):focus > * {
background: var(--tk-control-active);
}
.tk-menu-item.active > * {
background : var(--tk-control-active);
border-color: var(--tk-control-shadow);
margin : 1px 0 0 1px;
}
.tk-menu-separator {
border : 0 solid var(--tk-control-shadow);
border-width : 1px 0 0 0;
height : 0;
margin-bottom: 1px;
}
/*********************************** Radio ***********************************/
.tk-radio {
align-items: center;
column-gap : 2px;
}
.tk-radio .tk-icon {
align-items : center;
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
border-radius: 50%;
box-sizing : border-box;
display : flex;
height : 10px;
width : 10px;
}
.tk-radio .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-radio);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-radio);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-radio[aria-checked="true"] .tk-icon:before {
background: var(--tk-window-text);
}
.tk-radio:focus .tk-icon {
background: var(--tk-control-active);
}
.tk-radio[aria-checked="true"]:focus .tk-icon:before {
background: var(--tk-control-text);
}
.tk-radio.active[aria-checked="false"]:focus .tk-icon:before {
background: var(--tk-control-shadow);
}
/******************************** Scroll Bar *********************************/
.tk-scrollbar {
background: var(--tk-control-highlight);
box-shadow: 0 0 0 1px var(--tk-control-shadow) inset;
box-sizing: border-box;
}
.tk-scrollbar .tk-thumb,
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
background: var(--tk-control);
border : 1px solid var(--tk-control-border);
box-sizing: border-box;
color : var(--tk-control-text);
}
.tk-scrollbar:focus .tk-thumb,
.tk-scrollbar:focus .tk-unit-down,
.tk-scrollbar:focus .tk-unit-up {
background: var(--tk-control-active);
}
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
height: 13px;
width : 13px;
}
.tk-scrollbar .tk-unit-down:before,
.tk-scrollbar .tk-unit-up:before {
background : currentColor;
content : "";
display : block;
height : 100%;
mask-image : var(--tk-scroll);
mask-position: center;
mask-repeat : no-repeat;
mask-size : 100%;
width : 100%;
-webkit-mask-image : var(--tk-scroll);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100%;
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-down:before {
transform: rotate(-90deg);
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-up:before {
transform: rotate(90deg);
}
.tk-scrollbar[aria-orientation="vertical"] .tk-unit-down:before {
}
.tk-scrollbar[aria-orientation="vertical"] .tk-unit-up:before {
transform: rotate(180deg);
}
.tk-scrollbar .tk-unit-down.tk-active:before,
.tk-scrollbar .tk-unit-up.tk-active:before {
mask-size: calc(100% - 2px);
-webkit-mask-size: calc(100% - 2px);
}
.tk-scrollbar[aria-disabled="true"] .tk-unit-down,
.tk-scrollbar[aria-disabled="true"] .tk-unit-up,
.tk-scrollbar.tk-full .tk-unit-down,
.tk-scrollbar.tk-full .tk-unit-up {
background: var(--tk-control);
border-color: var(--tk-control-shadow);
color : var(--tk-control-shadow);
}
.tk-scrollbar .tk-block-down,
.tk-scrollbar .tk-block-up {
background : var(--tk-control-highlight);
border-color: var(--tk-control-shadow);
border-style: solid;
border-width: 0 1px;
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-block-down,
.tk-scrollbar[aria-orientation="horizontal"] .tk-block-up {
border-width: 1px 0;
}
.tk-scrollbar .tk-block-down.tk-active,
.tk-scrollbar .tk-block-up.tk-active {
background: var(--tk-control-shadow);
}
.tk-scrollbar[aria-disabled="true"] .tk-thumb,
.tk-scrollbar[aria-disabled="true"] .tk-block-down,
.tk-scrollbar[aria-disabled="true"] .tk-block-up,
.tk-scrollbar.tk-full .tk-thumb,
.tk-scrollbar.tk-full .tk-block-down,
.tk-scrollbar.tk-full .tk-block-up {
visibility: hidden;
}
/******************************** Scroll Pane ********************************/
.tk-scrollpane {
background: var(--tk-control);
}
.tk-scrollpane > .tk-scrollbar {
border: 0 solid var(--tk-control);
}
.tk-scrollpane > .tk-scrollbar[aria-orientation="horizontal"] {
border-width: 1px 0 0 0;
}
.tk-scrollpane > .tk-scrollbar[aria-orientation="vertical"] {
border-width: 0 0 0 1px;
}
/******************************** Split Pane *********************************/
.tk-splitpane > [role="separator"][aria-orientation="horizontal"] {
cursor: ns-resize;
height: 3px;
}
.tk-splitpane > [role="separator"][aria-orientation="vertical"] {
cursor: ew-resize;
width : 3px;
}
.tk-splitpane > [role="separator"]:focus {
background: var(--tk-splitter-focus);
z-index : 1;
}
/********************************* Text Box **********************************/
.tk-textbox {
background: var(--tk-window);
border : 1px solid var(--tk-control-shadow);
color : var(--tk-window-text);
margin : 0;
padding : 2px;
}
/********************************** Windows **********************************/
.tk-desktop {
background: var(--tk-desktop);
}
.tk-window > * {
border : 1px solid var(--tk-control-border);
box-shadow: 1px 1px 0 0 var(--tk-control-border);
margin : 0 1px 1px 0;
}
.tk-window > * > .tk-nw {left : -1px; top : -1px; height: 8px; width : 8px; }
.tk-window > * > .tk-n {left : 7px; top : -1px; right : 8px; height: 3px; }
.tk-window > * > .tk-ne {right: 0px; top : -1px; height: 8px; width : 8px; }
.tk-window > * > .tk-w {left : -1px; top : 7px; width : 3px; bottom: 8px; }
.tk-window > * > .tk-e {right: 0px; top : 7px; width : 3px; bottom: 8px; }
.tk-window > * > .tk-sw {left : -1px; bottom: 0px; height: 8px; width : 8px; }
.tk-window > * > .tk-s {left : 7px; bottom: 0px; right : 8px; height: 3px; }
.tk-window > * > .tk-se {right: 0px; bottom: 0px; height: 8px; width : 8px; }
.tk-window > * > .tk-title {
align-items : center;
background : var(--tk-window-blur-title);
border-bottom: 1px solid var(--tk-control-shadow);
box-sizing : border-box;
color : var(--tk-window-blur-title-text);
overflow : hidden;
padding : 1px;
position : relative;
}
.tk-window.two > * > .tk-title {
background: var(--tk-window-blur-title2);
}
.tk-window > * > .tk-title .tk-text {
cursor : default;
flex-basis : 0;
font-weight : bold;
min-width : 0;
overflow : hidden;
padding : 1px 1px 1px calc(1em + 3px);
text-align : center;
text-overflow: ellipsis;
user-select : none;
white-space : nowrap;
}
.tk-window > * > .tk-title .tk-close {
background: var(--tk-window-blur-close);
border : 1px solid var(--tk-control-shadow);
color : var(--tk-window-blur-close-text);
height : calc(1em - 1px);
margin : 1px 1px 1px 0;
overflow : none;
width : calc(1em - 1px);
}
.tk-window > * > .tk-title .tk-close:before {
background : currentColor;
content : "";
display : block;
height : 100%;
width : 100%;
mask-image : var(--tk-close);
mask-position: center;
mask-repeat : no-repeat;
mask-size : 100%;
-webkit-mask-image : var(--tk-close);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100%;
}
.tk-window > * > .tk-title .tk-close.active:before {
mask-size: calc(100% - 2px);
-webkit-mask-size: calc(100% - 2px);
}
.tk-window:focus-within > * > .tk-title {
background: var(--tk-window-title);
color : var(--tk-window-title-text);
}
.tk-window.two:focus-within > * > .tk-title {
background: var(--tk-window-title2);
}
.tk-window:focus-within > * > .tk-title .tk-close {
background: var(--tk-window-close);
color : var(--tk-window-close-text);
}
.tk-window > * > .tk-client {
background: var(--tk-control);
}
/************************************ CPU ************************************/
.tk-cpu .tk-main {
height: 100%;
width : 100%;
}
.tk-cpu .tk-main > .tk-a,
.tk-cpu .tk-registers > .tk-a,
.tk-cpu .tk-registers > .tk-b {
box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow);
}
.tk-cpu .tk-main > .tk-a { margin : 3px; }
.tk-cpu .tk-main > [role="separator"] { margin : 1px -2px; }
.tk-cpu .tk-main > .tk-b { margin : 3px; }
.tk-cpu .tk-registers > .tk-a { margin-bottom: 3px; }
.tk-cpu .tk-registers > [role="separator"] { margin : -2px; }
.tk-cpu .tk-registers > .tk-b { margin-top : 3px; }
.tk-disassembler .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-disassembler .tk-view {
height: 100%;
}
.tk-disassembler .tk-metrics {
padding-bottom: 1px;
}
.tk-disassembler .tk {
cursor : default;
font-family: var(--tk-font-mono);
user-select: none;
white-space: nowrap;
}
.tk-disassembler .tk-bytes,
.tk-disassembler .tk-mnemonic,
.tk-disassembler .tk-operands {
padding: 0 0 1px calc(1.2em - 1px);
}
.tk-disassembler .tk-address {
padding-left: 1px;
}
.tk-disassembler .tk-operands {
padding-right: 1px;
}
.tk-disassembler .tk-selected {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk-disassembler:focus-within .tk-selected {
background: var(--tk-selected);
color : var(--tk-selected-text);
}
.tk-reglist .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-reglist .tk-list {
align-items: center;
}
.tk-reglist .tk-expand {
align-items : center;
border-radius : 2px;
display : flex;
height : 11px;
justify-content: center;
width : 11px;
}
.tk-reglist .tk-expand:before {
content : "";
height : 100%;
display : block;
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-reglist .tk-expand:focus {
background: var(--tk-control-active);
}
.tk-reglist .tk-expand[aria-expanded]:before {
background: var(--tk-window-text);
}
.tk-reglist .tk-expand[aria-expanded]:focus:before {
background: var(--tk-control-text);
}
.tk-reglist .tk-expand[aria-expanded="false"]:before {
mask-image: var(--tk-expand);
-webkit-mask-image: var(--tk-expand);
}
.tk-reglist .tk-expand[aria-expanded="true"]:before {
mask-image: var(--tk-collapse);
-webkit-mask-image: var(--tk-collapse);
}
.tk-reglist .tk-name {
padding: 0 0.5em 0 1px;
}
.tk-reglist .tk-textbox {
background: transparent;
border : none;
padding : 0;
width : 1.5em;
}
.tk-reglist.tk-program .tk-textbox:not(.tk-mono) {
text-align: right;
width : 6em;
}
.tk-reglist .tk-expansion {
align-items : center;
column-gap : 0.8em;
margin-bottom: 2px;
padding : 2px 0 0 1.5em;
}
.tk-reglist .tk-expansion .tk-number .tk-label {
align-items : center;
display : flex;
justify-content: center;
min-width : 12px;
}
.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"][aria-checked="true"]
.tk-icon:before {
background: var(--tk-control-shadow);
}
.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"] .tk-contents,
.tk-reglist .tk-expansion .tk-number[disabled] *,
.tk-reglist .tk-expansion .tk-label[disabled],
.tk-reglist .tk-expansion .tk-textbox[disabled] {
color: var(--tk-control-shadow);
}
/********************************** Memory ***********************************/
.tk-window .tk-memory {
height: 100%;
width : 100%;
}
.tk-memory .tk-editor {
box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow);
height : calc(100% - 6px);
margin : 3px;
width : calc(100% - 6px);
}
.tk-memory .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-memory .tk-metrics,
.tk-memory .tk-view * {
padding-bottom: 1px;
cursor : default;
font-family : var(--tk-font-mono);
user-select : none;
}
.tk-memory .tk-byte {
border : 0 solid transparent;
padding : 0 1px;
text-align: center;
}
.tk-memory .tk-byte:not(.tk-15) {
margin-right: calc(0.6em - 1px);
}
.tk-memory .tk-address,
.tk-memory .tk-byte.tk-7 {
margin-right: calc(1.2em - 1px);
}
.tk-memory .tk-byte.tk-selected {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk-memory .tk-editor:focus-within .tk-byte.tk-selected {
background: var(--tk-selected);
color : var(--tk-selected-text);
}

26
app/theme/light.css Normal file
View File

@ -0,0 +1,26 @@
:root {
--tk-control : #eeeeee;
--tk-control-active : #cccccc;
--tk-control-border : #000000;
--tk-control-highlight : #f8f8f8;
--tk-control-shadow : #6c6c6c;
--tk-control-text : #000000;
--tk-desktop : #cccccc;
--tk-selected : #008542;
--tk-selected-blur : #325342;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #00000080;
--tk-window : #ffffff;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text: #eeeeee;
--tk-window-blur-title : #aac4d5;
--tk-window-blur-title2 : #dbc4b8;
--tk-window-blur-title-text: #444444;
--tk-window-close : #ee9999;
--tk-window-close-text : #ffffff;
--tk-window-text : #000000;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

6
app/theme/radio.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458332 2.6458332" version="1.1">
<g>
<circle style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" cx="1.3229166" cy="1.3229166" r="0.66145831" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 361 B

BIN
app/theme/roboto.woff2 Normal file

Binary file not shown.

6
app/theme/scroll.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 1.4552083,0.66145833 0.52916666,1.5874999 V 2.2489583 L 1.4552083,1.3229166 2.38125,2.2489583 V 1.5874999 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 484 B

70
app/theme/virtual.css Normal file
View File

@ -0,0 +1,70 @@
:root {
--tk-control : #000000;
--tk-control-active : #550000;
--tk-control-border : #ff0000;
--tk-control-highlight : #550000;
--tk-control-shadow : #aa0000;
--tk-control-text : #ff0000;
--tk-desktop : #000000;
--tk-selected : #550000;
--tk-selected-blur : #550000;
--tk-selected-blur-text : #ff0000;
--tk-selected-text : #ff0000;
--tk-splitter-focus : #ff000099;
--tk-window : #000000;
--tk-window-blur-close : #000000;
--tk-window-blur-close-text: #aa0000;
--tk-window-blur-title : #000000;
--tk-window-blur-title2 : #000000;
--tk-window-blur-title-text: #aa0000;
--tk-window-close : #550000;
--tk-window-close-text : #ff0000;
--tk-window-text : #ff0000;
--tk-window-title : #550000;
--tk-window-title2 : #550000;
--tk-window-title-text : #ff0000;
}
input {
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
}
/******************************** Scroll Bar *********************************/
.tk-scrollbar .tk-thumb,
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
background : #aa0000;
border-color: #550000;
color : #000000;
}
.tk-scrollbar:focus .tk-thumb,
.tk-scrollbar:focus .tk-unit-down,
.tk-scrollbar:focus .tk-unit-up {
background : #ff0000;
border-color: #aa0000;
}
.tk-scrollbar[aria-disabled="true"] .tk-thumb,
.tk-scrollbar[aria-disabled="true"] .tk-unit-down,
.tk-scrollbar[aria-disabled="true"] .tk-unit-up,
.tk-scrollbar.tk-full .tk-thumb,
.tk-scrollbar.tk-full .tk-unit-down,
.tk-scrollbar.tk-full .tk-unit-up {
background : #550000;
border-color: #aa0000;
color : #aa0000;
}
.tk-scrollbar .tk-block-down,
.tk-scrollbar .tk-block-up {
background : #550000;
border-color: #aa0000;
}
.tk-window > * > .tk-client > .tk-memory {
box-shadow: 0 0 0 1px #000000, 0 0 0 2px #ff0000;
}

387
app/toolkit/Button.js Normal file
View File

@ -0,0 +1,387 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Button //
///////////////////////////////////////////////////////////////////////////////
// Push, toggle or radio button
class Button extends Component {
static Component = Component;
//////////////////////////////// Constants ////////////////////////////////
// Types
static BUTTON = 0;
static RADIO = 1;
static TOGGLE = 2;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-button",
focusable: true,
role : "button",
tagName : "div",
style : {
display : "inline-block",
userSelect: "none"
}
});
// Configure instance fields
options = options || {};
this.attribute = options.attribute || "aria-pressed";
this.group = null;
this.isEnabled = null;
this.isSelected = false;
this.text = null;
this.type = Button.BUTTON;
// Configure contents
this.contents = document.createElement("div");
this.append(this.contents);
// Configure component
this.setEnabled(!("enabled" in options) || options.enabled);
if ("group" in options)
options.group.add(this);
this.setText (options.text);
this.setType (options.type);
if ("selected" in options)
this.setSelected(options.selected);
// Configure event handlers
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "Enter": // Fallthrough
case " " :
this.click();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Pointer down
onPointerDown(e) {
this.focus();
// Error checking
if (
!this.isEnabled ||
this.element.hasPointerCapture(e.pointerId) ||
e.button != 0
) return;
// Configure event
this.element.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.add("active");
}
// Pointer move
onPointerMove(e) {
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList[
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
}
// Pointer up
onPointerUp(e) {
// Error checking
if (
!this.isEnabled ||
e.button != 0 ||
!this.element.hasPointerCapture(e.pointerId)
) return;
// Configure event
this.element.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.remove("active");
// Item is an action
let bounds = this.getBounds();
if (this.menu == null && Toolkit.isInside(this.element, e))
this.click();
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically activate the button
click() {
if (this instanceof Toolkit.CheckBox)
this.setSelected(this instanceof Toolkit.Radio||!this.isSelected);
this.event("action");
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("aria-disabled", enabled ? null : "true");
}
// Specify whether the toggle or radio button is selected
setSelected(selected) {
selected = !!selected;
// Take no action
if (selected == this.isSelected)
return;
// Processing by button type
switch (this.type) {
case Button.RADIO :
if (selected && this.group != null)
this.group.deselect();
// Fallthrough
case Button.TOGGLE:
this.isSelected = selected;
this.setAttribute(this.attribute, selected);
}
}
// Specify the widget's display text
setText(text) {
this.text = (text || "").toString().trim();
this.translate();
}
// Specify what kind of button this is
setType(type) {
switch (type) {
case Button.BUTTON:
this.type = type;
this.setAttribute(this.attribute, null);
this.setSelected(false);
break;
case Button.RADIO : // Fallthrough
case Button.TOGGLE:
this.type = type;
this.setAttribute(this.attribute, this.isSelected);
this.setSelected(this.isSelected);
break;
default: return;
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
super.translate();
if (this.contents != null)
this.contents.innerText = this.gui.translate(this.text, this);
}
}
///////////////////////////////////////////////////////////////////////////////
// CheckBox //
///////////////////////////////////////////////////////////////////////////////
// On/off toggle box
class CheckBox extends Button {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
attribute: "aria-checked",
className: "tk tk-checkbox",
role : "checkbox",
style : {},
type : Button.TOGGLE
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Default styles override
for (let entry of Object.entries({
display : "inline-grid",
gridTemplateColumns: "max-content auto"
})) if (!(entry[0] in uptions.style))
uptions.style[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
this.contents.classList.add("tk-contents");
// Configure icon
this.icon = document.createElement("div");
this.icon.className = "tk tk-icon";
this.icon.setAttribute("aria-hidden", "true");
this.prepend(this.icon);
}
}
///////////////////////////////////////////////////////////////////////////////
// Radio //
///////////////////////////////////////////////////////////////////////////////
// Single selection box
class Radio extends CheckBox {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
className: "tk tk-radio",
role : "radio",
type : Button.RADIO
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
}
}
///////////////////////////////////////////////////////////////////////////////
// Group //
///////////////////////////////////////////////////////////////////////////////
// Radio button or menu item group
class Group extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app) {
super(app, {
tagName: "div",
style : {
height : "0",
position: "absolute",
width : "0"
}
});
// Configure instance fields
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add an item
add(item) {
// Error checking
if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1)
return item;
// Configure component
this.setAttribute("role",
item instanceof Toolkit.Radio ? "radiogroup" : "group");
// Configure item
if (item.group != null)
item.group.remove(item);
item.group = this;
// Add the item to the collection
item.id = item.id || Toolkit.id();
this.items.push(item);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
return item;
}
// Remove all items
clear() {
this.items.splice();
this.setAttribute("aria-owns", "");
}
// Un-check all items in the group
deselect() {
for (let item of this.items)
if (item.isSelected && "setSelected" in item)
item.setSelected(false);
}
// Remove an item
remove(item) {
// Error checking
let index = this.items.indexOf(item);
if (index == -1)
return;
// Remove the item from the collection
this.items.splice(index, 1);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
item.group = null;
return item;
}
}
export { Button, CheckBox, Group, Radio };

312
app/toolkit/Component.js Normal file
View File

@ -0,0 +1,312 @@
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Component //
///////////////////////////////////////////////////////////////////////////////
// Abstract class representing a distinct UI element
class Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options, defaults) {
// Configure instance fields
this.children = [];
this.gui = gui || this;
this.label = null;
this.resizeObserver = null;
this.substitutions = {};
this.toolTip = null;
// Configure default options
let uptions = options || {};
options = {};
Object.assign(options, uptions);
options.style = options.style || {};
defaults = defaults || {};
defaults.style = defaults.style || {};
for (let key of Object.keys(defaults))
if (!(key in options))
options[key] = defaults[key];
for (let key of Object.keys(defaults.style))
if (!(key in options.style))
options.style[key] = defaults.style[key];
this.visibility = !!options.visibility;
// Configure element
this.element = document.createElement(
("tagName" in options ? options.tagName : null) || "div");
if (Object.keys(options.style).length != 0)
Object.assign(this.element.style, options.style);
if ("className" in options && options.className)
this.element.className = options.className;
if ("focusable" in options)
this.setFocusable(options.focusable, options.tabStop);
if ("id" in options)
this.setId(options.id);
if ("role" in options && options.role )
this.element.setAttribute("role", options.role);
if ("visible" in options)
this.setVisible(options.visible);
// Configure component
this.setAttribute("name", options.name || "");
this.setLabel (options.label || "");
this.setToolTip (options.toolTip || "");
// Configure substitutions
if ("substitutions" in options) {
for (let sub of Object.entries(options.substitutions))
this.setSubstitution(sub[0], sub[1], true);
this.translate();
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a child component
add(component) {
// The component is already a child of this component
let index = this.children.indexOf(component);
if (index != -1)
return index;
// The component has a different parent already
if (component.parent != null)
component.parent.remove(component);
// Add the child component to this component
component.parent = this;
this.children.push(component);
if ("addHook" in this)
this.addHook(component);
else this.append(component);
if ("addedHook" in component)
component.addedHook(this);
return this.children.length - 1;
}
// Listen for events
addEventListener(type, listener, useCapture) {
let callback = e=>{
e.component = this;
return listener(e);
};
// Register the listener for the event type
this.element.addEventListener(type, callback, useCapture);
// Listen for resize events on the element
if (type == "resize" && this.resizeObserver == null) {
this.resizeObserver = new ResizeObserver(
()=>this.event("resize"));
this.resizeObserver.observe(this.element);
}
return callback;
}
// Add a DOM element as a sibling after this component
after(child) {
let element = child instanceof Element ? child : child.element;
this.element.after(element);
}
// Add a DOM element to the end of this component's children
append(child) {
let element = child instanceof Element ? child : child.element;
this.element.append(element);
}
// Add a DOM element as a sibling before this component
before(child) {
let element = child instanceof Element ? child : child.element;
this.element.before(element);
}
// Request non-focus on this component
blur() {
this.element.blur();
}
// Determine whether this component contains another or an element
contains(child) {
// Child is an element
if (child instanceof Element)
return this.element.contains(child);
// Child is a component
for (let component = child; component; component = component.parent)
if (component == this)
return true;
return false;
}
// Request focus on the component
focus() {
this.element.focus();
}
// Retrieve the current DOM position of the element
getBounds() {
return this.element.getBoundingClientRect();
}
// Determine whether this component currently has focus
hasFocus() {
return document.activeElement == this.element;
}
// Determine whether the component is visible
isVisible() {
// Common visibility test
if (
!document.contains(this.element) ||
this.parent && !this.parent.isVisible()
) return false;
// Overridden visibility test
if ("visibleHook" in this) {
if (!this.visibleHook())
return false;
}
// Default visibility test
else {
let style = getComputedStyle(this.element);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
}
// Add a DOM element to the beginning of this component's children
prepend(child) {
let element = child instanceof Element ? child : child.element;
this.element.prepend(element);
}
// Remove a child component
remove(component) {
let index = this.children.indexOf(component);
// The component does not belong to this component
if (index == -1)
return -1;
// Remove the child component from this component
this.children.splice(index, 1);
if ("removeHook" in this)
this.removeHook(component);
else component.element.remove();
if ("removedHook" in component)
component.removedHook(this);
return index;
}
// Remove an event listener
removeEventListener(type, listener, useCapture) {
this.element.removeEventListener(type, listener, useCapture);
}
// Specify an HTML attribute's value
setAttribute(name, value) {
value =
value === false ? false :
value === null || value === undefined ? "" :
value.toString().trim()
;
if (value === "")
this.element.removeAttribute(name);
else this.element.setAttribute(name, value);
}
// Specify whether or not the element is focusable
setFocusable(focusable, tabStop) {
if (!focusable)
this.element.removeAttribute("tabindex");
else this.element.setAttribute("tabindex",
tabStop || tabStop === undefined ? "0" : "-1");
}
// Specify a localization key for the accessible name label
setLabel(key) {
this.label = key;
this.translate();
}
// Specify the DOM Id for this element
setId(id) {
this.id = id = id || null;
this.setAttribute("id", id);
}
// Specify text to substitute within localized contexts
setSubstitution(key, text, noTranslate) {
let ret = this.substitutions[key] || null;
// Providing new text
if (text !== null)
this.substitutions[key] = text.toString();
// Removing an association
else if (key in this.substitutions)
delete this.substitutions[key];
// Update display text
if (!noTranslate)
this.translate();
return ret;
}
// Specify a localization key for the tool tip text
setToolTip(key) {
this.toolTip = key;
this.translate();
}
// Specify whether the component is visible
setVisible(visible) {
let prop = this.visibility ? "visibility" : "display";
if (!!visible)
this.element.style.removeProperty(prop);
else this.element.style[prop] = this.visibility ? "hidden" : "none";
}
///////////////////////////// Package Methods /////////////////////////////
// Dispatch an event
event(type, fields) {
this.element.dispatchEvent(Toolkit.event(type, this, fields));
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
if (this.label)
this.setAttribute("aria-label", this.gui.translate(this.label, this));
if (this.toolTip)
this.setAttribute("title", this.gui.translate(this.toolTip, this));
}
};
export { Component };

125
app/toolkit/DropDown.js Normal file
View File

@ -0,0 +1,125 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// DropDown //
///////////////////////////////////////////////////////////////////////////////
// Text entry field
class DropDown extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-dropdown",
tagName : "select"
});
// Configure instance fields
this.isEnabled = null;
this.options = [];
// Configure component
options = options || {};
this.setEnabled(!("enabled" in options) || options.enabled);
if ("options" in options)
this.setOptions(options.options);
this.setSelectedIndex(
("selectedIndex" in options ? options : this).selectedIndex);
// Configure event handlers
this.addEventListener("keydown" , e=>e.stopPropagation());
this.addEventListener("pointerdown", e=>e.stopPropagation());
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically change the selection
change() {
this.element.dispatchEvent(this.event("input"));
}
// Retrieve the current selection index
getSelectedIndex() {
return this.element.selectedIndex;
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("disabled", enabled ? null : "true");
}
// Specify the list contents
setOptions(options) {
// Error checking
if (!Array.isArray(options))
return;
// Erase the list of options
this.options.splice(0);
this.element.replaceChildren();
// Add options from the input
for (let option of options) {
if (typeof option != "string")
continue;
this.options.push(option);
this.element.add(document.createElement("option"));
}
// Update the display text
this.translate();
}
// Specify the current selection
setSelectedIndex(index) {
// Error checking
if (typeof index != "number" || isNaN(index))
return this.element.selectedIndex;
index = Math.round(index);
if (index < -1 || index >= this.options.length)
return this.element.selectedIndex;
// Configure element and instance fields
return this.element.selectedIndex = index;
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
super.translate();
// Error checking
if (!this.options)
return;
// Update the list items
for (let x = 0; x < this.options.length; x++) {
this.element.item(x).innerText =
this.gui.translate(this.options[x], this);
}
}
}
export { DropDown };

748
app/toolkit/MenuBar.js Normal file
View File

@ -0,0 +1,748 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Menu //
///////////////////////////////////////////////////////////////////////////////
// Pop-up menu container, child of MenuItem
class Menu extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className : "tk tk-menu",
role : "menu",
tagName : "div",
visibility: true,
visible : false,
style : {
position: "absolute",
}
});
// Trap pointer events
this.addEventListener("pointerdown", e=>{
e.stopPropagation();
e.preventDefault();
});
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for parent.add()
addedHook(parent) {
this.setAttribute("aria-labelledby", parent.id);
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuSeparator //
///////////////////////////////////////////////////////////////////////////////
// Separator between groups of menu items
class MenuSeparator extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-separator",
role : "separator",
tagName : "div"
});
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuItem //
///////////////////////////////////////////////////////////////////////////////
// Individual menu selection
class MenuItem extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-item",
focusable: true,
tabStop : false,
tagName : "div"
});
options = options || {};
// Configure instance fields
this.isEnabled = null;
this.isExpanded = false;
this.menu = null;
this.menuBar = null;
this.text = null;
this.type = null;
// Configure element
this.contents = document.createElement("div");
this.append(this.contents);
this.eicon = document.createElement("div");
this.eicon.className = "tk tk-icon";
this.contents.append(this.eicon);
this.etext = document.createElement("div");
this.etext.className = "tk tk-text";
this.contents.append(this.etext);
// Configure event handlers
this.addEventListener("blur" , e=>this.onBlur (e));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
// Configure widget
this.gui.localize(this);
this.setEnabled("enabled" in options ? !!options.enabled : true);
this.setId (Toolkit.id());
this.setText (options.text);
this.setType (options.type, options.checked);
}
///////////////////////////// Event Handlers //////////////////////////////
// Focus lost
onBlur(e) {
// An item in a different menu is receiving focus
if (this.menu != null) {
if (
!this .contains(e.relatedTarget) &&
!this.menu.contains(e.relatedTarget)
) this.setExpanded(false);
}
// Item is an action
else if (e.component == this)
this.element.classList.remove("active");
// Simulate a bubbling event sequence
if (this.parent)
this.parent.onBlur(e);
}
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "ArrowDown":
// Error checking
if (!this.parent)
break;
// Top-level: open the menu and focus its first item
if (this.parent == this.menuBar) {
if (this.menu == null)
return;
this.setExpanded(true);
this.listItems()[0].focus();
}
// Sub-menu: cycle to the next sibling
else {
let items = this.parent.listItems();
items[(items.indexOf(this) + 1) % items.length].focus();
}
break;
case "ArrowLeft":
// Error checking
if (!this.parent)
break;
// Sub-menu: close and focus parent
if (
this.parent != this.menuBar &&
this.parent.parent != this.menuBar
) {
this.parent.setExpanded(false);
this.parent.focus();
}
// Top-level: cycle to previous sibling
else {
let menu = this.parent == this.menuBar ?
this : this.parent;
let items = this.menuBar.listItems();
let prev = items[(items.indexOf(menu) +
items.length - 1) % items.length];
if (menu.isExpanded)
prev.setExpanded(true);
prev.focus();
}
break;
case "ArrowRight":
// Error checking
if (!this.parent)
break;
// Sub-menu: open the menu and focus its first item
if (this.menu != null && this.parent != this.menuBar) {
this.setExpanded(true);
(this.listItems()[0] || this).focus();
}
// Top level: cycle to next sibling
else {
let menu = this;
while (menu.parent != this.menuBar)
menu = menu.parent;
let expanded = this.menuBar.expandedMenu() != null;
let items = this.menuBar.listItems();
let next = items[(items.indexOf(menu) + 1) % items.length];
next.focus();
if (expanded)
next.setExpanded(true);
}
break;
case "ArrowUp":
// Error checking
if (!this.parent)
break;
// Top-level: open the menu and focus its last item
if (this.parent == this.menuBar) {
if (this.menu == null)
return;
this.setExpanded(true);
let items = this.listItems();
(items[items.length - 1] || this).focus();
}
// Sub-menu: cycle to previous sibling
else {
let items = this.parent.listItems();
items[(items.indexOf(this) +
items.length - 1) % items.length].focus();
}
break;
case "End":
{
// Error checking
if (!this.parent)
break;
// Focus last sibling
let expanded = this.isExpanded &&
this.parent == this.menuBar;
let items = this.parent.listItems();
let last = items[items.length - 1] || this;
last.focus();
if (expanded)
last.setExpanded(true);
}
break;
case "Enter":
case " ":
// Do nothing
if (!this.isEnabled)
break;
// Action item: activate the menu item
if (this.menu == null)
this.activate(this.type == "check" && e.key == " ");
// Sub-menu: open the menu and focus its first item
else {
this.setExpanded(true);
let items = this.listItems();
if (items[0])
items[0].focus();
}
break;
case "Escape":
// Error checking
if (!this.parent)
break;
// Top-level (not specified by WAI-ARIA)
if (this.parent == this.menuBar) {
if (this.isExpanded)
this.setExpanded(false);
else this.menuBar.exit();
}
// Sub-menu: close and focus parent
else {
this.parent.setExpanded(false);
this.parent.focus();
}
break;
case "Home":
{
// Error checking
if (!this.parent)
break;
// Focus first sibling
let expanded = this.isExpanded &&
this.parent == this.menuBar;
let first = this.parent.listItems()[0] || this;
first.focus();
if (expanded)
first.setExpanded(true);
}
break;
// Do not handle the event
default: return;
}
// The event was handled
e.stopPropagation();
e.preventDefault();
}
// Pointer press
onPointerDown(e) {
this.focus();
// Error checking
if (
!this.isEnabled ||
this.element.hasPointerCapture(e.pointerId) ||
e.button != 0
) return;
// Configure event
if (this.menu == null)
this.element.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
if (this.menu != null)
this.setExpanded(!this.isExpanded);
else this.element.classList.add("active");
}
// Pointer move
onPointerMove(e) {
// Hovering over a menu when a sibling menu is already open
let expanded = this.parent && this.parent.expandedMenu();
if (this.menu != null && expanded != null && expanded != this) {
// Configure component
this.setExpanded(true);
this.focus();
// Configure event
e.stopPropagation();
e.preventDefault();
return;
}
// Not dragging
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Not an action item
if (this.menu != null)
return;
// Check if the cursor is within the bounds of the component
this.element.classList[
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
}
// Pointer release
onPointerUp(e) {
// Error checking
if (
!this.isEnabled ||
e.button != 0 ||
(this.parent && this.parent.hasFocus() ?
this.menu != null :
!this.element.hasPointerCapture(e.pointerId)
)
) return;
// Configure event
this.element.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Item is an action
let bounds = this.getBounds();
if (this.menu == null && Toolkit.isInside(this.element, e))
this.activate();
}
///////////////////////////// Public Methods //////////////////////////////
// Invoke an action command
activate(noExit) {
if (this.menu != null)
return;
if (this.type == "check")
this.setChecked(!this.isChecked);
if (!noExit)
this.menuBar.exit();
this.element.dispatchEvent(Toolkit.event("action", this));
}
// Add a separator between groups of menu items
addSeparator(options) {
let sep = new Toolkit.MenuSeparator(this, options);
this.add(sep);
return sep;
}
// Produce a list of child items
listItems(invisible) {
return this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
(invisible || c.isVisible())
);
}
// Specify whether the menu item is checked
setChecked(checked) {
if (this.type != "check")
return;
this.isChecked = !!checked;
this.setAttribute("aria-checked", this.isChecked);
}
// Specify whether the menu item can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("aria-disabled", enabled ? null : "true");
if (!enabled)
this.setExpanded(false);
}
// Specify whether the sub-menu is open
setExpanded(expanded) {
// State is not changing
expanded = !!expanded;
if (this.menu == null || expanded === this.isExpanded)
return;
// Position the sub-menu
if (expanded) {
let bndGUI = this.gui .getBounds();
let bndMenu = this.menu.getBounds();
let bndThis = this .getBounds();
let bndParent = !this.parent ? bndThis : (
this.parent == this.menuBar ? this.parent : this.parent.menu
).getBounds();
this.menu.element.style.left = Math.max(0,
Math.min(
(this.parent && this.parent == this.menuBar ?
bndThis.left : bndThis.right) - bndParent.left,
bndGUI.right - bndMenu.width
)
) + "px";
this.menu.element.style.top = Math.max(0,
Math.min(
(this.parent && this.parent == this.menuBar ?
bndThis.bottom : bndThis.top) - bndParent.top,
bndGUI.bottom - bndMenu.height
)
) + "px";
}
// Close all open sub-menus
else for (let child of this.listItems())
child.setExpanded(false);
// Configure component
this.isExpanded = expanded;
this.setAttribute("aria-expanded", expanded);
this.menu.setVisible(expanded);
if (expanded)
this.element.classList.add("active");
else this.element.classList.remove("active");
}
// Specify the widget's display text
setText(text) {
this.text = (text || "").toString().trim();
this.translate();
}
// Specify what kind of menu item this is
setType(type, arg) {
this.type = type = (type || "").toString().trim() || "normal";
switch (type) {
case "check":
this.setAttribute("role", "menuitemcheckbox");
this.setChecked(arg);
break;
default: // normal
this.setAttribute("role", "menuitem");
this.setAttribute("aria-checked", null);
break;
}
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for add()
addHook(component) {
// Convert to sub-menu
if (this.menu == null) {
this.menu = new Toolkit.Menu(this);
this.after(this.menu);
this.setAttribute("aria-haspopup", "menu");
this.setAttribute("aria-expanded", "false");
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
// Add the child component
component.menuBar = this.menuBar;
this.menu.append(component);
if (component instanceof Toolkit.MenuItem && component.menu != null)
this.menu.append(component.menu);
// Configure icon mode
this.checkIcons();
}
// Check whether any child menu items contain icons
checkIcons() {
if (this.menu == null)
return;
if (this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
c.menu == null &&
c.type != "normal"
).length != 0)
this.menu.element.classList.add("icons");
else this.menu.element.classList.remove("icons");
}
// Replacement behavior for remove()
removeHook(component) {
// Remove the child component
component.element.remove();
if (component instanceof Toolkit.MenuItem && component.menu != null)
component.menu.element.remove();
// Convert to action item
if (this.children.length == 0) {
this.menu.element.remove();
this.menu = null;
this.setAttribute("aria-haspopup", null);
this.setAttribute("aria-expanded", "false");
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
}
// Regenerate localized display text
translate() {
super.translate();
if (!("contents" in this))
return;
this.etext.innerText = this.gui.translate(this.text, this);
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve the currently expanded sub-menu, if any
expandedMenu() {
return this.children.filter(c=>c.isExpanded)[0] || null;
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuBar //
///////////////////////////////////////////////////////////////////////////////
// Application menu bar
class MenuBar extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-bar",
focusable: false,
tagName : "div",
tabStop : true,
role : "menubar",
style : {
position: "relative",
zIndex : "1"
}
});
// Configure instance fields
this.focusTarget = null;
this.menuBar = this;
// Configure event handlers
this.addEventListener("blur" , e=>this.onBlur (e), true);
this.addEventListener("focus" , e=>this.onFocus (e), true);
this.addEventListener("keydown", e=>this.onKeyDown(e), true);
// Configure widget
this.gui.localize(this);
}
///////////////////////////// Event Handlers //////////////////////////////
// Focus lost
onBlur(e) {
if (this.contains(e.relatedTarget))
return;
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
let menu = this.expandedMenu();
if (menu != null)
menu.setExpanded(false);
}
// Focus gained
onFocus(e) {
if (this.contains(e.relatedTarget))
return;
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, false);
this.focusTarget = e.relatedTarget;
}
// Key pressed
onKeyDown(e) {
if (e.key != "Tab")
return;
e.stopPropagation();
e.preventDefault();
this.exit();
}
///////////////////////////// Public Methods //////////////////////////////
// Produce a list of child items
listItems(invisible) {
return this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
(invisible || c.isVisible())
);
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for add()
addHook(component) {
component.menuBar = this.menuBar;
this.append(component);
if (component instanceof Toolkit.MenuItem && component.menu != null)
this.append(component.menu);
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
}
// Return control to the application
exit() {
this.onBlur({ relatedTarget: null });
if (this.focusTarget)
this.focusTarget.focus();
else document.activeElement.blur();
}
// Replacement behavior for remove()
removeHook(component) {
component.element.remove();
if (component instanceof Toolkit.MenuItem && component.menu != null)
component.menu.element.remove();
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve the currently expanded menu, if any
expandedMenu() {
return this.children.filter(c=>c.isExpanded)[0] || null;
}
};
export { Menu, MenuBar, MenuItem, MenuSeparator };

1149
app/toolkit/ScrollBar.js Normal file

File diff suppressed because it is too large Load Diff

145
app/toolkit/TextBox.js Normal file
View File

@ -0,0 +1,145 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// TextBox //
///////////////////////////////////////////////////////////////////////////////
// Text entry field
class TextBox extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-textbox",
tagName : "input"
});
this.element.type = "text";
this.setAttribute("spellcheck", "false");
// Configure instance fields
this.isEnabled = null;
this.maxLength = null;
this.pattern = null;
// Configure component
options = options || {};
this.setEnabled(!("enabled" in options) || options.enabled);
if ("maxLength" in options)
this.setMaxLength(options.maxLength);
if ("pattern" in options)
this.setPattern(options.pattern);
this.setText (options.text);
// Configure event handlers
this.addEventListener("blur" , e=>this.commit ( ));
this.addEventListener("pointerdown", e=>e.stopPropagation( ));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "ArrowLeft":
case "ArrowRight":
e.stopPropagation();
return;
case "Enter":
this.commit();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically commit the text box
commit() {
this.event("action");
}
// Retrieve the control's value
getText() {
return this.element.value;
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("disabled", enabled ? null : "true");
}
// Specify the maximum length of the text
setMaxLength(length) {
// Remove limitation
if (length === null) {
this.maxLength = null;
this.setAttribute("maxlength", null);
return;
}
// Error checking
if (typeof length != "number" || isNaN(length))
return;
// Range checking
length = Math.floor(length);
if (length < 0)
return;
// Configure component
this.maxLength = length;
this.setAttribute("maxlength", length);
}
// Specify a regex pattern for valid text characters
setPattern(pattern) {
/*
Disabled because user agents may not prevent invalid input
// Error checking
if (pattern && typeof pattern != "string")
return;
// Configure component
this.pattern = pattern = pattern || null;
this.setAttribute("pattern", pattern);
*/
}
// Specify the widget's display text
setText(text = "") {
this.element.value = text.toString();
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
}
export { TextBox };

337
app/toolkit/Toolkit.js Normal file
View File

@ -0,0 +1,337 @@
import { Component } from /**/"./Component.js";
import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ;
import { DropDown } from /**/"./DropDown.js" ;
import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ;
import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js";
import { TextBox } from /**/"./TextBox.js" ;
import { Desktop, Window } from /**/"./Window.js" ;
///////////////////////////////////////////////////////////////////////////////
// Toolkit //
///////////////////////////////////////////////////////////////////////////////
// Top-level user interface manager
let Toolkit = globalThis.Toolkit = (class GUI extends Component {
static initializer() {
// Static state
this.nextId = 0;
// Locale presets
this.NO_LOCALE = { id: "(Null)" };
// Component classes
this.components = [];
Button .setToolkit(this); this.components.push(Button .Component);
Component.setToolkit(this); this.components.push( Component);
DropDown .setToolkit(this); this.components.push(DropDown .Component);
MenuBar .setToolkit(this); this.components.push(MenuBar .Component);
ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component);
TextBox .setToolkit(this); this.components.push(TextBox .Component);
Window .setToolkit(this); this.components.push(Window .Component);
this.Button = Button;
this.CheckBox = CheckBox;
this.Component = Component;
this.Desktop = Desktop;
this.DropDown = DropDown;
this.Group = Group;
this.Menu = Menu;
this.MenuBar = MenuBar;
this.MenuItem = MenuItem;
this.MenuSeparator = MenuSeparator;
this.Radio = Radio;
this.ScrollBar = ScrollBar;
this.ScrollPane = ScrollPane;
this.SplitPane = SplitPane;
this.TextBox = TextBox;
this.Window = Window;
return this;
}
///////////////////////////// Static Methods //////////////////////////////
// Monitor resize events on an element
static addResizeListener(element, listener) {
// Establish a ResizeObserver
if (!("resizeListeners" in element)) {
element.resizeListeners = [];
element.resizeObserver = new ResizeObserver(
(e,o)=>element.dispatchEvent(this.event("resize")));
element.resizeObserver.observe(element);
}
// Associate the listener
if (element.resizeListeners.indexOf(listener) == -1) {
element.resizeListeners.push(listener);
element.addEventListener("resize", listener);
}
}
// Stop monitoring resize events on an element
static clearResizeListeners(element) {
while ("resizeListeners" in element)
this.removeResizeListener(element, element.resizeListeners[0]);
}
// Produce a custom event object
static event(type, component, fields) {
let event = new Event(type, {
bubbles : true,
cancelable: true
});
if (component)
event.component = component;
if (fields)
Object.assign(event, fields);
return event;
}
// Produce a unique element ID
static id() {
return "tk" + (this.nextId++);
}
// Determine whether an object is a component
// The user agent may not resolve imports to the same classes
static isComponent(o) {
return !!this.components.find(c=>o instanceof c);
}
// Determine whether a pointer event is inside an element
static isInside(element, e) {
let bounds = element.getBoundingClientRect();
return (
e.offsetX >= 0 && e.offsetX < bounds.width &&
e.offsetY >= 0 && e.offsetY < bounds.height
);
}
// Generate a list of focusable child elements
static listFocusables(element) {
return Array.from(element.querySelectorAll(
"*:not(*:not(a[href], area, button, details, input, " +
"textarea, select, [tabindex='0'])):not([disabled])"
)).filter(e=>{
for (; e instanceof Element; e = e.parentNode) {
let style = getComputedStyle(e);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
});
}
// Stop monitoring resize events on an element
static removeResizeListener(element, listener) {
// Error checking
if (!("resizeListeners" in element))
return;
let index = element.resizeListeners.indexOf(listener);
if (index == -1)
return;
// Remove the listener
element.removeEventListener("resize", element.resizeListeners[index]);
element.resizeListeners.splice(index, 1);
// No more listeners: delete the ResizeObserver
if (element.resizeListeners.length == 0) {
element.resizeObserver.unobserve(element);
delete element.resizeListeners;
delete element.resizeObserver;
}
}
// Compute pointer event screen coordinates
static screenCoords(e) {
return {
x: e.screenX / window.devicePixelRatio,
y: e.screenY / window.devicePixelRatio
};
}
///////////////////////// Initialization Methods //////////////////////////
constructor(options) {
super(null, options);
// Configure instance fields
this.locale = Toolkit.NO_LOCALE;
this.localized = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Specify the locale to use for translated strings
setLocale(locale) {
this.locale = locale || Toolkit.NO_LOCALE;
for (let component of this.localized)
component.translate();
}
// Translate a string in the selected locale
translate(key, component) {
// Front-end method
if (key === undefined) {
super.translate();
return;
}
// Working variables
let subs = component ? component.substitutions : {};
key = (key || "").toString().trim();
// Error checking
if (this.locale == null || key == "")
return key;
// Resolve the key first in the substitutions then in the locale
let text = key;
key = key.toLowerCase();
if (key in subs)
text = subs[key];
else if (key in this.locale)
text = this.locale[key];
else return "!" + text.toUpperCase();
// Process all substitutions
for (;;) {
// Working variables
let sIndex = 0;
let rIndex = -1;
let lIndex = -1;
let zIndex = -1;
// Locate the inner-most {} or [] pair
for (;;) {
let match = Toolkit.subCtrl(text, sIndex);
// No control characters found
if (match == -1)
break;
sIndex = match + 1;
// Processing by control character
switch (text.charAt(match)) {
// Opening a substitution group
case "{": rIndex = match; continue;
case "[": lIndex = match; continue;
// Closing a recursion group
case "}":
if (rIndex != -1) {
lIndex = -1;
zIndex = match;
}
break;
// Closing a literal group
case "]":
if (lIndex != -1) {
rIndex = -1;
zIndex = match;
}
break;
}
break;
}
// Process a recursion substitution
if (rIndex != -1) {
text =
text.substring(0, rIndex) +
this.translate(
text.substring(rIndex + 1, zIndex),
component
) +
text.substring(zIndex + 1)
;
}
// Process a literal substitution
else if (lIndex != -1) {
text =
text.substring(0, lIndex) +
text.substring(lIndex + 1, zIndex)
.replaceAll("{", "{{")
.replaceAll("}", "}}")
.replaceAll("[", "[[")
.replaceAll("]", "]]")
+
text.substring(zIndex + 1)
;
}
// No more substitutions
else break;
}
// Unescape all remaining control characters
return (text
.replaceAll("{{", "{")
.replaceAll("}}", "}")
.replaceAll("[[", "[")
.replaceAll("]]", "]")
);
}
///////////////////////////// Package Methods /////////////////////////////
// Reduce an object to a single level of depth
static flatten(obj, ret = {}, id) {
for (let entry of Object.entries(obj)) {
let key = (id ? id + "." : "") + entry[0].toLowerCase();
let value = entry[1];
if (value instanceof Object)
this.flatten(value, ret, key);
else ret[key] = value;
}
return ret;
}
// Register a component for localization management
localize(component) {
if (this.localized.indexOf(component) != -1)
return;
this.localized.push(component);
component.translate();
}
// Locate a substitution control character in a string
static subCtrl(text, index) {
for (; index < text.length; index++) {
let c = text.charAt(index);
if ("{}[]".indexOf(c) == -1)
continue;
if (index < text.length - 1 || text.charAt(index + 1) != c)
return index;
index++;
}
return -1;
}
}).initializer();
export { Toolkit };

699
app/toolkit/Window.js Normal file
View File

@ -0,0 +1,699 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Window //
///////////////////////////////////////////////////////////////////////////////
// Standalone movable dialog
class Window extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className : "tk tk-window",
focusable : true,
role : "dialog",
tabStop : false,
tagName : "div",
visibility: true,
style : {
position: "absolute"
}
});
// Configure instance fields
this.firstShown = false;
this.lastFocus = null;
// DOM container
this.contents = document.createElement("div");
this.contents.style.display = "flex";
this.contents.style.flexDirection = "column";
this.element.append(this.contents);
// Sizing borders
this.borders = {}
this.border("n" ); this.border("w" );
this.border("e" ); this.border("s" );
this.border("nw"); this.border("ne");
this.border("sw"); this.border("se");
// Title bar
this.titleBar = document.createElement("div");
this.titleBar.className = "tk tk-title";
this.titleBar.style.display = "flex";
this.contents.append(this.titleBar);
this.titleBar.addEventListener(
"pointerdown", e=>this.onTitlePointerDown(e));
this.titleBar.addEventListener(
"pointermove", e=>this.onTitlePointerMove(e));
this.titleBar.addEventListener(
"pointerup" , e=>this.onTitlePointerUp (e));
// Title bar text
this.titleText = document.createElement("div");
this.titleText.className = "tk tk-text";
this.titleText.id = Toolkit.id();
this.titleText.style.flexGrow = "1";
this.titleText.style.position = "relative";
this.titleBar.append(this.titleText);
this.setAttribute("aria-labelledby", this.titleText.id);
// Close button
this.titleClose = document.createElement("div");
this.titleClose.className = "tk tk-close";
this.titleClose.setAttribute("aria-hidden", "true");
this.titleBar.append(this.titleClose);
this.titleClose.addEventListener(
"pointerdown", e=>this.onClosePointerDown(e));
this.titleClose.addEventListener(
"pointermove", e=>this.onClosePointerMove(e));
this.titleClose.addEventListener(
"pointerup" , e=>this.onClosePointerUp (e));
// Window client area
this.client = document.createElement("div");
this.client.className = "tk tk-client";
this.client.style.flexGrow = "1";
this.client.style.minHeight = "0";
this.client.style.minWidth = "0";
this.client.style.overflow = "hidden";
this.client.style.position = "relative";
this.contents.append(this.client);
// User agent behavior override
let observer = new ResizeObserver(
()=>this.titleBar.style.width =
this.client.getBoundingClientRect().width + "px"
);
observer.observe(this.client);
// Configure element
this.setAttribute("aria-modal", "false");
this.setBounds(
options.x , options.y,
options.width, options.height
);
// Configure component
this.gui.localize(this);
this.setTitle (options.title );
this.setCloseToolTip(options.closeToolTip);
this.addEventListener("focus" , e=>this.onFocus(e), true);
this.addEventListener("keydown" , e=>this.onWindowKeyDown (e));
this.addEventListener("pointerdown", e=>this.onWindowPointerDown(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Border pointer down
onBorderPointerDown(e, edge) {
if (e.target.hasPointerCapture(e.pointerId) || e.button != 0)
return;
e.target.setPointerCapture(e.pointerId);
e.preventDefault();
let bndClient = this.client.getBoundingClientRect();
let bndWindow = this .getBounds ();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
let coords = Toolkit.screenCoords(e);
this.drag = {
clickX : coords.x,
clickY : coords.y,
mode : "resize",
pointerId : e.pointerId,
startHeight: bndClient.height,
startWidth : bndClient.width,
startX : bndWindow.x - bndDesktop.x,
startY : bndWindow.y - bndDesktop.y,
target : e.target
};
}
// Border pointer move
onBorderPointerMove(e, edge) {
if (!e.target.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
let bndWindow = this.getBounds();
this["resize" + edge.toUpperCase()](
Toolkit.screenCoords(e),
this.client .getBoundingClientRect(),
this.parent ? this.parent.getBounds() : bndWindow,
bndWindow,
this.titleBar.getBoundingClientRect()
);
}
// Border pointer up
onBorderPointerUp(e, edge) {
if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0)
return;
e.target.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
}
// Close pointer down
onClosePointerDown(e) {
if (this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleClose.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.titleClose.classList.add("active");
this.drag = {
mode: "close",
x : e.offsetX,
y : e.offsetY
};
}
// Close pointer move
onClosePointerMove(e) {
if (!this.titleClose.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
if (Toolkit.isInside(this.titleClose, e))
this.titleClose.classList.add("active");
else this.titleClose.classList.remove("active");
}
// Close pointer up
onClosePointerUp(e) {
if (!this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleClose.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.titleClose.classList.remove("active");
if (Toolkit.isInside(this.titleClose, e))
this.element.dispatchEvent(Toolkit.event("close", this));
this.drag = null;
}
// Focus capture
onFocus(e) {
// Bring this window to the foreground of its siblings
if (!this.contains(e.relatedTarget) && this.parent)
this.parent.bringToFront(this);
// The target is not the window itself
if (e.target != this.element) {
this.lastFocus = e.target;
return;
}
// Select the first focusable child
if (this.lastFocus == null)
this.lastFocus = Toolkit.listFocusables(this.element)[0] || null;
// Send focus to the most recently focused element
if (this.lastFocus)
this.lastFocus.focus();
}
// Title pointer down
onTitlePointerDown(e) {
if (this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleBar.setPointerCapture(e.pointerId);
e.preventDefault();
let bndWindow = this.getBounds();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
let coords = Toolkit.screenCoords(e);
this.drag = {
clickX : coords.x,
clickY : coords.y,
mode : "move",
pointerId: e.pointerId,
startX : bndWindow.x - bndDesktop.x,
startY : bndWindow.y - bndDesktop.y
};
}
// Title pointer move
onTitlePointerMove(e) {
if (!this.titleBar.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
let coords = Toolkit.screenCoords(e);
let valid = this.getValidLocations(
this.drag.startX + coords.x - this.drag.clickX,
this.drag.startY + coords.y - this.drag.clickY
);
this.setLocation(valid.x, valid.y);
}
// Title pointer up
onTitlePointerUp(e) {
if (!this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleBar.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.drag = null;
}
// Window key press
onWindowKeyDown(e) {
// Process by key
switch (e.key) {
// Undo un-committed bounds modifications
case "Escape":
// Not dragging
if (this.drag == null)
return;
// Moving
if (this.drag.mode == "move") {
this.titleBar.releasePointerCapture(this.drag.pointerId);
this.setLocation(this.drag.startX, this.drag.startY);
this.drag = null;
}
// Resizing
else if (this.drag.mode == "resize") {
this.drag.target
.releasePointerCapture(this.drag.pointerId);
this.setBounds(
this.drag.startX , this.drag.startY,
this.drag.startWidth, this.drag.startHeight
);
this.drag = null;
}
break;
// Transfer focus to another element
case "Tab":
default: return;
}
// The event was handled
e.stopPropagation();
e.preventDefault();
}
// Window pointer down
onWindowPointerDown(e) {
this.focus(e);
e.stopPropagation();
e.preventDefault();
}
///////////////////////////// Public Methods //////////////////////////////
// Add a DOM element to this component's element
append(child) {
let element = child instanceof Element ? child : child.element;
this.client.append(element);
}
// Position the window in the center of the parent Desktop
center() {
if (!this.parent)
return;
let bndParent = this.parent.getBounds();
let bndWindow = this .getBounds();
this.setLocation(
Math.max(Math.floor((bndParent.width - bndWindow.width ) / 2), 0),
Math.max(Math.floor((bndParent.height - bndWindow.height) / 2), 0)
);
}
// Programmatically close the window
close() {
this.event("close");
}
// Add a DOM element to the beginning of this component's children
prepend(child) {
let element = child instanceof Element ? child : child.element;
this.element.prepend(element);
}
// Specify a new position and size for the window
setBounds(x, y, width, height) {
this.setSize(width, height);
this.setLocation(x, y);
}
// Specify the over text for the close button
setCloseToolTip(key) {
this.closeToolTip = key;
this.translate();
}
// Specify a new position for the window
setLocation(x, y) {
Object.assign(this.element.style, {
left: Math.round(parseFloat(x) || 0) + "px",
top : Math.round(parseFloat(y) || 0) + "px"
});
}
// Specify a new size for the window
setSize(width, height) {
Object.assign(this.client.style, {
width : Math.max(Math.round(parseFloat(width ) || 0, 32)) + "px",
height: Math.max(Math.round(parseFloat(height) || 0, 32)) + "px"
});
}
// Specify the window title text
setTitle(key) {
this.title = key;
this.translate();
}
// Specify whether the component is visible
setVisible(visible) {
super.setVisible(visible);
if (!visible || this.firstShown)
return;
this.firstShown = true;
this.event("firstshow", this);
}
///////////////////////////// Package Methods /////////////////////////////
// Ensure the window is partially visible within its desktop
contain() {
let valid = this.getValidLocations();
this.setLocation(valid.x, valid.y);
}
// Determine the range of valid window coordinates
getValidLocations(x, y) {
// Measure the bounding boxes of the relevant elements
let bndClient = this.client .getBoundingClientRect();
let bndWindow = this .getBounds ();
let bndTitleBar = this.titleBar.getBoundingClientRect();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
// Compute the minimum and maximum valid window coordinates
let ret = {
maxX: bndDesktop .width - bndTitleBar.height -
bndTitleBar.x + bndWindow .x,
maxY: bndDesktop .height - bndClient .y +
bndWindow .y,
minX: bndTitleBar.height - bndWindow .width +
bndWindow .right - bndTitleBar.right,
minY: 0
};
// Compute the effective "best" window coordinates
ret.x = Math.max(ret.minX, Math.min(ret.maxX,
x === undefined ? bndWindow.x - bndDesktop.x : x));
ret.y = Math.max(ret.minY, Math.min(ret.maxY,
y === undefined ? bndWindow.y - bndDesktop.y : y));
return ret;
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
if (!this.titleText)
return;
this.titleText.innerText = this.gui.translate(this.title, this);
if (this.closeToolTip)
this.titleClose.setAttribute("title",
this.gui.translate(this.closeToolTip, this));
else this.titleClose.removeAttribute("title");
}
///////////////////////////// Private Methods /////////////////////////////
// Produce a border element and add it to the window
border(edge) {
let border = this.borders[edge] = document.createElement("div");
border.className = "tk tk-" + edge;
border.style.cursor = edge + "-resize";
border.style.position = "absolute";
this.contents.append(border);
border.addEventListener(
"pointerdown", e=>this.onBorderPointerDown(e, edge));
border.addEventListener(
"pointermove", e=>this.onBorderPointerMove(e, edge));
border.addEventListener(
"pointerup" , e=>this.onBorderPointerUp (e, edge));
}
// Compute client bounds when resizing on the east border
constrainE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let w = this.drag.startWidth + coords.x - this.drag.clickX;
w = Math.max(w, bndTitleBar.height * 4);
if (bndClient.x - bndDesktop.x < 0)
w = Math.max(w, bndDesktop.x - bndClient.x + bndTitleBar.height);
return w;
}
// Compute client bounds when resizing on the north border
constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let delta = coords.y - this.drag.clickY;
let y = this.drag.startY + delta;
let h = this.drag.startHeight - delta;
let min = Math.max(0, bndClient.bottom - bndDesktop.bottom);
if (h < min) {
delta = min - h;
h += delta;
y -= delta;
}
if (y < 0) {
h += y;
y = 0;
}
return {
height: h,
y : y
};
}
// Compute client bounds when resizing on the south border
constrainS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
return Math.max(0, this.drag.startHeight+coords.y-this.drag.clickY);
}
// Compute client bounds when resizing on the west border
constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let delta = coords.x - this.drag.clickX;
let x = this.drag.startX + delta;
let w = this.drag.startWidth - delta;
let min = bndTitleBar.height * 4;
if (bndClient.right - bndDesktop.right > 0) {
min = Math.max(min, bndClient.right -
bndDesktop.right + bndTitleBar.height);
}
if (w < min) {
delta = min - w;
w += delta;
x -= delta;
}
return {
x : x,
width: w
};
}
// Resize on the east border
resizeE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
this.drag.startHeight
);
}
// Resize on the north border
resizeN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
this.drag.startX , con.y,
this.drag.startWidth, con.height
);
}
// Resize on the northeast border
resizeNE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
this.drag.startX, con.y,
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
con.height
);
}
// Resize on the northwest border
resizeNW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let conN = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
let conW = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(conW.x, conN.y, conW.width, conN.height);
}
// Resize on the south border
resizeS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.drag.startWidth,
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
);
}
// Resize on the southeast border
resizeSE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar)
);
}
// Resize on the southwest border
resizeSW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
con.x , this.drag.startY,
con.width,
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar)
);
}
// Resize on the west border
resizeW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
con.x , this.drag.startY,
con.width, this.drag.startHeight
);
}
};
///////////////////////////////////////////////////////////////////////////////
// Desktop //
///////////////////////////////////////////////////////////////////////////////
// Parent container for encapsulating groups of Windows
class Desktop extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-desktop",
role : "group",
tagName : "div",
style : {
position: "relative"
}
});
// Configure event handlers
this.addEventListener("resize", e=>this.onResize(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Element resized
onResize(e) {
for (let wnd of this.children)
wnd.contain();
}
///////////////////////////// Public Methods //////////////////////////////
// Re-order windows to bring a particular one to the foreground
bringToFront(wnd) {
// The window is not a child of this Desktop
let index = this.children.indexOf(wnd);
if (index == -1)
return;
// The window is already in the foreground
let afters = this.children.slice(index + 1).map(c=>c.element);
if (afters.length == 0)
return;
// Record scroll pane positions
let scrolls = [];
for (let after of afters)
for (let scroll of
after.querySelectorAll(".tk-scrollpane > .tk-viewport")) {
scrolls.push({
element: scroll,
left : scroll.scrollLeft,
top : scroll.scrollTop
});
}
// Update window collection
wnd.element.before(... this.children.slice(index+1).map(c=>c.element));
this.children.splice(index, 1);
this.children.push(wnd);
// Restore scroll pane positions
for (let scroll of scrolls) {
Object.assign(scroll.element, {
scrollLeft: scroll.left,
scrollTop : scroll.top
});
}
}
// Position a window in the center of the viewable area
center(wnd) {
// The window is not a child of the desktop pane
if (this.children.indexOf(wnd) == -1)
return;
let bndDesktop = this.getBounds();
let bndWindow = wnd .getBounds();
wnd.setLocation(
Math.max(0, Math.round((bndDesktop.width - bndWindow.width) / 2)),
Math.max(0, Math.round((bndDesktop.height-bndWindow.height) / 2))
);
}
};
export { Desktop, Window };

View File

@ -3,154 +3,121 @@
/********************************* Constants *********************************/ /***************************** Utility Functions *****************************/
/* Memory access address masks by data type */ /* Read a data unit from a memory buffer */
static const uint32_t TYPE_MASKS[] = { static int32_t busReadBuffer(uint8_t *mem, int type) {
0x07FFFFFF, /* S8 */
0x07FFFFFF, /* U8 */
0x07FFFFFE, /* S16 */
0x07FFFFFE, /* U16 */
0x07FFFFFC /* S32 */
};
/* Little-endian implementation */
#ifdef VB_LITTLEENDIAN
switch (type) {
case VB_S8 : return *(int8_t *)mem;
case VB_U8 : return * mem;
case VB_S16: return *(int16_t *)mem;
case VB_U16: return *(uint16_t *)mem;
}
return *(int32_t *)mem;
/* Generic implementation */
#else
switch (type) {
case VB_S8 : return (int8_t) *mem;
case VB_U8 : return *mem;
case VB_S16: return (int16_t ) (int8_t) mem[1] << 8 | mem[0];
case VB_U16: return (uint16_t) mem[1] << 8 | mem[0];
}
return (int32_t ) mem[3] << 24 | (uint32_t) mem[2] << 16 |
(uint32_t) mem[1] << 8 | mem[0];
#endif
/*************************** Sub-Module Functions ****************************/
/* Read a typed value from a buffer in host memory */
static int32_t busReadBuffer(uint8_t *data, int type) {
/* Processing by data type */
switch (type) {
/* Generic implementation */
#ifndef VB_LITTLE_ENDIAN
case VB_S8 : return ((int8_t *)data)[0];
case VB_U8 : return data [0];
case VB_S16: return (int32_t) ((int8_t *)data)[1] << 8 | data[0];
case VB_U16: return (int32_t) data [1] << 8 | data[0];
case VB_S32: return
(int32_t) data[3] << 24 | (int32_t) data[2] << 16 |
(int32_t) data[1] << 8 | data[0];
/* Little-endian host */
#else
case VB_S8 : return *(int8_t *) data;
case VB_U8 : return * data;
case VB_S16: return *(int16_t *) data;
case VB_U16: return *(uint16_t *) data;
case VB_S32: return *(int32_t *) data;
#endif
}
return 0; /* Unreachable */
} }
/* Write a typed value to a buffer in host memory */ /* Write a data unit to a memory buffer */
static void busWriteBuffer(uint8_t *data, int type, int32_t value) { static void busWriteBuffer(uint8_t *mem, int type, int32_t value) {
/* Processing by data type */ /* Little-endian implementation */
switch (type) { #ifdef VB_LITTLEENDIAN
switch (type) {
case VB_S16: case VB_U16: *(uint16_t *)mem = value; return;
case VB_S8 : case VB_U8 : * mem = value; return;
}
*(int32_t *)mem = value;
/* Generic implementation */ /* Generic implementation */
#ifndef VB_LITTLE_ENDIAN #else
case VB_S32: data[3] = value >> 24; switch (type) {
data[2] = value >> 16; /* Fallthrough */ case VB_S32:
case VB_S16: /* Fallthrough */ mem[3] = value >> 24;
case VB_U16: data[1] = value >> 8; /* Fallthrough */ mem[2] = value >> 16;
case VB_S8 : /* Fallthrough */ /* Fallthrough */
case VB_U8 : data[0] = value; case VB_S16:
case VB_U16:
/* Little-endian host */ mem[1] = value >> 8;
#else }
case VB_S8 : /* Fallthrough */ mem[0] = value;
case VB_U8 : * data = value; return; #endif
case VB_S16: /* Fallthrough */
case VB_U16: *(uint16_t *) data = value; return;
case VB_S32: *(int32_t *) data = value; return;
#endif
}
} }
/***************************** Library Functions *****************************/ /***************************** Module Functions ******************************/
/* Read a typed value from the simulation bus */ /* Read a data unit from the bus */
static void busRead(VB *sim, uint32_t address, int type, int32_t *value) { static int32_t busRead(VB *sim, uint32_t address, int type, int debug) {
/* Working variables */ /* Force address alignment */
address &= TYPE_MASKS[type]; address &= ~((uint32_t) TYPE_SIZES[type] - 1);
*value = 0;
/* Process by address range */ /* Process by address range */
switch (address >> 24) { switch (address >> 24 & 7) {
case 0: break; /* VIP */ case 0 : return 0; /* VIP */
case 1: break; /* VSU */ case 1 : return 0 * debug; /* VSU */
case 2: break; /* Misc. I/O */ case 2 : return 0; /* Miscellaneous hardware */
case 3: break; /* Unmapped */ case 3 : return 0; /* Unmapped */
case 4: break; /* Game Pak expansion */ case 4 : return 0; /* Game pak expansion */
case 5 : return /* WRAM */
case 5: /* WRAM */ busReadBuffer(&sim->wram[address & 0xFFFF], type);
*value = busReadBuffer(&sim->wram[address & 0x0000FFFF], type); case 6 : return sim->cart.sram == NULL ? 0 : /* Game pak RAM */
break; busReadBuffer(&sim->cart.sram
[address & (sim->cart.sramSize - 1)], type);
case 6: /* Game Pak RAM */ default: return sim->cart.rom == NULL ? 0 : /* Game pak ROM */
if (sim->cart.ram != NULL) { busReadBuffer(&sim->cart.rom
*value = busReadBuffer( [address & (sim->cart.romSize - 1)], type);
&sim->cart.ram[address & sim->cart.ramMask], type);
}
break;
case 7: /* Game Pak ROM */
if (sim->cart.rom != NULL) {
*value = busReadBuffer(
&sim->cart.rom[address & sim->cart.romMask], type);
}
break;
} }
} }
/* Write a typed value to the simulation bus */ /* Write a data unit to the bus */
static void busWrite(VB*sim,uint32_t address,int type,int32_t value,int debug){ static void busWrite(
VB *sim, uint32_t address, int type, uint32_t value, int debug) {
/* Working variables */ /* Force address alignment */
address &= TYPE_MASKS[type]; address &= ~((uint32_t) TYPE_SIZES[type] - 1);
/* Process by address range */ /* Process by address range */
switch (address >> 24) { switch (address >> 24 & 7) {
case 0: break; /* VIP */ case 0 : return; /* VIP */
case 1: break; /* VSU */ case 1 : return; /* VSU */
case 2: break; /* Misc. I/O */ case 2 : return; /* Miscellaneous hardware */
case 3: break; /* Unmapped */ case 3 : return; /* Unmapped */
case 4: break; /* Game Pak expansion */ case 4 : return; /* Game pak expansion */
case 5 : /* WRAM */
case 5: /* WRAM */ busWriteBuffer(&sim->wram[address & 0xFFFF], type, value);
busWriteBuffer(&sim->wram[address & 0x0000FFFF], type, value); return;
break; case 6 : /* Cartridge RAM */
if (sim->cart.sram != NULL)
case 6: /* Game Pak RAM */ busWriteBuffer(&sim->cart.sram
if (sim->cart.ram != NULL) { [address & (sim->cart.sramSize - 1)], type, value);
busWriteBuffer( return;
&sim->cart.ram[address & sim->cart.ramMask], type, value); default: /* Cartridge ROM */
} if (debug && sim->cart.rom != NULL)
break; busWriteBuffer(&sim->cart.rom
[address & (sim->cart.romSize - 1)], type, value);
case 7: /* Game Pak ROM */
if (debug && sim->cart.rom != NULL) {
busWriteBuffer(
&sim->cart.rom[address & sim->cart.romMask], type, value);
}
break;
} }
} }
#endif /* VBAPI */ #endif /* VBAPI */

3272
core/cpu.c

File diff suppressed because it is too large Load Diff

553
core/vb.c
View File

@ -1,138 +1,49 @@
#ifndef VBAPI #ifdef VB_EXPORT
#define VBAPI #define VBAPI VB_EXPORT
#else
#define VBAPI
#endif #endif
/* Header includes */
#include <float.h> #include <float.h>
#include <vb.h> #include <vb.h>
/*********************************** Types ***********************************/ /********************************* Constants *********************************/
/* Simulation state */ /* Type sizes */
struct VB { static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 };
/* Game Pak */
struct {
uint8_t *ram; /* Save RAM */
uint8_t *rom; /* Program ROM */
uint32_t ramMask; /* Size of SRAM - 1 */
uint32_t romMask; /* Size of ROM - 1 */
} cart;
/* CPU */
struct {
/* Cache Control Word */
struct {
uint8_t ice; /* Instruction Cache Enable */
} chcw;
/* Exception Cause Register */
struct {
uint16_t eicc; /* Exception/Interrupt Cause Code */
uint16_t fecc; /* Fatal Error Cause Code */
} ecr;
/* Program Status Word */
struct {
uint8_t ae; /* Address Trap Enable */
uint8_t cy; /* Carry */
uint8_t ep; /* Exception Pending */
uint8_t fiv; /* Floating Invalid */
uint8_t fov; /* Floating Overflow */
uint8_t fpr; /* Floading Precision */
uint8_t fro; /* Floating Reserved Operand */
uint8_t fud; /* Floading Underflow */
uint8_t fzd; /* Floating Zero Divide */
uint8_t i; /* Interrupt Level */
uint8_t id; /* Interrupt Disable */
uint8_t np; /* NMI Pending */
uint8_t ov; /* Overflow */
uint8_t s; /* Sign */
uint8_t z; /* Zero */
} psw;
/* Other registers */
uint32_t adtre; /* Address Trap Register for Execution */
uint32_t eipc; /* Exception/Interrupt PC */
uint32_t eipsw; /* Exception/Interrupt PSW */
uint32_t fepc; /* Fatal Error PC */
uint32_t fepsw; /* Fatal Error PSW */
uint32_t pc; /* Program Counter */
int32_t program[32]; /* Program registers */
uint32_t sr29; /* System register 29 */
uint32_t sr31; /* System register 31 */
/* Working data */
union {
struct {
uint32_t dest;
uint64_t src;
} bs; /* Arithmetic bit strings */
struct {
uint32_t address;
int32_t value;
} data; /* Data accesses */
} aux;
/* Other state */
uint32_t clocks; /* Master clocks to wait */
uint16_t code[2]; /* Instruction code units */
uint16_t exception; /* Exception cause code */
int halt; /* CPU is halting */
uint16_t irq; /* Interrupt request lines */
int length; /* Instruction code length */
uint32_t nextPC; /* Address of next instruction */
int operation; /* Current operation ID */
int step; /* Operation sub-task ID */
} cpu;
/* Other system state */
uint8_t wram[0x10000]; /* System RAM */
/* Application data */
vbOnException onException; /* CPU exception */
vbOnExecute onExecute; /* CPU instruction execute */
vbOnFetch onFetch; /* CPU instruction fetch */
vbOnRead onRead; /* CPU instruction read */
vbOnWrite onWrite; /* CPU instruction write */
void *tag; /* User data */
};
/***************************** Library Functions *****************************/ /********************************** Macros ***********************************/
/* Sign-extend an integer of variable width */ /* Sign-extend a value of some number of bits to 32 bits */
static int32_t SignExtend(int32_t value, int32_t bits) { #define SignExtend(v,b) \
#ifndef VB_SIGNED_PROPAGATE ((v) | (((v) & (1 << ((b) - 1))) ? (uint32_t) 0xFFFFFFFF << (b) : 0))
value &= ~((uint32_t) 0xFFFFFFFF << bits);
bits = (int32_t) 1 << (bits - (int32_t) 1);
return (value ^ bits) - bits;
#else
return value << (32 - bits) >> (32 - bits);
#endif
}
/******************************** Sub-Modules ********************************/
/*************************** Subsystem Components ****************************/
/* Component includes */
#include "bus.c" #include "bus.c"
#include "cpu.c" #include "cpu.c"
/***************************** Library Functions *****************************/ /***************************** Module Functions ******************************/
/* Process a simulation for a given number of clocks */ /* Process a simulation for some number of clocks */
static int sysEmulate(VB *sim, uint32_t clocks) { static int sysEmulate(VB *sim, uint32_t clocks) {
return int broke;
cpuEmulate(sim, clocks) broke = cpuEmulate(sim, clocks);
; return broke;
} }
/* Determine how many clocks are guaranteed to process */ /* Determine the number of clocks before a break condititon could occur */
static uint32_t sysUntil(VB *sim, uint32_t clocks) { static uint32_t sysUntil(VB *sim, uint32_t clocks) {
clocks = cpuUntil(sim, clocks); clocks = cpuUntil(sim, clocks);
return clocks; return clocks;
@ -140,252 +51,258 @@ static uint32_t sysUntil(VB *sim, uint32_t clocks) {
/******************************* API Commands ********************************/ /******************************* API Functions *******************************/
/* Associate two simulations as peers, or remove an association */
void vbConnect(VB *sim1, VB *sim2) {
/* Disconnect */
if (sim2 == NULL) {
if (sim1->peer != NULL)
sim1->peer->peer = NULL;
sim1->peer = NULL;
return;
}
/* Disconnect any existing link associations */
if (sim1->peer != NULL && sim1->peer != sim2)
sim1->peer->peer = NULL;
if (sim2->peer != NULL && sim2->peer != sim1)
sim2->peer->peer = NULL;
/* Link the two simulations */
sim1->peer = sim2;
sim2->peer = sim1;
}
/* Process one simulation */ /* Process one simulation */
VBAPI int vbEmulate(VB *sim, uint32_t *clocks) { int vbEmulate(VB *sim, uint32_t *clocks) {
int brk; /* A callback requested a break */ int broke; /* The simulation requested an application break */
uint32_t until; /* Clocks guaranteed to process */ uint32_t until; /* Maximum clocks before a break could happen */
while (*clocks != 0) {
until = sysUntil(sim, *clocks); /* Process the simulation until a break condition occurs */
brk = sysEmulate(sim, until); do {
*clocks -= until; until = *clocks;
if (brk) until = sysUntil (sim, until);
return brk; /* TODO: return 1 */ broke = sysEmulate(sim, until);
} *clocks -= until;
return 0; } while (!broke && *clocks > 0);
return broke;
} }
/* Process multiple simulations */ /* Process multiple simulations */
VBAPI int vbEmulateEx(VB **sims, int count, uint32_t *clocks) { int vbEmulateMulti(VB **sims, int count, uint32_t *clocks) {
int brk; /* A callback requested a break */ int broke; /* The simulation requested an application break */
uint32_t until; /* Clocks guaranteed to process */ uint32_t until; /* Maximum clocks before a break could happen */
int x; /* Iterator */ int x; /* Iterator */
while (*clocks != 0) {
/* Process simulations until a break condition occurs */
do {
broke = 0;
until = *clocks; until = *clocks;
for (x = count - 1; x >= 0; x--) for (x = 0; x < count; x++)
until = sysUntil(sims[x], until); until = sysUntil (sims[x], until);
for (x = 0; x < count; x++)
brk = 0; broke |= sysEmulate(sims[x], until);
for (x = count - 1; x >= 0; x--)
brk |= sysEmulate(sims[x], until);
*clocks -= until; *clocks -= until;
if (brk) } while (!broke && *clocks > 0);
return brk; /* TODO: return 1 */
return broke;
}
/* Retrieve a current breakpoint callback */
void* vbGetCallback(VB *sim, int type) {
void **field; /* Pointer to field within simulation */
/* Select the field to update */
switch (type) {
case VB_ONEXCEPTION: field = (void *) &sim->onException; break;
case VB_ONEXECUTE : field = (void *) &sim->onExecute ; break;
case VB_ONFETCH : field = (void *) &sim->onFetch ; break;
case VB_ONREAD : field = (void *) &sim->onRead ; break;
case VB_ONWRITE : field = (void *) &sim->onWrite ; break;
default: return NULL;
}
/* Retrieve the simulation field */
return *field;
}
/* Retrieve the value of PC */
uint32_t vbGetProgramCounter(VB *sim) {
return sim->cpu.pc;
}
/* Retrieve the value of a program register */
int32_t vbGetProgramRegister(VB *sim, int id) {
return id < 1 || id > 31 ? 0 : sim->cpu.program[id];
}
/* Retrieve the ROM buffer */
void* vbGetROM(VB *sim, uint32_t *size) {
if (size != NULL)
*size = sim->cart.romSize;
return sim->cart.rom;
}
/* Retrieve the SRAM buffer */
void* vbGetSRAM(VB *sim, uint32_t *size) {
if (size != NULL)
*size = sim->cart.sramSize;
return sim->cart.sram;
}
/* Retrieve the value of a system register */
uint32_t vbGetSystemRegister(VB *sim, int id) {
switch (id) {
case VB_ADTRE: return sim->cpu.adtre;
case VB_CHCW : return sim->cpu.chcw.ice << 1;
case VB_EIPC : return sim->cpu.eipc;
case VB_EIPSW: return sim->cpu.eipsw;
case VB_FEPC : return sim->cpu.fepc;
case VB_FEPSW: return sim->cpu.fepsw;
case VB_PIR : return 0x00005346;
case VB_TKCW : return 0x000000E0;
case 29 : return sim->cpu.sr29;
case 30 : return 0x00000004;
case 31 : return sim->cpu.sr31;
case VB_ECR : return
(uint32_t) sim->cpu.ecr.fecc << 16 | sim->cpu.ecr.eicc;
case VB_PSW : return
(uint32_t) sim->cpu.psw.i << 16 |
(uint32_t) sim->cpu.psw.np << 15 |
(uint32_t) sim->cpu.psw.ep << 14 |
(uint32_t) sim->cpu.psw.ae << 13 |
(uint32_t) sim->cpu.psw.id << 12 |
(uint32_t) sim->cpu.psw.fro << 9 |
(uint32_t) sim->cpu.psw.fiv << 8 |
(uint32_t) sim->cpu.psw.fzd << 7 |
(uint32_t) sim->cpu.psw.fov << 6 |
(uint32_t) sim->cpu.psw.fud << 5 |
(uint32_t) sim->cpu.psw.fpr << 4 |
(uint32_t) sim->cpu.psw.cy << 3 |
(uint32_t) sim->cpu.psw.ov << 2 |
(uint32_t) sim->cpu.psw.s << 1 |
(uint32_t) sim->cpu.psw.z
;
} }
return 0; return 0;
} }
/* Retrieve the game pack RAM buffer */ /* Prepare a simulation state instance for use */
VBAPI void* vbGetCartRAM(VB *sim, uint32_t *size) { void vbInit(VB *sim) {
if (size != NULL)
*size = sim->cart.ram == NULL ? 0 : sim->cart.ramMask + 1;
return sim->cart.ram;
}
/* Retrieve the game pack ROM buffer */ /* Breakpoint callbacks */
VBAPI void* vbGetCartROM(VB *sim, uint32_t *size) { sim->onException = NULL;
if (size != NULL) sim->onExecute = NULL;
*size = sim->cart.rom == NULL ? 0 : sim->cart.romMask + 1; sim->onFetch = NULL;
return sim->cart.rom; sim->onRead = NULL;
} sim->onWrite = NULL;
/* Retrieve the exception callback handle */ /* System */
VBAPI vbOnException vbGetExceptionCallback(VB *sim) { sim->peer = NULL;
return sim->onException;
}
/* Retrieve the execute callback handle */ /* Cartridge */
VBAPI vbOnExecute vbGetExecuteCallback(VB *sim) {
return sim->onExecute;
}
/* Retrieve the fetch callback handle */
VBAPI vbOnFetch vbGetFetchCallback(VB *sim) {
return sim->onFetch;
}
/* Retrieve the value of the program counter */
VBAPI uint32_t vbGetProgramCounter(VB *sim) {
return sim->cpu.pc;
}
/* Retrieve the value in a program register */
VBAPI int32_t vbGetProgramRegister(VB *sim, int index) {
return index < 1 || index > 31 ? 0 : sim->cpu.program[index];
}
/* Retrieve the read callback handle */
VBAPI vbOnRead vbGetReadCallback(VB *sim) {
return sim->onRead;
}
/* Retrieve the value in a system register */
VBAPI uint32_t vbGetSystemRegister(VB *sim, int index) {
return index < 0 || index > 31 ? 0 : cpuGetSystemRegister(sim, index);
}
/* Retrieve a simulation's userdata pointer */
VBAPI void* vbGetUserData(VB *sim) {
return sim->tag;
}
/* Retrieve the write callback handle */
VBAPI vbOnWrite vbGetWriteCallback(VB *sim) {
return sim->onWrite;
}
/* Initialize a simulation instance */
VBAPI VB* vbInit(VB *sim) {
sim->cart.ram = NULL;
sim->cart.rom = NULL; sim->cart.rom = NULL;
sim->onExecute = NULL; sim->cart.romSize = 0;
sim->onFetch = NULL; sim->cart.sram = NULL;
sim->onRead = NULL; sim->cart.sramSize = 0;
sim->onWrite = NULL;
/* Everything else */
vbReset(sim); vbReset(sim);
return sim;
} }
/* Read a value from the memory bus */ /* Read a data unit from the bus */
VBAPI int32_t vbRead(VB *sim, uint32_t address, int type) { int32_t vbRead(VB *sim, uint32_t address, int type, int debug) {
int32_t value; return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 :
if (type < 0 || type > 4) busRead(sim, address, type, debug);
return 0;
busRead(sim, address, type, &value);
return value;
} }
/* Simulate a hardware reset */ /* Simulate a hardware reset */
VBAPI VB* vbReset(VB *sim) { void vbReset(VB *sim) {
uint32_t x; /* Iterator */ uint32_t x; /* Iterator */
/* Subsystem components */
cpuReset(sim);
/* WRAM (the hardware does not do this) */ /* WRAM (the hardware does not do this) */
for (x = 0; x < 0x10000; x++) for (x = 0; x < 0x10000; x++)
sim->wram[x] = 0x00; sim->wram[x] = 0x00;
}
/* CPU (normal) */ /* Specify a breakpoint callback */
sim->cpu.exception = 0; void* vbSetCallback(VB *sim, int type, void *callback) {
sim->cpu.halt = 0; void **field; /* Pointer to field within simulation */
sim->cpu.irq = 0; void *prev; /* Previous value within field */
sim->cpu.pc = 0xFFFFFFF0;
cpuSetSystemRegister(sim, VB_ECR, 0x0000FFF0, 1);
cpuSetSystemRegister(sim, VB_PSW, 0x00008000, 1);
/* CPU (extra, hardware does not do this) */ /* Select the field to update */
sim->cpu.adtre = 0x00000000; switch (type) {
sim->cpu.eipc = 0x00000000; case VB_ONEXCEPTION: field = (void *) &sim->onException; break;
sim->cpu.eipsw = 0x00000000; case VB_ONEXECUTE : field = (void *) &sim->onExecute ; break;
sim->cpu.fepc = 0x00000000; case VB_ONFETCH : field = (void *) &sim->onFetch ; break;
sim->cpu.fepsw = 0x00000000; case VB_ONREAD : field = (void *) &sim->onRead ; break;
sim->cpu.sr29 = 0x00000000; case VB_ONWRITE : field = (void *) &sim->onWrite ; break;
sim->cpu.sr31 = 0x00000000; return NULL;
cpuSetSystemRegister(sim, VB_CHCW, 0x00000000, 1); }
for (x = 0; x < 32; x++)
sim->cpu.program[x] = 0x00000000;
/* CPU (other) */ /* Update the simulation field */
prev = *field;
*field = callback;
return prev;
}
/* Specify a new value for PC */
uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
value &= 0xFFFFFFFE;
sim->cpu.busWait = 0;
sim->cpu.causeCode = 0;
sim->cpu.clocks = 0; sim->cpu.clocks = 0;
sim->cpu.nextPC = 0xFFFFFFF0; sim->cpu.fetch = 0;
sim->cpu.operation = CPU_FETCH; sim->cpu.pc = value;
sim->cpu.step = 0; sim->cpu.state = CPU_FETCH;
sim->cpu.substring = 0;
return sim; return value;
}
/* Specify a game pak RAM buffer */
VBAPI int vbSetCartRAM(VB *sim, void *sram, uint32_t size) {
if (sram != NULL) {
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
return 1;
sim->cart.ramMask = size - 1;
}
sim->cart.ram = sram;
return 0;
}
/* Specify a game pak ROM buffer */
VBAPI int vbSetCartROM(VB *sim, void *rom, uint32_t size) {
if (rom != NULL) {
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
return 1;
sim->cart.romMask = size - 1;
}
sim->cart.rom = rom;
return 0;
}
/* Specify a new exception callback handle */
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback) {
vbOnException prev = sim->onException;
sim->onException = callback;
return prev;
}
/* Specify a new execute callback handle */
VBAPI vbOnExecute vbSetExecuteCallback(VB *sim, vbOnExecute callback) {
vbOnExecute prev = sim->onExecute;
sim->onExecute = callback;
return prev;
}
/* Specify a new fetch callback handle */
VBAPI vbOnFetch vbSetFetchCallback(VB *sim, vbOnFetch callback) {
vbOnFetch prev = sim->onFetch;
sim->onFetch = callback;
return prev;
}
/* Specify a new value for the program counter */
VBAPI uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
sim->cpu.operation = CPU_FETCH;
sim->cpu.pc = sim->cpu.nextPC = value & 0xFFFFFFFE;
sim->cpu.step = 0;
return sim->cpu.pc;
} }
/* Specify a new value for a program register */ /* Specify a new value for a program register */
VBAPI int32_t vbSetProgramRegister(VB *sim, int index, int32_t value) { int32_t vbSetProgramRegister(VB *sim, int id, int32_t value) {
return index < 1 || index > 31 ? 0 : (sim->cpu.program[index] = value); return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value);
} }
/* Specify a new read callback handle */ /* Supply a ROM buffer */
VBAPI vbOnRead vbSetReadCallback(VB *sim, vbOnRead callback) { int vbSetROM(VB *sim, void *rom, uint32_t size) {
vbOnRead prev = sim->onRead;
sim->onRead = callback; /* Check the buffer size */
return prev; if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0)
return 0;
/* Configure the ROM buffer */
sim->cart.rom = (uint8_t *) rom;
sim->cart.romSize = size;
return 1;
}
/* Supply an SRAM buffer */
int vbSetSRAM(VB *sim, void *sram, uint32_t size) {
/* Check the buffer size */
if (size == 0 || ((size - 1) & size) != 0)
return 0;
/* Configure the SRAM buffer */
sim->cart.sram = (uint8_t *) sram;
sim->cart.sramSize = size;
return 1;
} }
/* Specify a new value for a system register */ /* Specify a new value for a system register */
VBAPI uint32_t vbSetSystemRegister(VB *sim, int index, uint32_t value) { uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) {
return index < 0 || index > 31 ? 0 : return cpuSetSystemRegister(sim, id, value, 1);
cpuSetSystemRegister(sim, index, value, 1);
} }
/* Specify a new write callback handle */ /* Write a data unit to the bus */
VBAPI vbOnWrite vbSetWriteCallback(VB *sim, vbOnWrite callback) { void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) {
vbOnWrite prev = sim->onWrite; if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES)
sim->onWrite = callback; busWrite(sim, address, type, value, debug);
return prev;
}
/* Determine the size of a simulation instance */
VBAPI size_t vbSizeOf() {
return sizeof (VB);
}
/* Specify a simulation's userdata pointer */
VBAPI void* vbSetUserData(VB *sim, void *tag) {
void *prev = sim->tag;
sim->tag = tag;
return prev;
}
/* Write a value to the memory bus */
VBAPI int32_t vbWrite(VB *sim, uint32_t address, int type, int32_t value) {
if (type < 0 || type > 4)
return 0;
busWrite(sim, address, type, value, 1);
return vbRead(sim, address, type);
} }

203
core/vb.h
View File

@ -1,5 +1,5 @@
#ifndef VB_H_ #ifndef __VB_H__
#define VB_H_ #define __VB_H__
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -17,15 +17,15 @@ extern "C" {
/********************************* Constants *********************************/ /********************************* Constants *********************************/
/* Callback IDs */ /* Memory access types */
#define VB_EXCEPTION 0 #define VB_CANCEL -1
#define VB_EXECUTE 1 #define VB_S8 0
#define VB_FETCH 2 #define VB_U8 1
#define VB_FRAME 3 #define VB_S16 2
#define VB_READ 4 #define VB_U16 3
#define VB_WRITE 5 #define VB_S32 4
/* System registers */ /* System register IDs */
#define VB_ADTRE 25 #define VB_ADTRE 25
#define VB_CHCW 24 #define VB_CHCW 24
#define VB_ECR 4 #define VB_ECR 4
@ -37,62 +37,157 @@ extern "C" {
#define VB_PSW 5 #define VB_PSW 5
#define VB_TKCW 7 #define VB_TKCW 7
/* Memory access data types */ /* PC types */
#define VB_S8 0 #define VB_PC 0
#define VB_U8 1 #define VB_PC_FROM 1
#define VB_S16 2 #define VB_PC_TO 2
#define VB_U16 3
#define VB_S32 4 /* Breakpoint callback types */
#define VB_ONEXCEPTION 0
#define VB_ONEXECUTE 1
#define VB_ONFETCH 2
#define VB_ONREAD 3
#define VB_ONWRITE 4
/*********************************** Types ***********************************/ /*********************************** Types ***********************************/
/* Simulation state */ /* Forward references */
typedef struct VB VB; typedef struct VB VB;
/* Callbacks */ /* Memory access */
typedef int (*vbOnException)(VB *sim, uint16_t *cause); typedef struct {
typedef int (*vbOnExecute )(VB *sim, uint32_t address, const uint16_t *code, int length); uint32_t address; /* Bus address being accessed */
typedef int (*vbOnFetch )(VB *sim, int fetch, uint32_t address, int32_t *value, uint32_t *cycles); uint32_t clocks; /* Number of clocks required to complete */
typedef int (*vbOnRead )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles); int32_t value; /* Value read (callback's responsibility) or to write */
typedef int (*vbOnWrite )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles, int *cancel); int8_t type; /* Data type of value */
} VB_ACCESS;
/* CPU instruction */
typedef struct {
/* Public fields */
uint32_t address; /* Bus address */
uint16_t bits[2]; /* Binary instruction code */
uint8_t size; /* Size in bytes of the instruction */
/* Implementation fields */
int32_t aux[2]; /* Auxiliary storage for CAXI and bit strings */
uint8_t id; /* Internal operation ID */
} VB_INSTRUCTION;
/* Breakpoint callbacks */
typedef int (*VB_EXCEPTIONPROC)(VB *, uint16_t);
typedef int (*VB_EXECUTEPROC )(VB *, VB_INSTRUCTION *);
typedef int (*VB_FETCHPROC )(VB *, int, VB_ACCESS *);
typedef int (*VB_READPROC )(VB *, VB_ACCESS *);
typedef int (*VB_WRITEPROC )(VB *, VB_ACCESS *);
/* Simulation state */
struct VB {
/* Game pak */
struct {
uint8_t *rom; /* Active ROM buffer */
uint32_t romSize; /* Size of ROM data */
uint8_t *sram; /* Active SRAM buffer */
uint32_t sramSize; /* Size of SRAM data */
} cart;
/* CPU */
struct {
/* System registers */
uint32_t adtre; /* Address trap register for execution */
uint32_t eipc; /* Exception/Interrupt PC */
uint32_t eipsw; /* Exception/Interrupt PSW */
uint32_t fepc; /* Fatal error PC */
uint32_t fepsw; /* Fatal error PSW */
uint32_t sr29; /* Unknown system register */
uint32_t sr31; /* Unknown system register */
/* Cache control word */
struct {
int8_t ice; /* Instruction cache enable */
} chcw;
/* Exception cause register */
struct {
uint16_t eicc; /* Exception/interrupt cause code */
uint16_t fecc; /* Fatal error cause code */
} ecr;
/* Program status word */
struct {
int8_t ae; /* Address trap enable */
int8_t cy; /* Carry */
int8_t ep; /* Exception pending */
int8_t fiv; /* Floating invalid */
int8_t fov; /* Floating overflow */
int8_t fpr; /* Floating precision */
int8_t fro; /* Floating reserved operand */
int8_t fud; /* Floating underflow */
int8_t fzd; /* Floating zero divide */
int8_t i; /* Interrupt level */
int8_t id; /* Interrupt disable */
int8_t np; /* NMI pending */
int8_t ov; /* Overflow */
int8_t s; /* Sign */
int8_t z; /* Zero */
} psw;
/* Other registers */
uint32_t pc; /* Program counter */
int32_t program[32]; /* program registers */
/* Other fields */
VB_ACCESS access; /* Memory access descriptor */
VB_INSTRUCTION inst; /* Instruction descriptor */
uint8_t irq[5]; /* Interrupt request lines */
uint8_t busWait; /* Memory access counter */
uint16_t causeCode; /* Exception cause code */
uint32_t clocks; /* Clocks until next action */
int16_t fetch; /* Index of fetch unit */
uint8_t state; /* Operations state */
uint8_t substring; /* A bit string operation is in progress */
} cpu;
/* Breakpoint callbacks */
VB_EXCEPTIONPROC onException; /* CPU exception */
VB_EXECUTEPROC onExecute; /* Instruction execute */
VB_FETCHPROC onFetch; /* Instruction fetch */
VB_READPROC onRead; /* Memory read */
VB_WRITEPROC onWrite; /* Memory write */
/* Other fields */
VB *peer; /* Communications peer */
uint8_t wram[0x10000]; /* Main memory */
};
/******************************* API Commands ********************************/ /******************************* API Commands ********************************/
VBAPI int vbEmulate (VB *sim, uint32_t *clocks); VBAPI void vbConnect (VB *sim1, VB *sim2);
VBAPI int vbEmulateEx (VB **sims, int count, uint32_t *clocks); VBAPI int vbEmulate (VB *sim, uint32_t *clocks);
VBAPI void* vbGetCallback (VB *sim, int id); VBAPI int vbEmulateMulti (VB **sims, int count, uint32_t *clocks);
VBAPI void* vbGetCartRAM (VB *sim, uint32_t *size); VBAPI void* vbGetCallback (VB *sim, int type);
VBAPI void* vbGetCartROM (VB *sim, uint32_t *size); VBAPI uint32_t vbGetProgramCounter (VB *sim);
VBAPI vbOnException vbGetExceptionCallback(VB *sim); VBAPI int32_t vbGetProgramRegister (VB *sim, int id);
VBAPI vbOnExecute vbGetExecuteCallback (VB *sim); VBAPI void* vbGetROM (VB *sim, uint32_t *size);
VBAPI vbOnFetch vbGetFetchCallback (VB *sim); VBAPI void* vbGetSRAM (VB *sim, uint32_t *size);
VBAPI uint32_t vbGetProgramCounter (VB *sim); VBAPI uint32_t vbGetSystemRegister (VB *sim, int id);
VBAPI int32_t vbGetProgramRegister (VB *sim, int index); VBAPI void vbInit (VB *sim);
VBAPI vbOnRead vbGetReadCallback (VB *sim); VBAPI int32_t vbRead (VB *sim, uint32_t address, int type, int debug);
VBAPI uint32_t vbGetSystemRegister (VB *sim, int index); VBAPI void vbReset (VB *sim);
VBAPI void* vbGetUserData (VB *sim); VBAPI void* vbSetCallback (VB *sim, int type, void *callback);
VBAPI vbOnWrite vbGetWriteCallback (VB *sim); VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
VBAPI VB* vbInit (VB *sim); VBAPI int32_t vbSetProgramRegister (VB *sim, int id, int32_t value);
VBAPI int32_t vbRead (VB *sim, uint32_t address, int type); VBAPI int vbSetROM (VB *sim, void *rom, uint32_t size);
VBAPI VB* vbReset (VB *sim); VBAPI int vbSetSRAM (VB *sim, void *sram, uint32_t size);
VBAPI int vbSetCartRAM (VB *sim, void *sram, uint32_t size); VBAPI uint32_t vbSetSystemRegister (VB *sim, int id, uint32_t value);
VBAPI int vbSetCartROM (VB *sim, void *rom, uint32_t size); VBAPI void vbWrite (VB *sim, uint32_t address, int type, int32_t value, int debug);
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback);
VBAPI vbOnExecute vbSetExecuteCallback (VB *sim, vbOnExecute callback);
VBAPI vbOnFetch vbSetFetchCallback (VB *sim, vbOnFetch callback);
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
VBAPI int32_t vbSetProgramRegister (VB *sim, int index, int32_t value);
VBAPI vbOnRead vbSetReadCallback (VB *sim, vbOnRead callback);
VBAPI uint32_t vbSetSystemRegister (VB *sim, int index, uint32_t value);
VBAPI void* vbSetUserData (VB *sim, void *tag);
VBAPI vbOnWrite vbSetWriteCallback (VB *sim, vbOnWrite callback);
VBAPI size_t vbSizeOf ();
VBAPI int32_t vbWrite (VB *sim, uint32_t address, int type, int32_t value);
@ -100,4 +195,4 @@ VBAPI int32_t vbWrite (VB *sim, uint32_t address, int type,
} }
#endif #endif
#endif /* VB_H_ */ #endif /* __VB_H__ */

View File

@ -1,4 +1,4 @@
Copyright (C) 2024 Guy Perfect Copyright (C) 2022 Guy Perfect
This software is provided 'as-is', without any express or implied This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages warranty. In no event will the authors be held liable for any damages

View File

@ -1,7 +1,7 @@
.PHONY: help .PHONY: help
help: help:
@echo @echo
@echo "Virtual Boy Emulator - October 10, 2024" @echo "Virtual Boy Emulator - April 20, 2022"
@echo @echo
@echo "Target build environment is any Debian with the following packages:" @echo "Target build environment is any Debian with the following packages:"
@echo " emscripten" @echo " emscripten"
@ -27,35 +27,28 @@ build:
.PHONY: bundle .PHONY: bundle
bundle: bundle:
@java web/Bundle.java vbemu @java app/Bundle.java vbemu
.PHONY: clean .PHONY: clean
clean: clean:
@rm -f vbemu_*.html web/core/core.wasm @rm -f vbemu_*.html app/core/core.wasm
.PHONY: core .PHONY: core
core: core:
# GCC generic @gcc core/vb.c -I core \
@gcc core/vb.c -I core -c -o /dev/null \ -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
-Werror -std=c90 -Wall -Wextra -Wpedantic @gcc core/vb.c -I core -D VB_LITTLEENDIAN \
# GCC compilation control -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
@gcc core/vb.c -I core -c -o /dev/null \ @emcc core/vb.c -I core \
-Werror -std=c90 -Wall -Wextra -Wpedantic \ -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC @emcc core/vb.c -I core -D VB_LITTLEENDIAN \
# Clang generic -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
@emcc core/vb.c -I core -c -o /dev/null \
-Werror -std=c90 -Wall -Wextra -Wpedantic
# Clang compilation control
@emcc core/vb.c -I core -c -o /dev/null \
-Werror -std=c90 -Wall -Wextra -Wpedantic \
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC
.PHONY: wasm .PHONY: wasm
wasm: wasm:
@emcc -o web/core/core.wasm web/core/wasm.c core/vb.c -Icore \ @emcc -o app/core/core.wasm wasm/wasm.c core/vb.c -Icore \
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE \ -D VB_LITTLEENDIAN --no-entry -O2 -flto -s WASM=1 \
-D "VBAPI=__attribute__((used))" \ -D "VB_EXPORT=__attribute__((used))" \
--no-entry -O2 -flto -s WASM=1 \
-s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \ -s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \
-s MAXIMUM_MEMORY=4GB -fno-strict-aliasing -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
@rm -f web/core/*.wasm.tmp* @rm -f app/core/*.wasm.tmp*

126
wasm/wasm.c Normal file
View File

@ -0,0 +1,126 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten/emscripten.h>
#include <vb.h>
/////////////////////////////// Module Commands ///////////////////////////////
// Allocate and initialize multiple simulations
EMSCRIPTEN_KEEPALIVE VB** Create(int count) {
VB **sims = malloc(count * sizeof (void *));
for (int x = 0; x < count; x++)
vbReset(sims[x] = malloc(sizeof (VB)));
return sims;
}
// Delete a simulation
EMSCRIPTEN_KEEPALIVE void Destroy(VB *sim) {
free(&sim->cart.rom);
free(&sim->cart.sram);
free(sim);
}
// Proxy for free()
EMSCRIPTEN_KEEPALIVE void Free(void *ptr) {
free(ptr);
}
// Proxy for malloc()
EMSCRIPTEN_KEEPALIVE void* Malloc(int size) {
return malloc(size);
}
// Determine the size in bytes of a pointer
EMSCRIPTEN_KEEPALIVE int PointerSize() {
return sizeof (void *);
}
// Read multiple bytes from the bus
EMSCRIPTEN_KEEPALIVE void ReadBuffer(
VB* sim, uint8_t *dest, uint32_t address, uint32_t size) {
for (; size > 0; address++, size--, dest++)
*dest = vbRead(sim, address, VB_U8, 1);
}
// Supply a ROM buffer
EMSCRIPTEN_KEEPALIVE int SetROM(VB *sim, uint8_t *rom, uint32_t size) {
uint8_t *prev = vbGetROM(sim, NULL);
int ret = vbSetROM(sim, rom, size);
if (ret) {
free(prev);
vbReset(sim);
}
return ret;
}
// Write multiple bytes to the bus
EMSCRIPTEN_KEEPALIVE void WriteBuffer(
VB* sim, uint8_t *src, uint32_t address, uint32_t size) {
for (; size > 0; address++, size--, src++)
vbWrite(sim, address, VB_U8, *src, 1);
}
////////////////////////////// Debugger Commands //////////////////////////////
// Attempt to execute until the following instruction
static uint32_t RunNextPC;
static int RunNextProcB(VB *sim, int fetch, VB_ACCESS *acc) {
if (fetch == 0 && vbGetProgramCounter(sim) == RunNextPC)
return 1;
acc->value = vbRead(sim, acc->address, acc->type, 0);
return 0;
}
static int RunNextProcA(VB *sim, VB_INSTRUCTION *inst) {
RunNextPC = vbGetProgramCounter(sim) + inst->size;
vbSetCallback(sim, VB_ONEXECUTE, NULL);
vbSetCallback(sim, VB_ONFETCH, &RunNextProcB);
return 0;
}
EMSCRIPTEN_KEEPALIVE void RunNext(VB *sim0, VB *sim1) {
uint32_t clocks = 400000; // 1/50s
VB *sims[2];
vbSetCallback(sim0, VB_ONEXECUTE, &RunNextProcA);
if (sim1 != NULL) {
sims[0] = sim0;
sims[1] = sim1;
vbEmulateMulti(sims, 2, &clocks);
}
else vbEmulate(sim0, &clocks);
vbSetCallback(sim0, VB_ONFETCH, NULL);
}
// Execute one instruction
static uint32_t SingleStepPC;
static int SingleStepProc(VB *sim, int fetch, VB_ACCESS *acc) {
if (fetch == 0 && vbGetProgramCounter(sim) != SingleStepPC)
return 1;
acc->value = vbRead(sim, acc->address, acc->type, 0);
return 0;
}
EMSCRIPTEN_KEEPALIVE void SingleStep(VB *sim0, VB *sim1) {
uint32_t clocks = 400000; // 1/50s
VB *sims[2];
SingleStepPC = vbGetProgramCounter(sim0);
vbSetCallback(sim0, VB_ONFETCH, &SingleStepProc);
if (sim1 != NULL) {
sims[0] = sim0;
sims[1] = sim1;
vbEmulateMulti(sims, 2, &clocks);
}
else vbEmulate(sim0, &clocks);
vbSetCallback(sim0, VB_ONFETCH, NULL);
}