Introduce web core
This commit is contained in:
parent
53826584b8
commit
26a0357afa
|
@ -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);
|
|
@ -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 };
|
|
@ -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;
|
||||
}
|
||||
|
||||
}();
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue