# Publishing a Mirage game to itch.io
This document lists, in order, the steps that turn a game in its own
repository into a page that plays in the browser.
## What the game needs
- A nightly toolchain with the standard library's sources, and the
packaging subcommand:
```sh
rustup toolchain install nightly --component rust-src
cargo install cargo-mirage
```
The game runs on a worker over shared memory in the browser, which needs
the `atomics` target feature (and its vector math runs on `simd128`,
which every browser with WebGPU has), and a standard library built with it — so
`cargo mirage web` builds that library from its own sources, a
nightly-only step. There is no `rustup target add` step: the sources cover
the target. Run the command under nightly, as `cargo +nightly mirage web`,
or set it for the game's folder with `rustup override set nightly`; a
build under a stable toolchain stops before it starts and states that it
needs nightly.
`cargo-mirage` runs `wasm-bindgen` from inside itself, so there is no
`wasm-bindgen-cli` to install and keep in step. It is released beside the
engine against one exact `wasm-bindgen` version. A game whose lock
resolves another version stops the run, which names both versions, and
names the `cargo install` line for the release built against the game's
version if there is one. The engine's version is checked the same way,
down to its minor: this release writes the script the engine's workers
are started on, and the two spell one set of posted values between them,
which a patch release of the engine keeps.
- A `[[bin]]` target, or a `src/main.rs`, whose `main` calls `run`. The
browser build starts at that `main` and nowhere else. A second `[[bin]]`,
such as a desktop tool beside the game, does not count when its
`required-features` names a feature a build with no flag has off. Two
bins a build with no flag builds both need `--bin <name>` to pick one. In
a workspace holding more than one package, `-p <package>` names the game.
## The build
From the game's repository root:
```sh
cargo mirage web
```
That builds the target for `wasm32-unknown-unknown` in release, runs
`wasm-bindgen` over it into `dist/game.js` and `dist/game_bg.wasm`, writes
`dist/index.html` and `dist/worker.js`, the script every worker is started on,
copies the asset folders, checks itch's own limits, and
writes `dist/<target>-web.zip` with the page at the zip's root, which is what
itch serves. The folder is emptied first, so nothing an earlier run left is
uploaded.
The build states the browser's own flags as cargo configuration, so whatever
rustflags the game's own `.cargo/config.toml` states are kept beside them. A
`RUSTFLAGS` or `CARGO_ENCODED_RUSTFLAGS` variable replaces every flag instead,
so the build stops before it starts where either is set, and names
`.cargo/config.toml` as the place a game's own rustflags go.
The engine starts the game on a worker of its own, from `dist/worker.js`,
which the page names for it in `data-mirage-worker` on its body; a page the
game supplies itself, with `Config::with_canvas_id`, carries that attribute
beside its canvas and nothing more, or startup stops naming it. That one
script runs every worker of the run: the game thread, and, one per spawn the
game thread posts, each worker of the pool its parallel iterators run on, all
started by the engine on the page's own thread. The page and the workers all
run over one shared memory. How far that memory may grow is the game's to
state, in bytes:
```toml
[package.metadata.mirage]
web-memory = 2147483648
```
It is 1 GiB where the manifest states none, and it takes a whole number of
64 KiB blocks, up to the largest a browser build addresses, 4 GiB. A game
whose memory use passes the cap fails to allocate in the player's browser, so
raise it for a game whose meshes, textures and sounds need the room.
The page is the engine's own: it fills itself with the canvas the engine
creates, states the error that stopped a start where one did, and gives
`GPUCanvasContext` and `GPUAdapter` the instance check that Firefox
configurations running WebGPU without those interface globals need. Its
`<title>` is the game's:
```toml
[package.metadata.mirage]
title = "Deep Dive"
```
Where the manifest states no title, the built target's name is the title.
## The assets
```toml
[package.metadata.mirage]
assets = ["assets", "levels"]
```
A game's asset paths are relative to the workspace root: on the desktop that
is the folder the game is run from, and in the upload it is the page's own
folder. `cargo mirage web` builds `dist` at the workspace root and copies each
folder there under the same path, so a workspace game that names
`game/assets/sky.png` in `Config::with_assets` lists `game/assets` above and
reads `game/assets/sky.png` on both sides. For a game that is its own
workspace the two roots are the same folder. Where the manifest names no
folders, an `assets` folder at the workspace root is the one taken.
The paths in `Config::with_assets` are the game's to keep matching these
folders. A folder named with a leading `/`, one that climbs out of the
workspace, and one whose case differs from the disk's own are each refused by
name: itch's server reads a path by its exact case and refuses one that starts
at the root.
## The itch project settings
The names below are itch's own, from its HTML5 documentation.
- **SharedArrayBuffer support**: turn it on. It sits in **Embed options**,
which the project's edit page shows once the zip is uploaded. itch sends
`Cross-Origin-Opener-Policy: same-origin` and
`Cross-Origin-Embedder-Policy: require-corp` for an upload with it on, and
without those two headers the game starts no worker at all: the page loads
nothing and states that it is not cross-origin isolated, naming both headers
and this setting, and `crossOriginIsolated` reads false in the browser's
console.
- **Kind Of Game**: pick **HTML Game**, on a new project page or an existing
one.
- Upload the zip, and mark it as the file that is played in the browser,
with the checkbox on its own upload row. itch's documentation states only
that the **Embed options** section appears on the project's edit page
after an appropriate file is uploaded to be embedded; the checkbox's own
wording is from memory.
- **Embed options**: **Embed in page** takes the viewport dimensions, and
runs the game on the page at that size. Set them to the size the game
passes to `Config::with_size`, so the browser frame matches the desktop
window; the browser build itself ignores `with_size` and fills the page, so
the viewport sets the size the game draws at. **Click to launch in
fullscreen** takes no dimensions, and the game fills the viewer's screen.
**Fullscreen Button** adds a button over the **Embed in page** view that
does what **Click to launch in fullscreen** does.
- **Click to Play** is on by default, and the player's click is what loads
the game. Leave it on for a game with sound: itch's page, read on
2026-09-12, states that audio may be muted on some browsers when a project
starts on its own.
- The game thread and the pool its parallel iterators run on all run as
workers over shared memory, which is why the page must be served with the
two headers **SharedArrayBuffer support** above makes itch send.
- The game needs a browser with WebGPU. Chromium-based browsers run it as
shipped. Firefox on Linux ships WebGPU disabled, and a player turns it on
with `dom.webgpu.enabled` in `about:config`. State that on the game's
page: a player without WebGPU reads the engine's message over the canvas
and nothing else.
- On a Windows laptop with two GPUs, Firefox and Chrome both run WebGPU on
the GPU that drives the display, whatever the engine asks for. State on
the game's page that a player with a weak integrated GPU sets the browser
to the high-performance GPU in Windows' graphics settings (Settings,
System, Display, Graphics), or starts Chrome with
`--force_high_performance_gpu`.
The limits itch states for an HTML game's zip, read on 2026-09-12, are 1,000
files once it is opened, 500MB opened in total, 200MB for any one file, and
240 characters for any one path. `cargo mirage web` checks all four before it
writes the zip and names the file that is past one.
## Testing before the upload
```sh
cargo mirage serve # 8000 by default; `cargo mirage serve 8123` names another port
```
That packages the game and serves `dist` on `127.0.0.1`, printing the URL to
open. Serve the folder rather than opening `index.html` as a file: the module
import and the wasm read both need HTTP, and the server states
`game_bg.wasm` as `application/wasm`, which is what the page starts it from.
A first upload confirms the five things only the page on itch shows:
- the assets load from itch's own paths,
- the viewport frames the canvas as the game expects,
- a `Cursor::Held` pointer hold takes its lock inside itch's frame,
- sound starts after the player's click,
- what the game `Saves` is kept between visits.