OverviewThis WebsiteHome LabTrading BotEcosimProcedural Generation
Projects

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.


Toolchain

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.


Algorithm

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.


Dependency

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:

  1. Boundary vertices — changed from a Vec<LatLng> to a [LatLng; 10] with a u8 count 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.
  2. Face sets — the set of icosahedron faces a cell spans was stored as a HashSet. Replaced with FaceSet(u32), a 20-bit bitmap — one bit per face. Insert, contains, and iterate are all bitwise operations on a single register.
  3. Children iterators — grid traversal iterators that previously built intermediate Vecs now carry a u64 scratchpad. 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.


Challenge

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.


Challenge

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.


Build

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 shaders

Status

Proof 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.