"use strict"; import { Constants } from "./Constants.js"; // Instantiation guard const GUARD = Symbol(); ///////////////////////////////// DasmConfig ////////////////////////////////// // Disassembler option settings class DasmConfig { // Instance fields #bcondNotation; #conditionCase; #conditionCL; #conditionEZ; #conditionNotation; #hexCase; #hexNotation; #immediateNotation; #memoryNotation; #mnemonicCase; #operandOrder; #programCase; #programNotation; #setfNotation; #systemCase; #systemNotation; ///////////////////////// Initialization Methods ////////////////////////// constructor() { this.#bcondNotation = Constants.VBU.JOINED; this.#conditionCase = Constants.VBU.LOWER; this.#conditionCL = Constants.VBU.L; this.#conditionEZ = Constants.VBU.Z; this.#conditionNotation = Constants.VBU.NAMES; this.#hexCase = Constants.VBU.UPPER; this.#hexNotation = Constants.VBU["0X"]; this.#immediateNotation = Constants.VBU.NONE; this.#memoryNotation = Constants.VBU.OUTSIDE; this.#mnemonicCase = Constants.VBU.UPPER; this.#operandOrder = Constants.VBU.DEST_LAST; this.#programCase = Constants.VBU.LOWER; this.#programNotation = Constants.VBU.NAMES; this.#setfNotation = Constants.VBU.SPLIT; this.#systemCase = Constants.VBU.LOWER; this.#systemNotation = Constants.VBU.NAMES; } /////////////////////////// Property Accessors //////////////////////////// get bcondNotation() { return this.#bcondNotation; } set bcondNotation(value) { switch (value) { case Constants.VBU.JOINED: case Constants.VBU.SPLIT : break; default: return; } this.#bcondNotation = value; } get conditionCase() { return this.#conditionCase; } set conditionCase(value) { switch (value) { case Constants.VBU.LOWER: case Constants.VBU.UPPER: break; default: return; } this.#conditionCase = value; } get conditionCL() { return this.#conditionCL; } set conditionCL(value) { switch (value) { case Constants.VBU.C: case Constants.VBU.L: break; default: return; } this.#conditionCL = value; } get conditionEZ() { return this.#conditionEZ; } set conditionEZ(value) { switch (value) { case Constants.VBU.E: case Constants.VBU.Z: break; default: return; } this.#conditionEZ = value; } get conditionNotation() { return this.#conditionNotation; } set conditionNotation(value) { switch (value) { case Constants.VBU.NAMES : case Constants.VBU.NUMBERS: break; default: return; } this.#conditionNotation = value; } get hexCase() { return this.#hexCase; } set hexCase(value) { switch (value) { case Constants.VBU.LOWER: case Constants.VBU.UPPER: break; default: return; } this.#hexCase = value; } get hexNotation() { return this.#hexNotation; } set hexNotation(value) { switch (value) { case Constants.VBU["0X"] : case Constants.VBU.DOLLAR: case Constants.VBU.H : break; default: return; } this.#hexNotation = value; } get immediateNotation() { return this.#immediateNotation; } set immediateNotation(value) { switch (value) { case Constants.VBU.NONE : case Constants.VBU.NUMBER: break; default: return; } this.#immediateNotation = value; } get memoryNotation() { return this.#memoryNotation; } set memoryNotation(value) { switch (value) { case Constants.VBU.INSIDE : case Constants.VBU.OUTSIDE: break; default: return; } this.#memoryNotation = value; } get mnemonicCase() { return this.#mnemonicCase; } set mnemonicCase(value) { switch (value) { case Constants.VBU.LOWER: case Constants.VBU.UPPER: break; default: return; } this.#mnemonicCase = value; } get operandOrder() { return this.#operandOrder; } set operandOrder(value) { switch (value) { case Constants.VBU.DEST_FIRST: case Constants.VBU.DEST_LAST : break; default: return; } this.#operandOrder = value; } get programCase() { return this.#programCase; } set programCase(value) { switch (value) { case Constants.VBU.LOWER: case Constants.VBU.UPPER: break; default: return; } this.#programCase = value; } get programNotation() { return this.#programNotation; } set programNotation(value) { switch (value) { case Constants.VBU.NAMES : case Constants.VBU.NUMBERS: break; default: return; } this.#programNotation = value; } get setfNotation() { return this.#setfNotation; } set setfNotation(value) { switch (value) { case Constants.VBU.JOINED: case Constants.VBU.SPLIT : break; default: return; } this.#setfNotation = value; } get systemCase() { return this.#systemCase; } set systemCase(value) { switch (value) { case Constants.VBU.LOWER: case Constants.VBU.UPPER: break; default: return; } this.#systemCase = value; } get systemNotation() { return this.#systemNotation; } set systemNotation(value) { switch (value) { case Constants.VBU.NAMES : case Constants.VBU.NUMBERS: break; default: return; } this.#systemNotation = value; } } ////////////////////////////////// DasmLine /////////////////////////////////// // One line of disassembler output class DasmLine { // Instance fields #address; #addressText; #code; #codeText; #isPC; #mnemonicText; #operandText; ///////////////////////// Initialization Methods ////////////////////////// constructor() { if (arguments[0] != GUARD) throw new Error("Cannot be instantiated."); let line = arguments[1]; this.#address = line.address; this.#addressText = line.text.address; this.#code = line.code; this.#codeText = line.text.code; this.#isPC = line.isPC; this.#mnemonicText = line.text.mnemonic; this.#operandText = line.text.operands; } /////////////////////////// Property Accessors //////////////////////////// get address() { return this.#address; } get code () { return this.#code.slice(); } get isPC () { return this.#isPC; } get text () { return { address : this.#addressText, code : this.#codeText.slice(), mnemonic: this.#mnemonicText, operands: this.#operandText.slice() }; } ///////////////////////////// Public Methods ////////////////////////////// // Express self as a plain object toObject() { return { address: this.address, code : Array.from(this.#code), isPC : this.isPC, text : this.text }; } } ///////////////////////////////////// Sim ///////////////////////////////////// // Simulation instance class Sim extends HTMLElement { // Instance fields #anaglyph; // Anaglyph color values #canvas; // Canvas element #core; // Core proxy #emulating; // Current emulation status #keys; // Controller state #panning; // Audio stereo balance #peer; // Communication peer #pointer; // Pointer in core memory #volume; // Audio output volume ///////////////////////// Initialization Methods ////////////////////////// constructor() { if (arguments[0] != GUARD) throw new Error("Must be created via VB.create()"); super(); this.proxy = { construct : (core, pointer)=>this.#construct(core, pointer), isEmulating : ()=>this.#emulating, setEmulating: e =>this.#emulating = e, setPeer : p =>this.#peer = p }; } // Asynchronous constructor async #construct(core, pointer) { // Configure instance fields this.#anaglyph = [ VB.STEREO_RED, VB.STEREO_CYAN ]; this.#core = core; this.#emulating = false; this.#keys = Constants.VB.SGN; this.#panning = 0.0; this.#peer = null; this.#pointer = pointer; this.#volume = 1.0; delete this.proxy; // Create a for the video image let canvas = this.#canvas = document.createElement("canvas"); Object.assign(canvas, { width: 384, height: 224 }); canvas.style.imageRendering = "pixelated"; // Configure elements Object.assign(this.style, { display : "inline-block", height : "224px", position: "relative", width : "384px" }); Object.assign(canvas.style, { height : "100%", imageRendering: "pixelated", left : "0", position : "absolute", top : "0", width : "100%" }); this.append(canvas); // Send control of the canvas to the core worker let offscreen = canvas.transferControlToOffscreen(); await core.toCore({ command : "setCanvas", promised : true, sim : pointer, canvas : offscreen, transfers: [ offscreen ] }); return this; } /////////////////////////// Property Accessors //////////////////////////// get anaglyph() { return this.#anaglyph.slice(); } get core () { return this.#core.core ; } get keys () { return this.#keys ; } get panning () { return this.#panning ; } get peer () { return this.#peer ; } get volume () { return this.#volume ; } ///////////////////////////// Public Methods ////////////////////////////// // Delete the sim async delete() { // Unlink peer // Deallocate memory // Unlink core } // Disassemble from a simulation async disassemble(address, config, length, line) { // Error checking if (!Number.isSafeInteger(address) || address < 0 || address > 0xFFFFFFFF) throw new RangeError("Address must conform to Uint32."); if (config != null && !(config instanceof DasmConfig)) throw new TypeError("Config must be an instance of DasmConfig."); if (!Number.isSafeInteger(address) || length < 0) throw new RangeError("Length must be nonnegative."); if (!Number.isSafeInteger(line)) throw new TypeError("Line must be a safe integer."); // Request disassembly from the core let response = await this.#core.toCore({ command : "disassemble", promised: true, sim : this.#pointer, address : address, length : length, line : line, config : config == null ? null : { bcondNotation : config.bcondNotation, conditionCase : config.conditionCase, conditionCL : config.conditionCL, conditionEZ : config.conditionEZ, conditionNotation: config.conditionNotation, hexCase : config.hexCase, hexNotation : config.hexNotation, immediateNotation: config.immediateNotation, memoryNotation : config.memoryNotation, mnemonicCase : config.mnemonicCase, operandOrder : config.operandOrder, programCase : config.programCase, programNotation : config.programNotation, setfNotation : config.setfNotation, systemCase : config.systemCase, systemNotation : config.systemNotation } }); // Process the response return !response.success ? null : response.lines.map(l=>new DasmLine(GUARD, l)); } // Specify anaglyph colors async setAnaglyph(left, right) { // Error checking if (!Number.isSafeInteger(left ) || left < 0 || left > 0xFFFFFF) throw new RangeError("Left must conform to Uint24."); if (!Number.isSafeInteger(right) || right < 0 || right > 0xFFFFFF) throw new RangeError("Right must conform to Uint24."); if ( left & 0xFF0000 && right & 0xFF0000 || left & 0x00FF00 && right & 0x00FF00 || left & 0x0000FF && right & 0x0000FF ) throw new RangeError("Left and right overlap RGB channels."); // Configure instance fields this.#anaglyph[0] = left; this.#anaglyph[1] = right; // Send the colors to the core await this.#core.toCore({ command : "setAnaglyph", promised: true, sim : this.#pointer, left : left, right : right }); } // Specify a game pak RAM buffer setCartRAM(wram) { return this.#setCartMemory("setCartRAM", wram); } // Specify a game pak ROM buffer setCartROM(rom) { return this.#setCartMemory("setCartROM", rom); } // Specify new game pad keys async setKeys(keys) { // Error checking if (!Number.isSafeInteger(keys) || keys < 0 || keys > 0xFFFF) throw new RangeError("Keys must conform to Uint16."); if (keys == this.#keys) return; // Configure instance fields this.#keys = keys; // Send the keys to the core await this.#core.toCore({ command : "setKeys", promised: true, sim : this.#pointer, keys : keys }); } // Specify audio panning async setPanning(panning) { // Error checking if (!Number.isFinite(panning) ||panning < -1 || panning > +1) { throw new RangeError( "Panning must be a number from -1 to +1."); } // Configure instance fields this.#panning = panning; // Send the panning to the core await this.#core.toCore({ command : "setPanning", promised: true, sim : this.#pointer, panning : panning }); } // Specify a new communication peer async setPeer(peer = null) { // Error checking if (peer !== null && peer.#core != this.#core) throw new RangeError("Peer sim must belong to the same core."); // Configure peers on the core if (peer != this.#peer) await this.#core.setPeer(this, peer); } // Specify audio volume async setVolume(volume) { // Error checking if (!Number.isFinite(volume) ||volume < 0 || volume > 10) { throw new RangeError( "Volume must be a number from 0\u00d7 to 10\u00d7."); } // Configure instance fields this.#volume = volume; // Send the volume to the core await this.#core.toCore({ command : "setVolume", promised: true, sim : this.#pointer, volume : volume }); } ///////////////////////////// Private Methods ///////////////////////////// // Specify a game pak memory buffer async #setCartMemory(command, mem) { // Validation if (mem instanceof ArrayBuffer) mem = new Uint8Array(mem); if ( !(mem instanceof Uint8Array) && !(mem instanceof Uint8ClampedArray) ) mem = Uint8Array.from(mem); // Send the memory to the core let response = await this.#core.toCore({ command : command, promised : true, sim : this.#pointer, data : mem.buffer, transfers: [ mem.buffer ] }); return response.success; } } customElements.define("shrooms-vb", Sim); ///////////////////////////////////// VB ////////////////////////////////////// // Emulation core interface class VB { // Static fields static get DasmConfig() { return DasmConfig; } static get DasmLine () { return DasmLine; } static get Sim () { return Sim; } // Instance fields #audio; // Audio worklet #automatic; // Current automatic emulation group #commands; // Computed method table #core; // Core worker #proxy; // Self proxy for sim access #sims; // All sims #state; // Operations state //////////////////////////////// Constants //////////////////////////////// // Operations states static #SUSPENDED = Symbol(); static #RESUMING = Symbol(); static #EMULATING = Symbol(); static #SUSPENDING = Symbol(); // System registers static get ADTRE() { return Constants.VB.ADTRE; } static get CHCW () { return Constants.VB.CHCW ; } static get ECR () { return Constants.VB.ECR ; } static get EIPC () { return Constants.VB.EIPC ; } static get EIPSW() { return Constants.VB.EIPSW; } static get FEPC () { return Constants.VB.FEPC ; } static get FEPSW() { return Constants.VB.FEPSW; } static get PIR () { return Constants.VB.PIR ; } static get PSW () { return Constants.VB.PSW ; } static get TKCW () { return Constants.VB.TKCW ; } // Memory access data types static get S8 () { return Constants.VB.S8 ; } static get U8 () { return Constants.VB.U8 ; } static get S16() { return Constants.VB.S16; } static get U16() { return Constants.VB.U16; } static get S32() { return Constants.VB.S32; } // Option keys static get PSEUDO_HALT() { return Constants.VB.PSEUDO_HALT; } // Controller buttons static get PWR() { return Constants.VB.PWR; } static get SGN() { return Constants.VB.SGN; } static get A () { return Constants.VB.A ; } static get B () { return Constants.VB.B ; } static get RT () { return Constants.VB.RT ; } static get LT () { return Constants.VB.LT ; } static get RU () { return Constants.VB.RU ; } static get RR () { return Constants.VB.RR ; } static get LR () { return Constants.VB.LR ; } static get LL () { return Constants.VB.LL ; } static get LD () { return Constants.VB.LD ; } static get LU () { return Constants.VB.LU ; } static get STA() { return Constants.VB.STA; } static get SEL() { return Constants.VB.SEL; } static get RL () { return Constants.VB.RL ; } static get RD () { return Constants.VB.RD ; } // Disassembler options static get ["0X"] () { return Constants.VBU["0X"] ; } static get ABSOLUTE () { return Constants.VBU.ABSOLUTE ; } static get C () { return Constants.VBU.C ; } static get DEST_FIRST() { return Constants.VBU.DEST_FIRST; } static get DEST_LAST () { return Constants.VBU.DEST_LAST ; } static get DOLLAR () { return Constants.VBU.DOLLAR ; } static get E () { return Constants.VBU.E ; } static get H () { return Constants.VBU.H ; } static get INSIDE () { return Constants.VBU.INSIDE ; } static get JOINED () { return Constants.VBU.JOINED ; } static get L () { return Constants.VBU.L ; } static get LOWER () { return Constants.VBU.LOWER ; } static get NAMES () { return Constants.VBU.NAMES ; } static get NONE () { return Constants.VBU.NONE ; } static get NUMBER () { return Constants.VBU.NUMBER ; } static get NUMBERS () { return Constants.VBU.NUMBERS ; } static get OUTSIDE () { return Constants.VBU.OUTSIDE ; } static get RELATIVE () { return Constants.VBU.RELATIVE ; } static get SPLIT () { return Constants.VBU.SPLIT ; } static get UPPER () { return Constants.VBU.UPPER ; } static get Z () { return Constants.VBU.Z ; } // Anaglyph colors static get STEREO_CYAN () { return Constants.web.STEREO_CYAN ; } static get STEREO_GREEN () { return Constants.web.STEREO_GREEN ; } static get STEREO_MAGENTA() { return Constants.web.STEREO_MAGENTA; } static get STEREO_RED () { return Constants.web.STEREO_RED ; } ///////////////////////////// Static Methods ////////////////////////////// // Create a core instance static async create(options) { return await new VB(GUARD).#construct(options); } ///////////////////////// Initialization Methods ////////////////////////// constructor() { if (arguments[0] != GUARD) throw new Error("Must be created via VB.create()"); } // Asynchronous constructor async #construct(options) { // Configure instance fields this.#automatic = null; this.#sims = new Map(); this.#state = VB.#SUSPENDED; // Ensure default options options ??= {}; options.audioUrl ??= import.meta.resolve("./Audio.js"); options.coreUrl ??= import.meta.resolve("./Core.js"); options.wasmUrl ??= import.meta.resolve("./core.wasm"); // Core<->audio communications let channel = new MessageChannel(); // Audio output context let audio = new AudioContext({ latencyHint: "interactive", sampleRate : 41700 }); await audio.suspend(); // Audio node await audio.audioWorklet.addModule(options.audioUrl); audio = this.#audio = new AudioWorkletNode(audio, "shrooms-vb", { numberOfInputs : 0, numberOfOutputs : 1, outputChannelCount: [2] }); audio.connect(audio.context.destination); // Send one message channel port to the audio worklet await new Promise(resolve=>{ audio.port.onmessage = resolve; audio.port.postMessage({ core: channel.port1 }, [channel.port1]); }); audio.port.onmessage = null;//e=>this.#onAudio(e.data); // Core worker let core = this.#core = new Worker(options.coreUrl, {type: "module"}); core.promises = []; // Send the other message channel port to the core worker await new Promise(resolve=>{ core.onmessage = resolve; core.postMessage({ audio : channel.port2, wasmUrl: options.wasmUrl }, [ channel.port2 ]); }); core.onmessage = e=>this.#onCore(e.data); // Establish a concealed proxy for sim objects this.#proxy = { core : this, setPeer: (a,b)=>this.#setPeer(a,b), toCore : m=>this.#toCore(m) }; // Configure command table this.#commands = { // Will be used with subscriptions }; return this; } ///////////////////////////// Public Methods ////////////////////////////// // Create one or more sims async create(count = null) { // Error checking if (count !== null && (!Number.isSafeInteger(count) || count < 1)) { throw new RangeError( "Count must be a safe integer and at least 1."); } // Allocate memory in the core let response = await this.#toCore({ command : "createSims", promised: true, count : count ?? 1 }); // Produce Sim elements for each instance let sims = response.sims; for (let x = 0; x < (count ?? 1); x++) { let proxy = new Sim(GUARD).proxy; proxy.pointer = sims[x]; proxy.sim = sims[x] = await proxy.construct(this.#proxy, sims[x]); this.#sims.set(sims[x], proxy); this.#sims.set(proxy.pointer, proxy); } return count === null ? sims[0] : sims; } // Begin emulation async emulate(sims, clocks) { // Error checking if (sims instanceof Sim) sims = [sims]; if ( !Array.isArray(sims) || sims.length == 0 || sims.find(s=>!this.#sims.has(s)) ) { throw new TypeError("Must specify a Sim or array of Sims " + "that belong to this core."); } if (sims.find(s=>this.#sims.get(s).isEmulating())) throw new Error("Sims cannot already be part of emulation."); if ( clocks !== true && !(Number.isSafeInteger(clocks) && clocks >= 0) ) { throw new RangeError( "Clocks must be true or a nonnegative safe integer."); } // Cannot resume automatic emulation if (clocks === true && this.#state != VB.#SUSPENDED) return false; // Manage sims let proxies = sims .map(s=>this.#sims.get(s)); let pointers = proxies.map(p=>p.pointer); for (let sim of proxies) sim.setEmulating(true); // Clocked emulation if (clocks !== true) { let response = await this.#toCore({ command : "emulateClocked", promised: true, sims : pointers, clocks : clocks }); for (let sim of proxies) sim.setEmulating(false); return { broke : response.broke, clocks: response.clocks }; } // Resume automatic emulation this.#automatic = proxies; this.#state = VB.#RESUMING; if (this.#audio.context.state == "suspended") await this.#audio.context.resume(); await this.#toCore({ command : "emulateAutomatic", promised: true, sims : pointers }); this.#state = VB.#EMULATING; return true; } // Suspend automatic emulation async suspend() { // Error checking if (this.#state != VB.#EMULATING) return false; // Tell the core to stop emulating this.#state = VB.#SUSPENDING; await this.#toCore({ command : "suspend", promised: true }); // Configure state this.#state = VB.#SUSPENDED; for (let sim of this.#automatic) sim.setEmulating(false); return true; } ///////////////////////////// Private Methods ///////////////////////////// // Message received from core worker #onCore(message) { if (message.promised) this.#core.promises.shift()(message); if ("command" in message) this.#commands[message.command](message); } // Specify a new communication peer async #setPeer(sim, peer) { // Associate the peers on the core let response = await this.#toCore({ command : "setPeer", promised: true, sim : this.#sims.get(sim).pointer, peer : peer == null ? 0 : this.#sims.get(peer).pointer }); // Link sims this.#sims.get(sim).setPeer(peer); if (peer != null) this.#sims.get(peer).setPeer(sim); // Unlink orphaned sims for (let pointer of response.orphaned) this.#sims.get(pointer).setPeer(null); } // Send a message to the core worker async #toCore(message) { let transfers = message.transfers; if (transfers != null) delete message.transfers; return await new Promise(resolve=>{ if (message.promised) this.#core.promises.push(resolve); this.#core.postMessage(message, transfers ?? []); }); } } export { VB };