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. Compare ROVES_DIRECT_PRESENT=1 runs with default runs to see how much of
the frame egui actually costs.
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.
Experimental direct presentation
Desktop builds from main support fast path A, disabled by default:
ROVES_DIRECT_PRESENT=1 ROVES_PERF_LOG_INTERVAL_MS=1000 ./play dist/index.htmlOnly the exact value 1 enables it; unset or other values keep the composed renderer. Restart
the Shell after changing the variable. It requires exactly one active WebView filling the
physical window, no dialogs or status overlay, no egui focus, inactive AccessKit and no pending
egui texture updates. Splash and loading errors always use the composed renderer. Any failed
condition falls back in the same frame, including a dialog that closes during that update.
Servo still renders off-screen. Egui still runs to process input, dialogs and accessibility;
only tessellation and paint are skipped. Each direct frame increments direct_presents,
framebuffer_blits and window_presents; fallback frames increment composited_presents.
Desktop CI requires a real launch to report at least one positive direct_presents interval,
without performance thresholds on shared runners. Hardware A/B measurements are still needed
to quantify the gain. Fast path B, which would remove the off-screen buffer, is not implemented.
This is a runtime experiment, with no new Packmaster or mach bundle setting.
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 and whatever it directly references. |
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.