shrooms-vb-core/web/Core.js

565 lines
18 KiB
JavaScript

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