Engine demo // Rust

Nadir

A 2D top-down game engine for desktop, written in Rust from the ground up: its own OS bindings, OpenGL renderer, ECS, collision, codecs and tools, with no third-party crates. What plays below is the engine itself, drawn frame by frame in CI from the commit named beside it, published when that commit passed CI on main.

Live build // 33f37db

The field

A person walks a field made in LDtk, of tiles, trees and rocks: down to the path and along it past the pond, down it past the flowers and through a gap in a row of tall walls, then along the face of one until it ends. The camera follows them. They walk in four frames, facing the way they go, and are drawn in front of the walls they slide along, whose feet are above theirs: sprites are y-sorted by their feet. The water ripples and the flowers sway: animated tiles.

Frames
600 in 10 s, a tick each
Sprites a frame
57
Sprite batches
9.4
Tiles a frame
1,795
Mean tick
0.008 ms longest 0.029
World hash
298521738b5650ef

Drawn offscreen at 480 × 270 by llvmpipe (LLVM 21.1.8, 256 bits) on the CI runner, and encoded at twice the size.

10,000 sprites

10,000 people walk about on a lawn of grass sprites, among trees and rocks, each an entity that the engine moves and animates. The lawn and everything on it are one page of the cooked atlas, y-sorted, so a frame is one batch: the budget is 10,000 sprites at 60 frames a second in at most 30 draw calls, with no allocation in a frame.

Frames
600 in 10 s, a tick each
Sprites a frame
10,203
Sprite batches
1
Tiles a frame
0
Mean tick
0.169 ms longest 0.290
World hash
c47e62a4e2bb26d6

Drawn offscreen at 480 × 270 by llvmpipe (LLVM 21.1.8, 256 bits) on the CI runner, and encoded at twice the size.

5,000 entities

5,000 people jostle in a maze of walls 128 tiles square. Each collides at their feet, and every tick the engine moves them and slides them along the walls and one another, through the collision crate's spatial hash; a tenth are triggers, which walk through the others and notice them. The budget for 5,000 entities with collision is 4 ms a tick on a min-spec machine.

Frames
600 in 10 s, a tick each
Sprites a frame
5,001
Sprite batches
1
Tiles a frame
255
Mean tick
2.777 ms longest 3.029
World hash
28e943b7c2b0247e

Drawn offscreen at 480 × 270 by llvmpipe (LLVM 21.1.8, 256 bits) on the CI runner, and encoded at twice the size.

Architecture

The engine in two views of the C4 model, drawn by hand from its code: the system among the people and systems around it, then its programs and the files they pass between them. The crates inside the game and the cook, the game loop and the main types are drawn in the repository's diagrams.

System context

The game built on Nadir and Nadir's own tools, with the people, editors, system interfaces and services they deal with. The Windows and macOS windows are written but still wait on checks on those systems (roadmap M1.5, M1.6), and there the game runs without drawing until their OpenGL contexts land (M2.6, M2.7).

Nadir in its surroundings: a C4 system context diagram The game developer draws sprites in Aseprite, lays out levels in LDtk, and draws images and makes fonts in image editors and font generators; each saves into assets/ on the filesystem. The developer runs Nadir's tools, nadir-cook and xtask, which read assets/ and write target/cooked/ and game.kpak, and writes and runs the game built on Nadir, which reads the cooked files or game.kpak and writes nadir.log. The player plays the game with keyboard and mouse, watches the demo page and downloads releases. The game opens a window and reads its events through the OS window and input APIs, and draws through OpenGL 3.3 core, through EGL on Linux. Planned: OpenGL on Windows and macOS, then Metal (M2.6, M2.7, M10), the OS audio APIs (M6) and gamepad APIs (M8). GitHub Actions runs cargo xtask, deploys the demo page to GitHub Pages and publishes each tagged release to the release channels. Nadir FILESFilesystemassets/ sources,target/cooked/files andgame.kpak, thegame's log files EXTERNAL SYSTEMAsepritesprite sheets: PNG andJSON exports EXTERNAL SYSTEMLDtklevel projects: .ldtk and.ldtkl files EXTERNAL SYSTEMImage editors andfont generatorsPNG images, BMFont .fnt fonts SOFTWARE SYSTEMNadir's toolsnadir-cook, the asset cook,and xtask, the buildautomation SOFTWARE SYSTEMGame built on Nadirthe sandbox, linked with thenadir_engine facade PERSONGame developerwrites the game, draws itssprites, lays out its levels EXTERNAL SYSTEMGitHub Actionsworkflows CI, CodeQL,Pages and Release PERSONPlayerplays the game EXTERNAL SYSTEMGitHub Pagesthe demo page atjabbott.dev/Nadir EXTERNAL SYSTEMRelease channelsGitHub release and aptrepository, the cook'simage on ghcr.io, thecrates on crates.io EXTERNAL SYSTEMOS window andinput APIsX11 on Linux, Win32 onWindows, AppKit on macOS EXTERNAL SYSTEMGPU driverOpenGL 3.3 core throughEGL, on Linux PLANNEDGPU on Windowsand macOSWGL and NSOpenGLcontexts, then Metal PLANNEDOS audio APIsALSA, WASAPI, CoreAudio PLANNEDGamepad APIsevdev, XInput,GameController draws sprites lays out levels draws images,makes fonts runs cargo xtask writes and runs exports intoassets/ saves projectsinto assets/ saves intoassets/ reads assets/,writes target/cooked/and game.kpak reads the cookedfiles or game.kpak,writes nadir.log runscargo xtask deploys thedemo page publishes a tagged release plays with keyboardand mouse watches the demo,downloads theLinux build downloadsa release opens a window,reads its events draws throughOpenGL 3.3 core planned: M2.6,M2.7, M10 planned: M6 planned: M8

Scroll sideways to see all of it.

Each arrow as text
  • Game developer → Aseprite: draws sprites
  • Game developer → LDtk: lays out levels
  • Game developer → Image editors and font generators: draws images, makes fonts
  • Game developer → Nadir's tools: runs cargo xtask
  • Game developer → Game built on Nadir: writes and runs
  • Aseprite → Filesystem: exports into assets/
  • LDtk → Filesystem: saves projects into assets/
  • Image editors and font generators → Filesystem: saves into assets/
  • Nadir's tools → Filesystem: reads assets/, writes target/cooked/ and game.kpak
  • Game built on Nadir → Filesystem: reads the cooked files or game.kpak, writes nadir.log
  • GitHub Actions → Nadir's tools: runs cargo xtask
  • GitHub Actions → GitHub Pages: deploys the demo page
  • GitHub Actions → Release channels: publishes a tagged release
  • Player → Game built on Nadir: plays with keyboard and mouse
  • Player → GitHub Pages: watches the demo, downloads the Linux build
  • Player → Release channels: downloads a release
  • Game built on Nadir → OS window and input APIs: opens a window, reads its events
  • Game built on Nadir → GPU driver: draws through OpenGL 3.3 core
  • Game built on Nadir → GPU on Windows and macOS: planned: M2.6, M2.7, M10
  • Game built on Nadir → OS audio APIs: planned: M6
  • Game built on Nadir → Gamepad APIs: planned: M8

Containers

The workspace's programs and the files they pass. The cook turns assets/ into loose files and one pack, game.kpak: a game built with the dev feature reads the loose files, and one built without it reads the pack, or the loose files where there is no pack. The sandbox reads the folder assets beside its executable when there is one, as in the release files and the download above, and target/cooked/ otherwise.

Nadir's programs and the files between them: a C4 container diagram Inside the Nadir workspace: xtask, which the game developer runs and GitHub Actions runs as cargo xtask ci, golden, fuzz-smoke, site and dist; nadir-cook, which xtask runs to cook, reading assets/ and writing the loose files in target/cooked/, skipping jobs and files that did not change, and packing every output into target/cooked/game.kpak; the game executable, the sandbox, which the developer runs, and xtask builds in release and runs for site and dist: with the dev feature it reads the loose files, and without it the pack when it is there, else the loose files; nadir-bench, which xtask runs for bench; and target/dist/ and target/site/, which xtask's dist and site write. The game writes its logs to the user's logs folder, and opens its window, reads its events and draws through the OS and GPU driver. Planned (M5.8): the game reloads what changed in the loose files. Nadir workspaceSOFTWARE SYSTEM EXTERNAL SYSTEMGitHub Actionsworkflows CI, Pages andRelease PERSONGame developer CONTAINER: RUST BINARYxtaskrun as cargo xtask: the checks, cook,golden, fuzz-smoke, bench, site, dist FILEStarget/dist/nadir-VERSION-TARGET:sandbox, nadir-cook,assets/game.kpak, as.zip or .tar.gz, and.debs on Linux FILEStarget/site/the demo page, itsvideos and posters, theLinux download CONTAINER: RUST BINARYnadir-benchthe benchmark harness CONTAINER: LIBRARY, CLInadir-cooknadir-cook [--force]SOURCE OUT CONTAINER: RUST BINARYGame executable:sandboxon nadir_engine.cargo run -p sandbox,or cargo dev for thedev feature FILESassets/PNG images andsprite.jsonc sidecars,Aseprite JSON, LDtkprojects, data JSONCand prefabs FILEStarget/cooked/loose filesatlas QOI pages, KBJ andfont files, data KBJ,levels .klvl, the cache.cache.kbj FILEStarget/cooked/game.kpakthe pack: every cooked file,KLZ where that is smaller FILESThe user'slogs foldernadir.log and thelogs of the threeruns before EXTERNAL SYSTEMOS and GPUdriverwindow, events,OpenGL runs cargo xtask ci, golden,fuzz-smoke, site and dist runs runs dist: lays out,checks and packs site: captures,encodes withGStreamer, packsthe download bench: cargo run--release --packagenadir-bench cook: cargo run --release--package nadir-cook.With --watch: builds itonce, runs it on eachchange site and dist: buildit in release andrun it reads writes, skipping jobsand files that didnot change packs everyoutput into with dev: readsthe loose files planned M5.8:reloads whatchanged without dev: readsthe pack when it isthere, else theloose files writes window, events,OpenGL

Scroll sideways to see all of it.

Each arrow as text
  • GitHub Actions → xtask: runs cargo xtask ci, golden, fuzz-smoke, site and dist
  • Game developer → xtask: runs
  • Game developer → Game executable: sandbox: runs
  • xtask → target/dist/: dist: lays out, checks and packs
  • xtask → target/site/: site: captures, encodes with GStreamer, packs the download
  • xtask → nadir-bench: bench: cargo run --release --package nadir-bench
  • xtask → nadir-cook: cook: cargo run --release --package nadir-cook. With --watch: builds it once, runs it on each change
  • xtask → Game executable: sandbox: site and dist: build it in release and run it
  • nadir-cook → assets/: reads
  • nadir-cook → target/cooked/ loose files: writes, skipping jobs and files that did not change
  • nadir-cook → target/cooked/game.kpak: packs every output into
  • Game executable: sandbox → target/cooked/ loose files: with dev: reads the loose files
  • Game executable: sandbox → target/cooked/ loose files: planned M5.8: reloads what changed
  • Game executable: sandbox → target/cooked/game.kpak: without dev: reads the pack when it is there, else the loose files
  • Game executable: sandbox → The user's logs folder: writes
  • Game executable: sandbox → OS and GPU driver: window, events, OpenGL
  • A person
  • Part of Nadir
  • Outside Nadir
  • A cylinder: files on disk
  • Dashed: planned, not built yet

Features

26 shipped, 3 in progress and 13 planned. Each feature is marked from the roadmap's ticks at 33f37db, so the list moves as the tasks land.

Quality // Shipped

No third-party crates

The game is built from this workspace's crates alone: its OS bindings, OpenGL backend, math, ECS, codecs and tools are written here, and beyond Rust's standard library it calls only the system's own libraries, such as Xlib, EGL and OpenGL. The build fails if the lock file gains a package from outside the workspace, and each crate may use only the crates the architecture's dependency matrix allows.

  • M0.1
  • M0.6

Quality // Shipped

Guardrails on every push

Each push is formatted, linted with warnings as errors, tested, and checked for dependencies, layering and unsafe code on Windows, macOS and Linux. Unsafe code is allowed in three crates only, and the audit fails any unsafe block without a SAFETY comment.

  • M0.6
  • M0.8

Quality // Shipped

Fuzzing and benchmarks

A mutation fuzzer of our own runs each parser of the codec crate in CI, with a hang watchdog, and saves an input that panics or hangs one. A benchmark harness reports medians and 95th percentiles against a recorded baseline.

  • M0.7

Core // Shipped

Math, color and geometry

Vectors, 3 × 3 matrices, 2D affine transforms, rectangles, and colors with sRGB conversions, checked by property tests such as inverse round trips and symmetric intersections.

  • M0.2

Core // Shipped

Handles, hashing and random numbers

Generational arenas and typed handles that catch a stale reference, FNV-1a, a fast hasher for maps, CRC-32 and the PCG32 generator, each checked against reference values.

  • M0.3

Core // Shipped

Logging, time and test tools

Log macros with rotating files and an in-memory ring, calendar time without the OS, a command-line parser, profile scopes, and a property-test helper that prints the seed of a failure so that it can be run again.

  • M0.4

Content // Shipped

JSON and JSONC

A parser and writer for JSON, with comments and trailing commas allowed in JSONC. Bad input gets an error with its line and column, never a panic, and limits on depth, sizes and counts keep a hostile file from exhausting memory.

  • M0.5

Platform // In progress

Native windows on every desktop

Windows, keys, mouse, focus, resizing, DPI scaling and borderless fullscreen through each system's own API, declared by hand. Linux, through X11 loaded at run time, is done. The Win32 and Cocoa versions build and pass CI, and will be done once they are checked by hand on Windows and macOS.

  • M1.1
  • M1.2
  • M1.5
  • M1.6

Simulation // Shipped

Fixed-tick main loop

The simulation steps exactly 60 times a second whatever the frame rate, and each frame draws between the last two ticks, so motion is smooth at 60, 144 or uncapped frames a second. A tap between two ticks still counts, and a long stall is clamped instead of spiraling into catch-up ticks.

  • M1.3
  • M3.4

Platform // In progress

Log files and fatal errors

On Linux the game finds its folders where the system expects them, writes each run's log there and keeps the last three, and logs and reports an error that ends the game before it exits. Windows and macOS will follow, with their windows.

  • M1.4
  • M1.5
  • M1.6

Graphics // In progress

OpenGL 3.3 on every desktop

An OpenGL 3.3 core context with vsync through EGL on Linux, and an offscreen context that the tests and these recordings draw with. WGL on Windows and NSOpenGL on macOS will follow.

  • M2.1
  • M2.2
  • M2.6
  • M2.7

Graphics // Shipped

A small graphics interface

A thin layer over OpenGL 3.3 core: textures, buffers, render targets, pipelines, instanced draws, scissor and readback, with a state cache. Using a destroyed resource is an error rather than a wrong frame, and a shader that fails to compile reports the driver's log.

  • M2.3

Quality // Shipped

Golden-image tests

Scenes of the renderer are drawn offscreen on Mesa's software renderer in CI and compared with committed reference images within a small tolerance. A scene that differs uploads what it drew and an image of where.

  • M2.4
  • M2.5

Graphics // Shipped

Sprites, y-sorting and a pixel-perfect camera

Sprites are sorted by layer and by where their feet are, so what stands lower on screen is drawn in front, then packed into instanced batches. A 2D camera draws at a fixed virtual resolution and scales it up by whole numbers, letterboxed, so every pixel stays square.

  • M3.3

Graphics // Shipped

10,000 sprites with no allocation

The stress scene draws 10,203 sprites in 1 batch, all from one page of the cooked atlas. A test counts every allocation the engine makes through Rust's allocator in the real loop and finds none in 100 steady frames, and sorting and batching such a frame takes about 0.74 ms on an i7-11800H.

  • M3.5

Simulation // Shipped

Input actions

Keys and mouse buttons map to named actions, buttons or 2D axes such as WASD, latched once a tick so that nothing pressed between ticks is lost. A text mode mutes the game's bindings while a text field has the keyboard, and the actions can be recorded for replays.

  • M3.1

Content // Shipped

Asset loading

Typed handles to assets loaded from files: the same path gives the same handle, and a development build shows a placeholder for anything missing. Worker threads read and decode the files while the game runs, and each frame makes the decoded ones within a budget of 2 ms: asked for a thousand at once, an optimized build spends under 4 ms of any frame on them. Assets load in scopes, global, level or transient, and unloading a scope frees what no other scope holds, GPU textures included.

  • M3.2
  • M5.3

Simulation // Shipped

Entity component system

Generational entities with sparse-set storage, resources, events, deferred commands and a schedule of stages with run conditions; a borrow conflict names the system that caused it. The engine's own systems move, collide, animate and draw entities.

  • M4.1
  • M4.4

Simulation // Shipped

Reflection

Macros describe structs and enums as dynamic values and back, with a registry of types and warnings for unknown fields. Asset handles are written as their files' paths and loaded again when read, and the engine's sprites, colliders and animators are reflected too: a sprite can name a frame of a cooked atlas, a collider its layers by name, an animator its clip. The determinism tests hash worlds through it.

  • M4.2

Simulation // Shipped

Collision and sliding movement

Boxes and circles, tile grids and a spatial hash, with move-and-slide in up to 16 substeps so that movers follow their path and slide along walls without snagging, plus sweeps, triggers and queries. 5,000 colliding entities tick in about 2.4 ms on an i7-11800H.

  • M4.3
  • M4.8

Graphics // Shipped

Tilemaps

Tile layers drawn a chunk at a time from a cache, culled to the view, with animated tiles. Tall tiles become y-sorted sprites, and the same map gives the collision grid: the field in the demo is one.

  • M4.5

Graphics // Shipped

Sprite animation

Clips of frames that loop, or play once and send an event at the end, played by an Animator component and mirrored by the way the entity faces.

  • M4.6

Quality // Shipped

Deterministic replays

The input is recorded tick by tick and played back with no window. A test replays a stored recording and compares a hash of the reflected world with the stored sequence, so a change that makes the simulation nondeterministic fails it. The hash leaves out what animation rewrites each frame, a sprite's frame and the way it faces.

  • M4.7

Content // Shipped

QOI and PNG images

QOI encoding and decoding for the game's textures, and a PNG decoder, with an inflate and Adler-32 of its own, for the asset cooker. Each has hand-made test vectors and a fuzz target.

  • M2.4
  • M5.1

Content // Shipped

Compression, binary JSON, fonts and WAV

KLZ, an LZ4-style block compression, and KBJ, a binary JSON with one encoding per value, both of which round-trip; and readers for BMFont's text format, as AngelCode, Hiero and other tools write it, and for WAV. All four reject malformed input and run under the fuzzer.

  • M5.2

Tools // Shipped

Asset cooking

An offline pipeline packs each folder's PNG images, Aseprite sprite sheet exports and bitmap fonts into an atlas: it trims each frame and glyph to its pixels, packs them into square pages with padding and extruded edges, keeps tile sets as pages of their own, and writes QOI pages with a file of where each frame went and the animation clips of the exports' tags, which it checks with the game's own reader, and a file of each font's glyphs. Data files become KBJ. The same files cook to the same bytes, and an incremental cache cooks again only what changed. The game loads an atlas before its first frame and finds its sprites and clips by name, each trimmed frame placed, and mirrored, where its whole cell would be: the demo's walking person comes from an export in Aseprite's format, written by hand, drawn pixel for pixel as before.

  • M5.4
  • M5.9
  • M5.5

Content // Shipped

Levels and prefabs

Prefabs inherit from one another: a prefab's components apply over its base's, and a level's overrides over both, onto one value, and a cycle, a chain of more than 16, an unknown component or a value that does not fit is refused, naming the file and the field, with no entity left behind. A level's entities spawn through their prefabs. The engine reads LDtk 1.5.3's projects and level files, and its own level and prefab files, strictly and under the fuzzer: a project saved by a version of LDtk other than 1.5.x is refused with the version to save it with, and every refusal names where in the file it is. The cook turns each level of an LDtk project into a level file, by conventions for what LDtk has no notion of: animated and tall tiles from a tile set's custom data, the grid from the Collision layer, and each entity the prefab of its name, its fields overriding the prefab's. What a level file cannot hold, such as a translucent layer or two tiles in one cell, is refused, naming the file and the member. A level loads whole, in place of the last: the engine draws its tile layers over its background, collides with its grid, spawns its tall tiles and its entities, and removes what it made when the next comes. The demo's field is an LDtk project, cooked to the map it was built from, cell for cell, with its trees and rocks as prefabs, and the game now draws it from its level file, frame for frame as it drew the map it built by hand.

  • M5.10
  • M5.11
  • M5.12
  • M5.6

Content // Shipped

Pack files

The cook writes every file it cooks into one pack, each file of the same bytes stored once, aligned, and compressed where that makes it smaller, and reads it back with the game's reader before writing it. The engine reads packs strictly and under the fuzzer, through the same virtual file system as loose files: a header and a table of contents checked against the limits, and the table against its CRC-32, before anything is allocated, each file found by a binary search of its path's hash, and each file's bytes checked against their CRC-32 as it is read, so that a damaged one is refused, naming its path. Release builds read the pack when there is one, and the loose files otherwise, and the release files and the download ship the pack alone; a pack that does not open fails every load with its error, and development builds read the loose files.

  • M5.13
  • M5.14
  • M5.7

Tools // Shipped

Hot reload

An edit saved while the sandbox runs in a development build, beside the cook's watch, is drawn in its window within half a second: a sprite's pixels, an Aseprite export's clips, a prefab, the LDtk level, or a shader. The cook, built once, cooks the edit again in about a fifth of a second and rewrites only the files whose bytes changed; the game's watchers look at the loaded files and the shaders every 250 ms. Each reload lands behind the handles the game holds: a texture draws its new texels, an atlas's clips are replaced by name, a prefab is replaced in its library, and the level is made again around its tile layers, clips and assets, its props spawned anew. A file that fails to load, or a shader that does not compile or fit its program, is logged, and what it would replace stays. With nothing changed, a frame allocates nothing, and release builds hold none of it. On Windows and macOS it comes with drawing there.

  • M5.15
  • M5.16
  • M5.17
  • M5.8

Audio // Planned

Audio mixer

A mixer of our own will play WAV and QOA-compressed sound through voices and buses, with constant-power panning, positional attenuation, fades and streaming, and a soak test will count underruns.

  • M6.1
  • M6.2
  • M6.6
  • M6.7

Platform // Planned

Audio output on every desktop

Sound will go out through ALSA on Linux, WASAPI on Windows and AudioUnit on macOS, and recover when a device is unplugged or plugged back in.

  • M6.3
  • M6.4
  • M6.5

Graphics // Planned

Bitmap text

Text will be drawn from bitmap fonts, with wrapping, alignment, kerning and color markup.

  • M7.1

Tools // Planned

Developer tools

An immediate-mode debug UI will carry a stats overlay, a profiler that exports traces, an entity inspector, a console, debug drawing and time controls, all left out of release builds.

  • M7.2
  • M7.3
  • M7.4
  • M7.5
  • M7.6

Platform // Planned

Gamepads

Gamepads will work through evdev, XInput and GameController.framework, with hotplugging, deadzones, bindings, and button prompts that follow the device in use.

  • M8.1
  • M8.2
  • M8.3
  • M8.4

Simulation // Planned

Game states, menus and settings

A state machine will run the game's screens and level transitions, menus will work with keys, mouse or gamepad, and a settings file will keep what it does not understand.

  • M9.1
  • M9.2
  • M9.3

Content // Planned

Saves

Saves will be written atomically with a backup and a checksum, and migrated from older versions.

  • M9.4

Simulation // Planned

Pathfinding and enemy AI

A* with a budget and path smoothing, and flow fields, will steer the sandbox's enemies.

  • M9.5

Release // Planned

Crash reports

Every panic will leave a report, and on Windows a native crash will leave a minidump, with symbols archived to read them.

  • M9.6
  • M11.3

Release // Planned

A complete level

One finished level of the real game will be built with the content pipeline, and played through by someone outside the team on each system.

  • M9.7

Graphics // Planned

Metal on macOS

A Metal backend will sit behind the same graphics interface, with every shader in MSL, match the OpenGL golden images, and become the default on macOS.

  • M10.1
  • M10.2
  • M10.3
  • M10.4

Release // Planned

Signed packages for every desktop

Packages for Windows, macOS and Linux will be signed and notarized in CI, and smoke-tested from the package before each release.

  • M11.1
  • M11.2
  • M11.4

Release // Planned

Release readiness

Every performance budget will be measured on a min-spec machine, the parsers will get a security pass, players will get documentation, and a release-candidate dry run will walk the release checklist.

  • M11.5
  • M11.6
  • M11.7
  • M11.8