diff --git a/web/Audio.js b/web/Audio.js new file mode 100644 index 0000000..093560f --- /dev/null +++ b/web/Audio.js @@ -0,0 +1,101 @@ +"use strict"; + +//////////////////////////////////// Audio //////////////////////////////////// + +// Dedicated audio output processor +class Audio extends AudioWorkletProcessor { + + // Instance fields + buffers; // Input sample buffer queue + core; // Communications with core thread + dom; // Communications with DOM thread + offset; // Offset into oldest buffer + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor() { + super(); + this.port.onmessage = async e=>{ + await this.#construct(e.data.core); + this.port.postMessage(0); + }; + } + + // Asynchronous constructor + async #construct(core) { + + // Configure instance fields + this.buffers = []; + this.core = core; + this.dom = this.port; + this.offset = 0; + + // Configure communications + this.core.onmessage = e=>this.#onCore(e.data); + this.dom .onmessage = e=>this.#onDOM (e.data); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Produce output samples (called by the user agent) + process(inputs, outputs, parameters) { + let output = outputs[0]; + let length = output [0].length; + let empty = null; + + // Process all samples + for (let x = 0; x < length;) { + + // No bufferfed samples are available + if (this.buffers.length == 0) { + for (; x < length; x++) + output[0][x] = output[1][x] = 0; + break; + } + + // Transfer samples from the oldest buffer + let buffer = this.buffers[0]; + let y = this.offset; + for (; x < length && y < buffer.length; x++, y+=2) { + output[0][x] = buffer[y ]; + output[1][x] = buffer[y + 1]; + } + + // Advance to the next buffer + if (y == buffer.length) { + if (empty == null) + empty = []; + empty.push(this.buffers.shift().buffer); + this.offset = 0; + } + + // Buffer is not empty + else this.offset = y; + } + + // Return emptied sample buffers to the core thread + if (empty != null) + this.core.postMessage(empty, empty); + + return true; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Message received from core thread + #onCore(e) { + this.buffers.push(new Float32Array(e)); + } + + // Message received from DOM thread + #onDOM(e) { + } + +} +registerProcessor("shrooms-vb", Audio); diff --git a/web/Constants.js b/web/Constants.js new file mode 100644 index 0000000..09e57f2 --- /dev/null +++ b/web/Constants.js @@ -0,0 +1,91 @@ +let Constants = { + + // Core + VB: { + + // System registers + ADTRE: 25, + CHCW : 24, + ECR : 4, + EIPC : 0, + EIPSW: 1, + FEPC : 2, + FEPSW: 3, + PIR : 6, + PSW : 5, + TKCW : 7, + + // Memory access data types + S8 : 0, + U8 : 1, + S16: 2, + U16: 3, + S32: 4, + F32: 5, + + // Option keys + PSEUDO_HALT: 0, + + // Controller buttons + PWR: 0x0001, + SGN: 0x0002, + A : 0x0004, + B : 0x0008, + RT : 0x0010, + LT : 0x0020, + RU : 0x0040, + RR : 0x0080, + LR : 0x0100, + LL : 0x0200, + LD : 0x0400, + LU : 0x0800, + STA: 0x1000, + SEL: 0x2000, + RL : 0x4000, + RD : 0x8000 + }, + + // Utility + VBU: { + + // Disassembler options + "0X" : 0, + C : 1, + DEST_FIRST: 1, + DEST_LAST : 0, + DOLLAR : 1, + E : 0, + H : 2, + INSIDE : 1, + JOINED : 0, + L : 0, + LOWER : 1, + NAMES : 1, + NUMBERS : 0, + OUTSIDE : 0, + SPLIT : 1, + UPPER : 0, + Z : 1 + }, + + // Web interface + web: { + + // Break types + BREAK_FRAME: 1, + BREAK_POINT: 2, + + // Extra properties + EXT_PIXELS : 0, + EXT_SAMPLES: 1, + + // Anaglyph colors + STEREO_CYAN : 0x00C6F0, + STEREO_GREEN : 0x00B400, + STEREO_MAGENTA: 0xC800FF, + STEREO_RED : 0xFF0000 + } + +}; + +export { Constants }; diff --git a/web/Core.js b/web/Core.js new file mode 100644 index 0000000..5bedb76 --- /dev/null +++ b/web/Core.js @@ -0,0 +1,564 @@ +"use strict"; +import { Constants } from "./Constants.js"; + + + +//////////////////////////////////// Core ///////////////////////////////////// + +// Emulation processor +new class Core { + + // Instance fields + audio; // Audio communication + automatic; // Automatic emulation state + clocked; // Clocked emulation state + dom; // DOM communication + mallocs; // Memory allocations by pointer + pointerType; // TypedArray for WebAssembly pointers + sims; // Simulations by pointer + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor() { + onmessage = async e=>{ + await this.#construct(e.data.audio, e.data.wasmUrl); + this.dom.postMessage(0); + }; + } + + // Asynchronous constructor + async #construct(audio, wasmUrl) { + + // Configure instance fields + this.mallocs = new Map(); + this.sims = new Map(); + + // DOM thread communication + this.dom = globalThis; + this.dom.onmessage = e=>this[e.data.command](e.data); + + // Instantiate the WebAssembly module + this.wasm = (await WebAssembly.instantiateStreaming( + fetch(wasmUrl), { + env: { + emscripten_notify_memory_growth: ()=>this.#onGrowth() + } + })); + Object.assign(this, this.wasm.instance.exports); + this.pointerType = this.PointerSize() == 8 ? + BigUint64Array : Uint32Array; + + // Configure audio state + this.audio = audio; + audio.buffers = [0,0,0].map(v=>new Float32Array(41700 / 50 * 2)); + audio.samples = + this.#malloc(41700 / 50 * 2, audio, "samples", Float32Array); + audio.onmessage = e=>this.#onAudio(e.data); + + // Configure emulation states + this.automatic = { emulating: false }; + this.clocked = {}; + for (let s of [ this.automatic, this.clocked ]) { + s.clocks = this.#malloc(1, s, "clocks" , Uint32Array); + s.pointers = this.#malloc(1, s, "pointers", this.pointerType); + s.sims = []; + } + + } + + + + ////////////////////////////// Core Commands ////////////////////////////// + + // Instantiate sims + createSims(message) { + let sims = new Array(message.count); + let size = this.vbSizeOf(); + + // Process all sims + for (let x = 0; x < message.count; x++) { + let sim = { + canvas : null, + keys : Constants.VB.SGN, + pointer : sims[x] = this.CreateSim(), + volume : 1 + }; + this.sims.set(sim.pointer, sim); + + // Video + this.SetAnaglyph(sim.pointer, + Constants.web.STEREO_RED, Constants.web.STEREO_CYAN); + sim.pixels = this.#noalloc( + this.GetExt(sim.pointer, Constants.web.EXT_PIXELS), + 384*224*4, sim, "pixels", Uint8ClampedArray + ); + sim.image = new ImageData(sim.pixels, 384, 224); + + // Audio + sim.samples = this.#noalloc( + this.GetExt(sim.pointer, Constants.web.EXT_SAMPLES), + 41700 / 50 * 2, sim, "samples", Float32Array + ); + + } + + this.dom.postMessage({ + sims : sims, + promised: true + }); + } + + // Produce disassembly from a sim + disassemble(message) { + + // Disassemble from the simulation + let dasm = message.config == null ? + this.vbuDisassemble( + message.sim, + message.address, + 0, + message.length, + message.line + ) + : + this.Disassemble( + message.config.bcondNotation, + message.config.conditionCase, + message.config.conditionCL, + message.config.conditionEZ, + message.config.conditionNotation, + message.config.hexCase, + message.config.hexNotation, + message.config.memoryNotation, + message.config.mnemonicCase, + message.config.operandOrder, + message.config.programCase, + message.config.programNotation, + message.config.setfNotation, + message.config.systemCase, + message.config.systemNotation, + message.sim, + message.address, + message.length, + message.line + ) + ; + + // A memory error occurred + if (dasm == 0) { + this.dom.postMessage({ + promised: message.promised, + success : false + }); + return; + } + + // Retrieve all disassembly data into a working buffer + let pointer = this.Realloc(0, message.length * 17 * 4); + let buffer = new Uint32Array( + this.memory.buffer, pointer, message.length * 17); + this.GetDasm(pointer, dasm, message.length); + + // Consume output lines + let lines = new Array(message.length); + for (let x = 0, z = 0; x < lines.length; x++) { + let line = lines[x] = { text: {} }; + line.address = buffer[z++]; + line.code = new Array(buffer[z++]); + for (let y = 0; y < line.code.length; y++) + line.code[y] = buffer[z++]; + z += 4 - line.code.length; + line.isPC = buffer[z++] != 0; + line.text.address = this.#string(dasm + buffer[z++], true); + line.text.code = new Array(line.code.length); + for (let y = 0; y < line.code.length; y++) + line.text.code[y] = this.#string(dasm + buffer[z++], true); + z += 4 - line.code.length; + line.text.mnemonic = this.#string(dasm + buffer[z++], true); + line.text.operands = new Array(buffer[z++]); + for (let y = 0; y < line.text.operands.length; y++) + line.text.operands[y] = this.#string(dasm + buffer[z++], true); + z += 3 - line.text.operands.length; + } + + // Memory cleanup + this.Realloc(pointer, 0); + this.Realloc(dasm , 0); + + // Send response + this.dom.postMessage({ + success : true, + lines : lines, + promised: message.promised + }); + } + + // Emulate automatically + emulateAutomatic(message) { + + // Configure sims + this.automatic.pointers = this.#realloc( + this.automatic.pointers, message.sims.length); + for (let x = 0; x < message.sims.length; x++) { + this.automatic.pointers[x] = message.sims[x]; + this.automatic.sims [x] = this.sims.get(message.sims[x]); + } + + // Notify the DOM thread + this.dom.postMessage({ promised: true }); + + // Begin automatic emulation + this.automatic.emulating = true; + this.#autoEmulate(); + } + + // Emulate for a given number of clocks + emulateClocked(message) { + + // Configure sims + this.clocked.pointers = this.#realloc( + this.clocked.pointers, message.sims.length); + for (let x = 0; x < message.sims.length; x++) { + this.clocked.pointers[x] = message.sims[x]; + this.clocked.sims [x] = this.sims.get(message.sims[x]); + } + + // Process simulations + let broke = false; + this.clocked.clocks[0] = message.clocks; + while (!broke && this.clocked.clocks[0] != 0) { + + // Process simulations until a suspension + this.Emulate( + this.clocked.pointers.pointer, + message.sims.length, + this.clocked.clocks.pointer + ); + + // Monitor break conditions + for (let x = 0; x < message.sims.length; x++) { + let sim = this.clocked.sims[x]; + sim.breaks = this.GetBreaks(sim.pointer); + if (breaks & Constants.web.BREAK_POINT) + broke = true; + } + + } + + // Update images + for (let sim of this.clocked.sims) { + if (!(sim.breaks & Constants.web.BREAK_FRAME)) + continue; + this.GetPixels(sim.pointer); + sim.context.putImageData(sim.image, 0, 0); + } + + // Notify DOM thread + this.dom.postMessage({ + promised: true, + broke : broke, + clocks : this.clocked.clocks[0] + }); + } + + // Specify anaglyph colors + setAnaglyph(message) { + this.SetAnaglyph(message.sim, message.left, message.right); + this.dom.postMessage({ promised: true }); + } + + // Specify the OffscreenCanvas that goes with a sim + setCanvas(message) { + let sim = this.sims.get(message.sim); + sim.canvas = message.canvas; + sim.context = sim.canvas.getContext("2d"); + sim.context.putImageData(sim.image, 0, 0); + this.dom.postMessage({ promised: true }); + } + + // Specify a game pak RAM buffer + setCartRAM(message) { + this.#setCartMemory(message.sim, message.data, + this.vbGetCartRAM, this.vbSetCartRAM); + } + + // Specify a game pak ROM buffer + setCartROM(message) { + this.#setCartMemory(message.sim, message.data, + this.vbGetCartROM, this.vbSetCartROM); + } + + // Specify new game pad keys + setKeys(message) { + this.vbSetKeys(message.sim, message.keys); + this.dom.postMessage({ promised: true }); + } + + // Specify a new communication peer + setPeer(message) { + let orphaned = []; + let prev = this.vbGetPeer(message.sim); + if (prev != message.peer) { + if (prev != 0) // Sim's previous peer has been orphaned + orphaned.push(prev); + if (message.peer != 0) { + prev = this.vbGetPeer(message.peer); + if (prev != null) // Peer's previous peer has been orphaned + orphaned.push(prev); + } + this.vbSetPeer(message.sim, message.peer); + } + this.dom.postMessage({ + orphaned: orphaned, + promised: true + }); + } + + // Specify audio volume + setVolume(message) { + this.sims.get(message.sim).volume = message.volume; + this.dom.postMessage({ promised: true }); + } + + // Suspend automatic emulation + suspend(message) { + this.automatic.emulating = false; + this.dom.postMessage({ promised: true }); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Message from audio thread + #onAudio(e) { + + // Output staged images + if (this.automatic.emulating && this.audio.buffers.length == 0) { + for (let sim of this.automatic.sims) + sim.context.putImageData(sim.image, 0, 0); + } + + // Acquire the emptied buffers and resume emulation + this.audio.buffers.push(... e.map(b=>new Float32Array(b))); + this.#autoEmulate(); + } + + // WebAssembly memory has grown + #onGrowth() { + for (let prev of this.mallocs.values()) { + let buffer = new prev.constructor( + this.memory.buffer, prev.pointer, prev.size); + Object.assign(buffer, { + assign : prev.assign, + pointer: prev.pointer, + size : prev.size, + target : prev.target + }); + this.mallocs.set(buffer.pointer, buffer); + this.#updateTarget(buffer); + } + for (let sim of this.sims) + sim.image = new ImageData(sim.pixels, 384, 224); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Automatic emulation processing + #autoEmulate() { + + // Error checking + if (!this.automatic.emulating) + return; + + // Process all remaining audio buffers + while (this.audio.buffers.length != 0) { + + // Reset sample output + for (let sim of this.automatic.sims) { + this.vbSetSamples(sim.pointer, sim.samples.pointer, + Constants.VB.F32, 41700 / 50); + } + + // Process all clocks + this.automatic.clocks[0] = 400000; // 0.02s + while (this.automatic.clocks[0] != 0) { + this.Emulate( + this.automatic.pointers.pointer, + this.automatic.sims.length, + this.automatic.clocks.pointer + ); + + // Too many buffers left to output video + if (this.audio.buffers.length > 2) + continue; + + // Stage the next video image + for (let sim of this.automatic.sims) { + let breaks = this.GetBreaks(sim.pointer); + if (breaks & Constants.web.BREAK_FRAME) + this.GetPixels(sim.pointer); + } + + } + + // Mix and output audio samples + let buffer = this.audio.buffers.shift(); + if (this.automatic.sims.length > 1) { + for (let x = 0; x < buffer.length; x++) + buffer[x] = 0; + for (let sim of this.automatic.sims) + for (let x = 0; x < buffer.length; x++) + buffer[x] += sim.samples[x] * sim.volume; + for (let x = 0; x < buffer.length; x++) + buffer[x] = Math.min(Math.max(-1, buffer[x]), +1); + } else { + for (let x = 0; x < buffer.length; x++) + buffer[x] = this.automatic.sims[0].samples[x]; + } + this.audio.postMessage(buffer.buffer, [ buffer.buffer ]); + + // Output staged images if there's one audio buffer to go + if (this.audio.buffers.length != 1) + continue; + for (let sim of this.automatic.sims) + sim.context.putImageData(sim.image, 0, 0); + } + + } + + // Delete an allocated buffer in WebAssembly memory + #free(buffer) { + this.mallocs.delete(buffer.pointer); + this.Realloc(buffer.pointer, 0); + } + + // Allocate memory in WebAssembly and register the buffer + #malloc(count, target = null, assign = null, type = Uint8ClampedArray) { + return this.#noalloc( + this.Realloc(0, count * type.BYTES_PER_ELEMENT), + count, target, assign, type + ); + } + + // Register a buffer in WebAssembly memory without allocating it + #noalloc(pointer, count, target=null, assign=null, type=Uint8ClampedArray){ + let buffer = new type(this.memory.buffer, pointer, count); + Object.assign(buffer, { + assign : assign?.split("."), + count : count, + pointer: pointer, + target : target + }); + this.mallocs.set(pointer, buffer); + return buffer; + } + + // Resize a previously allocated buffer in WebAssembly memory + #realloc(prev, count) { + this.mallocs.delete(prev.pointer); + let pointer = this.Realloc(prev.pointer, + count * prev.constructor.prototype.BYTES_PER_ELEMENT); + let buffer = new prev.constructor(this.memory.buffer, pointer, count); + Object.assign(buffer, { + assign : prev.assign, + count : count, + pointer: pointer, + target : prev.target + }); + this.mallocs.set(pointer, buffer); + this.#updateTarget(buffer); + return buffer; + } + + // Compute anaglyph color values + #setAnaglyph(sim, left, right) { + + // Split out the RGB channels + let color = left | right; + let stereo = [ + color >> 16 & 0xFF, + color >> 8 & 0xFF, + color & 0xFF + ]; + + // Compute scaled RGB values by output level + sim.anaglyph = new Array(256); + for (let x = 0; x < 256; x++) { + let level = sim.anaglyph[x] = new Array(3); + for (let y = 0; y < 3; y++) + level[y] = Math.round(x * stereo[y] / 255.0); + } + + // Determine which channels are in each eye + sim.anaglyph.left = []; + sim.anaglyph.right = []; + for (let x = 0, y = 16; x < 3; x++, y -= 8) { + if (left >> y & 0xFF) + sim.anaglyph.left .push(x); + if (right >> y & 0xFF) + sim.anaglyph.right.push(x); + } + + } + + // Specify a game pak memory buffer + #setCartMemory(sim, mem, getter, setter) { + + // Working variables + let cart = new Uint8Array(mem); + let prev = getter(sim); + let cur = this.Realloc(0, cart.length); + mem = new Uint8Array(this.memory.buffer, cur, cart.length); + + // Transfer the data into core memory + for (let x = 0; x < mem.length; x++) + mem[x] = cart[x]; + + // Assign the ROM to the simulation + let success = setter(sim, cur, mem.length) == 0; + if (success) { + if (prev != 0) + this.Realloc(prev, 0); + } else this.Realloc(cur, 0); + + // Reply to the DOM thread + this.dom.postMessage({ + success : success, + promised: true + }); + } + + // Read a C string from WebAssembly memory + #string(address, indirect = false) { + if (address == 0) + return null; + + if (indirect) { + let next = new this.pointerType(this.memory.buffer, address, 1)[0]; + address = next; + } + + let length = 0; + let memory = new Uint8Array(this.memory.buffer); + for (let addr = address; memory[addr++] != 0; length++); + return (Array.from(memory.slice(address, address + length)) + .map(b=>String.fromCodePoint(b)).join("")); + } + + // Update an allocated buffer's assignment in its monitor object + #updateTarget(buffer) { + if (buffer.target == null) + return; + let obj = buffer.target; + let assign = buffer.assign.slice(); + while (assign.length > 1) + obj = obj[assign.shift()]; + obj[assign[0]] = buffer; + } + +}(); diff --git a/web/VB.js b/web/VB.js new file mode 100644 index 0000000..f475755 --- /dev/null +++ b/web/VB.js @@ -0,0 +1,894 @@ +"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; + #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.#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 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 + #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.#peer = null; + this.#pointer = pointer; + this.#volume = 1; + 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 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, + 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 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 > 1) + throw new RangeError("Volume must be a number from 0 to 1."); + + // 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 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 }; diff --git a/web/wasm.c b/web/wasm.c new file mode 100644 index 0000000..14a07ca --- /dev/null +++ b/web/wasm.c @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include + + + +////////////////////////////////// Constants ////////////////////////////////// + +// Break conditions +#define BREAK_FRAME 1 +#define BREAK_POINT 2 + +// Extra properties +#define EXT_PIXELS 0 +#define EXT_SAMPLES 1 + + + +//////////////////////////////////// Types //////////////////////////////////// + +// Additional monitor state for simulations +typedef struct { + int32_t breaks; + uint32_t left[256]; + uint8_t pixels[384 * 224 * 4]; + uint32_t right[256]; + float samples[41700 * 2]; +} Ext; + + + +////////////////////////////////// Callbacks ////////////////////////////////// + +// Frame callback +int wasmOnFrame(VB *sim) { + ((Ext *) vbGetUserData(sim))->breaks |= BREAK_FRAME; + return 1; +} + + + +/////////////////////////////// Module Exports //////////////////////////////// + +// Instantiate a simulation +EMSCRIPTEN_KEEPALIVE void* CreateSim() { + size_t sizeOfSim = vbSizeOf(); + uint8_t *pointer = malloc(sizeOfSim + sizeof (Ext)); + + // Configure sim + VB *sim = vbInit((VB *) pointer); + vbSetFrameCallback(sim, &wasmOnFrame); + vbSetOption(sim, VB_PSEUDO_HALT, 1); + + // Configure extra + Ext *ext = (Ext *) (pointer + sizeOfSim); + ext->breaks = 0; + vbSetUserData(sim, ext); + + // Initialize pixels with opaque black + for (unsigned x = 0; x < 384 * 224; x++) + ((uint32_t *) ext->pixels)[x] = 0xFF000000; + + return sim; +} + +// Disassemble from a simulation +EMSCRIPTEN_KEEPALIVE void* Disassemble( + int bcondNotation, int conditionCase, int conditionCL, int conditionEZ, + int conditionNotation, int hexCase, int hexNotation, int memoryNotation, + int mnemonicCase, int operandOrder, int programCase, int programNotation, + int setfNotation, int systemCase, int systemNotation, + VB *sim, uint32_t address, unsigned length, int line +) { + VBU_DasmConfig config; + config.bcondNotation = bcondNotation; + config.conditionCase = conditionCase; + config.conditionCL = conditionCL; + config.conditionEZ = conditionEZ; + config.conditionNotation = conditionNotation; + config.hexCase = hexCase; + config.hexNotation = hexNotation; + config.memoryNotation = memoryNotation; + config.mnemonicCase = mnemonicCase; + config.operandOrder = operandOrder; + config.programCase = programCase; + config.programNotation = programNotation; + config.setfNotation = setfNotation; + config.systemCase = systemCase; + config.systemNotation = systemNotation; + return vbuDisassemble(sim, address, &config, length, line); +} + +// Process simulations +EMSCRIPTEN_KEEPALIVE int Emulate(VB **sims, unsigned count, uint32_t *clocks) { + for (unsigned x = 0; x < count; x++) + ((Ext *) vbGetUserData(sims[x]))->breaks = 0; + return vbEmulateEx(sims, count, clocks); +} + +// Retrieve the break condition flags for a sim +EMSCRIPTEN_KEEPALIVE int32_t GetBreaks(VB *sim) { + return ((Ext *) vbGetUserData(sim))->breaks; +} + +// Serialize disassembled lines into a linear buffer +EMSCRIPTEN_KEEPALIVE void GetDasm(uint32_t *buffer, void *dasm, int count) { + for (int x = 0; x < count; x++) { + VBU_DasmLine *line = &((VBU_DasmLine *) dasm)[x]; + + // Numeric data + *buffer++ = line->address; + *buffer++ = line->codeLength; + for (int y = 0; y < 4; y++) + *buffer++ = line->code[y]; + *buffer++ = line->isPC; + + // Text data -- Store offset of string pointer from start of dasm + *buffer++ = (uint32_t) ((void *) &line->text.address - dasm); + for (int y = 0; y < 4; y++) + *buffer++ = (uint32_t) ((void *) &line->text.code[y] - dasm); + *buffer++ = (uint32_t) ((void *) &line->text.mnemonic - dasm); + *buffer++ = line->text.operandsLength; + for (int y = 0; y < 3; y++) + *buffer++ = (uint32_t) ((void *) &line->text.operands[y] - dasm); + } +} + +// Retrieve an extra property +EMSCRIPTEN_KEEPALIVE void* GetExt(VB *sim, int id) { + Ext *ext = (Ext *) vbGetUserData(sim); + switch (id) { + case EXT_PIXELS : return ext->pixels; + case EXT_SAMPLES: return ext->samples; + } + return NULL; +} + +// Retrieve anaglyph-tinted pixels from a sim +EMSCRIPTEN_KEEPALIVE void GetPixels(VB *sim) { + Ext *ext = (Ext *) vbGetUserData(sim); + uint8_t *pixels = ext->pixels; + vbGetPixels(sim, pixels, 4, 384 * 4, pixels + 1, 4, 384 * 4); + for (unsigned x = 0; x < 384 * 224 * 4; x += 4, pixels += 4) + *(uint32_t *) pixels = ext->left[pixels[0]] | ext->right[pixels[1]]; +} + +// Determine the size in bytes of a pointer +EMSCRIPTEN_KEEPALIVE int PointerSize() { + return sizeof (void *); +} + +// Memory management +EMSCRIPTEN_KEEPALIVE void* Realloc(void *data, size_t size) { + return realloc(data, size); +} + +// Specify anaglyph colors +EMSCRIPTEN_KEEPALIVE void SetAnaglyph(VB *sim, uint32_t left, uint32_t right) { + Ext *ext = (Ext *) vbGetUserData(sim); + + // Erase all RGB values + for (int x = 0; x < 256; x++) + ext->left[x] = ext->right[x] = 0xFF000000; + + // Process all RGB channels + for (int c = 0, shift = 16; c < 3; c++, shift -= 8) { + double max; // Magnitude of channel value + uint32_t *dest; // Lookup data + + // Select the magnitude and lookup channel + dest = ext->left; + max = (left >> shift & 0xFF) / 255.0; + if (max == 0) { + dest = ext->right; + max = (right >> shift & 0xFF) / 255.0; + if (max == 0) + continue; + } + + // Compute the resulting RGB values + for (int x = 0; x < 256; x++) + *dest++ |= (uint32_t) (x * max + 0.5) << (16 - shift); + } + +}