const utf8 = new TextDecoder();
export class Rsemu {
constructor(instance) {
this.e = instance.exports;
}
static async load(url) {
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {});
return new Rsemu(instance);
}
bytes(ptr, len) {
return new Uint8Array(this.e.memory.buffer, ptr, len);
}
output(len) {
return len === 0 ? new Uint8Array(0) : this.bytes(this.e.rsemu_output_ptr(), len).slice();
}
text(len) {
return len === 0 ? "" : utf8.decode(this.bytes(this.e.rsemu_output_ptr(), len));
}
input(data) {
const ptr = this.e.rsemu_input_reserve(data.length);
this.bytes(ptr, data.length).set(data);
return data.length;
}
version() {
return utf8.decode(this.bytes(this.e.rsemu_version_ptr(), this.e.rsemu_version_len()));
}
echo(value) {
return this.e.rsemu_echo(value) >>> 0;
}
error() {
return this.text(this.e.rsemu_error());
}
machines() {
const out = [];
for (let i = 0; i < this.e.rsemu_machine_count(); i++) {
out.push({
index: i,
name: this.text(this.e.rsemu_machine_name(i)),
summary: this.text(this.e.rsemu_machine_summary(i)),
media: this.text(this.e.rsemu_machine_media(i)),
});
}
return out;
}
boot(index, image) {
const len = image ? this.input(image) : 0;
if (!this.e.rsemu_boot(index, len)) {
throw new Error(this.error() || "boot failed");
}
}
shutdown() {
this.e.rsemu_shutdown();
}
reset() {
if (!this.e.rsemu_reset()) throw new Error(this.error() || "reset failed");
}
get running() {
return this.e.rsemu_is_running() !== 0;
}
runFrame() {
return this.e.rsemu_run_frame() !== 0;
}
runFrames(n) {
return this.e.rsemu_run_frames(n);
}
frameMs() {
return Number(this.e.rsemu_frame_period_ns()) / 1e6;
}
nowNs() {
return Number(this.e.rsemu_now_ns());
}
stateHash() {
const hash = BigInt.asUintN(64, this.e.rsemu_state_hash());
return "0x" + hash.toString(16).padStart(16, "0");
}
get hasVideo() {
return this.e.rsemu_has_video() !== 0;
}
get width() {
return this.e.rsemu_frame_width();
}
get height() {
return this.e.rsemu_frame_height();
}
frameSerial() {
return Number(this.e.rsemu_frame_serial());
}
imageData() {
const len = this.e.rsemu_frame_len();
if (len === 0) return null;
const pixels = new Uint8ClampedArray(this.e.memory.buffer, this.e.rsemu_frame_ptr(), len);
return new ImageData(pixels, this.width, this.height);
}
get hasAudio() {
return this.e.rsemu_has_audio() !== 0;
}
audioSetRate(hz) {
return this.e.rsemu_audio_set_rate(hz) !== 0;
}
get audioRate() {
return this.e.rsemu_audio_rate();
}
get audioChannels() {
return this.e.rsemu_audio_channels();
}
audioFrames() {
return this.e.rsemu_audio_frames();
}
audioView(frames) {
const count = frames * Math.max(1, this.audioChannels);
return new Float32Array(this.e.memory.buffer, this.e.rsemu_audio_ptr(), count);
}
audioConsume(frames) {
return this.e.rsemu_audio_consume(frames);
}
audioDropped() {
return Number(this.e.rsemu_audio_dropped());
}
static BUTTONS = {
a: 0x80,
b: 0x40,
select: 0x20,
start: 0x10,
up: 0x08,
down: 0x04,
left: 0x02,
right: 0x01,
};
setButtons(port, mask) {
this.e.rsemu_set_buttons(port, mask);
}
buttons(port) {
return this.e.rsemu_buttons(port);
}
get hasConsole() {
return this.e.rsemu_has_console() !== 0;
}
consoleWrite(text) {
const bytes = new TextEncoder().encode(text);
this.input(bytes);
return this.e.rsemu_console_write(bytes.length);
}
consoleRead() {
return this.output(this.e.rsemu_console_read());
}
save() {
const len = this.e.rsemu_save();
if (len === 0) throw new Error(this.error() || "save failed");
return this.output(len);
}
load(bytes) {
const len = this.input(bytes);
if (!this.e.rsemu_load(len)) throw new Error(this.error() || "load failed");
}
}