Shell
The engine itself — a container that runs your built game.
What the Shell is
The Shell is the heart of Roves — the actual compiled engine binary. Everything else on this wiki (Packmaster, the GitHub Action) doesn't replace it — both just build or download this exact same Shell on your behalf and hand you the result already put together.
As the name suggests, the Shell is a container: an empty, pre-built native application
that knows how to load and run a dist — your game's already-built web output, whatever
your own bundler (Vite, webpack, or anything else that produces a browser-runnable
index.html) produces — as a real, double-click desktop app. No browser chrome, no toolbar,
no address bar — one window that opens your game and nothing else. It's the same role a
Tauri or Electron shell plays, just for a different underlying engine; see
Why Roves? for that comparison.
The Shell is already built for you
You don't need to compile anything to get a working Shell. Every tagged engine version
publishes a ready-to-run Shell — with no game content in it yet — on the
Releases page, starting at
v0.1.0. Each platform is published twice: a plain build and a _steam-suffixed one
compiled with the Steam bridge
built in.
| Platform | Download | Contains |
|---|---|---|
| Windows | roves_shell_windows.zip / _steam.zip | play.exe + the DLLs (incl. GStreamer's) it needs |
| macOS | roves_shell_macos.zip / _steam.zip | play.app, whose Contents/MacOS/play is the engine binary |
| Linux | roves_shell_linux.zip / _steam.zip | play (expects GStreamer already installed via your distro) |
These are optimized (--release) builds with the real GStreamer media stack (bundled
automatically on Windows/macOS). Pick the version your game targets, download the zip for
your platform, and extract it anywhere — that's the whole install step.
Inserting your dist into the Shell
This is the part that actually matters: turning that empty container plus your built game into something playable. Every path below — Packmaster included — ends up doing the exact same thing under the hood, so pick whichever fits how you work.
Nothing here requires Rust, Python, or mach — just a downloaded Shell and your already-built
game.
-
Build your game with your own bundler, however you already do it. This produces a folder — commonly named
dist/— containing anindex.htmlplus whatever assets it references. -
Download and extract the matching Shell zip from the table above.
-
Copy your build output next to the Shell binary (
play.exe/play.app/play), keeping it nameddistso it lines up with the Shell's own default expectations. -
Tell the Shell where to find it. Two ways, depending on whether you want a double-click-runnable copy or you're just testing:
-
A
launch.jsonnext to the binary (recommended — this is exactly whatmach bundle/Packmaster generate for you):launch.json { "url": "dist/index.html" }With this file in place, double-clicking the Shell binary opens your game directly — no arguments needed. This is what actually makes the container double-click-runnable.
-
Pass the path as an argument, for a quick check without creating any file:
from a terminal, or a shortcut's target ./play dist/index.htmlAny argument on the command line skips
launch.jsonentirely, so use this for testing, not for what you hand to a player.
-
No launch.json and no argument? The Shell isn't broken
Without either, the container has nothing to run — it just opens the stock Servo homepage. That's expected: it means no dist has been pointed at it yet, not that something failed.
This covers loose files only
Hand-writing launch.json this way ships your dist as plain, individually browsable files
(equivalent to mach bundle --content-compress=none). The compressed tar+zstd packing
described in Content packing
is produced by roves-content-packer, invoked either through mach bundle or by Packmaster
linking that same crate as a library — there's no manual equivalent for that path.
launch.json also accepts window_size and extra args — see the flags table further down
for what each corresponds to; a hand-written file only needs url to work.
Building the Shell yourself
This is the least important section for most people. You only need this if you're working on Roves itself, need a flag Packmaster doesn't expose, or want to understand what's actually happening underneath the paths above.
Roves' Shell is not a from-scratch engine — it's a vendored, customized fork of
Servo, built with Servo's own mach tooling. Every customization on top
of stock Servo (no browser chrome, the single-executable bundle, the boot splash, the
roves: protocol bridge, optional Steam) is tracked, file by file, in
CUSTOMIZATIONS.md.
git clone https://github.com/DRincs-Productions/roves
cd rovesBuild and bundle
./mach bootstrap
./mach build --releaseAdd --features steam if you need the Steam
bridge compiled in.
--features mimalloc (since v0.5.2, experimental) swaps the global memory allocator for
mimalloc. By default Windows uses the system allocator
and macOS/Linux use jemalloc. It exists for allocator A/B measurements and has not been shown to
be faster yet, so don't ship it in a game unless your own measurements say it helps. The
perf-ab pre-release on the Roves repository publishes a Windows build of each for comparison.
./mach bundle --content-dir path/to/dist --output releasemach bundle is what turns that build plus your dist into the container described above,
producing exactly the same launch.json-driven layout — no separate launcher process, no
roves-content-packer binary shipped alongside it (packed-content extraction happens
in-process, inside the engine binary itself, which already links that crate as a library):
| Platform | Portable output |
|---|---|
| Windows | play.exe next to a handful of DLLs it needs directly, with GStreamer's plugin DLLs (and their own private codec dependencies) tucked into a lib/ subfolder |
| macOS | play.app, whose Contents/MacOS/play is the engine binary |
| Linux | play |
Flags
| Flag | Default | Description |
|---|---|---|
--content-dir <dir> | — | Web content to copy/pack into the bundle. |
--html-file <path> | dist/index.html | Entry HTML file, relative to the bundle root. |
--output, -o <dir> | <build dir>/bundle | Where to write the bundle. |
--window-size <WxH> | 1280x720 | Initial window size in logical pixels. |
--diagnostic-script | off | Ship a diagnose.bat/diagnose.sh next to the binary that launches the game from a console and prints its exit code plus roves.log inline. See Diagnostics. |
| (trailing args) | — | Anything else is forwarded verbatim to the running game as CLI args (written into launch.json). |
Performance diagnostics
Shell releases since v0.5.1 expose an
opt-in counter stream for comparing idle and animated workloads without changing the game.
Set ROVES_PERF_LOG_INTERVAL_MS to a positive reporting interval before launching the Shell:
ROVES_PERF_LOG_INTERVAL_MS=10000 ./play dist/index.htmlThe normal roves.log receives aggregate [roves-perf] lines with real SDL events versus
wait timeouts, redraws queued or coalesced, dispatched redraws, WebView paints, egui
runs/tessellations/paints, framebuffer blits, composed or direct presents, and final window
presents. The counters are disabled by default and create no sampling timer or background thread.
Each interval also emits one [roves-perf-time] line with the total and worst-case wall-clock
time, in microseconds, of four shell phases: webview_paint (Servo/WebRender painting the
page), egui_run (the egui update pass), shell_paint (the off-screen blit plus any egui
painting) and present (the final window swap). Divide a total by the matching count from the
[roves-perf] line for a per-frame average. GL work is asynchronous, so GPU cost usually shows
up in whichever phase waits on it, typically present under VSync; the times are CPU-side and
not a GPU profiler.
For meaningful Roves/Chrome comparisons, keep the page, release build, window size, warm-up and sample duration identical; do not treat results from a debug build as representative.
Since Shell v0.7.2, two more aids work on a game you have already bundled:
ROVES_EXTRA_ARGSappends engine options to the launch of a bundled game, which otherwise ignores the command line. For example,ROVES_EXTRA_ARGS="-p 5"prints the engine's own time profile every 5 seconds, split into script, layout, painting and so on.ROVES_V8_FLAGSpasses extra options to the JavaScript engine, V8 (since Shellv0.7.8). It is meant for diagnosing garbage-collection pauses together with the[roves-gc]lines.[roves-gc]lines appear inroves.logwhenever a JavaScript garbage collection pauses the game for 8 ms or more, with the JavaScript heap size at that moment. Frequent long pauses point at a game that allocates a lot per frame.
Shader cache
Since Shell v0.7.8, the graphics shaders compiled for your game are saved in the game's
settings folder and reused on the next launch, like Chrome's GPU shader cache. The first launch
compiles them when they are first needed; later launches avoid those stalls. The cache is rebuilt
automatically after a graphics driver update.
Minimized or covered window
Since Shell v0.7.8, the Shell behaves like Chrome when the game window is minimized, hidden or
completely covered by other windows. document.visibilityState becomes hidden and a
visibilitychange event fires, and timers and animations slow down to save CPU and GPU.
Everything resumes when the window is visible again. Listen for visibilitychange to pause your
music, as you would in a browser.
Downloaded assets are cached on disk
Since Shell v0.7.7, images, audio and other files your game downloads from the internet are
saved in a disk cache in the game's own data folder. They are reused on later launches for as
long as the server's caching headers (Cache-Control, Expires) allow, so set a sensible
max-age on your asset host. Files are never cached when the request carries cookies or an
Authorization header. Each game also has its own settings folder, next to its roves.log.
Graphics card
Since Shell v0.7.5, on Windows laptops with both an integrated and a dedicated GPU (NVIDIA
Optimus, AMD switchable graphics), games run on the dedicated GPU by default. The driver control
panel can still override this per game. WebXR (VR/AR) is not available in the Shell.
Direct presentation
The development tree caches actual WebGL drawing-buffer dimensions until the next canvas
resize, avoiding repeated synchronous queries from JavaScript to the WebGL thread. This is
not yet a released performance claim. Benchmark the same production page and dimensions in
Roves and Chrome; GPU usage must be measured per process. Web Animations playback remains
incomplete and disabled, and AudioWorklet is not implemented, so loading motion or Tone.js
alone does not establish full compatibility. Current measurements are maintained in the
engine repository's docs/ROVES_PERFORMANCE.md.
Web Audio frequency analysis in the development tree uses RealFFT/RustFFT instead of a
quadratic direct transform. It preserves the Blackman window, Web Audio normalization and
smoothing and reuses FFT buffers. This benefits AnalyserNode frequency reads; it does not
add AudioWorklet support or imply that every Tone.js operation becomes faster.
The development analyser transport uses a bounded Crossbeam ArrayQueue to retain the latest
32768 samples and import them when JavaScript reads analysis data. It removes the per-block
DOM tasks and their rooted DOM callback. Whole-game CPU savings remain to be measured;
this change is not yet a versioned release. Audio graph processing and downmix still run.
Commit 605d486b079 passed the ordered patch validation and all desktop, Windows, macOS,
Linux, Android, and iOS CI jobs (desktop workflow).
Chrome DevTools MCP verified the transport fixture with eight analyzers both unread and read
at about 60 Hz. Those are functional checks only; the post-change Roves runtime has not yet
been verified, and no CPU comparison is claimed.
Animation comparison fixtures disable the background panels' FPS polling. The Three.js
diagnostic panel disposes geometry/material and removes its canvas on unmount. Re-run both
browser baselines with the same corrected page; fixture savings are not engine speedups.
Optimized Windows builds from commit 20f899dcfbb pass numerical CI checks and local WebGL
resize/readback and audio spectrum checks. In matched headless API tests, spectrum reads at
2048/8192 samples dropped from about 27/432 ms to 0.03/0.09 ms. These are individual API
measurements, not whole-game CPU, GPU or RAM results; the engine performance notes include
the reproduction pages, sample counts and limitations. The experimental perf-ab builds
are separate from the versioned shell consumed by Packmaster and roves-action.
Since Shell v0.7.4, when a game fills the window and no Shell dialog, status overlay or
accessibility tool is active, the Shell copies the game's frame straight to the window instead
of drawing it through its own interface layer. Anything else (a dialog, the boot splash, an
error page) automatically uses the full path for that frame. Nothing needs to be configured:
the earlier ROVES_DIRECT_PRESENT variable is no longer read. With the performance counters
on, direct frames show up as direct_presents and the others as composited_presents.
Custom game icon
| Flag | Default | Description |
|---|---|---|
--icon-png <path> | auto-detect | Window/taskbar icon (PNG), applied after packaging — no compile needed, works for a prebuilt shell too. Not supported on macOS yet (its own Dock/app icon has no runtime override). |
--icon-ico <path> | auto-detect | Windows-only .exe icon (multi-size .ico), patched into the bundled play.exe in place via rcedit. |
"Auto-detect" means: with neither flag given, mach bundle looks for an icon.png/icon.ico
sitting directly in --content-dir before falling back to Roves' own branding — many
bundlers already emit one there for their own PWA manifest, so a game that already has one
gets its own icon for free, no flag needed. Pass either flag explicitly to override this.
Content packing
How --content-dir gets packed into the bundle — see
Content packing for the
full design (why, the boot-set/lazy-extraction split, the archive layout). These flags tune
or disable it:
| Flag | Default | Description |
|---|---|---|
--content-compress | auto | auto: pack into a handful of tar+zstd archives. none: copy in as loose, individually browsable files. |
--content-compression-level | 1 | zstd level for --content-compress=auto. Low by default — speed over ratio. |
--content-max-pack-size | 500M | Split archives so none exceeds this size. |
--content-exclude <glob> | — | Repeatable. Leave matching files as loose, uncompressed files (e.g. a save-data folder that shouldn't sit inside a read-only archive). |
--content-boot-include <glob> | — | Repeatable. Extra files eagerly extracted at boot, beyond the html file, whatever it directly references and every JavaScript/CSS file (always extracted at boot since Shell v0.7.6). |
Distributing installers
Each platform also has one installable alternative to the portable output above, wrapping the exact same staged files — nothing extra is left behind once the installer is built:
| Platform | Installer flag | Produces |
|---|---|---|
| Windows | --msi | An installable .msi |
| macOS | --dmg | play.app wrapped in an installable .dmg |
| Linux | --deb | An installable .deb |
| Flag | Default | Description |
|---|---|---|
--package-name <name> | roves | Package name for --deb/--msi/--dmg. |
--package-version <ver> | 0.0.0 | Package version for --deb/--msi/--dmg. |
Extra args after --output are forwarded to the game, not to mach
mach bundle's own flags (--package-name, --package-version, --msi, ...) are for
naming/shaping the installer. Anything else on the command line is written into the
bundle's launch.json and handed to the running game at launch as real CLI args — the two
are validated against each other so one can't accidentally leak into the other.