Documents every module's responsibility, the tick pipeline, the sim::lighting shared-truth invariant (sun direction shared between Rust and WGSL via shader_source.rs), and the test coverage. Companion to DEPLOY.md — that one covers ops, this one covers code.
6.5 KiB
voxel-game architecture
Functional-core / imperative-shell split. Every game-logic
transformation is a pure function (value, value) -> value; the shell
threads them through the GPU + winit + JS bridges each tick.
Module map
src/
├── lib.rs module declarations + run()
├── main.rs native binary entry
├── app.rs IMPERATIVE SHELL — App + winit handler + tick().
│ Composes sim/ + net/ + render/ through bridges.
├── bridges.rs THE ONLY MUTABLE-STATE OWNER. thread_local cells
│ for touch input, game status, network, settings,
│ plus the wasm-bindgen JS interface. Typed
│ accessor functions hide the RefCells.
├── camera.rs Camera + InputState + KbHeld (pure value types).
├── world.rs World + Chunk + Block + raycast + natural_surface_y.
├── mesh.rs Vertex + build_chunk_mesh (greedy meshing + AO);
│ emit_oriented_box + name_hash for remote players.
├── proto.rs Wire format for the multiplayer server.
├── shader.wgsl Terrain + sky + outline pipelines (WGSL).
├── post.wgsl Post-process pass-through.
├── shader_source.rs Assembles shader source by prepending Rust-
│ derived constants (DAY_PERIOD, SUN_OFFSET) to
│ shader.wgsl — one source of truth shared with
│ sim::lighting.
│
├── sim/ PURE SIMULATION CORE. No GPU, no winit, no JS.
│ ├── mod.rs re-exports
│ ├── body.rs PlayerBody { feet, velocity, hp, alive, ... }
│ ├── collision.rs AABB + sweep_axis(world, feet, delta, axis) →
│ │ (Vec3, bool)
│ ├── edit.rs apply_edit(world, EditRec) + chunks_for_edit
│ ├── event.rs SimEvent { Landed, VoidDeath, BlockEdited }
│ ├── input.rs TouchBridge + merge_held(kb, bridge) → KbHeld
│ ├── lighting.rs ⭐ SHARED SUN/SKY TRUTH ⭐
│ │ sun_direction(t), day_strength, twilight_amount,
│ │ LightingFrame::at(t), is_in_direct_sun(world, p, t).
│ │ Constants DAY_PERIOD / SUN_OFFSET exported to the
│ │ WGSL shader via shader_source.rs so the visual
│ │ sun direction can never disagree with the
│ │ mechanical one consumed by future mob burn /
│ │ plant growth / shade pathfinding.
│ ├── physics.rs step_movement(world, body, MoveInput) →
│ │ MoveOutcome { body', [SimEvent] }
│ ├── spawn.rs find_safe_spawn, fall_damage
│ └── visibility.rs compute_visible_chunks (frustum + radial cull)
│
├── net/ PURE NETWORK PARSING
│ └── mod.rs parse_inbox([raw line]) → [NetEvent]
│
└── render/ GPU SHELL — wgpu device, surface, pipelines
├── mod.rs Renderer struct + new() + all draw methods
├── pipelines.rs Pipeline + bind-group-layout factories
├── scene_target.rs depth / scene-color / post-bind-group helpers
└── uniform.rs CameraUniform (matches WGSL camera.frame)
+ OutlineVertex + ChunkBuffers
Tick pipeline
app::App::tick() is one frame, in this order:
dt + scaled shader_time
↓
(paused?) clear inputs, render last frame, return
↓
drain touch bridge → per-tick input snapshot
↓
honor respawn-request one-shot
↓
drain net inbox → parse_inbox → fold NetEvents into world + remote-player map
↓
send Hello on (re)connection
↓
apply mouse look to camera (yaw/pitch)
↓
sim::step_movement(world, body, MoveInput) → (body', [SimEvent])
↓
fold sim events into damage
↓
camera.position = body.feet + EYE_HEIGHT * Y
↓
raycast → break/place → maybe broadcast edit
↓
apply damage; periodic state broadcast
↓
render_frame(settings, outline) → Renderer
The shell only mediates between pure morphisms and side-effecting boundaries (GPU upload, network outbox, JS thread-locals).
The lighting invariant
Why this matters: in Minecraft-class games it's common for the visual shadow renderer and the "mob burns in sunlight" logic to use different sun directions / different occlusion tests, and they drift. A mob can stand in visible shade and burn anyway.
We avoid that by construction:
DAY_PERIOD, SUN_OFFSET (defined once in sim::lighting)
│
┌───────────┴───────────┐
▼ ▼
sim::lighting::sun_direction(t) shader_source::wgsl_constants_header()
│ │ (prepended to shader.wgsl)
▼ ▼
is_in_direct_sun(world, p, t) shader.wgsl::sun_direction(t)
│ │
▼ ▼
mob burn, plant growth, AI pathing visual shading (terrain + sky)
When real shadows land in the shader, they ray-march the same voxel
grid using the same sun_direction(t). Visual shadow edges will
agree with mechanical sun edges by construction.
Test coverage (53 tests as of this writing)
mesh: 8 — winding correctness, greedy meshing, AO darkening, oriented-box windingsim::body: 3 — damage, immunity-when-dead, respawn resetsim::collision: 3 — air sweep, ground snap, no-clip invariantsim::edit: 2 — u8↔Block roundtrip + garbage fallbacksim::input: 5 — merge_held release semantics (the joystick-stick bug)sim::lighting: 4 — night below horizon, lit at noon, occluded under overhead, ramp shapessim::physics: 4 — gravity, jump-only-when-grounded, Landed event, void death, dead-body-doesn't-movesim::spawn: 7 — fall damage boundaries; spawn anchored to natural surfacenet: 4 — malformed dropped, Welcome / Edit / orderworld: 4 — terrain determinism, raycast hits/missesshader_source: 2 — header contains constants, terrain source contains bodyapp: (no unit tests — orchestrator; coverage comes from the pure pieces it composes)
Running: cargo test --lib from the repo root.