GPU Shaders
SPIR-V shaders written entirely in Rust using Rust-GPU. The vertex shader computes H3 hexagonal cell boundaries on the GPU — no mesh data uploaded from the CPU.
- Rust
- Rust-GPU
- SPIR-V
- H3
Shaders in Rust
Rust-GPU is a compiler backend that emits SPIR-V bytecode from Rust source,
replacing GLSL/HLSL entirely. Shader code lives in a regular Rust crate
with #![no_std] and #![no_main] — entry points are ordinary functions annotated with #[spirv(vertex)] and #[spirv(fragment)]. The same Rust type system, borrow checker, and toolchain that
governs the rest of the project also governs the shaders.
H3 on the GPU
The vertex shader takes two per-vertex inputs: a cell_index (an H3 64-bit cell identifier) and a vertex_index (0–9, one per boundary point). Using h3o_noalloc — a no-allocation H3 port designed for constrained environments —
it looks up the cell's geographic boundary on the GPU, converts the
lat/lng coordinates to a 3D point on the unit sphere, and outputs the
clip-space position.
The key benefit: the CPU only uploads a list of H3 cell indices. All geometry is generated in the shader — no mesh serialisation, no vertex buffer construction, no round-trips.
Forking h3o for the GPU
The upstream h3o crate is a complete Rust implementation of the H3 geospatial indexing
system. In a normal context it works well, but its geometry APIs
— polygon tiling, dissolving cell sets into polygons, vertex graph
operations — depend on Vec, HashSet, and HashMap. SPIR-V has no heap allocator, so those types don't exist. The entire
geom module had to be excised.
The resulting fork, h3o_noalloc, keeps all of the core indexing and boundary logic and replaces every
dynamic collection with a fixed-size equivalent:
- Boundary vertices — changed from a
Vec<LatLng>to a[LatLng; 10]with au8count field. Ten is the maximum possible boundary vertices for any H3 cell (a pentagon crossing an icosahedron edge), so the size is exact and provable at compile time. - Face sets — the set of icosahedron faces a cell spans was stored as a
HashSet. Replaced withFaceSet(u32), a 20-bit bitmap — one bit per face. Insert, contains, and iterate are all bitwise operations on a single register. - Children iterators — grid traversal iterators that previously built intermediate
Vecs now carry au64scratchpad. All state fits in a single integer.
The geometry APIs (Tiler, Solvent, Plotter) are feature-gated behind feature = "geom" and require std. The shader crate never enables that feature.
Floating-Point on the GPU
H3's boundary computation is built on f64 trigonometry — sin, cos, atan2, asin. The Vulkan GLSL.std.450 extended instruction set only supports
16- and 32-bit floats; 64-bit transcendentals are not available.
The fix is a dedicated math-spirv.rs module, selected at compile time via #[cfg(target_arch = "spirv")]. Every transcendental casts down to f32, calls the SPIR-V intrinsic, then casts back:
fn sin(x: f64) -> f64 { (x as f32).sin() as f64 }
fn atan2(y: f64, x: f64) -> f64 { (y as f32).atan2(x as f32) as f64 }The precision loss from the f64 → f32 → f64 round-trip is acceptable for rendering cell outlines — H3 cell
boundaries are defined in integer coordinates and only converted to
lat/lng at the last step — but would be problematic for indexing
operations that rely on exact integer arithmetic. Those stay in integer
space throughout. The three math backends (math-std.rs, math-libm.rs, math-spirv.rs) are otherwise identical in interface, so the same h3o_noalloc code
compiles for CPU tests and GPU production without any conditional logic
in the algorithm itself.
Error Handling Without std
Rust's std::error::Error trait and Display are not available in no_std SPIR-V targets. h3o's error types implement both. The fork gates those
trait implementations behind #[cfg(not(target_arch = "spirv"))] — the error types still exist and Result still propagates correctly inside the shader, but they carry no
human-readable message. This keeps the full Result-based API intact for CPU unit tests while satisfying the GPU
constraints.
cargo gpu
The shader crate is compiled with cargo gpu, the Rust-GPU build driver, which invokes the SPIR-V backend and
produces a .spv binary ready to be loaded by Bevy's asset pipeline at runtime.
cargo gpu build --package shadersProof of Concept
The vertex and fragment shaders compile and produce correct cell boundaries on screen. The current fragment shader outputs a flat colour; simulation state visualisation — colouring cells by biome, population density, or temperature — is the intended next step once the simulation data pipeline is in place.