Keeping browser-emulator storage reliable across browsers
Browser emulation is more than compiling native code to WebAssembly. A useful runtime must also store game data safely, work with different browser capabilities, and avoid blocking the page while the emulator is starting. This note describes the engineering boundaries Emu666 uses around the Origin Private File System (OPFS), Emscripten WasmFS, and Web Workers.
It is a technical explanation, not a performance benchmark or a compatibility certification.
The important boundary: which thread owns storage?
OPFS can be accessed from JavaScript and through a WasmFS backend. Those paths are not interchangeable. In particular, synchronously proxying WasmFS operations for OPFS paths from the browser's main thread can deadlock on some Safari versions.
The design therefore keeps two responsibilities separate:
- Before the emulator starts, JavaScript uses the browser's native OPFS APIs for data that must already exist.
- After startup, the emulator mounts its OPFS-backed file system from a pthread worker, rather than mounting it from the main thread.
This separation prevents a storage call made during initialization from being overwritten by a later mount, and avoids tying the page's responsiveness to a synchronous file-system proxy.
The startup flow
Browser page
│
├─ JavaScript writes required data with native OPFS APIs
│
├─ Emscripten creates the WebAssembly runtime
│
└─ A pthread worker mounts the WasmFS OPFS backend
│
└─ The emulator reads its mounted storage and starts
The detail matters: a path that belongs to the OPFS-backed runtime is not treated as a generic module.FS path on the main thread. Small temporary data can stay in the in-memory file system; persistent data follows the native OPFS path until the worker-owned runtime is ready.
Capability probes instead of browser-name rules
Browser names do not tell the whole story. The compatibility report checks the capabilities the runtime actually needs, including WebAssembly, shared memory, threads, BigInt integration, OPFS, workers, WebGL, audio features, and the Gamepad API.
The user-agent is used only to display a readable browser and operating-system label. A capability probe decides whether a feature is available. You can run the same checks in your browser at the compatibility report.
Writing safely across browser implementations
Some browsers provide createWritable() for OPFS files, while others require a worker-based SyncAccessHandle path. The storage layer keeps that browser difference behind one interface. It also waits for each queued write before closing a file, which avoids losing the final chunks on stricter implementations.
For large data, the runtime uses streaming file operations rather than loading a complete file into the WebAssembly heap. This is both more predictable for memory use and safer for long-running sessions.
What this note does not claim
- It does not claim that every browser or device is supported.
- It does not publish a universal performance comparison.
- It does not provide game files, firmware, download sources, or guidance for bypassing copyright.
- It does not replace browser-vendor documentation or a reproducible test for a particular application.
The goal is narrower: document a practical worker-and-storage boundary that makes a browser-based WebAssembly runtime easier to reason about, test, and improve.
Reproduce the relevant checks
Use the browser compatibility report to see the current browser's feature results. For implementation details, the public discussion should always be paired with a minimal, non-game-specific reproduction and an explicit browser/version matrix.