# How RomM Integrates EmulatorJS for In-Browser Playback: A Technical Deep Dive

> Discover how RomM integrates EmulatorJS for seamless in-browser playback. Learn about global properties, Vue component orchestration, save states, and iOS compatibility.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: deep-dive
- Published: 2026-07-05

---

**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`](https://github.com/rommapp/romm/blob/main/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`](https://github.com/rommapp/romm/blob/main/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 content
- **`window.EJS_biosUrl`** – The firmware/BIOS file URL for consoles requiring it
- **`window.EJS_threads`** – Boolean flag indicating threading support
- **`window.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`](https://github.com/rommapp/romm/blob/main/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:

1. Waits for the `gameManager` object to become available via `waitForGameManager`
2. Injects any server-provided save files or save states using `loadSave` and `loadState`
3. 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_onSaveSave` receives the binary data and calls RomM’s `saveSave` utility, which POSTs the data to the server
- **Loading**: When selecting a save slot, `EJS_onLoadSave` retrieves 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 menu
- **`createExitEmulationButton`** – Overrides the default exit behavior to properly clean up resources
- **`createSaveQuitButton`** – Combines save-and-quit functionality for streamlined session management

These controls are defined in [`utils.ts`](https://github.com/rommapp/romm/blob/main/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`](https://github.com/rommapp/romm/blob/main/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:

```typescript
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:

```typescript
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:

1. **Router Resolution** – [`frontend/src/v2/router/routes.ts`](https://github.com/rommapp/romm/blob/main/frontend/src/v2/router/routes.ts) maps `/play/:romId` to the [`EmulatorJS.vue`](https://github.com/rommapp/romm/blob/main/EmulatorJS.vue) wrapper component
2. **Global Setup** – [`Player.vue`](https://github.com/rommapp/romm/blob/main/Player.vue) sets `window.EJS_*` globals with core selection and download URLs
3. **Script Injection** – The browser loads [`EmulatorJS.js`](https://github.com/rommapp/romm/blob/main/EmulatorJS.js) (referenced in [`index.html`](https://github.com/rommapp/romm/blob/main/index.html)), which reads the globals
4. **Game Boot** – Emulator downloads the ROM and fires `EJS_onGameStart`
5. **State Injection** – RomM waits for `gameManager` availability, then injects any existing saves via `loadEmulatorJSSave`
6. **User Interaction** – Custom buttons handle quick-load, exit, and save-and-quit operations
7. **Data Persistence** – Save events trigger `saveState` and `saveSave` to sync with the RomM backend
8. **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 in [`Player.vue`](https://github.com/rommapp/romm/blob/main/Player.vue) before the emulator script loads
- The Vue component orchestrates the runtime via lifecycle hooks and callbacks like `EJS_onGameStart` and `EJS_onSaveSave`
- Utility functions in [`utils.ts`](https://github.com/rommapp/romm/blob/main/utils.ts) handle 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`](https://github.com/rommapp/romm/blob/main/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`](https://github.com/rommapp/romm/blob/main/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.