// Interface between application and WebAssembly worker thread class Core { //////////////////////////////// Constants //////////////////////////////// /* Register types */ static VB_PROGRAM = 0; static VB_SYSTEM = 1; static VB_OTHER = 2; /* System registers */ static VB_ADTRE = 25; static VB_CHCW = 24; static VB_ECR = 4; static VB_EIPC = 0; static VB_EIPSW = 1; static VB_FEPC = 2; static VB_FEPSW = 3; static VB_PIR = 6; static VB_PSW = 5; static VB_TKCW = 7; /* Other registers */ static VB_PC = 0; ///////////////////////// Initialization Methods ////////////////////////// constructor() { // Configure instance fields this.promises = []; } async init(coreUrl, wasmUrl, audioUrl) { // Open audio output stream this.audio = new AudioContext({ latencyHint: "interactive", sampleRate : 41700 }); await this.audio.suspend(); // Launch the audio thread await this.audio.audioWorklet.addModule( Core.url(audioUrl, "AudioThread.js", /***/"./AudioThread.js")); let node = new AudioWorkletNode(this.audio, "AudioThread", { numberOfInputs : 0, numberOfOutputs : 1, outputChannelCount: [2] }); node.connect(this.audio.destination); // Attach a second MessagePort to the audio thread let channel = new MessageChannel(); this.audio.port = channel.port1; await new Promise(resolve=>{ node.port.onmessage = resolve; node.port.postMessage(channel.port2, [channel.port2]); }); this.audio.port.onmessage = m=>this.onAudio(m.data); // Launch the core thread this.core = new Worker( Core.url(wasmUrl, "CoreThread.js", /***/"./CoreThread.js")); await new Promise(resolve=>{ this.core.onmessage = resolve; this.core.postMessage({ audio : node.port, wasmUrl: Core.url(wasmUrl, "core.wasm", /***/"./core.wasm") }, [node.port]); }); this.core.onmessage = m=>this.onCore(m.data); return this; } ///////////////////////////// Static Methods ////////////////////////////// // Select a URL in the same path as the current script static url(arg, name, bundled) { // The input argument was provided if (arg) return arg; // Running from a bundle distribution if (bundled.startsWith("blob:") || bundled.startsWith("data:")) return bundled; // Compute the URL for the given filename let url = new URL(import.meta.url).pathname; return url.substring(0, url.lastIndexOf("/") + 1) + name; } ///////////////////////////// Event Handlers ////////////////////////////// // Message received from audio thread onAudio(msg) { } // Message received from core thread onCore(msg) { // Process subscriptions if (msg.subscriptions && this.onsubscription instanceof Function) { for (let sub of msg.subscriptions) { let key = sub.subscription; delete sub.subscription; this.onsubscription(key, sub, this); } delete msg.subscriptions; } // The main thread is waiting on a reply if (msg.isReply) { delete msg.isReply; // For "create", produce sim objects if (msg.isCreate) { delete msg.isCreate; msg.sims = msg.sims.map(s=>({ pointer: s })); } // Notify the caller this.promises.shift()(msg); } } ///////////////////////////// Public Methods ////////////////////////////// // Create and initialize simulations create(count, options) { return this.message({ command: "create", count : count }, [], options); } // Delete a simulation delete(sim, options) { return this.message({ command: "delete", sim : sim.pointer }, [], options); } // Retrieve the value of all CPU registers getAllRegisters(sim, options) { return this.message({ command: "getAllRegisters", sim : sim.pointer }, [], options); } // Retrieve the value of a register getRegister(sim, type, id, options) { return this.message({ command: "getRegister", id : id, sim : sim.pointer, type : type }, [], options); } // Read multiple bytes from memory read(sim, address, length, options) { return this.message({ command: "read", address: address, length : length, sim : sim.pointer }, [], options); } // Refresh subscriptions refresh(subscriptions = null, options) { return this.message({ command : "refresh", subscriptions: subscriptions }, [], options); } // Simulate a hardware reset reset(sim, options) { return this.message({ command: "reset", sim : sim.pointer }, [], options); } // Execute until the next current instruction runToNext(sims, options) { return this.message({ command: "runToNext", sims : Array.isArray(sims) ? sims.map(s=>s.pointer) : [ sims.pointer ] }, [], options); } // Specify a value for a register setRegister(sim, type, id, value, options) { return this.message({ command: "setRegister", id : id, sim : sim.pointer, type : type, value : value }, [], options); } // Specify a cartridge ROM buffer setROM(sim, data, options = {}) { data = data.slice(); return this.message({ command: "setROM", data : data, reset : !("reset" in options) || !!options.reset, sim : sim.pointer }, [data.buffer], options); } // Execute the current instruction singleStep(sims, options) { return this.message({ command: "singleStep", sims : Array.isArray(sims) ? sims.map(s=>s.pointer) : [ sims.pointer ] }, [], options); } // Cancel a subscription unsubscribe(subscription, options) { return this.message({ command : "unsubscribe", subscription: subscription }, [], options); } // Write multiple bytes to memory write(sim, address, data, options) { data = data.slice(); return this.message({ address: address, command: "write", data : data, sim : sim.pointer }, [data.buffer], options); } ///////////////////////////// Private Methods ///////////////////////////// // Send a message to the core thread message(msg, transfers, options = {}) { // Configure options if (!(options instanceof Object)) options = { reply: options }; if (!("reply" in options) || options.reply) msg.reply = true; if ("refresh" in options) msg.refresh = options.refresh; if ("subscription" in options) msg.subscription = options.subscription; if ("tag" in options) msg.tag = options.tag; // Send the command to the core thread return msg.reply ? new Promise(resolve=>{ this.promises.push(resolve); this.core.postMessage(msg, transfers); }) : this.core.postMessage(msg, transfers); ; } } export { Core };