Full front-end rewrite

This commit is contained in:
2022-04-14 20:51:09 -05:00
parent fccb799a9c
commit 3cf006ba13
59 changed files with 9424 additions and 7228 deletions
+196
View File
@@ -0,0 +1,196 @@
import { Sim } from /**/"./Sim.js";
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
let RESTRICT = {};
let WASM_URL = url(/**/"./core.wasm" );
let WORKER_URL = url(/**/"./CoreWorker.js");
///////////////////////////////////////////////////////////////////////////////
// Core //
///////////////////////////////////////////////////////////////////////////////
// Environment manager for simulated Virtual Boys
class Core {
//////////////////////////////// Constants ////////////////////////////////
// States
static IDLE = 0;
static RUNNING = 1;
///////////////////////////// Static Methods //////////////////////////////
// Create a new instance of Core
static create(options) {
return new Core(RESTRICT).init(options);
}
///////////////////////// Initialization Methods //////////////////////////
// Stub constructor
constructor(restrict) {
if (restrict != RESTRICT) {
throw "Cannot instantiate Core directly. " +
"Use Core.create() instead.";
}
}
// Substitute constructor
async init(options = {}) {
// Configure instance fields
this.length = 0;
this.onsubscriptions = null;
this.resolutions = [];
this.state = Core.IDLE;
this.worker = new Worker(WORKER_URL);
this.worker.onmessage = e=>this.onMessage(e.data);
// Issue a create command
if ("sims" in options)
await this.create(options.sims, WASM_URL);
// Only initialize the WebAssembly module
else this.send("init", false, { wasm: WASM_URL });
return this;
}
///////////////////////////// Event Handlers //////////////////////////////
// Worker message received
onMessage(data) {
// Process a promised response
if ("response" in data)
this.resolutions.shift()(data.response);
// Process subscriptions
if (this.onsubscriptions && data.subscriptions)
this.onsubscriptions(data.subscriptions);
}
///////////////////////////// Public Methods //////////////////////////////
// Associate two simulations as peers, or remove an association
connect(a, b, options = {}) {
return this.send({
command: "connect",
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Create and initialize new simulations
async create(sims, wasm) {
let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0);
// Execute the command in the core thread
let response = await this.send({
command: "create",
sims : numSims,
wasm : wasm
});
// Process the core thread's response
let ret = [];
for (let x = 0; x < numSims; x++, this.length++)
ret.push(this[this.length] =
new Sim(this, response[x], this.length));
return sims === undefined ? ret[0] : ret;
}
// Delete a simulation
destroy(sim, options = {}) {
// Configure simulation
sim = this[sim] || sim;
if (sim.core != this)
return;
let ptr = sim.destroy();
// State management
for (let x = sim.index + 1; x < this.length; x++)
(this[x - 1] = this[x]).index--;
delete this[--this.length];
// Execute the command on the core thread
return this.send({
command: "destroy",
respond: !("respond" in options) || !!options.respond,
sim : ptr
});
}
// Attempt to run until the next instruction
runNext(a, b, options = {}) {
return this.send({
command: "runNext",
refresh: !!options.refresh,
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Execute one instruction
singleStep(a, b, options = {}) {
return this.send({
command: "singleStep",
refresh: !!options.refresh,
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Unsubscribe from frame data
unsubscribe(key, sim = 0) {
this.send({
command: "unsubscribe",
key : key,
respond: false,
sim : sim
});
}
///////////////////////////// Private Methods /////////////////////////////
// Send a message to the Worker
send(data = {}, transfers = []) {
// Create the message object
Object.assign(data, {
respond: !("respond" in data) || !!data.respond,
run : !("run" in data) || !!data.run
});
// Do not wait on a response
if (!data.respond)
this.worker.postMessage(data, transfers);
// Wait for the response to come back
else return new Promise((resolve, reject)=>{
this.resolutions.push(response=>resolve(response));
this.worker.postMessage(data, transfers);
});
}
}
///////////////////////////////////////////////////////////////////////////////
export { Core };
+378
View File
@@ -0,0 +1,378 @@
"use strict";
// Un-sign a 32-bit integer
// Emscripten is sign-extending uint32_t and Firefox can't import in Workers
let u32 = (()=>{
let U32 = new Uint32Array(1);
return x=>{ U32[0] = x; return U32[0]; };
})();
///////////////////////////////////////////////////////////////////////////////
// CoreWorker //
///////////////////////////////////////////////////////////////////////////////
// Thread manager for Core commands
new class CoreWorker {
///////////////////////// Initialization Methods //////////////////////////
// Stub constructor
constructor() {
onmessage = async e=>{
await this.init(e.data.wasm);
onmessage = e=>this.onCommand(e.data, false);
onmessage(e);
};
}
// Substitute constructor
async init(wasm) {
// Load the WebAssembly module
let imports = {
env: { emscripten_notify_memory_growth: ()=>this.onMemory() }
};
this.wasm = await (typeof wasm == "string" ?
WebAssembly.instantiateStreaming(fetch(wasm), imports) :
WebAssembly.instantiate ( wasm , imports)
);
// Configure instance fields
this.api = this.wasm.instance.exports;
this.frameData = null;
this.isRunning = false;
this.memory = this.api.memory.buffer;
this.ptrSize = this.api.PointerSize();
this.ptrType = this.ptrSize == 4 ? Uint32Array : Uint64Array;
this.subscriptions = {};
}
///////////////////////////// Event Handlers //////////////////////////////
// Message from audio thread
onAudio(frames) {
}
// Message from main thread
onCommand(data) {
// Subscribe to the command
if (data.subscribe) {
let sub = data.sim || 0;
sub = this.subscriptions[sub] || (this.subscriptions[sub] = {});
sub = sub[data.subscribe] = {};
Object.assign(sub, data);
delete sub.promised;
delete sub.run;
delete sub.subscribe;
}
// Execute the command
if (data.run)
this[data.command](data);
// Process all subscriptions to refresh any debugging interfaces
if (data.refresh)
this.doSubscriptions(data.sim ? [ data.sim ] : data.sims);
// Reply to the main thread
if (data.respond) {
postMessage({
response: data.response
}, data.transfers);
}
}
// Memory growth
onMemory() {
this.memory = this.api.memory.buffer;
}
//////////////////////////////// Commands /////////////////////////////////
// Associate two simulations as peers, or remove an association
connect(data) {
this.api.vbConnect(data.sims[0], data.sims[1]);
}
// Allocate and initialize a new simulation
create(data) {
let ptr = this.api.Create(data.sims);
data.response = new this.ptrType(this.memory, ptr, data.sims).slice();
data.transfers = [ data.response.buffer ];
this.api.Free(ptr);
}
// Delete a simulation
destroy(data) {
this.api.Destroy(data.sim);
}
// Locate instructions for disassembly
disassemble(data) {
let decode; // Address of next row
let index; // Index in list of next row
let rows = new Array(data.rows);
let pc = u32(this.api.vbGetProgramCounter(data.sim));
let row; // Located output row
// The target address is before or on the first row of output
if (data.row <= 0) {
decode = u32(data.target - 4 * Math.max(0, data.row + 10));
// Locate the target row
for (;;) {
row = this.dasmRow(data.sim, decode, pc);
if (u32(data.target - decode) < row.size)
break;
decode = u32(decode + row.size);
}
// Locate the first row of output
for (index = data.row; index < 0; index++) {
decode = u32(decode + row.size);
row = this.dasmRow(data.sim, decode, pc);
}
// Prepare to process remaining rows
decode = u32(decode + row.size);
rows[0] = row;
index = 1;
}
// The target address is after the first row of output
else {
let circle = new Array(data.row + 1);
let count = Math.min(data.row + 1, data.rows);
let src = 0;
decode = u32(data.target - 4 * (data.row + 10));
// Locate the target row
for (;;) {
row = circle[src] = this.dasmRow(data.sim, decode, pc);
decode = u32(decode + row.size);
if (u32(data.target - row.address) < row.size)
break;
src = (src + 1) % circle.length;
}
// Copy entries from the circular buffer to the output list
for (index = 0; index < count; index++) {
src = (src + 1) % circle.length;
rows[index] = circle[src];
}
}
// Locate any remaining rows
for (; index < data.rows; index++) {
let row = rows[index] = this.dasmRow(data.sim, decode, pc);
decode = u32(decode + row.size);
}
// Respond to main thread
data.response = {
pc : pc,
rows : rows,
scroll: data.scroll
};
}
// Retrieve all CPU program registers
getProgramRegisters(data) {
let ret = data.response = new Uint32Array(32);
for (let x = 0; x < 32; x++)
ret[x] = this.api.vbGetProgramRegister(data.sim, x);
data.transfers = [ ret.buffer ];
}
// Retrieve the value of a system register
getSystemRegister(data) {
data.response = u32(this.api.vbGetSystemRegister(data.sim, data.id));
}
// Retrieve all CPU system registers (including PC)
getSystemRegisters(data) {
data.response = {
adtre: u32(this.api.vbGetSystemRegister(data.sim, 25)),
chcw : u32(this.api.vbGetSystemRegister(data.sim, 24)),
ecr : u32(this.api.vbGetSystemRegister(data.sim, 4)),
eipc : u32(this.api.vbGetSystemRegister(data.sim, 0)),
eipsw: u32(this.api.vbGetSystemRegister(data.sim, 1)),
fepc : u32(this.api.vbGetSystemRegister(data.sim, 2)),
fepsw: u32(this.api.vbGetSystemRegister(data.sim, 3)),
pc : u32(this.api.vbGetProgramCounter(data.sim )),
pir : u32(this.api.vbGetSystemRegister(data.sim, 6)),
psw : u32(this.api.vbGetSystemRegister(data.sim, 5)),
tkcw : u32(this.api.vbGetSystemRegister(data.sim, 7)),
[29] : u32(this.api.vbGetSystemRegister(data.sim, 29)),
[30] : u32(this.api.vbGetSystemRegister(data.sim, 30)),
[31] : u32(this.api.vbGetSystemRegister(data.sim, 31))
};
}
// Read bytes from the simulation
read(data) {
let ptr = this.api.Malloc(data.length);
this.api.ReadBuffer(data.sim, ptr, data.address, data.length);
let buffer = new Uint8Array(this.memory, ptr, data.length).slice();
this.api.Free(ptr);
data.response = {
address: data.address,
bytes : buffer
};
data.transfers = [ buffer.buffer ];
}
// Attempt to execute until the following instruction
runNext(data) {
this.api.RunNext(data.sims[0], data.sims[1]);
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
if (data.sims[1])
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
data.response = {
pc: pc
}
}
// Specify a new value for PC
setProgramCounter(data) {
data.response =
u32(this.api.vbSetProgramCounter(data.sim, data.value));
}
// Specify a new value for a program register
setProgramRegister(data) {
data.response = this.api.vbSetProgramRegister
(data.sim, data.id, data.value);
}
// Specify a ROM buffer
setROM(data) {
let ptr = this.api.Malloc(data.rom.length);
let buffer = new Uint8Array(this.memory, ptr, data.rom.length);
for (let x = 0; x < data.rom.length; x++)
buffer[x] = data.rom[x];
data.response = !!this.api.SetROM(data.sim, ptr, data.rom.length);
}
// Specify a new value for a system register
setSystemRegister(data) {
data.response = u32(this.api.vbSetSystemRegister
(data.sim, data.id, data.value));
}
// Execute one instruction
singleStep(data) {
this.api.SingleStep(data.sims[0], data.sims[1]);
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
if (data.sims[1])
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
data.response = {
pc: pc
}
}
// Unsubscribe from frame data
unsubscribe(data) {
let sim = data.sim || 0;
if (sim in this.subscriptions) {
let subs = this.subscriptions[sim];
delete subs[data.key];
if (Object.keys(subs).length == 0)
delete this.subscriptions[sim];
}
}
// Write bytes to the simulation
write(data) {
let ptr = this.api.Malloc(data.bytes.length);
let buffer = new Uint8Array(this.memory, ptr, data.bytes.length);
for (let x = 0; x < data.bytes.length; x++)
buffer[x] = data.bytes[x];
this.api.WriteBuffer(data.sim, ptr, data.address, data.bytes.length);
this.api.Free(ptr);
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve basic information for a row of disassembly
dasmRow(sim, address, pc) {
let bits = this.api.vbRead(sim, address, 3 /* VB_U16 */);
let opcode = bits >> 10 & 63;
let size = (
opcode < 0b101000 || // Formats I through III
opcode == 0b110010 || // Illegal
opcode == 0b110110 // Illegal
) ? 2 : 4;
// Establish row information
let row = {
address: address,
bytes : [ bits & 0xFF, bits >> 8 ],
size : u32(address + 2) == pc ? 2 : size
//size : Math.min(u32(pc - address), size)
};
// Read additional bytes
if (size == 4) {
bits = this.api.vbRead(sim, address + 2, 3 /* VB_U16 */);
row.bytes.push(bits & 0xFF, bits >> 8);
}
return row;
}
// Process subscriptions and send a message to the main thread
doSubscriptions(sims) {
let message = { subscriptions: {} };
let transfers = [];
// Process all simulations
for (let sim of sims) {
// There are no subscriptions for this sim
if (!(sim in this.subscriptions))
continue;
// Working variables
let subs = message.subscriptions[sim] = {};
// Process all subscriptions
for (let sub of Object.entries(this.subscriptions[sim])) {
// Run the command
this[sub[1].command](sub[1]);
// Add the response to the message
if (sub[1].response) {
subs[sub[0]] = sub[1].response;
delete sub[1].response;
}
// Add the transferable objects to the message
if (sub[1].transfers) {
transfers.push(... sub[1].transfers);
delete sub[1].transfers;
}
}
}
// Send the message to the main thread
if (Object.keys(message).length != 0)
postMessage(message, transfers);
}
}();
+151
View File
@@ -0,0 +1,151 @@
///////////////////////////////////////////////////////////////////////////////
// Sim //
///////////////////////////////////////////////////////////////////////////////
// One simulated Virtual Boy
class Sim {
///////////////////////// Initialization Methods //////////////////////////
constructor(core, sim, index) {
this.core = core;
this.index = index;
this.sim = sim;
}
///////////////////////////// Public Methods //////////////////////////////
// Locate CPU instructions
disassemble(target, row, rows, scroll, options = {}) {
return this.core.send({
command : "disassemble",
row : row,
rows : rows,
scroll : scroll,
sim : this.sim,
subscribe: options.subscribe,
target : target
});
}
// Retrieve all CPU program registers
getProgramRegisters(options = {}) {
return this.core.send({
command : "getProgramRegisters",
sim : this.sim,
subscribe: options.subscribe
});
}
// Retrieve the value of a system register
getSystemRegister(id, options = {}) {
return this.core.send({
command : "getSystemRegister",
id : id,
sim : this.sim,
subscribe: options.subscribe
});
}
// Retrieve all CPU system registers (including PC)
getSystemRegisters(options = {}) {
return this.core.send({
command : "getSystemRegisters",
sim : this.sim,
subscribe: options.subscribe
});
}
// Read multiple bytes from the bus
read(address, length, options = {}) {
return this.core.send({
address : address,
command : "read",
debug : !("debug" in options) || !!options.debug,
length : length,
sim : this.sim,
subscribe: options.subscribe
});
}
// Specify a new value for PC
setProgramCounter(value, options = {}) {
return this.core.send({
command: "setProgramCounter",
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Specify a new value for a program register
setProgramRegister(id, value, options = {}) {
return this.core.send({
command: "setProgramRegister",
id : id,
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Specify the current ROM buffer
setROM(rom, options = {}) {
return this.core.send({
command: "setROM",
rom : rom,
refresh: !!options.refresh,
sim : this.sim
}, [ rom.buffer ]);
}
// Specify a new value for a system register
setSystemRegister(id, value, options = {}) {
return this.core.send({
command: "setSystemRegister",
id : id,
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Ubsubscribe from frame data
unsubscribe(key) {
return this.core.unsubscribe(key, this.sim);
}
// Write multiple bytes to the bus
write(address, bytes, options = {}) {
return this.core.send({
address : address,
command : "write",
bytes : bytes,
debug : !("debug" in options) || !!options.debug,
refresh : !!options.refresh,
sim : this.sim,
subscribe: options.subscribe
},
bytes instanceof ArrayBuffer ? [ bytes ] :
bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] :
undefined
);
}
///////////////////////////// Package Methods /////////////////////////////
// The simulation has been destroyed
destroy() {
let sim = this.sim;
this.core = null;
this.sim = 0;
return sim;
}
}
export { Sim };