Add raster support to aruminium — RenderPipelineState + RenderPassDescriptor + RenderEncoder
context
aruminium today is compute-only (Pipeline wraps MTLComputePipelineState,
Encoder is a compute encoder, Texture has format query but no render-target
construction). It is the highest-performance Metal driver in the Rust
ecosystem for compute and zero-copy memory — but it can't draw triangles.
evy is a unified-memory game engine designed for Apple Silicon first. The earlier architectural choice was "wgpu for raster, aruminium for compute, share one MTLDevice between them." Concrete numbers showed that choice cost 9–13 ms per frame at typical AAA load (draw-call overhead, mesh upload bandwidth, lost MTLSharedEvent sync, WGSL→MSL inefficiency). At a 16.6 ms budget, that's most of the frame.
The corrected architecture: on Apple Silicon, aruminium owns the renderer
entirely — both raster and compute. wgpu becomes the portable fallback for
non-Apple platforms only. The previous device-sharing constructors were
reverted in commit 48f1bc8. This proposal adds the raster API aruminium
needs to become the complete renderer.
scope — two phases
phase 1: minimum useful raster
Enough to draw a triangle with a vertex+fragment shader pair into a color texture and present it. Roughly mirrors aruminium's existing compute path (Pipeline + Encoder + dispatch) but for the render pipeline.
New types in src/render/ (a new submodule, mirroring how the existing
modules organize compute):
// src/render/pipeline.rs
// src/render/pass.rs
// src/render/encoder.rs
Texture extension: add render-target construction.
// extend src/texture.rs
LOC: ~600 lines of impl + ~200 lines of tests + a examples/triangle.rs.
phase 2: production raster
Adds what real game engines need:
- Depth/stencil:
DepthAttachmentDesc, depth pipeline state, depth texture format support, depth compare functions - MSAA: sample-count on pipeline, MS-texture construction, resolve attachment support
- Vertex descriptors:
MTLVertexDescriptorfor typed vertex input layout (currently we'd have to encode vertex format inline in the vertex shader; that's not how production engines work) - Indexed draws:
drawIndexedPrimitives:with index buffer + offset - Indirect draws: GPU-driven rendering with
drawPrimitives:indirectBuffer: - Blending state: per-attachment blend factors + operations
- Cull mode + winding:
setCullMode:,setFrontFacingWinding: - Depth bias:
setDepthBias:slopeScale:clamp:
LOC: another ~600 lines + tests.
what's deferred (phase 3+, not in this proposal)
- Argument buffers (Apple-specific binding optimization)
- Tile shaders (Apple-specific imageblock workflow)
- Mesh shaders (post-MTL3 feature)
- Acceleration structures (raytracing)
acceptance criteria
src/render/module withpipeline.rs,pass.rs,encoder.rs- Phase 1 impls land: RenderPipeline, RenderPassDescriptor (color only),
RenderEncoder (basic),
Gpu::render_target - Phase 2 impls land: depth/stencil, MSAA + resolve, vertex descriptors, indexed draws, blend state, cull/winding
examples/triangle.rs— minimal renderable triangle, runnable on M-seriesexamples/textured_quad.rs— quad with vertex buffer + sampled textureexamples/depth_cube.rs— phase-2 demo: depth-tested rotating cube- Tests: ~10 unit tests covering descriptor construction, encoder lifecycle, pipeline state object cache, lifetime safety
specs/README.mdupdated with the render API surfacecargo test --workspace,cargo clippy --workspace -- -W warnings,cargo fmt --allall cleancargo run --example triangledisplays a triangle on M-series- Existing compute examples (
vecadd,matmul) still pass
what stays unchanged
- All existing compute API (
Pipeline,Encoder,Commands,Queue,Buffer,Texturequery methods,Shader,ShaderLib) Gpu::open(),Gpu::new_command_queue(), retain/release semantics- FFI layer (
src/ffi/*) - examples that don't touch raster (vecadd, matmul, bench)
unimem,acpu,ranesiblings
scope estimate
- session 1: phase 1 (RenderPipeline + RenderPassDescriptor + RenderEncoder
- render_target Texture + triangle example) — ~6 pomodoros
- session 2: phase 1 polish + textured_quad example + tests — ~4 pomodoros
- session 3: phase 2 first half (depth/stencil + cull/winding + indexed draws) — ~6 pomodoros
- session 4: phase 2 second half (MSAA + vertex descriptors + blend state)
- depth_cube example + tests + spec — ~6 pomodoros
total: ~3–4 sessions (~22 pomodoros).
why now
evy step 3 is now reframed: aruminium = renderer (raster + compute) on
Apple Silicon. Without raster support, evy can't render anything on Apple
without falling back to wgpu — which we've explicitly rejected on
performance and architectural grounds (see commit 48f1bc8 body, evy
spec §5.2 after revision).
This is the foundational upstream piece. evy steps 5 (bevy_mesh on unimem), 6 (bevy_transform), 8 (glia + neural materials), 9 (mir tier passes), 14 (bevy_pbr REWRITE) all depend on aruminium being a complete renderer.
conventions to follow (from honeycrisp/aruminium/CLAUDE.md)
- atomic commits per phase (or smaller — one commit per new file is fine)
- conventional prefix:
feat: aruminium raster — <what> - no Co-Authored-By trailers
- format clean, clippy clean, tests pass, examples run
- 500-line per-file limit (split if exceeded)
- specs/ stays canonical — update in the same commit as the code
- never push without explicit request
- do not touch
Cargo.tomldependency versions - do not touch FFI signatures in
src/ffi/*(must match Metal.framework)
references
- aruminium current source:
src/{device,command,encoder,pipeline,texture,shader}.rs - Metal documentation: render pipeline + render encoder + render pass descriptor (Apple's developer docs; the type/method names map 1:1)
- corsix Metal reference for raw selectors if needed
- evy spec §5.2 (renderer composition), §10 (rendering model), §3.2 (capability matrix — Apple Silicon gets the full aruminium path)