OverviewThis WebsiteHome LabTrading BotEcosimProcedural Generation
Source Code

src/pages/projects/gpu_shaders.rs

use leptos::*;
use leptos_meta::{Meta, Title};

use super::data::ALL_PROJECTS;

#[component]
pub fn GpuShaders() -> impl IntoView {
    let project = ALL_PROJECTS
        .iter()
        .find(|p| p.slug == "gpu-shaders")
        .unwrap();
    let skills_view = project
        .skills
        .iter()
        .map(|&s| view! { <li>{s}</li> })
        .collect::<Vec<_>>();

    view! {
        <Title text="GPU Shaders – Peter Pinto"/>
        <Meta name="description" content="SPIR-V GPU shaders written in Rust via Rust-GPU, implementing H3 hexagonal cell boundary generation entirely on the GPU."/>
        <div class="page">
            <span class="eyebrow">"Projects"</span>
            <h1>"GPU " <em style="font-style:italic; color: var(--accent)">"Shaders"</em></h1>
            <p class="lead">
                "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."
            </p>

            <ul class="skills-list" style="margin-top: 1.5rem;">
                {skills_view}
            </ul>

            <hr class="divider"/>

            // ── Rust-GPU ──────────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Toolchain"</span>
                <h2>"Shaders in Rust"</h2>
                <p>
                    <a href="https://github.com/Rust-GPU/Rust-GPU" target="_blank" rel="noopener noreferrer" class="prose-link">"Rust-GPU"</a>
                    " 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 "
                    <code class="inline-code">"#![no_std]"</code>
                    " and "
                    <code class="inline-code">"#![no_main]"</code>
                    " — entry points are ordinary functions annotated with "
                    <code class="inline-code">"#[spirv(vertex)]"</code>
                    " and "
                    <code class="inline-code">"#[spirv(fragment)]"</code>
                    ". The same Rust type system, borrow checker, and toolchain that
                    governs the rest of the project also governs the shaders."
                </p>
            </section>

            <hr class="divider"/>

            // ── GPU-Side H3 ───────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Algorithm"</span>
                <h2>"H3 on the GPU"</h2>
                <p>
                    "The vertex shader takes two per-vertex inputs: a "
                    <code class="inline-code">"cell_index"</code>
                    " (an H3 64-bit cell identifier) and a "
                    <code class="inline-code">"vertex_index"</code>
                    " (0–9, one per boundary point). Using "
                    <code class="inline-code">"h3o_noalloc"</code>
                    " — 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."
                </p>
                <p style="margin-top: 1rem;">
                    "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."
                </p>
            </section>

            <hr class="divider"/>

            // ── h3o_noalloc ───────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Dependency"</span>
                <h2>"Forking h3o for the GPU"</h2>
                <p>
                    "The upstream "
                    <code class="inline-code">"h3o"</code>
                    " 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 "
                    <code class="inline-code">"Vec"</code>
                    ", "
                    <code class="inline-code">"HashSet"</code>
                    ", and "
                    <code class="inline-code">"HashMap"</code>
                    ". SPIR-V has no heap allocator, so those types don't exist. The entire
                    "
                    <code class="inline-code">"geom"</code>
                    " module had to be excised."
                </p>
                <p style="margin-top: 1rem;">
                    "The resulting fork, "
                    <code class="inline-code">"h3o_noalloc"</code>
                    ", keeps all of the core indexing and boundary logic and replaces every
                    dynamic collection with a fixed-size equivalent:"
                </p>
                <ol class="project-steps">
                    <li>
                        <strong>"Boundary vertices"</strong>
                        " — changed from a "
                        <code class="inline-code">"Vec<LatLng>"</code>
                        " to a "
                        <code class="inline-code">"[LatLng; 10]"</code>
                        " with a "
                        <code class="inline-code">"u8"</code>
                        " 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."
                    </li>
                    <li>
                        <strong>"Face sets"</strong>
                        " — the set of icosahedron faces a cell spans was stored as a "
                        <code class="inline-code">"HashSet"</code>
                        ". Replaced with "
                        <code class="inline-code">"FaceSet(u32)"</code>
                        ", a 20-bit bitmap — one bit per face. Insert, contains, and iterate
                        are all bitwise operations on a single register."
                    </li>
                    <li>
                        <strong>"Children iterators"</strong>
                        " — grid traversal iterators that previously built intermediate
                        "
                        <code class="inline-code">"Vec"</code>
                        "s now carry a "
                        <code class="inline-code">"u64"</code>
                        " scratchpad. All state fits in a single integer."
                    </li>
                </ol>
                <p style="margin-top: 1rem;">
                    "The geometry APIs ("
                    <code class="inline-code">"Tiler"</code>
                    ", "
                    <code class="inline-code">"Solvent"</code>
                    ", "
                    <code class="inline-code">"Plotter"</code>
                    ") are feature-gated behind "
                    <code class="inline-code">"feature = \"geom\""</code>
                    " and require "
                    <code class="inline-code">"std"</code>
                    ". The shader crate never enables that feature."
                </p>
            </section>

            <hr class="divider"/>

            // ── Floating Point ────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Challenge"</span>
                <h2>"Floating-Point on the GPU"</h2>
                <p>
                    "H3's boundary computation is built on "
                    <code class="inline-code">"f64"</code>
                    " trigonometry — "
                    <code class="inline-code">"sin"</code>
                    ", "
                    <code class="inline-code">"cos"</code>
                    ", "
                    <code class="inline-code">"atan2"</code>
                    ", "
                    <code class="inline-code">"asin"</code>
                    ". The Vulkan GLSL.std.450 extended instruction set only supports
                    16- and 32-bit floats; 64-bit transcendentals are not available."
                </p>
                <p style="margin-top: 1rem;">
                    "The fix is a dedicated "
                    <code class="inline-code">"math-spirv.rs"</code>
                    " module, selected at compile time via "
                    <code class="inline-code">"#[cfg(target_arch = \"spirv\")]"</code>
                    ". Every transcendental casts down to "
                    <code class="inline-code">"f32"</code>
                    ", calls the SPIR-V intrinsic, then casts back:"
                </p>
                <div class="code-block" style="margin-top: 1.25rem;">
                    <pre><code>"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 }"</code></pre>
                </div>
                <p style="margin-top: 1rem;">
                    "The precision loss from the "
                    <code class="inline-code">"f64 → f32 → f64"</code>
                    " 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 ("
                    <code class="inline-code">"math-std.rs"</code>
                    ", "
                    <code class="inline-code">"math-libm.rs"</code>
                    ", "
                    <code class="inline-code">"math-spirv.rs"</code>
                    ") 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."
                </p>
            </section>

            <hr class="divider"/>

            // ── Error Handling ────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Challenge"</span>
                <h2>"Error Handling Without std"</h2>
                <p>
                    "Rust's "
                    <code class="inline-code">"std::error::Error"</code>
                    " trait and "
                    <code class="inline-code">"Display"</code>
                    " are not available in "
                    <code class="inline-code">"no_std"</code>
                    " SPIR-V targets. h3o's error types implement both. The fork gates those
                    trait implementations behind "
                    <code class="inline-code">"#[cfg(not(target_arch = \"spirv\"))]"</code>
                    " — the error "
                    <em>"types"</em>
                    " still exist and "
                    <code class="inline-code">"Result"</code>
                    " still propagates correctly inside the shader, but they carry no
                    human-readable message. This keeps the full "
                    <code class="inline-code">"Result"</code>
                    "-based API intact for CPU unit tests while satisfying the GPU
                    constraints."
                </p>
            </section>

            <hr class="divider"/>

            // ── Build ─────────────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Build"</span>
                <h2>"cargo gpu"</h2>
                <p>
                    "The shader crate is compiled with "
                    <code class="inline-code">"cargo gpu"</code>
                    ", the Rust-GPU build driver, which invokes the SPIR-V backend and
                    produces a "
                    <code class="inline-code">".spv"</code>
                    " binary ready to be loaded by Bevy's asset pipeline at runtime."
                </p>
                <div class="code-block" style="margin-top: 1.25rem;">
                    <pre><code>"cargo gpu build --package shaders"</code></pre>
                </div>
            </section>

            <hr class="divider"/>

            // ── Status ────────────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Status"</span>
                <h2>"Proof of Concept"</h2>
                <p>
                    "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."
                </p>
            </section>
        </div>
    }
}