Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shipping a Game

A build turns a project into a folder someone else can run: an executable, and the assets it needs, and nothing else. This page is what the Build panel does and how to make it produce something that starts on a machine that is not yours.

Presets

A .buildpreset is one way of building the project — “Linux release”, “Windows”, “handheld”. It is an ordinary asset: it lives in assets/, it is created from the Asset Browser’s context menu, and it is edited in the Inspector like anything else. The Build panel only holds the list, the button, and cargo’s output.

Presets belong in version control. They are configuration, and a project usually has more than one. The Build panel’s list is where you pick which one to build; no preset is “the” preset.

What a build produces

build/
  linux/
    My Game.x86_64    the executable, named for its platform
    assets.kpack      the scenes and everything they reference
    project.kooch     which scene the game opens with
  windows/
    My Game.exe
    assets.kpack
    project.kooch
    libstdc++-6.dll   ) mingw's C++ runtime — the game does not
    libgcc_s_seh-1.dll  ) start on Windows without these three
    libwinpthread-1.dll )

Each platform gets a folder of its own under the preset’s output directory, so a preset that builds both does not have the second overwrite the first.

The extension follows the platform — .exe on Windows, .x86_64 on Linux, the same convention Unity and Godot use. A folder holding both is then unambiguous.

Which scene it opens with

Right-click a .scene in the Asset Browser → Set as Main Scene. That scene is marked with a ▶ and an accent-coloured name from then on, and it is the one both Play and a built game start from.

It is stored in project.kooch, which travels beside the executable — that is the only reason a shipped game can know. Nothing else in the package says which of five scenes is the first one.

🔴 This did not work before. main_scene existed in the manifest and nothing read it: a game opened assets/scenes/default.scene whatever the field said, so a project whose starting scene had any other name shipped a build that started somewhere else — or started empty, with no error anywhere.

A project with no project.kooch beside the binary still falls back to assets/scenes/default.scene, which is what every build did until now.

What travels, and what does not

Only what the game reaches — and reaches is followed all the way down. Packaging starts from the project’s files, collects the GUIDs they reference, then follows those assets to the GUIDs they reference, and repeats until nothing new appears:

level.scene  →  floor.ron  →  grid.png

A scene names a material, the material names a texture, and all three travel. Each asset is collected once however many things point at it, and a cycle — two prefabs naming each other — terminates rather than hanging the build.

An asset nobody reaches stays behind, and so does every .buildpreset — a build does not ship the instructions for making itself.

🔴 This used to stop at the first step: scenes and prefabs were read and nothing else was. The material shipped and its texture did not, and because a missing GUID is silent the game started and drew the 1×1 white fallback — a textured surface that looks like somebody authored it flat.

Assets only your code names

A GUID built in Rust is not something the packager can find by reading files: a scene loaded by path, an asset chosen from a table, an id assembled from a string. Declare those in project.kooch:

build: (
    include: ["assets/meshes/suzanne.glb"],
),

Paths are resolved against the project first and the engine second. Each one is a root of the same walk, so declaring a material brings the textures it names — you never have to list what is underneath.

A declared path that matches no file is reported in the build log and does not stop the build.

Source does not travel either. A shipped game is a compiled binary; the src/ folder is what produced it, not part of it.

This is why anything imported through the Asset Browser lands under assets/ whatever folder was selected. The split is what makes “what does the game need” answerable at all:

FolderHoldsShips
assets/scenes, meshes, textures, materialsthe parts that are used
src/components, systems, main.rscompiled in, not copied
.kooch/the pack key, local statenever

The asset pack

With pack_assets on — the default — everything lands in a single encrypted assets.kpack rather than as loose files. It is compressed with zstd and sealed with AES-256-GCM, including the index, so the file does not even reveal the names of what is inside it.

Scenes go in the pack too. A scene is the structure of the whole game; leaving it in plain text beside an encrypted pack would protect the textures and publish the design.

Turn pack_assets off while working out why a build behaves differently from the editor — then the files are right there to look at.

⚠️ The key has to be inside the binary for the binary to read the pack. This raises the cost of taking your assets; it does not make it impossible, and nothing does. A game hands its meshes to the GPU in the clear because that is what drawing them means.

The key

Each project gets its own, generated once and kept at .kooch/pack.key. It is not in version control — a repository carrying it has published it, and history keeps it published after the file is deleted. That is the same line Godot draws between export_presets.cfg and its encryption key.

One key per project, so breaking one says nothing about the next.

For CI, set KOOCH_PACK_KEY to the key’s hex and nothing is written into the checkout. Keep it in the secret store, not the repository.

The two modes

A preset’s mode is what the build is for. There are two, and both are optimised.

ReleaseProfiling
Optimisationsfull — LTO, one codegen unitthe same
Profilerabsent from the binarycompiled in
Open portnone0.0.0.0:8585
Give it to peopleyesnever (#558)

Profiling streams every frame to the editor’s Profiler panel, CPU scopes and per-pass GPU timings alike — the only way to find out where a frame goes on the hardware the game has to run on. See Profiling.

🔴 Release is not “the profiler switched off”. With the feature absent, every scope in the engine expands to nothing at compile time and there is no socket to open. Nothing can be turned back on at runtime.

⚠️ There is deliberately no debug mode. A build compiled without optimisations runs several times slower — the editor’s own debug build measured 14.31 ms a frame against 4.94 ms for its release build — so profiling one tells you about that build and not about your game. The handheld’s entire budget is 13.9 ms.

Keep the two as separate presets — “handheld, profiled” beside “handheld” — so the ordinary build cannot acquire a socket because somebody left a dropdown on the wrong entry.

Running on another machine: min_glibc

A game built on an up-to-date desktop often refuses to start on a Steam Deck or a handheld, with a message about a missing symbol version. glibc is forward compatible and not backward: a binary linked against 2.43 does not run against 2.42, and the error says nothing about what to do.

Set min_glibc to the oldest version the build has to run on and it links against that instead:

ValueRuns on
emptythis machine and anything newer — the default
2.28Debian 10, RHEL 8, and everything since — what Godot’s Linux exports target
2.31Ubuntu 20.04 and newer

Leave it empty while iterating locally; set it before handing the build to anyone.

It applies to the Linux half of a preset and no other: Windows has no glibc, and the floor is simply not carried there. One preset can set a floor and build both.

What it needs

Two tools, neither of which requires root — which matters on an immutable distribution, where there is no dnf install to reach for:

cargo install cargo-zigbuild
# and zig itself, one tarball from https://ziglang.org/download/
#   tar xf zig-*.tar.xz -C ~/.local/opt
#   ln -s ~/.local/opt/zig-*/zig ~/.local/bin/zig

The build checks for both before compiling and names what is missing. A missing toolchain that surfaces ten minutes in, as a linker error, is the thing that check exists to prevent.

The field is ignored for targets that are not *-linux-gnu; there is no glibc to have a floor.

Cancelling

The Build panel’s Cancel stops cargo. Nothing is packaged, so a half-built executable never reaches the output folder — packaging only runs when cargo exits clean.

Building for more than one platform

A preset has a checkbox per platform — Linux and Windows — and ticking both builds both from one press, one after the other. They are never built in parallel: two cargos on one machine fight over the same target/ lock and interleave their output into a log nobody can read.

Each platform’s Rust target has to be installed. Every enabled platform is checked before the first one starts compiling, with the rustup target add line to run — so a missing Windows target is reported up front rather than after Linux has spent ten minutes building.

Windows also needs the mingw-w64 toolchain, and both halves of it: metis is C and meshopt is C++. Having only the C compiler is a real state to be in — Fedora ships mingw64-gcc and mingw64-gcc-c++ as separate packages — and it fails inside a build script well after cargo has accepted the target.

So both are checked up front, by the exact names cc-rs looks for, and the refusal says what to install:

rpm-ostree install mingw64-gcc mingw64-gcc-c++          # Fedora, Bazzite
sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64   # Debian, Ubuntu
sudo pacman -S mingw-w64-gcc                            # Arch

The C23 workaround metis requires is passed for you.

The three DLLs beside a Windows build

meshopt is C++, so the executable links mingw’s C++ standard library — and on mingw that library is a DLL that ships with the compiler, not with Windows. Leave it behind and the build runs on the machine that made it and nowhere else, failing with a Windows dialog naming a file.

They travel automatically, stripped of their debug symbols on the way (Fedora’s libstdc++-6.dll is 29.7 MB installed and 2.5 MB shipped), and the build fails rather than producing a folder that looks complete and holds a game that cannot start.

Do not delete them. They are three unexplained files beside a game, which is exactly the shape of a file somebody tidies away — and the game stops working the moment they go.

They are not linked statically because that does not work: mingw’s libstdc++.a mixes static and dynamic symbols, so -static-libstdc++ and -static both leave the import in place. The problem is open upstream in rust-lang/rust#65911.

A preset with no platform ticked builds nothing, and says so instead of guessing that you meant this machine.

Presets written before the checkboxes

They carried a target_triple instead. It is read once, on load, and turned into the matching checkbox — an empty one meaning the machine the editor is running on. A triple naming a platform with no checkbox opens with none ticked and warns, rather than silently building something the preset never asked for.