Skip to main content

Shadertoys ported to Rust GPU

· 4 min read
Christian Legnitto
Rust GPU and Rust CUDA maintainer

We ported a few popular Shadertoy shaders over to Rust using Rust GPU. The process was straightforward and we want to share some highlights.

The code is available on GitHub.

Shadertoy screenshot

What is Rust GPU?

Rust GPU is a project that allows you to write code for GPUs using the Rust programming language. GPUs are typically programmed using specialized languages like WGSL, GLSL, MSL, or HLSL. Rust GPU changes this by letting you use Rust to write GPU programs (often called "shaders" or "kernels").

These Rust GPU programs are then compiled into SPIR-V, a low-level format that most GPUs understand. Since SPIR-V is the format Vulkan uses, Rust GPU makes it possible to integrate Rust-based GPU programs into any Vulkan-compatible workflow.

For more details, check out the Rust GPU website or the GitHub repository.

Shared code between CPU and GPU

Sharing data between the CPU and GPU is common in shader programming. This often requires special tooling or manual effort. Using Rust on both sides made this seamless:

#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct ShaderConstants {
pub width: u32,
pub height: u32,
pub time: f32,
pub cursor_x: f32,
pub cursor_y: f32,
pub drag_start_x: f32,
pub drag_start_y: f32,
pub drag_end_x: f32,
pub drag_end_y: f32,
pub mouse_left_pressed: u32,
pub mouse_left_clicked: u32,
}

Note that on both the CPU and the GPU we are using the bytemuck crate for the Pod and Zeroable derives. This crate is unmodified and integrated directly from crates.io. Many no_std + no alloc Rust crates work on the GPU!

Traits, generics, and macros

Rust GPU supports traits. We used traits to encapsulate shader-specific operations in reusable ergonomic abstractions:

pub trait FloatExt {
fn gl_fract(self) -> Self;
fn rem_euclid(self, rhs: Self) -> Self;
fn gl_sign(self) -> Self;
fn deg_to_radians(self) -> Self;
fn step(self, x: Self) -> Self;
}

While there are still some rough edges, generics mostly work as expected. We used them to support multiple channel types without duplicating logic:

pub struct State<C0, C1> {
inputs: Inputs<C0, C1>,
cam_point_at: Vec3,
cam_origin: Vec3,
time: f32,
ldir: Vec3,
}

Rust macros also function normally. Using macros allowed us to reduce repetitive code further.

macro_rules! deriv_impl {
($ty:ty) => {
impl Derivative for $ty {
deriv_fn!(ddx, OpDPdx, false);
deriv_fn!(ddx_fine, OpDPdxFine, true);
deriv_fn!(ddx_coarse, OpDPdxCoarse, true);
deriv_fn!(ddy, OpDPdy, false);
deriv_fn!(ddy_fine, OpDPdyFine, true);
deriv_fn!(ddy_coarse, OpDPdyCoarse, true);
deriv_fn!(fwidth, OpFwidth, false);
deriv_fn!(fwidth_fine, OpFwidthFine, true);
deriv_fn!(fwidth_coarse, OpFwidthCoarse, true);
}
};
}

// Applied easily to multiple types:
deriv_impl!(f32);
deriv_impl!(Vec2);
deriv_impl!(Vec3A);
deriv_impl!(Vec4);

Standard Rust tools

Want to typecheck the shaders? cargo check. Build them? cargo build. Run in release mode? cargo run --release. Gate code at compile time? Use features.

If you run clippy on the shaders, you'll see it complains about many things as we intentionally kept the Rust versions of shaders similar to their original GLSL versions.

This is one of Rust GPU's big advantages: you can use all the Rust tools you're already familiar with.

Improving the Rust ecosystem

While porting shaders, we also contributed back to the ecosystem by identifying and fixing several issues in wgpu and naga:

These fixes help everyone using wgpu and naga, not just users of Rust GPU.

Come join us!

While we hit some sharp edges, porting Shadertoy shaders to Rust with Rust GPU was reasonably straightforward. Rust GPU is definitely ready for shader experimentation.

We're eager to add more users and contributors! We will be working on revamping the onboarding and documentation soon. To follow along or get involved, check out the rust-gpu repo on GitHub.