Architecture Overview
Nightshade is a layered dependency graph. Each layer builds on the one below it, and no layer references anything above it. This chapter walks the layers from the bottom up.
Feature Layer: Terrain, Particles, NavMesh, Grass
|
Application Layer: State Trait, Main Loop, Event Bus
|
Rendering Layer: Render Graph -> Passes -> Materials -> Textures -> Shaders
|
Simulation Layer: Physics (Rapier), Animation, Audio (Kira)
|
Core Layer: World, Transform Hierarchy, Input, Time, Windowing
|
Foundation Layer: ECS (freecs), Math (nalgebra_glm), GPU (wgpu)
Foundation layer
The bottom of the graph is three independent systems that everything else depends on.
freecs is the ECS. Its ecs! macro generates a World struct, the per-component storage in struct-of-arrays layout, the query methods, and the entity allocator. The whole thing compiles down to typed accessors over packed Vecs with no virtual dispatch and no unsafe. The result is the kind of cache-coherent access that a hand-rolled archetype storage gives.
nalgebra_glm is the math library. Vectors (Vec2, Vec3, Vec4), matrices (Mat4), quaternions (Quat), and the linear algebra operations that go with them. Nightshade uses nalgebra_glm exclusively. There is no glam anywhere in the codebase.
wgpu is the GPU API. One Rust surface targets Vulkan, Metal, DirectX 12, and WebGPU. Every rendering pass in Nightshade is built against wgpu directly.
Core layer
Built on the foundation, the core layer manages per-frame state and the entity hierarchy.
World is the central data container generated by the freecs::ecs! macro. It holds all entity storage and a Resources struct of global singletons grouped by domain (timing, input, graphics settings, caches, physics world, audio engine, and the rest).
The transform hierarchy propagates LocalTransform through parent-child relationships to compute GlobalTransform matrices each frame. A dirty-flag system ensures only modified subtrees recompute. Manually setting a transform requires a mark_local_transform_dirty(world, entity) call so the next propagation pass picks up the change.
Input aggregates keyboard, mouse, and gamepad state each frame into world.resources.input. Both polling (is_key_pressed) and event-driven (on_keyboard_input) access patterns are available.
Time lives at world.resources.window.timing. The fields are delta_time, frames_per_second, uptime_milliseconds, frame_counter, and raw vs. speed-adjusted variants.
Windowing wraps winit. Window creation, event handling, and surface management all flow through it. On native platforms, secondary windows are supported through world.resources.secondary_windows.
Simulation layer
Systems that update world state every frame, independent of rendering.
Rapier3D runs the physics at a fixed 60 Hz timestep, with interpolation so rendering stays smooth between physics ticks. Rigid bodies, colliders, character controllers, joints, and raycasting are exposed through the physics feature flag.
Animation samples skeletal animations loaded from glTF files. Blending, crossfading, speed control, and looping are all part of the runtime. Bone transforms are written directly into the ECS each frame so the renderer reads them the same way it reads any other transform.
Kira drives audio. Sound playback, spatial positioning, and distance attenuation sit behind the audio feature flag.
Rendering layer
The rendering layer turns ECS state into pixels.
The render graph is a dependency-driven frame graph built on petgraph. Passes declare which resources they read and write through named slots. The graph builds the dependency edges, topologically sorts the passes, computes resource lifetimes, aliases transient GPU memory between non-overlapping passes, and picks the load and store operations that fit.
Passes implement the PassNode trait. Each pass owns its GPU pipelines and bind group layouts. The built-in passes cover shadow mapping, PBR mesh rendering, skeletal animation, grass, particles, text, UI, and post-processing (SSAO, SSGI, SSR, bloom, depth of field, tonemapping, and effects).
Materials use a PBR metallic-roughness workflow stored in a MaterialRegistry. Materials reference textures by name and attach to entities via MaterialRef.
Textures live in a TextureCache that handles GPU upload and format conversion. The way to load a texture is to push a WorldCommand::LoadTexture onto the command queue.
Shaders are written in WGSL and embedded into the binary at compile time via include_str!.
Application layer
The interface between the engine and game code.
The State trait is what the game struct implements. Its methods (initialize, run_systems, configure_render_graph, the input handlers) are called by the engine at fixed points in the frame lifecycle.
The main loop drives the lifecycle. It processes events, updates input, calls run_systems, dispatches events, runs the frame schedule, renders, and presents.
The frame schedule is a data-driven ordered list of engine system functions stored at world.resources.schedules.frame. It dispatches audio, camera, physics, animation, transform propagation, and cleanup systems each frame. A game can insert, remove, or reorder systems from initialize.
The event bus at world.resources.event_bus provides decoupled communication between systems. Typed app events and input messages flow through it.
Feature layer
High-level gameplay systems built on everything below.
| Feature | Dependencies |
|---|---|
| Terrain | Rendering (mesh generation, tessellation), Physics (heightfield collider) |
| Particles | Rendering (GPU billboard pass), ECS (emitter component) |
| NavMesh | Physics (geometry), ECS (agent component), Core (transforms) |
| Grass | Rendering (instanced GPU pass), ECS (region component) |
Data flow per frame
A frame's data flow runs the layers top to bottom and then renders.
Input Events (winit)
|
v
Input State (world.resources.input)
|
v
Game Logic (State::run_systems)
|
v
ECS Mutations (spawn, despawn, set components)
|
v
FrameSchedule Dispatch:
|-- Audio initialization and update
|-- Camera aspect ratios
|-- Physics Step (Rapier simulation, sync back to ECS)
|-- Animation (bone transforms written to ECS)
|-- Transform Propagation (LocalTransform -> GlobalTransform)
|-- Instanced mesh caches, text sync
|-- Input reset, deferred commands, cleanup
|
v
Render Graph Execution
|-- Shadow Depth Pass (reads GlobalTransform, Light)
|-- Geometry Passes (reads GlobalTransform, RenderMesh, MaterialRef)
|-- Post-Processing (reads scene_color, depth, normals)
|-- UI Pass (reads retained UI state)
|-- Blit Pass (writes to swapchain)
|
v
Present (wgpu surface present)
The game touches the top of this graph (input state, game logic, ECS mutations). The engine handles the rest.
Feature flags
Most subsystems are opt-in through Cargo features.
| Flag | What it enables |
|---|---|
engine | Core rendering, ECS, transforms, input |
physics | Rapier3D physics simulation |
audio | Kira audio playback |
gamepad | Gamepad input via gilrs |
assets | Image and HDR loading via the image crate |
steam | Steamworks integration |
scene_graph | Scene serialization |