60 fps @ 4K
Sustained average ≥ 60 on a scripted multi-biome pan-and-fly. Phase 2 exit gate.
A high-performance voxel sandbox engine and game, written in Rust. AI-first by architecture — every player action and world query flows through a structured command bus that AI agents, mods, and our own test harness use as peer clients via MCP.
A two-minute clip from the live build, plus the latest panoramas — one of them in colored-LOD debug mode so you can see how the world is sliced into streaming levels.
A private learning project to practice software engineering at a serious level: custom engine, strict best practices, maximum performance focus. The game itself is a long-term target; the engine work is the primary learning vehicle.
A persistent, procedurally generated voxel sandbox in the spirit of Minecraft, but with far deeper open-ended progression, Factorio-style automation, a custom high-performance engine, and a native AI agent interface as a first-class citizen — the project's defining USP.
Performance, efficiency, and optimization are non-negotiable in this project. They are preconditions for every merge, not something polished later.
The defining architectural constraint, from day one. Player input, AI agents over MCP, and mods all dispatch the same commands and read the same queries. There is no separate "AI mode" — the AI is a peer client of the same bus the player uses.
Move · Look · PlaceVoxel · BreakVoxel · Quit · …
Every player action is a structured command. Player input translates winit events into the same enum any other client emits.
VoxelAt · PlayerPose · LoadedBrickCount · BiomeAt · …
A single perception API returns surroundings, inventory, stats, biome, lighting. UI and agents share it byte-for-byte.
Recorded sessions replay byte-identically.
All state changes flow through the bus, time-stamped. Free debugging, regression tests, and AI training data — and a Phase 1 acceptance test that locks it in.
Claude drives the running game through the same MCP surface.
The dev AI is a peer client. Walk scripted scenarios, verify invariants, reproduce bugs, smoke-test new features. Dogfooding the USP is our continuous QA.
From Concept.md § 3 — the constraints that decide what merges and what doesn't.
Custom engine, multithreaded, data-oriented, multi-km view distance. Every milestone carries a wall-clock perf budget; no budget means no merge.
Every player action and world query is a structured command, shared by human input, AI agents over MCP, and mods. No parallel mod API, no separate AI mode.
The development AI drives the game as a peer client of the same AI interface. Dogfooding the USP is the continuous QA loop.
Fully destructible / buildable world. No duping. Everything must be mined.
No "Netherite + Elytra" dead end. Always something to improve. Capability/tech-tier progression rewards new systems, not just bigger numbers.
Factorio-like full automation of mining and production chains. Every production chain fully automatable by design.
Deep modding API, identical to the AI / player command API. One surface to learn, maintain, and document.
One number isn't enough. The ceiling proves the engine lights up a strong GPU at high settings. The floor proves the client stays playable on weak hardware. The floor gate is not optional.
Sustained average ≥ 60 on a scripted multi-biome pan-and-fly. Phase 2 exit gate.
Single-player, client embeds world + generator. No dedicated server. CPU mesher fallback retained for this gate.
Every PR ships with the measured budget number, the target hardware, and a line-by-line delta from the previous milestone. Below: the wins that earned the most ground, drawn straight from PR descriptions. Both before and after numbers are verbatim — no synthetic averages, no aggregated lines that quietly mix metrics.
Async ring of in-flight slots, shared Arc<MesherPipelines> across all 5 LOD streamers. Initial GPU integration was a regression on flying scenes (700 → 400 fps) until six layered optimisations — encoder coalescing, lazy creation, slot-count tuning — recovered parity. The standing-still win is the headline; the architectural unlock for #94, #98, #100 is the real story.
Per-brick frustum cull moved entirely to compute. The cull_cpu Tracy span isn't entered when effective_frustum_culling() == Gpu.
Hi-Z mip chain after the depth prepass. Six layered shader optimisations (cached bind groups, half-res mip 0, SPD-style 5-mip-per-pass) earned the win.
CPU frustum cost reduced to zero in the GPU path; occlusion stage cuts dense-cave fragment work materially. The 7.5% open-scene overhead is the price of the test infrastructure when there's nothing to find — counted as fair trade.
Two single-line insights, back-to-back. #52 prunes stale entries from the dirty queue each dispatch instead of walking them. #53 skips the per-LOD scan entirely when the camera's brick coordinate hasn't changed. Combined: 5.4× steady-state FPS for ~zero added complexity.
Brick-load sun-column recomputes deferred onto a per-frame µs-budgeted side queue; edits use an incremental darken-relight BFS bounded by light radius. Mean is flat (within noise) — what changed is the worst case. Streaming bursts spread their cost instead of spiking one frame.
The architectural pivot of the M7-X-3b series. Eliminates per-slot readback latency by having the GPU mesher write straight into the persistent MeshCache slabs. Removes the GPU back-pressure bottleneck that was strangling cold-start; unlocks the downstream chain (#90 → #94).
The dispatch queue used to re-score every brick every frame. Caching the scores and rebuilding only every 16 frames closes a 47-second cold-start gap to 3 seconds (−93%). +17% drain rate; +111% fps cold-start. Sets up #94 to land later the same day.
Hillis-Steele inclusive scan replaces a single-threaded serial loop. 256-thread workgroup, 8 barrier rounds. ~14× speedup; render-side bonus from shorter mesher dispatches reducing storage-buffer barrier waits.
Most slices in a solid brick are AIR with no exposed faces. A workgroup-shared any_non_air atomic gates the lane-0 greedy-merge scan; empty slices return zero quads instead of paying the full 1024-cell scan.
Stacks on #98. Per-row row_has_any flags skip the lane-0 walk over entirely-empty rows. Cumulative effect against main is the headline: brick throughput +20% per cold-start (52808 → 63344).
Skip uploads of trivial bricks (uniformly dark or uniformly fully lit). Pool shrinks from 3.2 GiB to 191 MiB — a 3 GiB reduction per LOD. Drain rate +49%. The blueprint that #166 then mirrored on the sun channel and #160-165 generalised to dynamic growth.
Multi-LOD GPU pools used to allocate at full cap upfront, OOM-ing on iGPU. The grow-on-demand series replaces the conservative-cap mitigation with the proper architectural fix: small initial cap, grow at the streamer-driven trigger, free-list ordering preserved across grow boundaries. Bind-group revision counter rebuilds consumers atomically.
Content-addressed brick snapshots with BLAKE3 hash, atomic manifest swap, off-thread Compactor with cancellation. Six PRs in one session (#204 → #209) close the Concept doc's last open question on persistence.
| PR | date | change | gain | tag |
|---|---|---|---|---|
| #20 | Apr 25 | scalable graphics settings framework — Low / Med / High / Ultra runtime reconfig | — | M1 |
| #22 | Apr 25 | GPU-driven draw — slab cache, depth prepass, indirect MultiDraw | — | M2 |
| #28 | Apr 25 | LOD render integration — multi-km horizon | — | M3-4 |
| #30 | Apr 25 | L1-L4 surface cap + F8 skip-bypass | 14 → 18.9 ms stream-sweep | M3-5 |
| #31 | Apr 25 | edit cascade — L0 edits propagate up to L1-L4 | ~1 ms / edit | M3-6 |
| #36 | Apr 25 | parallelize GPU mesher — per-slice workgroups + prefix sum | ~4× cold-fuzz | M5-4a |
| #40 | Apr 26 | flip Hi-Z default-on for High / Ultra | +37.5% valley | M6-4 |
| #49 / #51 | Apr 26 | live-frame telemetry + per-stage breakdown (stream / mesh / upload) | infra | bench |
| #62 | Apr 26 | per-vertex lighting through GPU mesher + shader (visible) | flat no regress | M7-4b |
| #66 | Apr 27 | cascaded LOD lightmaps + boundary slabs | −28% p99 | M7-X-2/4 |
| #67 | Apr 27 | visibility raycast + inverse-square block light | visual | M7-X-3a |
| #71 | Apr 27 | per-vertex sampling + Minecraft-style AO | +61% iGPU held 6.2× | M7-X-5 |
| #75 | Apr 27 | persistent GPU brickmap — residency + hash | arch | M7-X-3b PR1 |
| #77 | Apr 27 | persistent GPU lightmap — block-channel pool | arch | M7-X-3b PR2b |
| #78 | Apr 27 | GPU compute block-light propagator | 8× edit dispatch | M7-X-3b PR2c |
| #84 | Apr 28 | cold-start drain fix at Ultra — frustum-priority + budget | 12 min → 3 min | M7-X-8 |
| #85 | Apr 28 | cap dirty set + drop dispatch prune (neighbour-fan) | 30-43 → 4-10 ms mesh_us | M7-X-8 |
| #88 | Apr 29 | prewarm cache slabs at GPU-mesher takeover | −34% p99 | M7-X-9 |
| #90 | Apr 30 | top-K dispatch selection (vs full sort) | −22% frame, +28% fps | M7-X-11 |
| #92 | Apr 30 | parallel build_mask + downsample shader work | −57% pass_count | M7-X-13 |
| #96 | Apr 30 | SSAO replaces per-vertex AO (seams gone) | +3.7 ms iGPU held 6.2× | M7-X-18 |
| #105 → #108 | May 1 | M7-X-7 GPU sun propagator series — persistent sun-lightmap, per-frame dispatch, batched readback, Box-alloc skip | arch | M7-X-7 |
| #119 | May 2 | flip production L0 mesher to GPU-pool-read lighting | arch | M7-X-7 |
| #122 | May 2 | per-LOD GPU pools + flip gpu_sun_propagator default-on | arch | Phase B PR1 |
| #127 | May 2 | drop 25% pool-sizing headroom (VRAM-1) | vd=256 fits 4 GiB iGPU | VRAM-1 |
| #130 | May 3 | L0 pool LRU eviction + adapter-aware capacity | 2625 → 2462 µs vd=384 iGPU | VRAM-2c |
| #135 | May 3 | per-LOD pool allocation + VRAM cap | 5% peak VRAM vd=384 iGPU | Phase B-1 b |
| #137 | May 3 | mesher reads per-LOD pools at L1+ — visual gate | 5431 µs noise | Phase B-1 d |
| #139 → #142 | May 3 | multi-shard pool prep — brickmap → lightmap → sun-lightmap → switch-dispatch | arch | Phase B-2 |
| #147 | May 5 | distance fog over outer L4 ring | visual | § 3.1 |
| #148 | May 6 | integer-keyed map → FxHash | tighten | § 2.3 |
| #151 | May 6 | rate-limit L0 load-propagation GPU upload | stalls gone | § 4.5 |
| #152 | May 6 | pre-size per-LOD hash tables on view distance | tighten | § 2.3 |
| #153 | May 7 | pre-size dirty + in_flight queues on view distance | tighten | § 2.3 |
| #154 | May 7 | pool BFS dist + queue scratch in propagate_brick | tighten | § 1.2 |
| #155 / #156 | May 7 | initial sun-guess + in-brick lateral spread (cave fade at cold-start) | visual | § 4.4 A/A2 |
| #157 | May 7 | stack-allocate blocked_above scratch | tighten | § 1.2b |
| #158 | May 7 | stack-allocate sun_in scratch | tighten | § 1.2 |
| #160 → #162 | May 8 | grow APIs · GpuBrickmap → GpuLightmap → GpuSunLightmap, bind-group revision wiring | plumbing | § 2.6 1-3 |
| #164 | May 8 | GPU pool lazy-grow at startup (4 doubling grows during cold-start) | 2.4 GiB → 256 MiB −9× | § 2.6 phase 4 |
| #166 | May 8 | sparse sun-channel lightmap pool (mirror of #146) | arch | § 1.1b |
| #168 | May 9 | rate-limited L1+ GPU pool load path | arch | § 1.1b |
| #170 | May 9 | HDR pipeline + Reinhard tonemap | −535 µs noise | § 3.4 |
| #176 | May 9 | exposure tune 1.5 → 2.5, ambient floor 0.05 → 0.02 | visual | § 3.4 |
| #177 | May 9 | half-res bloom + separable Gaussian blur | visual | § 3.3 |
| #181 | May 12 | coalesce FreeList push (mesh-cache eviction phase 1) | refactor | § 2.4 |
| #184 | May 12 | GPU compaction for mesh-cache eviction | arch | § 2.4 phase 2 |
| #187 | May 12 | skip mesher dispatch + invalidate mesh on eviction | fix | § 4.4 sticky-sun |
| #188 | May 12 | cache worldgen underground mask per (level, BrickCoord) | tighten | § 3.4 |
| #189 | May 13 | wire MSAA 4× anti-aliasing | visual | § 3.2 |
| #190 → #193 | May 13 | per-quad vertex pipeline · A → B.1 → B.2 → C, deletes legacy path | arch | § 1.5 |
| #195 | May 13 | incremental score cache for CPU mesh-streamer (mirror of #93) | tighten | § 1.6 |
| #198 → #201 | May 14 | GPU sun propagator · scaffold → real LDS-iterative BFS shader → wire L0 streaming-load | arch | § 2.5 |
| #202 | May 14 | load-time block-light propagation fix | fix | § 4.4 b |
| #203 | May 14 | spawn warmup + loading screen | UX | § 2.2 |
| #204 → #208 | May 14 | M8 plumbing · overlay + BLAKE3 → SnapshotWriter → Compactor → hydrate → wire main loop | arch | M8 PR1-5 |
| #223 | May 15 | batch template writes + spare quad slab in compact() | 34.5 → ~10 ms compact_us | M9 b1 r3 |
| #228 | May 15 | sprint fly 12.5 → 50 m/s + slow_frame threshold 250 → 50 ms | diag | Bug A |
| #231 | May 15 | replace warm-stable with packed-queue bench (--bench default 60s) | infra | bench |
| #234 | May 16 | lighting drain skips the 7× neighbour fan | 7 → 1 dispatch composition | Phase 2.B PR-F |
| #235 | May 16 | speculative-bright + final-on-bounding (per-brick cascade 7 → 2) | mesh p50 −45%, frame p50 −10% | Phase 2.B PR-G |
| #240 | May 16 | per-frame GPU dispatch rate-limit on L0 load loop — safety net for PR-2 | infra enables PR-2 | Bug A PR-1 |
| #241 | May 16 | bounded upward probe in compute_entry_sun_for_brick — re-land #227 under cap | 65 → 78 fps cold · 58 → 66 fps sprint | Bug A PR-2 |
| #242 | May 16 | par_iter L0 light-prep on rayon | 78 → 83 fps cold · 66 → 72 fps sprint | Bug A PR-3 |
| #243 | May 16 | velocity-weighted candidate priority (deprioritise bricks behind direction of travel) | structural curving motion | Bug A PR-5 |
methodology: numbers quoted verbatim from PR descriptions in the private repo. Ceiling measurements taken on RX 6800 XT at 2560×1440 or 4K Ultra with view distance 640 m unless noted. Floor measurements taken on Radeon 880M iGPU (notebook proxy for the Steam Deck APU) at 2560×1600 Ultra / vd=256 m. Per-PR measurements are not directly comparable across PRs (different hardware, view distance, and metric per row). For continuous time-series, see the gaym-perf CI gate in the repo.
Phase 1 wraps the engine backbone. Phase 2 turns it into a playable world. Phases 3–5 layer game systems, networking, and the public AI/modding surface on top.
Cargo workspace · command bus · sparse brickmap · single-biome procedural gen · async streaming · binary greedy meshing · forward renderer · capsule physics · mine + place · event-log persistence · MCP harness · Tracy + perf gates.
GPU-driven render, LOD cascade, voxel lighting, event-log compaction landed. Up next: terrain realism, biomes, day/night, inventory + crafting, survival basics, and the first-party blueprint tool.
Automated mining, crafting / processing, logistics / transport. Capability-tier progression rewards. Mobs and combat earliest here, alongside progression. Linked-chest crafting unlocks; basic chests get upgrade paths.
Authoritative CLI server. Delta sync rides the same event log used for single-player persistence. Server flags govern external MCP access policy; local mods and in-game NPC agents continue to use the bus directly.
foundation · PlayerId envelope, wire codec, per-player Players map, headless gaym-server crate (#210 → #213, landed early under M8 tailwind)
External-facing MCP with auth, polished public endpoint. Full modding API. FSR / upscaling integration. Note: the AI-first architecture ships from Phase 1 — Phase 5 only opens the public door.
Hand-curated chapters from the real git history. Solo project, trunk-based, every change goes through a self-reviewed PR — even when the reviewer and the author are the same person.
Workspace scaffold lands first. Cargo workspace with the initial nine-crate split, CI running fmt + clippy + test on every push, an empty winit window cleared by wgpu. Then the architectural spine: the command / query bus, with deterministic replay locked in by acceptance test before a single piece of gameplay code exists.
The remaining ten Phase 1 milestones land in the next 24 hours. Brickmap with a homogeneous-brick fast path, deterministic procedural terrain, async streaming around the camera, binary greedy meshing, the forward renderer hitting the Phase 1 perf gate, capsule physics with auto-step, mine + place through the bus, append-only Zstd event-log persistence, and — the milestone that makes the USP real — the minimum-viable MCP endpoint that lets Claude drive the running game through the same bus the player uses.
Phase 2 opens with the graphics settings framework — Low / Medium / High / Ultra, runtime reconfig, no app restart. Then the engine track that will carry the rest of the phase: per-brick CPU draw calls give way to indirect MultiDraw behind a depth prepass; an N-level LOD cascade reaches multi-km horizons with transition cells; frustum culling moves to a compute shader feeding the indirect arg buffer; meshing follows it onto the GPU (CPU mesher retained as floor-gate fallback); and a single-phase Hi-Z occlusion pass cuts the dense-cave draw count by 40 %+.
M7 turns out to be the longest milestone of the project. Per-vertex sun + block lighting through the GPU mesher and shader. A flood-fill propagator with a per-frame budget guarded for the floor gate. Then the deeper rewrites: a GPU-side sun propagator, a multi-LOD lighting pool so distant LODs don't lose their light contribution, and a long tail of L1+ correctness work. Every iteration measured against the dual-gate budget; the CPU mesher stays callable on Low.
Self-audit phase. Numbered sections close every loose thread the engine track left behind: § 1.5 rebuilds the surface around PerQuad data and deletes the legacy vertex/index path; § 2.4 adds GPU compaction for mesh-cache eviction; § 3.2 wires real 4× MSAA; § 4.4 chases down a sticky-sun cave-flood lighting bug across three attempts. § 2.6 alone runs to 13 commits.
The 1m-vs-0.5m foot-gun. Player position lives in metres while every voxel-aware surface speaks integer voxel indices. Internal physics, shaders, brickmap, terrain and lighting all stay metric — but the boundary surfaces (MCP queries, HUD, debug) switch to voxel-space-native units. Query::Raycast drops RayHit.t_m; the bus's own doc gets to delete its TODO.
Phase 2 M8 in six PRs, single session. Closes Concept.md's last open question on persistence: content-addressed brick snapshots over an event-log baseline, BLAKE3-hashed, atomic manifest swap, off-thread Compactor with cancellation, snapshot-preferred hydrate with corruption fallback, idle + size-threshold cadence, and an acceptance suite that proves the 100 k-edit ratio (observed: 0.9 %), byte-for-byte round-trip, and recovery after kill-mid-compaction.
Phase 4 ground-laying shipped in the same M8 session. PlayerId envelope on every Recorded bus event (event-log v1 → v2), serde derives + wire codec on the bus, a per-player Players map refactor that collapses Camera / Body / Intent / Mode into one keyed collection, and the headless gaym-server crate skeleton with its own tick loop. No netcode yet — just the seams that make netcode possible without bleeding into the singleplayer hot path.
Phase 2's terrain-realism milestone opens. World-bounds constants pulled into core; the single-biome plain gives way to a ridged-FBM heightmap with domain warp; a LOD-invariant fix keeps mountain ridges crisp across cascade transitions. A closest-first lighting drain (M9 bug 1, round 1) lands as a side fix — the rounds that close it land in the next entry.
Rounds 1 and 2 didn't close it. Round 3 ships probe-first instrumentation for slab-grow and the L0 pipeline (#222), batched template writes plus a pre-allocated spare quad slab in compact() (#223 · compact_us 34.5 → ~10 ms), and the actual fix — recursive L4 coverage with an inverted-gate correction in compact() (#224). Three PRs, one session.
Two-day perf hunt. #226 opens with discriminator probes for the L0 load loop; #227 looked like a fix — bounded upward sun probe, O(N) → O(1) — until #230 reverted it after cold-start cratered 65 → 14 FPS at Ultra vd=640. The CPU win let the 3 ms L0 budget fire ~30× more GPU sun dispatches per frame and saturated the swapchain queue. The bench infrastructure itself wasn't measuring the right state: #231 replaces the warm-stable bench with a packed-queue gate that records the operating point the user actually plays at. #228 bumps sprint fly to 50 m/s and drops the slow_frame threshold to 50 ms; #229 + #232 split render_us into CPU acquire / encode / present + GPU color pass; #233 re-bakes the discriminator. Phase 2.B closes the lighting-side wins: #234 drops the 7× neighbour fan from lighting drain, #235's speculative-bright + final-on-bounding cuts per-brick same-LOD cascade from 7 → 2 bakes (mesh p50 −45 %, frame p50 −10 %, in_flight −50 %; the L0 dirty queue drains to zero and stays there).
Phase 2 of Bug A formally closes mid-investigation. A fresh attack — driven by a finding memo arguing the lighting-bottleneck framing was wrong — pins the actual root cause: a per-frame GPU dispatch throughput ceiling on the L0 load loop, not a lighting cost. Phase 1 ships in five PRs. #239 instruments per-frame L0 GPU dispatch counters (PR-0). #240 adds the per-frame GPU dispatch rate-limit on the L0 load loop (PR-1, the safety net). #241 re-lands #227's bounded probe under the new cap (PR-2 · cold-start 65 → 78 FPS · sprint 58 → 66 FPS — first 60 FPS sprint gate since the M9 saga). #242 moves L0 light-prep onto rayon (PR-3 · cold-start 78 → 83 FPS · sprint 66 → 72 FPS). #243 deprioritises bricks behind the player's direction of travel (PR-5, structural — visible win is for curving motion).
Every panorama up top is the result of dragging the engine out of one of these holes. Honest devlog — every shot has the actual bug in the caption.
Months of fights with the LOD streaming system: seams between levels, whole chunks missing, levels stacked on top of each other, holes the sky shines through.
Sun and block lighting going wrong in every direction — underground lit up, surface dark, cave torch failing, far LODs too dim.
Screen-space ambient occlusion flickering on distant terrain.
Issues caught by the worldgen preview HTML before they made it into the world.
Custom engine, no Unity, no Unreal. Z-up, right-handed. Sparse brickmap (32³) plus a five-level LOD cascade. Binary greedy meshing in compute, CPU fallback always callable.