Shadertoys ported to Rust GPU
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.
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
:
- Fixed a panic while processing SPIR-V
- Fixed incorrect translation of certain shader literals when targeting Metal
- Fixed a regression making it impossible to include raw SPIR-V
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.