pvbemu/web/core/Core.js

293 lines
7.9 KiB
JavaScript

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