Roves

Shell

The engine itself — a container that runs your built game.

Edit on GitHub

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.

PlatformDownloadContains
Windowsroves_shell_windows.zip / _steam.zipplay.exe + the DLLs (incl. GStreamer's) it needs
macOSroves_shell_macos.zip / _steam.zipplay.app, whose Contents/MacOS/play is the engine binary
Linuxroves_shell_linux.zip / _steam.zipplay (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.

  1. Build your game with your own bundler, however you already do it. This produces a folder — commonly named dist/ — containing an index.html plus whatever assets it references.

  2. Download and extract the matching Shell zip from the table above.

  3. Copy your build output next to the Shell binary (play.exe / play.app / play), keeping it named dist so it lines up with the Shell's own default expectations.

  4. 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.json next to the binary (recommended — this is exactly what mach 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.html

      Any argument on the command line skips launch.json entirely, 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 roves

Build and bundle

from the repo root
./mach bootstrap
./mach build --release

Add --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 release

mach 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):

PlatformPortable output
Windowsplay.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
macOSplay.app, whose Contents/MacOS/play is the engine binary
Linuxplay

Flags

FlagDefaultDescription
--content-dir <dir>—Web content to copy/pack into the bundle.
--html-file <path>dist/index.htmlEntry HTML file, relative to the bundle root.
--output, -o <dir><build dir>/bundleWhere to write the bundle.
--window-size <WxH>1280x720Initial window size in logical pixels.
--diagnostic-scriptoffShip 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.html

The 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.html

Only 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

FlagDefaultDescription
--icon-png <path>auto-detectWindow/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-detectWindows-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:

FlagDefaultDescription
--content-compressautoauto: pack into a handful of tar+zstd archives. none: copy in as loose, individually browsable files.
--content-compression-level1zstd level for --content-compress=auto. Low by default — speed over ratio.
--content-max-pack-size500MSplit 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:

PlatformInstaller flagProduces
Windows--msiAn installable .msi
macOS--dmgplay.app wrapped in an installable .dmg
Linux--debAn installable .deb
FlagDefaultDescription
--package-name <name>rovesPackage name for --deb/--msi/--dmg.
--package-version <ver>0.0.0Package 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.

On this page