# Nadir > Nadir is a 2D top-down game engine for desktop, written in Rust from the ground up by Joseph Abbott III, with no third-party crates: its own OS bindings, OpenGL renderer, ECS, collision, codecs and tools. https://jabbott.dev/Nadir/ plays the engine's sandbox game as commit 0cba65f draws it, recorded in CI, and lists the engine's features with how far each has come. Key facts: - Status: in development, 44 of 87 roadmap tasks done at commit 0cba65f of 2026-11-04 (M5.15 xtask: cook --watch; cook: write on change; M5.8 split (ADR-0042)) - Language: Rust 1.99.0, edition 2024; 18 crates in the workspace; third-party crates: None - Targets: Windows 10+ and Linux (X11) on x86_64 and aarch64, and macOS 12+ (Apple Silicon and Intel). Only Linux draws so far: the Windows and macOS windows (roadmap M1.5, M1.6, still to be checked by hand) stay empty until their OpenGL contexts land (M2.6, M2.7). - Out of scope: web and WASM, Android, iOS, consoles, 3D and networking - License: Apache-2.0 - Source: https://github.com/jabbott-iii/Nadir - Author: Joseph Abbott III, software engineer (https://jabbott.dev/resume/) ## Pages - [Engine demo](https://jabbott.dev/Nadir/): the recorded scenes, this build, the roadmap's milestones and every feature - [Repository](https://github.com/jabbott-iii/Nadir): the source, with the design in docs/ ## Scenes Each is drawn offscreen by the engine on the CI runner, one 60 Hz tick a frame, and encoded as VP9 WebM at twice the size: - [The field](https://jabbott.dev/Nadir/media/field.webm?v=0cba65f): 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.006 ms, the longest 0.023 ms. World hash: 298521738b5650ef. Drawn offscreen at 480 × 270 by llvmpipe (LLVM 20.1.2, 256 bits). - [10,000 sprites](https://jabbott.dev/Nadir/media/stress-sprites.webm?v=0cba65f): 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.123 ms, the longest 0.321 ms. World hash: c47e62a4e2bb26d6. Drawn offscreen at 480 × 270 by llvmpipe (LLVM 20.1.2, 256 bits). - [5,000 entities](https://jabbott.dev/Nadir/media/stress-entities.webm?v=0cba65f): 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.187 ms, the longest 2.316 ms. World hash: 28e943b7c2b0247e. Drawn offscreen at 480 × 270 by llvmpipe (LLVM 20.1.2, 256 bits). ## Download - [nadir-sandbox-linux-x86_64.tar.gz](https://jabbott.dev/Nadir/downloads/nadir-sandbox-linux-x86_64.tar.gz?v=0cba65f): the sandbox as this build drew it, for Linux on x86_64, X11 with OpenGL 3.3 and glibc 2.34 or newer; 0.9 MB; SHA-256 09e2bd9aa009070d2e28e352508d299308ee5fc8b182518d000e22e88e558fe4. Unpack it and run `./sandbox`; its README names the scenes and keys. ## Features 25 shipped, 4 in progress and 13 planned. Each feature is marked from the roadmap's ticks at 0cba65f, so the list moves as the tasks land. ### Shipped - No third-party crates (Quality): 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. Roadmap tasks: M0.1, M0.6. - Guardrails on every push (Quality): 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. Roadmap tasks: M0.6, M0.8. - Fuzzing and benchmarks (Quality): 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. Roadmap tasks: M0.7. - Math, color and geometry (Core): 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. Roadmap tasks: M0.2. - Handles, hashing and random numbers (Core): 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. Roadmap tasks: M0.3. - Logging, time and test tools (Core): 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. Roadmap tasks: M0.4. - JSON and JSONC (Content): 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. Roadmap tasks: M0.5. - Fixed-tick main loop (Simulation): 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. Roadmap tasks: M1.3, M3.4. - A small graphics interface (Graphics): 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. Roadmap tasks: M2.3. - Golden-image tests (Quality): 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. Roadmap tasks: M2.4, M2.5. - Sprites, y-sorting and a pixel-perfect camera (Graphics): 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. Roadmap tasks: M3.3. - 10,000 sprites with no allocation (Graphics): 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. Roadmap tasks: M3.5. - Input actions (Simulation): 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. Roadmap tasks: M3.1. - Asset loading (Content): 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. Roadmap tasks: M3.2, M5.3. - Entity component system (Simulation): 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. Roadmap tasks: M4.1, M4.4. - Reflection (Simulation): 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. Roadmap tasks: M4.2. - Collision and sliding movement (Simulation): 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. Roadmap tasks: M4.3, M4.8. - Tilemaps (Graphics): 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. Roadmap tasks: M4.5. - Sprite animation (Graphics): 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. Roadmap tasks: M4.6. - Deterministic replays (Quality): 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. Roadmap tasks: M4.7. - QOI and PNG images (Content): 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. Roadmap tasks: M2.4, M5.1. - Compression, binary JSON, fonts and WAV (Content): 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. Roadmap tasks: M5.2. - Asset cooking (Tools): 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. Roadmap tasks: M5.4, M5.9, M5.5. - Levels and prefabs (Content): 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 another version of LDtk 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. Roadmap tasks: M5.10, M5.11, M5.12, M5.6. - Pack files (Content): 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 their CRC-32 and the limits 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 alone, and the release files and the download ship nothing else; a pack that does not open fails every load with its error, and development builds read the loose files. Roadmap tasks: M5.13, M5.14, M5.7. ### In progress - Native windows on every desktop (Platform): 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. Roadmap tasks: M1.1, M1.2, M1.5, M1.6. - Log files and fatal errors (Platform): 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. Roadmap tasks: M1.4, M1.5, M1.6. - OpenGL 3.3 on every desktop (Graphics): 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. Roadmap tasks: M2.1, M2.2, M2.6, M2.7. - Hot reload (Tools): While the cook watches the assets, one saved is cooked again in about a fifth of a second for the sandbox's, by a cook built once, and the cook rewrites only the files whose bytes changed, each atlas page before the file that names it. The running game will reload what changed, textures, data, levels and shaders, behind the handles it holds, within a second of the save. Roadmap tasks: M5.15, M5.16, M5.17, M5.8. ### Planned - Audio mixer (Audio): 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. Roadmap tasks: M6.1, M6.2, M6.6, M6.7. - Audio output on every desktop (Platform): 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. Roadmap tasks: M6.3, M6.4, M6.5. - Bitmap text (Graphics): Text will be drawn from bitmap fonts, with wrapping, alignment, kerning and color markup. Roadmap tasks: M7.1. - Developer tools (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. Roadmap tasks: M7.2, M7.3, M7.4, M7.5, M7.6. - Gamepads (Platform): Gamepads will work through evdev, XInput and GameController.framework, with hotplugging, deadzones, bindings, and button prompts that follow the device in use. Roadmap tasks: M8.1, M8.2, M8.3, M8.4. - Game states, menus and settings (Simulation): 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. Roadmap tasks: M9.1, M9.2, M9.3. - Saves (Content): Saves will be written atomically with a backup and a checksum, and migrated from older versions. Roadmap tasks: M9.4. - Pathfinding and enemy AI (Simulation): A* with a budget and path smoothing, and flow fields, will steer the sandbox's enemies. Roadmap tasks: M9.5. - Crash reports (Release): Every panic will leave a report, and on Windows a native crash will leave a minidump, with symbols archived to read them. Roadmap tasks: M9.6, M11.3. - A complete level (Release): 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. Roadmap tasks: M9.7. - Metal on macOS (Graphics): 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. Roadmap tasks: M10.1, M10.2, M10.3, M10.4. - Signed packages for every desktop (Release): Packages for Windows, macOS and Linux will be signed and notarized in CI, and smoke-tested from the package before each release. Roadmap tasks: M11.1, M11.2, M11.4. - Release readiness (Release): 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. Roadmap tasks: M11.5, M11.6, M11.7, M11.8. ## Roadmap milestones - M0, Foundation and guardrails: 8 of 8 tasks done - M1, A window and events on every desktop: 4 of 6 tasks done - M2, GPU on every desktop: 5 of 7 tasks done - M3, Sprites, camera, actions: a moving character: 5 of 5 tasks done - M4, Simulation: ECS, reflection, collision, tilemaps: 8 of 8 tasks done - M5, Content pipeline: 14 of 17 tasks done - M6, Audio: 0 of 7 tasks done - M7, Developer tools: 0 of 6 tasks done - M8, Gamepads: 0 of 4 tasks done - M9, Vertical slice: 0 of 7 tasks done - M10, Metal backend (macOS): 0 of 4 tasks done - M11, Production hardening and release: 0 of 8 tasks done ## Optional - [Roadmap](https://github.com/jabbott-iii/Nadir/blob/0cba65ffede87d52e812c92f8c523b00c7d2cf6b/docs/roadmap.md): every task, with its acceptance criteria - [Architecture](https://github.com/jabbott-iii/Nadir/blob/0cba65ffede87d52e812c92f8c523b00c7d2cf6b/docs/01-architecture.md): the crates, their dependency matrix, the main loop and threading - [Decisions](https://github.com/jabbott-iii/Nadir/blob/0cba65ffede87d52e812c92f8c523b00c7d2cf6b/docs/decisions.md): the architecture decision records - [jabbott.dev](https://jabbott.dev/llms.txt): the author's site: resume, engineering log and other projects