How RomM Integrates EmulatorJS for In-Browser Playback: A Technical Deep Dive
RomM embeds EmulatorJS by setting global window.EJS_* properties that the emulator reads on startup, then orchestrates the runtime through a Vue component that handles save states, custom UI controls, and iOS compatibility shims.
RomM is an open-source ROM management platform that enables browser-based emulation through deep integration with EmulatorJS. This article examines the rommapp/romm codebase to reveal how the frontend Vue application bootstraps the emulator, manages virtual file systems, and synchronizes save data between the client and server.
Global Configuration via Window Globals
Before the EmulatorJS script executes, RomM configures the environment by declaring a series of global properties on the window object. The Player.vue component defines these values during the onMounted lifecycle hook, populating the emulator’s core settings, ROM URLs, BIOS paths, and UI preferences.
In frontend/src/views/Player/EmulatorJS/Player.vue (lines 28-89), the component assigns:
window.EJS_core– The selected emulation core (e.g.,"snes9x","mgba")window.EJS_gameUrl– The API endpoint serving the ROM contentwindow.EJS_biosUrl– The firmware/BIOS file URL for consoles requiring itwindow.EJS_threads– Boolean flag indicating threading supportwindow.EJS_language– UI localization string (e.g.,"en-US")
These globals act as the contract between RomM and EmulatorJS, allowing the emulator to locate resources without direct API coupling.
Vue Component Runtime Orchestration
The Player.vue component serves as the runtime orchestrator, managing the emulator lifecycle from bootstrap to teardown. It handles script injection, event listener registration, and callback wiring for save states and net-play features.
Bootstrap and Lifecycle Hooks
When the component mounts, it stores user preferences (BIOS and core selections) in localStorage and registers event listeners for save management. The critical integration point is the window.EJS_onGameStart callback (lines 79-96), which fires when the emulator finishes initializing.
Inside this callback, RomM executes an asynchronous bootstrap sequence:
- Waits for the
gameManagerobject to become available viawaitForGameManager - Injects any server-provided save files or save states using
loadSaveandloadState - Initializes the custom UI button bar
Save State Synchronization
RomM overrides the default EmulatorJS save behavior by implementing window.EJS_onSaveSave and window.EJS_onLoadSave. These callbacks intercept the emulator’s binary save data and forward it to the backend API.
The flow works as follows:
- Saving: When the user triggers a save,
EJS_onSaveSavereceives the binary data and calls RomM’ssaveSaveutility, which POSTs the data to the server - Loading: When selecting a save slot,
EJS_onLoadSaveretrieves the data from the server and injects it into the emulator’s virtual file system
Custom UI Controls
RomM injects custom buttons into the EmulatorJS menu bar to provide platform-specific features:
createQuickLoadButton– Adds a quick-load button that bypasses the standard menucreateExitEmulationButton– Overrides the default exit behavior to properly clean up resourcescreateSaveQuitButton– Combines save-and-quit functionality for streamlined session management
These controls are defined in utils.ts and appended to the emulator’s UI during the onGameStart phase.
Utility Helpers for File System and Compatibility
All low-level interactions with the EmulatorJS virtual file system reside in frontend/src/views/Player/EmulatorJS/utils.ts. This module abstracts the complexity of IndexedDB cache management, file system writes, and mobile browser compatibility.
Virtual File System Operations
The loadEmulatorJSSave function (lines 38-51) handles injecting external save files into the running emulator:
import { loadEmulatorJSSave } from '@/views/Player/EmulatorJS/utils';
// Fetch save data from RomM backend
const saveData = await fetch(saveUrl).then(r => r.arrayBuffer());
// Write into EJS virtual FS and trigger load
loadEmulatorJSSave(new Uint8Array(saveData));
This utility writes the data to the emulator’s virtual path using FS.writeFile, then invokes gameManager.loadSaveFiles() to make the emulator aware of the new state.
Similarly, loadEmulatorJSState manages save states (snapshots of runtime memory) using gameManager.loadState().
Cache Management and iOS Shims
RomM implements two critical compatibility layers:
IndexedDB Cache Invalidation: The invalidateEmulatorJSRomCacheIfRenamed function (lines 118-169) clears the browser’s IndexedDB cache whenever a ROM’s filename changes, preventing the emulator from loading stale data.
iOS Fullscreen Support: The installIOSFullscreenShim function (lines 171-238) patches the browser’s fullscreen API on iOS devices, which lack native fullscreen support. This shim returns a cleanup function that restores original behavior when the player exits:
import { installIOSFullscreenShim } from '@/views/Player/EmulatorJS/utils';
const removeShim = installIOSFullscreenShim();
// ... emulation runs ...
removeShim(); // Restore original fullscreen behavior on exit
Step-by-Step Integration Flow
The complete RomM EmulatorJS integration follows this execution path:
- Router Resolution –
frontend/src/v2/router/routes.tsmaps/play/:romIdto theEmulatorJS.vuewrapper component - Global Setup –
Player.vuesetswindow.EJS_*globals with core selection and download URLs - Script Injection – The browser loads
EmulatorJS.js(referenced inindex.html), which reads the globals - Game Boot – Emulator downloads the ROM and fires
EJS_onGameStart - State Injection – RomM waits for
gameManageravailability, then injects any existing saves vialoadEmulatorJSSave - User Interaction – Custom buttons handle quick-load, exit, and save-and-quit operations
- Data Persistence – Save events trigger
saveStateandsaveSaveto sync with the RomM backend - Cache Maintenance – Stale IndexedDB entries are cleared on ROM rename; iOS shims are removed on component unmount
Summary
- RomM configures EmulatorJS through global
window.EJS_*properties set inPlayer.vuebefore the emulator script loads - The Vue component orchestrates the runtime via lifecycle hooks and callbacks like
EJS_onGameStartandEJS_onSaveSave - Utility functions in
utils.tshandle virtual file system operations, save state injection, and IndexedDB cache management - iOS compatibility is achieved through a fullscreen shim that patches browser APIs during the emulation session
- Save data flows bidirectionally between the emulator’s virtual file system and RomM’s backend API through typedArray buffers
Frequently Asked Questions
How does RomM handle save states with EmulatorJS?
RomM intercepts EmulatorJS save events through the window.EJS_onSaveSave and window.EJS_onSaveState callbacks. When triggered, these functions receive binary data from the emulator and forward it to the backend via saveSave and saveState utilities. For loading, loadEmulatorJSSave writes the server-provided data into the emulator’s virtual file system using FS.writeFile, then invokes gameManager.loadSaveFiles() to activate the save.
What is the purpose of the iOS fullscreen shim in RomM?
The installIOSFullscreenShim function in utils.ts patches the browser’s fullscreen API on iOS devices, which do not support the standard Fullscreen API used by EmulatorJS. This shim enables true fullscreen gameplay on iPhones and iPads by overriding native methods. The function returns a cleanup handler that restores original behavior when the user exits the emulation view, preventing side effects on other parts of the application.
How does RomM select the appropriate emulator core for a ROM?
Core selection logic resides in frontend/src/utils/index.ts, which exports getSupportedEJSCores and areThreadsRequiredForEJSCore. These utilities map the ROM’s platform (e.g., SNES, GBA) to compatible EmulatorJS cores and determine whether threading should be enabled. The selected core string (such as "snes9x" or "mgba") is assigned to window.EJS_core before the emulator initializes.
Why does RomM clear the IndexedDB cache when ROMs are renamed?
RomM implements invalidateEmulatorJSRomCacheIfRenamed to prevent the emulator from loading stale ROM data. EmulatorJS caches ROM files in the browser’s IndexedDB for offline capability. If a user renames a ROM file in RomM, the cached version under the old filename would persist and cause mismatches. This utility detects filename changes and clears the specific cache entry, ensuring the emulator always loads the current file version.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →