A no_std 3D graphics and physics engine for embedded systems: software rasterization, rigid/soft-body physics, skeletal animation, and effects tuned for MCUs.
Fork of embedded-gfx by Kezii, extended with textures, fog/dithering, DMA swapchains, AA, physics, BSP, and more.
- A graphics pipeline, spelled out — five stages (
pipeline::vertex→assemble→rasterize→shade→output), each with aStageKindmarker and a documented dependency direction, plus apreludefor the common types - Two raster layers, one config — the built-in
drawpath (hand-specialised perRenderMode, driven byrecord/execute) shares itsFogConfig/DitherConfigwith a zero-costFragmentShaderseam for custom materials and decorators (FogShader,DitherShader,ScreenTintShader,PaletteShader,WaterReflectShader) - Unified raster state —
pipeline::rasterize::draw::RasterStatebundles per-pass config (fog, dither, tint, palette, stipple, depth bias), so rasterizers take one borrowed context instead of a wide positional argument tail - One home per type — no glob re-exports; every type is reached through its owning module, so the public surface is explicit rather than accidental
- Record / execute — traverse once, rasterize from a fixed-capacity command buffer (
PrimitiveHeader+ packed typed descriptors for low RAM footprint) - Rendering — MVP + frustum/backface cull, Z-buffer, flat/Gouraud/Blinn-Phong, perspective textures, Bayer dither, Reinhard tonemapping, sub-pixel Q16.16 rasterization, lights, particles, LOD, HUD
- Physics & Navigation (features
physics,scene) — rigid bodies, joints, soft body, ray primitives, NavMesh A* pathfinding - Animation — skeletal LBS, vertex morphs, ABSM state machines, transform tracks, spline curves / tweens
- Embedded-friendly —
heaplesscaps, Cortex-M SWAR/DSP SIMD optimizations, async-agnostic swapchain present, silicon hardware offloading hooks (HardwareAccelerator)
![]() SSR water reflection + palette cycling |
![]() Blinn-Phong |
![]() Rigid body physics |
![]() Particles + fog |
![]() Soft-body cloth |
![]() Point lights |
cargo run --release --example screenshots --features "std,lighting,textured,raycast,scene,physics"[dependencies]
# Embedded (no_std) — slim default is row_width_240
embedded-3dgfx = { version = "0.6", default-features = false, features = ["row_width_320", "depth-u16"] }
# Orientation-style lit meshes
embedded-3dgfx = { version = "0.6", default-features = false, features = ["row_width_320", "depth-u16", "lighting"] }
# Desktop / simulator
embedded-3dgfx = { version = "0.6", features = ["std", "physics"] }| Recipe | Features |
|---|---|
| Minimal wireframe | default-features = false, row_width_320, depth-u16 |
| Lit mesh (Gouraud / Blinn / Toon) | add lighting |
| Retro / Doom-style | lighting, textured, raycast, hud |
| Physics demo | add physics |
use embedded_3dgfx::prelude::*;
let mut engine = K3dengine::new(320, 240);
engine.camera.set_position(Vector3::new(0.0, 0.0, 5.0).into());
let geometry = Geometry { vertices: &CUBE_VERTS, faces: &CUBE_FACES, /* ... */ };
let mut mesh = K3dMesh::new(geometry);
mesh.set_render_mode(RenderMode::Lines);
let mut commands = CommandBuffer::<512>::new();
engine.record(core::iter::once(&mesh), &mut commands, None).unwrap();
engine.execute(&mut display, &mut frame_ctx, &commands, None).unwrap();Everything the engine applies to a frame — fog, dither, screen tint, palette
quantization, stipple, depth bias — is lowered once into a
RasterState:
use embedded_3dgfx::pipeline::rasterize::draw::draw_zbuffered_with_state;
let state = engine.raster_state(320, 240);
draw_zbuffered_with_state(primitive, &mut fb, &mut zbuffer, &state);The crate is organised as a graphics pipeline. Each stage owns one module and consumes the previous stage's output:
| # | Stage | Module | Input → output |
|---|---|---|---|
| 1 | Stage::Vertex |
pipeline::vertex |
model-space mesh → clip-space vertices |
| 2 | Stage::Assemble |
pipeline::assemble |
clip-space vertices → screen-space DrawPrimitive |
| 3 | Stage::Rasterize |
pipeline::rasterize |
screen primitives → covered pixels |
| 4 | Stage::Shade |
pipeline::shade |
fragments → Rgb565 colours |
| 5 | Stage::Output |
pipeline::output |
framebuffer → presented frame |
pipeline::command_buffer is the transport between the record half (stages
1–2) and the execute half (stages 3–5); pipeline::renderer drives the execute
half, and engine is the frame driver on top. Shared per-pass configuration
lives in pipeline::effects.
The dependency direction is enforced by review, not the compiler: a stage may
depend on lower-numbered stages, effects, and the crate-root core types —
never on a higher-numbered stage. The single exception is that rasterize
invokes the fragment programs defined in shade, so shade sits below
rasterize in dependency order even though it runs afterwards.
Every stage has a StageKind marker so the pipeline is queryable at compile
time (Camera: Stage::Vertex, DrawPrimitive: Stage::Assemble,
RasterState: Stage::Rasterize, FlatColorShader: Stage::Shade,
DisplayError: Stage::Output).
| Tree | Contents |
|---|---|
pipeline::vertex |
mesh, shapes, bounds, camera, camera_controller, view_frustum, lod, transform |
pipeline::assemble |
primitive (DrawPrimitive) |
pipeline::rasterize |
raster, draw, coverage, texture, tilebin |
pipeline::shade |
shader, retro, dither, lights |
pipeline::output |
display_backend, swapchain, completion, hud |
pipeline::{command_buffer, renderer, effects} |
record/execute transport, execute driver, shared config |
engine |
K3dengine frame driver (record / execute) |
| core | color, config, error, simd_dsp, plus the prelude |
| subsystems | physics, raycast, bsp, navmesh, skeleton, animation, absm, tween, scene_format, scene_stream, … |
When using lit render modes (RenderMode::SolidLightDir, BlinnPhong, Toon, GouraudLightDir), the engine requires surface face normals in Geometry.normals (or vertex_normals) to evaluate light angles (N · L):
- Static Flash ROM Storage (Recommended for MCUs): Precompute face normals offline or at compile-time and store them alongside vertices as
&'static [[f32; 3]](0 RAM overhead). - On-Demand Helper: If authoring procedural geometry in code, use
Geometry::compute_face_normals_into(&verts, &faces, &mut out_normals)orGeometry::compute_face_normals(&verts, &faces):
let mut normals = [[0.0f32; 3]; CUBE_FACES.len()];
Geometry::compute_face_normals_into(&CUBE_VERTS, &CUBE_FACES, &mut normals);
let geometry = Geometry {
vertices: &CUBE_VERTS,
faces: &CUBE_FACES,
normals: &normals,
..Default::default()
};More patterns (particles, lights, fog, physics, skeleton, soft body, async present) live under examples/ and on docs.rs.
| Flag | Default | Description |
|---|---|---|
row_width_* |
240 |
Row-buffer width (96 / 160 / 240 / 320). Meant to be mutually exclusive; if a build enables several, the widest wins deterministically |
std |
off | Desktop helpers / perfcounter |
lighting |
off | SolidLightDir / Gouraud / Blinn / Toon / SectorBright + lights |
textured |
off | Texture modes + texture module (implies lighting) |
raycast |
off | Doom-style raycaster, BSP helpers, sector_lights |
scene |
off | Skeleton, character, particles, billboard, animation / scene stream |
hud |
off | HUD helpers |
painters |
off | Painter's algorithm helpers (painters module) |
physics |
off | Rigid body, soft body, physics raycast |
aa-heuristic / aa-coverage |
off | Triangle edge AA (coverage needs a W×H buffer) |
dsp / fixed-transform / fixed-raster |
off | Shared Q16.16 / quat path via embedded-dsp |
triple-buffering / embassy / dma2d |
off | Swapchain / Embassy / DMA2D hooks |
perfcounter / dwt-profiler / rtt-trace / itm-trace |
off | Timing / trace sinks |
Flash impact of the slim recipes is tracked in docs/feature-size.md (size_harness + CI budget).
| Feature | What you get |
|---|---|
aabb-cull |
Cached AABB, two-stage frustum cull, raycast broadphase |
render-layers |
Camera ↔ mesh layer bitmasks |
record-sort |
Priority / distance sort in record |
lod-crossfade |
LOD fade margins |
anim-blend |
Clip blending, bone slerp, skinned AABBs (also enables scene) |
gizmos |
AABB / frustum debug wireframes |
visibility-extras |
aabb-cull + render-layers + record-sort + lod-crossfade |
scene-extras |
All of the above |
embedded-3dgfx = { version = "0.6", features = ["std", "scene-extras"] }cargo test --test scene_extras --features "std,scene-extras"cargo run --example rotating_cube --features std
cargo run --example lighting_demo --features "std,lighting"
cargo run --example texture_mapping_demo --features "std,textured"
cargo run --example skeletal_animation_demo --features "std,scene"
cargo run --example star_striker_demo --features "std,lighting,scene"
# physics demos also need: --features "std,physics"Rendering: basic_rendering, rotating_cube, scene_viewer, lighting_demo, gouraud_demo, blinn_phong_demo, fog_dithering_demo, texture_mapping_demo, mesh_texture_demo, retro_presets_demo, bsp_builder_demo, dma_rendering_demo, billboard_demo, lod_demo, vertex_animation_demo, painters_algorithm_demo, boot_menu, stl_viewer, water_reflection_ssr_demo, hybrid_hud_sprite_demo, star_striker_demo, …
Physics: physics_rolling_ball, physics_bouncing_balls, physics_pendulum, physics_newtons_cradle, physics_stack_tower, cloth_simulation, jelly_cube_demo, raycast_demo, walkable_demo, capsule_physics_demo, …
| Resource | Topic |
|---|---|
MIGRATION.md |
Upgrading from the pre-pipeline module layout |
docs/app-integration.md |
Adding the engine to your application (with runnable templates) |
docs/caps-and-telemetry.md |
Caps, telemetry, CI budgets |
docs/feature-size.md |
Slim vs full flash (.text) budgets |
docs/backend-integration.md |
Board bring-up, memory sizing |
docs/asset-pipeline.md |
Offline assets / scene streaming |
tools/blender_addon |
Blender mesh / animation export add-on |
Typical target: Cortex-M4F/M33 with FPU; ~128 KB RAM minimum, ~512 KB+ recommended for double-buffer + Z + physics at 240×135.
cargo test --lib
cargo test --lib --features dma2d,depth-u16
cargo test --test scene_extras --features "std,scene-extras"Git hooks (fmt on commit / push): ./scripts/install-git-hooks.sh
PRs welcome — especially board backends, broad-phase spatial structures, and extra joint / collider types.
Aggregating surfaces — the prelude and the per-stage facades such as
pipeline::output — are public API for downstream crates. Code inside src/
must not import from them: internal modules name the module that actually
defines an item, so trimming an aggregator can never silently reshape the
internals. Glob imports are confined to #[cfg(test)] modules.
python3 .github/scripts/check_internal_imports.py # run before pushingThe check runs as the import-policy CI job. tests/, examples/ and
benches/ are consumers of the public API and may use the facades freely.
A module reached only through #[cfg(feature = "F")] mod x; cannot be compiled
without F, so a #[cfg(feature = "F")] inside it does nothing. Those gates
read as if they mattered and hide which ones are load-bearing, so they are
rejected:
python3 .github/scripts/check_feature_gates.py # run before pushingThe check runs as the feature-gate-policy CI job. It only reports gates that
are provably redundant — any(...) and not(...) establish nothing, and a
gate that still constrains a second feature is kept.
Dual-licensed under MIT OR Apache-2.0. See LICENSE-MIT, LICENSE-APACHE, and NOTICE.






