Function bodies 123 total
_addFn method · javascript · L219-L259 (41 LOC)public/js/libretro-adapter.js
_addFn(jsFunc, sig) {
const M = this.M;
// Path 1: standard Emscripten addFunction
if (typeof M.addFunction === 'function') {
return M.addFunction(jsFunc, sig);
}
// Path 2: direct WebAssembly.Table manipulation.
// Emscripten exposes the function table as Module.wasmTable (older builds)
// or Module.__indirect_function_table (Emscripten 2.x+).
const table = M.wasmTable ?? M.__indirect_function_table;
if (!(table instanceof WebAssembly.Table)) {
throw new Error(
'LibretroAdapter: cannot register callbacks — core must be compiled with ' +
'ALLOW_TABLE_GROWTH=1 and EXPORTED_RUNTIME_METHODS=["addFunction"], or the ' +
'WebAssembly.Table must be accessible via Module.wasmTable / ' +
'Module.__indirect_function_table'
);
}
// Try to grow the table by one slot and claim it.
// If the table was created with a fixed maximum (grow() throws RangeError),
// scan from index 1 for a null slot _registerCallbacks method · javascript · L265-L305 (41 LOC)public/js/libretro-adapter.js
_registerCallbacks() {
// bool env(unsigned cmd, void *data)
this._callbacks.env = this._addFn(
(cmd, data) => this._onEnvironment(cmd, data) ? 1 : 0,
'iii');
// void video_refresh(const void *data, unsigned width, unsigned height, size_t pitch)
this._callbacks.video = this._addFn(
(dataPtr, width, height, pitch) => this._onVideoRefresh(dataPtr, width, height, pitch),
'viiii');
// void audio_sample(int16_t left, int16_t right)
this._callbacks.audioSample = this._addFn((left, right) => {
if (this._audioMuted) return;
this._singleSampleBuf[0] = left;
this._singleSampleBuf[1] = right;
this._flushSamples(this._singleSampleBuf, 1);
}, 'vii');
// size_t audio_sample_batch(const int16_t *data, size_t frames)
this._callbacks.audioBatch = this._addFn(
(dataPtr, frames) => this._onAudioBatch(dataPtr, frames),
'iii');
// void input_poll(void) — input is already snapshotted before step(); no-_onEnvironment method · javascript · L313-L384 (72 LOC)public/js/libretro-adapter.js
_onEnvironment(cmd, data) {
const M = this.M;
const ENV = LibretroAdapter.ENV;
switch (cmd) {
case ENV.SET_PIXEL_FORMAT:
// Core declares its output pixel format before rendering begins.
this._pixelFormat = M.HEAP32[data >> 2];
return true;
case ENV.GET_CAN_DUPE:
// Signal that we accept duplicate/null frame pointers (CAN_DUPE=true).
// This lets the core skip sending identical frames and saves CPU.
M.HEAPU8[data] = 1;
return true;
case ENV.GET_SYSTEM_DIRECTORY:
case ENV.GET_SAVE_DIRECTORY:
// Return NULL pointer — no persistent filesystem is available in the browser.
M.HEAP32[data >> 2] = 0;
return true;
case ENV.GET_VARIABLE:
// The core is asking for a configuration variable value.
// Return NULL to instruct the core to use its built-in default.
M.HEAP32[(data + 4) >> 2] = 0;
return false;
case ENV.SET_GEOMETRY: {
_onVideoRefresh method · javascript · L396-L457 (62 LOC)public/js/libretro-adapter.js
_onVideoRefresh(dataPtr, width, height, pitch) {
if (!dataPtr) return; // null = duplicate frame (CAN_DUPE); nothing to do
if (width !== this._width || height !== this._height) this._resize(width, height);
const buf = this._frameBuffer;
const M = this.M;
const PF = LibretroAdapter.PIXEL_FORMAT;
if (this._pixelFormat === PF.XRGB8888) {
// Source: Uint32 per pixel, layout 0x00RRGGBB
// Target: 0xFF000000 | (B<<16) | (G<<8) | R
const src = M.HEAPU32;
const srcBase = dataPtr >> 2;
const pitchPx = pitch >> 2;
for (let y = 0; y < height; y++) {
const srcRow = srcBase + y * pitchPx;
const dstRow = y * width;
for (let x = 0; x < width; x++) {
const px = src[srcRow + x]; // 0x00RRGGBB
buf[dstRow + x] = 0xFF000000
| ((px & 0x000000FF) << 16) // B → bits 23-16
| (px & 0x0000FF00) // G → bits 15-8 (unchanged position)
| ((px & 0x00F_resize method · javascript · L460-L467 (8 LOC)public/js/libretro-adapter.js
_resize(width, height) {
this._width = width;
this._height = height;
this.canvas.width = width;
this.canvas.height = height;
this._imageData = this.ctx.createImageData(width, height);
this._frameBuffer = new Uint32Array(this._imageData.data.buffer);
}_onAudioBatch method · javascript · L471-L478 (8 LOC)public/js/libretro-adapter.js
_onAudioBatch(dataPtr, frames) {
if (!this._audioMuted && this._workletNode && frames > 0) {
// this.M.HEAP16 is the signed 16-bit view; dataPtr >> 1 converts byte
// offset to HEAP16 index.
this._flushSamples(this.M.HEAP16, frames, dataPtr >> 1);
}
return frames;
}_flushSamples method · javascript · L486-L492 (7 LOC)public/js/libretro-adapter.js
_flushSamples(src, frames, offset = 0) {
if (!this._workletNode) return;
const f32 = new Float32Array(frames * 2);
for (let i = 0; i < frames * 2; i++) f32[i] = src[offset + i] / 32768.0;
if (this._audioCtx?.state === 'suspended') this._audioCtx.resume();
this._workletNode.port.postMessage({ samples: f32 }, [f32.buffer]);
}About: code-quality intelligence by Repobility · https://repobility.com
_onInputState method · javascript · L496-L500 (5 LOC)public/js/libretro-adapter.js
_onInputState(port, device, _index, id) {
// Only handle RETRO_DEVICE_JOYPAD (device=1) for ports 0 and 1.
if (port >= 2 || device !== 1 || id >= 16) return 0;
return this._inputState[port][id];
}_initAudio method · javascript · L508-L532 (25 LOC)public/js/libretro-adapter.js
async _initAudio() {
try {
this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
await this._audioCtx.audioWorklet.addModule('/js/snes-audio-worklet.js');
this._workletNode = new AudioWorkletNode(
this._audioCtx,
'snes-audio-processor',
{ numberOfOutputs: 1, outputChannelCount: [2] }
);
const gain = this._audioCtx.createGain();
gain.gain.value = 0.5;
this._workletNode.connect(gain);
gain.connect(this._audioCtx.destination);
// Pre-fill with silence to stabilize the DRC buffer before the first frame.
const silence = new Float32Array(2048 * 2);
this._workletNode.port.postMessage({ samples: silence }, [silence.buffer]);
if (this._audioCtx.state === 'suspended') {
await this._audioCtx.resume().catch(() => {});
}
} catch (err) {
console.error('[LibretroAdapter] AudioWorklet setup failed, audio disabled:', err);
this._audioCtx = null;
setAudioMuted method · javascript · L535-L538 (4 LOC)public/js/libretro-adapter.js
setAudioMuted(muted) {
this._audioMuted = muted;
if (!muted && this._audioCtx?.state === 'suspended') this._audioCtx.resume();
}stopAudio method · javascript · L540-L546 (7 LOC)public/js/libretro-adapter.js
stopAudio() {
if (this._workletNode) { this._workletNode.disconnect(); this._workletNode = null; }
if (this._audioCtx && this._audioCtx.state !== 'closed') {
this._audioCtx.close();
this._audioCtx = null;
}
}loadROM method · javascript · L555-L606 (52 LOC)public/js/libretro-adapter.js
async loadROM(url) {
const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
if (!resp.ok) {
throw new Error(`ROM fetch failed: HTTP ${resp.status} — ${resp.statusText}`);
}
const buf = await resp.arrayBuffer();
await this._initAudio();
const M = this.M;
// Copy ROM bytes onto the WASM heap so we can pass a pointer to the core.
const romPtr = M._malloc(buf.byteLength);
if (!romPtr) throw new Error('WASM heap exhausted allocating ROM buffer');
M.HEAPU8.set(new Uint8Array(buf), romPtr);
// Build retro_game_info on the heap.
// struct { const char *path; const void *data; size_t size; const char *meta; }
// 4 × 4 bytes = 16 bytes on 32-bit WASM.
const infoPtr = M._malloc(16);
if (!infoPtr) {
M._free(romPtr);
throw new Error('WASM heap exhausted allocating retro_game_info struct');
}
M.HEAP32[(infoPtr ) >> 2] = 0; // path = NULL (use raw bytes)
M.HEAP32[(infoPtr + step method · javascript · L615-L641 (27 LOC)public/js/libretro-adapter.js
step(inputMap) {
if (!this._romLoaded) return;
const JOYPAD = LibretroAdapter.JOYPAD;
for (let port = 0; port < 2; port++) {
const pid = this.playerIds[port];
const bits = pid ? (inputMap[pid] ?? 0) : 0;
const state = this._inputState[port];
state.fill(0);
if (bits & InputBits.UP) state[JOYPAD.UP] = 1;
if (bits & InputBits.DOWN) state[JOYPAD.DOWN] = 1;
if (bits & InputBits.LEFT) state[JOYPAD.LEFT] = 1;
if (bits & InputBits.RIGHT) state[JOYPAD.RIGHT] = 1;
if (bits & InputBits.A) state[JOYPAD.A] = 1;
if (bits & InputBits.B) state[JOYPAD.B] = 1;
if (bits & InputBits.START) state[JOYPAD.START] = 1;
if (bits & InputBits.SELECT) state[JOYPAD.SELECT] = 1;
if (bits & InputBits.X) state[JOYPAD.X] = 1;
if (bits & InputBits.Y) state[JOYPAD.Y] = 1;
if (bits & InputBits.L) state[JOYPAD.L] = 1;
if (bits & InputBits.R)saveState method · javascript · L647-L660 (14 LOC)public/js/libretro-adapter.js
saveState() {
if (!this._romLoaded) return null;
const M = this.M;
const size = M._retro_serialize_size();
if (!size) return null;
const ptr = M._malloc(size);
if (!ptr) return null;
const ok = M._retro_serialize(ptr, size);
if (!ok) { M._free(ptr); return null; }
// slice() creates an independent copy — safe to free the WASM buffer immediately.
const snap = M.HEAPU8.slice(ptr, ptr + size);
M._free(ptr);
return snap;
}loadState method · javascript · L666-L675 (10 LOC)public/js/libretro-adapter.js
loadState(snap) {
if (!snap || !this._romLoaded) return;
const M = this.M;
const ptr = M._malloc(snap.byteLength);
if (!ptr) return;
M.HEAPU8.set(snap, ptr);
M._retro_unserialize(ptr, snap.byteLength);
M._free(ptr);
this._dirty = true;
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
render method · javascript · L678-L682 (5 LOC)public/js/libretro-adapter.js
render() {
if (!this._dirty || !this._imageData) return;
this.ctx.putImageData(this._imageData, 0, 0);
this._dirty = false;
}constructor method · javascript · L52-L103 (52 LOC)public/js/nes-adapter.js
constructor(canvas, playerIds) {
this.canvas = canvas;
this.playerIds = playerIds;
this.ctx = canvas.getContext('2d');
canvas.width = NESAdapter.NES_W;
canvas.height = NESAdapter.NES_H;
// Off-screen pixel buffer for fast canvas writes
this._imageData = this.ctx.createImageData(NESAdapter.NES_W, NESAdapter.NES_H);
this._pixels = new Uint32Array(this._imageData.data.buffer);
this._frameBuffer = null;
this._dirty = false;
this._romLoaded = false;
this._skipPtTile = false; // true for CHR ROM games (tiles immutable after loadROM)
this._btnMap = null;
// ── Audio ─────────────────────────────────────────────────────────────────
// Ring buffer (power-of-2 size for fast modulo via bitwise AND).
// 4096 samples @ 44100 Hz ≈ 93 ms of buffer — enough headroom for rollbacks
// up to 4 frames deep without audible glitches.
this._AUDIO_COUNT = 4096;
this._AUDIO_MASK = this._AUDIO_COUNT - 1_setupScriptProcessor method · javascript · L105-L138 (34 LOC)public/js/nes-adapter.js
_setupScriptProcessor() {
// ScriptProcessorNode with 512-sample buffers, 0 inputs, 2 output channels.
// Runs on the main thread but is scheduled by the audio thread — keeps latency low.
const sp = this._audioCtx.createScriptProcessor(512, 0, 2);
sp.onaudioprocess = (event) => {
const dstL = event.outputBuffer.getChannelData(0);
const dstR = event.outputBuffer.getChannelData(1);
const len = dstL.length;
const available = (this._audioWrite - this._audioRead) & this._AUDIO_MASK;
const toRead = Math.min(len, available);
// Drain ring buffer
for (let i = 0; i < toRead; i++) {
const idx = (this._audioRead + i) & this._AUDIO_MASK;
dstL[i] = this._audioL[idx];
dstR[i] = this._audioR[idx];
}
// Silence for any underrun (buffer was empty)
for (let i = toRead; i < len; i++) {
dstL[i] = 0;
dstR[i] = 0;
}
this._audioRead = (this._audioRead + toRead) & this._AUsetAudioMuted method · javascript · L145-L147 (3 LOC)public/js/nes-adapter.js
setAudioMuted(muted) {
this._audioMuted = muted;
}stopAudio method · javascript · L149-L157 (9 LOC)public/js/nes-adapter.js
stopAudio() {
if (this._scriptProcessor) {
this._scriptProcessor.disconnect();
this._scriptProcessor = null;
}
if (this._audioCtx && this._audioCtx.state !== 'closed') {
this._audioCtx.close();
}
}loadROM method · javascript · L165-L184 (20 LOC)public/js/nes-adapter.js
async loadROM(url) {
const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
if (!resp.ok) {
throw new Error(`ROM fetch error: HTTP ${resp.status} — ${resp.statusText}`);
}
const buf = await resp.arrayBuffer();
const bytes = new Uint8Array(buf);
// jsnes expects a binary string (char-per-byte)
let romStr = '';
for (let i = 0; i < bytes.length; i++) {
romStr += String.fromCharCode(bytes[i]);
}
this.nes.loadROM(romStr);
this._romLoaded = true;
// CHR ROM games have immutable pattern tiles — no need to save/restore ptTile
this._skipPtTile = this.nes.rom.vromCount > 0;
this._buildBtnMap();
}_buildBtnMap method · javascript · L186-L198 (13 LOC)public/js/nes-adapter.js
_buildBtnMap() {
const C = jsnes.Controller;
this._btnMap = [
[InputBits.UP, C.BUTTON_UP],
[InputBits.DOWN, C.BUTTON_DOWN],
[InputBits.LEFT, C.BUTTON_LEFT],
[InputBits.RIGHT, C.BUTTON_RIGHT],
[InputBits.A, C.BUTTON_A],
[InputBits.B, C.BUTTON_B],
[InputBits.START, C.BUTTON_START],
[InputBits.SELECT, C.BUTTON_SELECT],
];
}step method · javascript · L206-L229 (24 LOC)public/js/nes-adapter.js
step(inputMap) {
if (!this._romLoaded) return;
// Lazily resume AudioContext — browser autoplay policy suspends it until
// the user interacts. step() is called every frame; the first call after
// a key/click event will succeed and audio starts within one frame.
if (this._audioCtx.state === 'suspended') {
this._audioCtx.resume();
}
for (let i = 0; i < 2; i++) {
const pid = this.playerIds[i];
if (!pid) continue;
const input = inputMap[pid] ?? 0;
const ctrl = i + 1; // jsnes controllers are 1-indexed
for (const [bit, btn] of this._btnMap) {
if (input & bit) this.nes.buttonDown(ctrl, btn);
else this.nes.buttonUp(ctrl, btn);
}
}
this.nes.frame();
}Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
saveState method · javascript · L235-L358 (124 LOC)public/js/nes-adapter.js
saveState() {
if (!this._romLoaded) return null;
const cpu = this.nes.cpu;
const ppu = this.nes.ppu;
const mmap = this.nes.mmap;
// ── CPU ──────────────────────────────────────────────────────────────────
// Full 6502 address space: 65536 bytes (dominant serialisation cost)
const cpuMem = new Uint8Array(65536);
for (let i = 0; i < 65536; i++) cpuMem[i] = cpu.mem[i];
// CPU scalar registers — 21 fields packed into one Int32Array
const cpuRegs = new Int32Array([
cpu.cyclesToHalt,
cpu.irqRequested ? 1 : 0,
cpu.irqType,
cpu.REG_ACC, cpu.REG_X, cpu.REG_Y, cpu.REG_SP,
cpu.REG_PC, cpu.REG_PC_NEW, cpu.REG_STATUS,
cpu.F_CARRY, cpu.F_DECIMAL, cpu.F_INTERRUPT, cpu.F_INTERRUPT_NEW,
cpu.F_OVERFLOW, cpu.F_SIGN, cpu.F_ZERO,
cpu.F_NOTUSED, cpu.F_NOTUSED_NEW, cpu.F_BRK, cpu.F_BRK_NEW,
]);
// ── PPU ──────────────────────────────────────────────────────────────────
// VRAM — 32768 bytes (byte loadState method · javascript · L365-L481 (117 LOC)public/js/nes-adapter.js
loadState(snap) {
if (!snap || !this._romLoaded) return;
const cpu = this.nes.cpu;
const ppu = this.nes.ppu;
const mmap = this.nes.mmap;
// ── CPU ──────────────────────────────────────────────────────────────────
const { cpuMem } = snap;
for (let i = 0; i < 65536; i++) cpu.mem[i] = cpuMem[i];
const r = snap.cpuRegs;
cpu.cyclesToHalt = r[0];
cpu.irqRequested = r[1] !== 0;
cpu.irqType = r[2];
cpu.REG_ACC = r[3];
cpu.REG_X = r[4];
cpu.REG_Y = r[5];
cpu.REG_SP = r[6];
cpu.REG_PC = r[7];
cpu.REG_PC_NEW = r[8];
cpu.REG_STATUS = r[9];
cpu.F_CARRY = r[10];
cpu.F_DECIMAL = r[11];
cpu.F_INTERRUPT = r[12];
cpu.F_INTERRUPT_NEW = r[13];
cpu.F_OVERFLOW = r[14];
cpu.F_SIGN = r[15];
cpu.F_ZERO = r[16];
cpu.F_NOTUSED = r[17];
cpu.F_NOTUSED_NEW = r[18];
cpu.F_BRK render method · javascript · L492-L505 (14 LOC)public/js/nes-adapter.js
render() {
if (!this._dirty || !this._frameBuffer) return;
const fb = this._frameBuffer;
const pixels = this._pixels;
const len = NESAdapter.NES_W * NESAdapter.NES_H;
for (let i = 0; i < len; i++) {
pixels[i] = 0xFF000000 | fb[i];
}
this.ctx.putImageData(this._imageData, 0, 0);
this._dirty = false;
}NetworkClient class · javascript · L13-L79 (67 LOC)public/js/network.js
class NetworkClient extends EventTarget {
constructor() {
super();
/** @type {WebSocket|null} */
this.ws = null;
this.connected = false;
this._queue = []; // messages queued before the socket opens
}
/** Open the WebSocket connection. Resolves once the socket is ready. */
connect() {
return new Promise((resolve, reject) => {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
this.ws = new WebSocket(`${proto}//${location.host}`);
this.ws.onopen = () => {
this.connected = true;
// Flush any messages that were sent before connection opened
while (this._queue.length) this.ws.send(this._queue.shift());
resolve();
};
this.ws.onerror = (ev) => reject(new Error('WebSocket connection failed'));
this.ws.onmessage = (ev) => {
let msg;
try { msg = JSON.parse(ev.data); } catch { return; }
// Dispatch a typed CustomEvent for specific handlers,
// plusconstructor method · javascript · L14-L20 (7 LOC)public/js/network.js
constructor() {
super();
/** @type {WebSocket|null} */
this.ws = null;
this.connected = false;
this._queue = []; // messages queued before the socket opens
}connect method · javascript · L23-L53 (31 LOC)public/js/network.js
connect() {
return new Promise((resolve, reject) => {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
this.ws = new WebSocket(`${proto}//${location.host}`);
this.ws.onopen = () => {
this.connected = true;
// Flush any messages that were sent before connection opened
while (this._queue.length) this.ws.send(this._queue.shift());
resolve();
};
this.ws.onerror = (ev) => reject(new Error('WebSocket connection failed'));
this.ws.onmessage = (ev) => {
let msg;
try { msg = JSON.parse(ev.data); } catch { return; }
// Dispatch a typed CustomEvent for specific handlers,
// plus a generic 'message' event for catch-all handlers.
const typed = new CustomEvent(msg.type, { detail: msg });
const generic = new CustomEvent('message', { detail: msg });
this.dispatchEvent(typed);
this.dispatchEvent(generic);
};
this.ws.onclose = () =on method · javascript · L61-L64 (4 LOC)public/js/network.js
on(type, handler) {
this.addEventListener(type, (ev) => handler(ev.detail));
return this;
}send method · javascript · L67-L74 (8 LOC)public/js/network.js
send(msg) {
const raw = JSON.stringify(msg);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(raw);
} else {
this._queue.push(raw);
}
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
disconnect method · javascript · L76-L78 (3 LOC)public/js/network.js
disconnect() {
this.ws?.close();
}RollbackEngine class · javascript · L27-L265 (239 LOC)public/js/rollback.js
class RollbackEngine {
/**
* @param {object} opts
* @param {object} opts.emulator - emulator implementing the contract above
* @param {string} opts.localPlayerId - this client's player ID
* @param {string[]} opts.playerIds - ordered array of all player IDs
* @param {function} opts.readInput - () => number returns local input bitmask
* @param {function} [opts.onStats] - (stats) => void called each frame
*/
constructor({ emulator, localPlayerId, playerIds, readInput, onStats }) {
this.emulator = emulator;
this.localPlayerId = localPlayerId;
this.playerIds = playerIds;
this.readInput = readInput ?? (() => 0);
this.onStats = onStats ?? (() => {});
// ── Tuning ────────────────────────────────────────────────────────────
/** Frames of artificial local-input delay.
* Gives the peer's real input time to arrive before we need to
* predict, reducing rollback frequency. 2 frames ≈ 33 ms headroom. */
constructor method · javascript · L36-L86 (51 LOC)public/js/rollback.js
constructor({ emulator, localPlayerId, playerIds, readInput, onStats }) {
this.emulator = emulator;
this.localPlayerId = localPlayerId;
this.playerIds = playerIds;
this.readInput = readInput ?? (() => 0);
this.onStats = onStats ?? (() => {});
// ── Tuning ────────────────────────────────────────────────────────────
/** Frames of artificial local-input delay.
* Gives the peer's real input time to arrive before we need to
* predict, reducing rollback frequency. 2 frames ≈ 33 ms headroom. */
this.INPUT_DELAY = 2;
/** Maximum frames we are willing to roll back. */
this.MAX_ROLLBACK = 8;
this.TARGET_FPS = 60;
this.FRAME_MS = 1000 / this.TARGET_FPS;
// ── Frame counters ────────────────────────────────────────────────────
/** Current simulation frame (next frame to be simulated). */
this.frame = 0;
/** Last frame where every player's input is confirmed (no prediction). */
this.confirmedFrame = -1;
// ──start method · javascript · L90-L94 (5 LOC)public/js/rollback.js
start() {
this._running = true;
this._lastTime = performance.now();
this._rafId = requestAnimationFrame(this._loop.bind(this));
}stop method · javascript · L96-L100 (5 LOC)public/js/rollback.js
stop() {
this._running = false;
if (this._rafId) cancelAnimationFrame(this._rafId);
this._rafId = null;
}receiveRemoteInput method · javascript · L107-L130 (24 LOC)public/js/rollback.js
receiveRemoteInput(frame, playerId, input) {
if (playerId === this.localPlayerId) return;
// Check for misprediction (only if we already simulated this frame)
if (frame < this.frame) {
const used = this.usedInputs.get(frame);
if (used !== undefined && used[playerId] !== input) {
// Misprediction detected — schedule rollback to the earliest bad frame
if (frame > this.confirmedFrame) {
if (this._rollbackTo === null || frame < this._rollbackTo) {
this._rollbackTo = frame;
}
}
}
}
this._storeInput(frame, playerId, input);
// Update last-received watermark
const last = this.lastReceivedFrame.get(playerId) ?? -1;
if (frame > last) this.lastReceivedFrame.set(playerId, frame);
this._updateConfirmedFrame();
}_loop method · javascript · L134-L148 (15 LOC)public/js/rollback.js
_loop(timestamp) {
if (!this._running) return;
const delta = Math.min(timestamp - this._lastTime, 100); // cap spiral-of-death
this._lastTime = timestamp;
this._accumulator += delta;
while (this._accumulator >= this.FRAME_MS) {
this._tick();
this._accumulator -= this.FRAME_MS;
}
this.emulator.render();
this._rafId = requestAnimationFrame(this._loop.bind(this));
}_tick method · javascript · L150-L183 (34 LOC)public/js/rollback.js
_tick() {
// ① Queue local input with delay and broadcast
const localInput = this.readInput();
const queueFrame = this.frame + this.INPUT_DELAY;
this._storeInput(queueFrame, this.localPlayerId, localInput);
this._sendInput(queueFrame, localInput);
// ② Execute pending rollback
if (this._rollbackTo !== null) {
const rollTo = this._rollbackTo;
this._rollbackTo = null;
if (rollTo < this.frame && this.stateHistory.has(rollTo)) {
this._performRollback(rollTo);
}
}
// ③ Snapshot state BEFORE simulating this frame
this.stateHistory.set(this.frame, this.emulator.saveState());
// ④ Gather inputs (confirmed or predicted) and simulate
const inputs = this._gatherInputs(this.frame);
this.usedInputs.set(this.frame, inputs);
this.emulator.step(inputs);
// ⑤ Maintain bookkeeping
this._updateConfirmedFrame();
this._pruneHistory();
this._stats.frame = this.frame;
this._stats.confirmedFrame About: code-quality intelligence by Repobility · https://repobility.com
_storeInput method · javascript · L187-L190 (4 LOC)public/js/rollback.js
_storeInput(frame, playerId, input) {
if (!this.confirmedInputs.has(frame)) this.confirmedInputs.set(frame, new Map());
this.confirmedInputs.get(frame).set(playerId, input);
}_gatherInputs method · javascript · L192-L201 (10 LOC)public/js/rollback.js
_gatherInputs(frame) {
const result = {};
for (const pid of this.playerIds) {
const frameMap = this.confirmedInputs.get(frame);
result[pid] = (frameMap && frameMap.has(pid))
? frameMap.get(pid)
: this._predict(pid, frame);
}
return result;
}_predict method · javascript · L204-L210 (7 LOC)public/js/rollback.js
_predict(playerId, forFrame) {
for (let f = forFrame - 1; f >= Math.max(0, forFrame - this.MAX_ROLLBACK * 2); f--) {
const m = this.confirmedInputs.get(f);
if (m && m.has(playerId)) return m.get(playerId);
}
return 0;
}_performRollback method · javascript · L214-L232 (19 LOC)public/js/rollback.js
_performRollback(toFrame) {
const depth = this.frame - toFrame;
this.emulator.loadState(this.stateHistory.get(toFrame));
// Suppress audio during re-simulation — only the final (authoritative) frame
// should produce sound. The optional-chaining (?.) keeps this compatible with
// emulators that don't implement setAudioMuted (e.g. DemoGame/Pong).
this.emulator.setAudioMuted?.(true);
for (let f = toFrame; f < this.frame; f++) {
this.stateHistory.set(f, this.emulator.saveState());
const inputs = this._gatherInputs(f);
this.usedInputs.set(f, inputs);
this.emulator.step(inputs);
}
this.emulator.setAudioMuted?.(false);
this._stats.rollbacks++;
this._stats.maxRollbackDepth = Math.max(this._stats.maxRollbackDepth, depth);
}_updateConfirmedFrame method · javascript · L236-L245 (10 LOC)public/js/rollback.js
_updateConfirmedFrame() {
// The "safe" watermark is the min of:
// • local: we know inputs up through frame + INPUT_DELAY
// • remote: last received frame for each peer
let minFrame = this.frame + this.INPUT_DELAY;
for (const [, lastFrame] of this.lastReceivedFrame) {
minFrame = Math.min(minFrame, lastFrame);
}
if (minFrame > this.confirmedFrame) this.confirmedFrame = minFrame;
}_pruneHistory method · javascript · L247-L256 (10 LOC)public/js/rollback.js
_pruneHistory() {
const keepFrom = Math.max(0, this.confirmedFrame - 1);
for (const [f] of this.stateHistory) {
if (f < keepFrom) {
this.stateHistory.delete(f);
this.usedInputs.delete(f);
this.confirmedInputs.delete(f);
}
}
}_sendInput method · javascript · L261-L264 (4 LOC)public/js/rollback.js
_sendInput(frame, input) {
// Overridden by the application layer to actually transmit the input.
// Default no-op for single-player / offline use.
}SNESAdapter class · javascript · L35-L261 (227 LOC)public/js/snes-adapter.js
class SNESAdapter {
// Display canvas dimensions: native SNES resolution (256×224).
// The WASM screen buffer is allocated at 512×448 (SCREEN_BUF_W × SCREEN_BUF_H)
// for hi-res / compatibility reasons, but actual game pixels occupy only the
// top-left 256×224 region. ctx.putImageData clips to canvas bounds, so
// setting the canvas to 256×224 renders the correct game area automatically.
static SNES_W = 256;
static SNES_H = 224;
// WASM screen-buffer dimensions — must match SCREEN_BYTES in snes9x.js.
static SCREEN_BUF_W = 512;
static SCREEN_BUF_H = 448;
/**
* @param {HTMLCanvasElement} canvas
* @param {string[]} playerIds ordered player IDs (index 0 → controller 1, 1 → controller 2)
*/
constructor(canvas, playerIds) {
this.canvas = canvas;
this.playerIds = playerIds;
this.ctx = canvas.getContext('2d');
canvas.width = SNESAdapter.SNES_W;
canvas.height = SNESAdapter.SNES_H;
// ImageData must match the WASM screen buIf a scraper extracted this row, it came from Repobility (https://repobility.com)
constructor method · javascript · L51-L68 (18 LOC)public/js/snes-adapter.js
constructor(canvas, playerIds) {
this.canvas = canvas;
this.playerIds = playerIds;
this.ctx = canvas.getContext('2d');
canvas.width = SNESAdapter.SNES_W;
canvas.height = SNESAdapter.SNES_H;
// ImageData must match the WASM screen buffer (512×448), not the display
// canvas (256×224). putImageData clips to the canvas, so only the
// top-left 256×224 pixels — where the emulator places the game — are shown.
this._imageData = this.ctx.createImageData(SNESAdapter.SCREEN_BUF_W, SNESAdapter.SCREEN_BUF_H);
this._romLoaded = false;
this._audioMuted = false;
this._audioCtx = null;
this._workletNode = null;
}loadROM method · javascript · L76-L99 (24 LOC)public/js/snes-adapter.js
async loadROM(url) {
if (typeof window.snineX === 'undefined') {
throw new Error('snes9x not loaded — make sure snes9x.js is included before snes-adapter.js');
}
const resp = await fetch(`/rom-proxy?url=${encodeURIComponent(url)}`);
if (!resp.ok) {
throw new Error(`ROM fetch error: HTTP ${resp.status} — ${resp.statusText}`);
}
const buf = await resp.arrayBuffer();
// Wait for WASM to be ready (snes9x.js initialises asynchronously)
await this._waitForWasm();
// Set up the AudioWorklet before starting the emulator so the sample
// rate is known and audio is ready to receive the first frame's samples.
await this._initAudio();
const sampleRate = this._audioCtx?.sampleRate ?? 44100;
window.snineX.start(buf, sampleRate);
this._romLoaded = true;
}_initAudio method · javascript · L107-L142 (36 LOC)public/js/snes-adapter.js
async _initAudio() {
try {
this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
await this._audioCtx.audioWorklet.addModule('/js/snes-audio-worklet.js');
// Explicitly request stereo output — without this, some browsers default
// to mono (out[1] === undefined), which causes process() to crash silently.
this._workletNode = new AudioWorkletNode(
this._audioCtx,
'snes-audio-processor',
{ numberOfOutputs: 1, outputChannelCount: [2] }
);
const gain = this._audioCtx.createGain();
gain.gain.value = 0.5;
this._workletNode.connect(gain);
gain.connect(this._audioCtx.destination);
// Pre-fill the ring buffer to the DRC target level (2048 stereo sample-pairs
// ≈ 46 ms at 44 100 Hz) before the game loop starts. Starting at target
// prevents the proportional controller from applying a large initial
// correction and ensures the first real audio frame arrives int