You are working on the Svelte 5 frontend of SvelteBoy, a GameBoy DMG emulator.
Key Facts
- Framework: Svelte 5 with runes (
$state, $derived, $effect)
- Dev:
pnpm run dev | Build: pnpm run build
- WASM bridge:
src/emulator/ module — re-exports build/backend via wasmBridge.ts, facade in index.ts
- State management: Svelte stores in
src/stores/
File Map
| File/Dir |
Purpose |
src/emulator/index.ts |
Emulator+Debug facades, re-exports AudioSuspended/FrameStats/RenderFrames |
src/emulator/wasmBridge.ts |
re-export build/backend, frame/audio typed-array views |
src/emulator/loop.ts |
requestAnimationFrame loop, pre/postRun, FrameStats, callbacks |
src/emulator/audio.ts |
AudioContext, queue, fade, mute subs, suspendAudio/resumeAudio |
src/emulator/lifecycle.ts |
pause/unpause/reset/runUntilBreak, visibility, HMR |
src/emulator/saveState.ts |
QuickSave/QuickLoad + thumbnails |
src/emulator/rom.ts |
playRom, getRomBuffer, loadSaveGame |
src/inputs.ts |
Keyboard → joypad bitmask → setJoypad() WASM call |
src/types.ts |
TypeScript types mirroring WASM-exported shapes |
src/stores/playStores.ts |
GameFrames, KeyPressMap, emulator running state |
src/stores/debugStores.ts |
Breakpoints, logs, debug info reactive state |
src/stores/optionsStore.ts |
Volume, pixel size, FPS target |
src/stores/romStores.ts |
Loaded cartridge reference |
src/lib/Player.svelte |
Top-level game player container |
src/lib/PlayCanvas.svelte |
160×144 <canvas> — receives Uint8ClampedArray frame, calls putImageData |
src/lib/PlayerControls.svelte |
Play/Pause/Reset buttons |
src/lib/debug/Debugger.svelte |
Full debugger UI container |
src/lib/debug/CpuDebugInfo.svelte |
Registers, flags display |
src/lib/debug/Disassembler.svelte |
Disassembly view using getCartLines() |
src/lib/debug/HexView.svelte |
Memory hex dump using hexDump() |
src/lib/debug/LogView.svelte |
Serial/debug log viewer |
Calling WASM from the Frontend
Inside src/emulator/* use relative ./wasmBridge. Outside, import from ./emulator (facade).
// inside src/emulator/*
import {
initEmulator, runEmulator,
setJoypad, getDebugInfo, loadCartridgeRom,
getGameFrameView, getCgbGameFrameView, getAudioBufferView,
backendMemory
} from "./wasmBridge";
// Pass data TO wasm (bindings handle pointer/length automatically)
loadCartridgeRom(new Uint8Array(romArrayBuffer));
// Zero-copy frame view (helper wraps backendMemory + ptr)
const frame = getGameFrameView(); // Uint8Array(160*144) palette indices
// or RGBA (CGB): getCgbGameFrameView() — Uint16Array RGB555
// Get managed objects (copy via bindings)
const info = getDebugInfo(); // → plain JS object matching DebugInfo shape
Audio Pipeline
// Called in postRun after each emulation step
const count = getAudioBuffersToReadCount();
for (let i = 0; i < count; i++) {
const leftPtr = getAudioBufferToReadPointer(0);
const rightPtr = getAudioBufferToReadPointer(1);
const size = getAudioBuffersSize();
// Zero-copy typed array views
const left = new Float32Array(backendMemory.buffer, leftPtr, size);
const right = new Float32Array(backendMemory.buffer, rightPtr, size);
// Feed to Web Audio API
const buf = audioCtx.createBuffer(2, size, getAudioSampleRate());
buf.copyToChannel(left, 0);
buf.copyToChannel(right, 1);
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(audioCtx.destination);
src.start(nextAudioTime);
nextAudioTime += buf.duration;
}
markAudioBuffersRead(count);
Emulation Loop Pattern
Loop logic in src/emulator/loop.ts. Audio postRun registered via addPostRunCallback from audio.ts. Render callbacks fire when ≥1 GB frame advanced this rAF tick.
// loop.ts
function run(time: number) {
accumulator += wallDt * get(EmulatorSpeed);
while (accumulator >= GB_FRAME_MS && framesThisTick < MAX_CATCHUP) {
preRun(); // setJoypad(inputBits)
LastStopReason.set(runEmulator(GB_FRAME_MS));
postRun(); // fetchLogs, GameFrames++, debug snapshot, AutoSave, postRunCallbacks
accumulator -= GB_FRAME_MS; framesThisTick++;
}
if (framesThisTick > 0) renderCallbacks.forEach(cb => cb());
if (!get(EmulatorPaused)) requestAnimationFrame(run);
}
Emulator.AddRenderCallback / AddPostRunCallback for components (Player, AudioDebug) to hook in.
Svelte 5 Runes Patterns Used Here
<script lang="ts">
// Reactive state (replaces writable store in component)
let frame = $state<Uint8ClampedArray | null>(null);
// Derived (replaces $: reactive declaration)
let isRunning = $derived(frame !== null);
// Side effects (replaces onMount + $: with cleanup)
$effect(() => {
const unsub = someStore.subscribe(v => { ... });
return () => unsub();
});
</script>
Stores: When to Use Which
| Store |
Use for |
playStores.ts |
Emulator running state, current frame, key press map |
debugStores.ts |
Debug info snapshot, logs, breakpoints list |
optionsStore.ts |
User settings (volume, scale) — persisted to localStorage |
romStores.ts |
Currently loaded cartridge metadata |
InputType Bitmask (src/inputs.ts)
// Combined with bitwise OR, passed to setJoypad(keys)
Right=1, Left=2, Up=4, Down=8, A=16, B=32, Select=64, Start=128
$ARGUMENTS
1---2name: frontend3description: Prime context for working on the Svelte 5 frontend in /src/ — components, stores, WASM bridge, audio, canvas rendering4---56You are working on the **Svelte 5 frontend** of SvelteBoy, a GameBoy DMG emulator.78## Key Facts910- Framework: **Svelte 5** with runes (`$state`, `$derived`, `$effect`)11- Dev: `pnpm run dev` | Build: `pnpm run build`12- WASM bridge: `src/emulator/` module — re-exports `build/backend` via `wasmBridge.ts`, facade in `index.ts`13- State management: Svelte stores in `src/stores/`1415## File Map1617| File/Dir | Purpose |18|----------|---------|19| `src/emulator/index.ts` | `Emulator`+`Debug` facades, re-exports `AudioSuspended`/`FrameStats`/`RenderFrames` |20| `src/emulator/wasmBridge.ts` | re-export `build/backend`, frame/audio typed-array views |21| `src/emulator/loop.ts` | `requestAnimationFrame` loop, pre/postRun, FrameStats, callbacks |22| `src/emulator/audio.ts` | AudioContext, queue, fade, mute subs, `suspendAudio`/`resumeAudio` |23| `src/emulator/lifecycle.ts` | pause/unpause/reset/runUntilBreak, visibility, HMR |24| `src/emulator/saveState.ts` | QuickSave/QuickLoad + thumbnails |25| `src/emulator/rom.ts` | playRom, getRomBuffer, loadSaveGame |26| `src/inputs.ts` | Keyboard → joypad bitmask → `setJoypad()` WASM call |27| `src/types.ts` | TypeScript types mirroring WASM-exported shapes |28| `src/stores/playStores.ts` | `GameFrames`, `KeyPressMap`, emulator running state |29| `src/stores/debugStores.ts` | Breakpoints, logs, debug info reactive state |30| `src/stores/optionsStore.ts` | Volume, pixel size, FPS target |31| `src/stores/romStores.ts` | Loaded cartridge reference |32| `src/lib/Player.svelte` | Top-level game player container |33| `src/lib/PlayCanvas.svelte` | 160×144 `<canvas>` — receives `Uint8ClampedArray` frame, calls `putImageData` |34| `src/lib/PlayerControls.svelte` | Play/Pause/Reset buttons |35| `src/lib/debug/Debugger.svelte` | Full debugger UI container |36| `src/lib/debug/CpuDebugInfo.svelte` | Registers, flags display |37| `src/lib/debug/Disassembler.svelte` | Disassembly view using `getCartLines()` |38| `src/lib/debug/HexView.svelte` | Memory hex dump using `hexDump()` |39| `src/lib/debug/LogView.svelte` | Serial/debug log viewer |4041## Calling WASM from the Frontend4243Inside `src/emulator/*` use relative `./wasmBridge`. Outside, import from `./emulator` (facade).4445```typescript46// inside src/emulator/*47import {48 initEmulator, runEmulator,49 setJoypad, getDebugInfo, loadCartridgeRom,50 getGameFrameView, getCgbGameFrameView, getAudioBufferView,51 backendMemory52} from "./wasmBridge";5354// Pass data TO wasm (bindings handle pointer/length automatically)55loadCartridgeRom(new Uint8Array(romArrayBuffer));5657// Zero-copy frame view (helper wraps backendMemory + ptr)58const frame = getGameFrameView(); // Uint8Array(160*144) palette indices59// or RGBA (CGB): getCgbGameFrameView() — Uint16Array RGB5556061// Get managed objects (copy via bindings)62const info = getDebugInfo(); // → plain JS object matching DebugInfo shape63```6465## Audio Pipeline6667```typescript68// Called in postRun after each emulation step69const count = getAudioBuffersToReadCount();70for (let i = 0; i < count; i++) {71 const leftPtr = getAudioBufferToReadPointer(0);72 const rightPtr = getAudioBufferToReadPointer(1);73 const size = getAudioBuffersSize();74 // Zero-copy typed array views75 const left = new Float32Array(backendMemory.buffer, leftPtr, size);76 const right = new Float32Array(backendMemory.buffer, rightPtr, size);77 // Feed to Web Audio API78 const buf = audioCtx.createBuffer(2, size, getAudioSampleRate());79 buf.copyToChannel(left, 0);80 buf.copyToChannel(right, 1);81 const src = audioCtx.createBufferSource();82 src.buffer = buf;83 src.connect(audioCtx.destination);84 src.start(nextAudioTime);85 nextAudioTime += buf.duration;86}87markAudioBuffersRead(count);88```8990## Emulation Loop Pattern9192Loop logic in `src/emulator/loop.ts`. Audio postRun registered via `addPostRunCallback` from `audio.ts`. Render callbacks fire when ≥1 GB frame advanced this rAF tick.9394```typescript95// loop.ts96function run(time: number) {97 accumulator += wallDt * get(EmulatorSpeed);98 while (accumulator >= GB_FRAME_MS && framesThisTick < MAX_CATCHUP) {99 preRun(); // setJoypad(inputBits)100 LastStopReason.set(runEmulator(GB_FRAME_MS));101 postRun(); // fetchLogs, GameFrames++, debug snapshot, AutoSave, postRunCallbacks102 accumulator -= GB_FRAME_MS; framesThisTick++;103 }104 if (framesThisTick > 0) renderCallbacks.forEach(cb => cb());105 if (!get(EmulatorPaused)) requestAnimationFrame(run);106}107```108109`Emulator.AddRenderCallback` / `AddPostRunCallback` for components (Player, AudioDebug) to hook in.110111## Svelte 5 Runes Patterns Used Here112113```svelte114<script lang="ts">115 // Reactive state (replaces writable store in component)116 let frame = $state<Uint8ClampedArray | null>(null);117118 // Derived (replaces $: reactive declaration)119 let isRunning = $derived(frame !== null);120121 // Side effects (replaces onMount + $: with cleanup)122 $effect(() => {123 const unsub = someStore.subscribe(v => { ... });124 return () => unsub();125 });126</script>127```128129## Stores: When to Use Which130131| Store | Use for |132|-------|---------|133| `playStores.ts` | Emulator running state, current frame, key press map |134| `debugStores.ts` | Debug info snapshot, logs, breakpoints list |135| `optionsStore.ts` | User settings (volume, scale) — persisted to localStorage |136| `romStores.ts` | Currently loaded cartridge metadata |137138## InputType Bitmask (src/inputs.ts)139140```typescript141// Combined with bitwise OR, passed to setJoypad(keys)142Right=1, Left=2, Up=4, Down=8, A=16, B=32, Select=64, Start=128143```144145$ARGUMENTS