Introduce web core

This commit is contained in:
Guy Perfect 2024-11-01 15:03:49 -05:00
parent 53826584b8
commit 26a0357afa
5 changed files with 1837 additions and 0 deletions

101
web/Audio.js Normal file
View File

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

91
web/Constants.js Normal file
View File

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

564
web/Core.js Normal file
View File

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

894
web/VB.js Normal file
View File

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

187
web/wasm.c Normal file
View File

@ -0,0 +1,187 @@
#include <stdlib.h>
#include <stdio.h>
#include <emscripten/emscripten.h>
#include <vb.h>
#include <vbu.h>
////////////////////////////////// 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);
}
}