Skip to main content

Porting GPU shaders to Rust 30x faster with AI

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

I used AI to port virtually all of the shaders from Sascha Willems' popular Vulkan sample repo over to Rust using Rust GPU. This demonstrates Rust GPU is ready for production use.

With AI handling most of the codegen and me reviewing and tweaking the results, the port took about four days of part-time work—a 30x speedup compared to Sascha's manual port to Slang.

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.

Porting Vulkan shaders

I went through every sample in Sascha's Vulkan repo and ported around 90% of the shaders.

Each Rust shader compiles to valid SPIR-V via Rust GPU and drops into the original C++ Vulkan pipeline without modification. This lack of host-side changes demonstrates that Rust GPU shaders can integrate into non-Rust Vulkan workflows with only build system changes.

I was developing on macOS, so I used MoltenVK to run the examples. I compared the GLSL and Rust output manually. Because MoltenVK does not support some advanced Vulkan features, a few shaders could not be checked at runtime to confirm they worked. I plan to eventually double check such shaders on a GPU with native Vulkan support.

Porting these shaders proves that Rust GPU is ready for real use, but they aren't a good representation of what "Rust-native" shaders would look like. These shaders do not use advanced features supported in Rust GPU like traits, enums, feature flags, dependency management, lints, and others.

Rust GPU Vulkan support

I'm a maintainer of Rust GPU but not a graphics or GPU programmer by background. Going in, I didn't know which Vulkan features and techniques would actually work with Rust GPU. Vulkan is expansive. Rust GPU is a large and evolving project with new maintainers and little to no documentation.

I was pleasantly surprised to find out that Rust GPU supports all the major Vulkan shader types! In addition to simple examples covering a single shader type, most of the examples have multiple shaders of different types working together in a full pipeline. Rust GPU supports this in a flexible way. A single crate can output each shader to its own file or combine them into a single uber shader with multiple entry points. I did not use the uber shader route and instead matched what the C++ host code expected.

All Vulkan shader types supported by Rust GPU

Sascha Willems' Vulkan examples are a well-known reference for real-world GPU techniques. They cover more than just API basics and include advanced techniques like dynamic rendering, PBR, deferred shading, compute, and ray tracing. Again, I was pleasantly surprised to see that these techniques work in Rust GPU without any major issues!

List of shader techniques and features demonstrated

🟢 Core and Basics

📦 Data Binding

🖼️ Textures

📸 Framebuffer Techniques

🌑 Shadows and Lighting

🔁 Performance and Instancing

🧠 Compute and Simulation

🔺 Geometry & Tessellation

🧪 Ray Tracing

⚙️ Advanced Extensions

👁️ UI and Text

⚡ Effects and Demos

Successfully porting all these shaders demonstrates that Rust GPU can handle most major use-cases traditionally done in GLSL.

How I used AI

The project was conceived as an opportunity to use Claude Code for the first time. I've been wanting to use it for a while, but a billing issue on Anthropic's side prevented me from doing so. Naturally, it started working when I signed up for the more expensive Pro plan!

The loop

I worked interactively with Claude to port a single shader at a time. At first, I just asked Claude to "Port X GLSL shader to Rust using rust-gpu.” The results were not good. Claude would get confused and make obvious mistakes:

  • Name functions incorrectly
  • Forget to add shader crates to the workspace
  • Use incorrect crate names, paths, or imports. One thing it loved to do was hallucinate dependencies in Cargo.toml
  • Add build scripts or hardcode things that should be auto-detected
  • Get confused about where it was in the filesystem, affecting what it was reading, writing, or running

I would tell it what I wanted, it would do something wrong, I would tell it how to fix it, it would get that wrong, I would tell it how to fix that, and so on. Eventually, I had little fragments of directions that solved specific subproblems that kept coming up.

I took these subproblem fixes and aggregated them into the initial prompt for the next shader. Again, I would interactively work on the shader and gain more fragments or tweaks to the existing ones. Over a few hours, this evolved into a detailed, repeatable, high-quality initial prompt. Once I had this "golden prompt," things went very smoothly and I was largely babysitting.

Golden prompt

Here are your instructions. Refer back to them frequently to make sure you are on plan:

Let’s rewrite the glsl / slang shaders in rust using rust-gpu. Pick an unimplemented one that looks the easiest and tell me which you picked to work on (don’t do computeraytracing—other raytracing is fine—or conservativeraster or bufferdeviceaddress or variablerateshading or texturesparseresidency or oit or variablerateshading). The glsl/slang/rust versions of the logical shader should all be semantically equivalent. We will use the existing C++ harness to run them. Always use rust dependencies from the workspace and make the new shader consistent with the other rust shaders. We do not need build.rs, building is handled by an external script.

spirv_std doesn’t have a glam feature, it is always built in at spirv_std::glam. The shader entry points have to be named something specific to be picked up by the C++. The C++ handles rust differently than the other languages that require “main”, it is main*[x] with the shader type (check the other rust shaders)

The shader must always be built with a build script as it sets up stuff in the env. You can run it with cd /Users/legnitto/src/vulkan_shader_examples/shaders/rust && python3 compileshaders.py. If you are building one shader while iterating on it, you can use: python3 compileshaders.py [shader]

Be sure to add any new crates to the cargo workspace. Do not create any manifest.json files, they are created when building.

Shader crate names must be globally unique in the rust workspace. If there is a conflict, name it [groupname]-[shadername] in the cargo.toml package section, similar to existing ones. Only do it for the conflict.

If a shader needs a Vulkan capability or extension, it is set in the crate’s Cargo.toml. Example: [package.metadata.rust-gpu.build] capabilities = ["ImageQuery"]

To test the shader, you must first compile the test / example binary. Example:

cmake --build build --target texturecubemap

Then you can run the example binary with the shader. Example:

cd /Users/legnitto/src/vulkan_shader_examples && VK_ICD_FILENAMES=~/VulkanSDK/1.4.313.1/macOS/share/vulkan/icd.d/MoltenVK_icd.json ./build/bin/computeraytracing -s rust

Note you can swap “rust” for “glsl” to test the glsl shaders so we can compare the output.

Use straightforward idiomatic rust things like constants for PI, TAU, and EPSILON where possible. Do not use bytemuck. Glam has many similar apis to glsl, use them when possible. You also might need spirv_std::num_traits::Float to access some rust apis on floats.

Care should be taken to make sure padding of input and output structs is correct.

Iterate on the shader until it compiles and is semantically equal to the glsl/slang shader. If you get stuck, look at the other rust shaders for what they do. Look line by line in the glsl, slang and rust shaders and make sure they have equivalent semantics that will produce equivalent output. do not copy obvious bugs in the source shaders. Ask if there is some glsl feature that rust-gpu does not support. If the semantics / logic / output is not the same, fix it and check again. Compile the test binary if it does not exist and run it. Then run the same thing with glsl so I can compare. Set the timeout to 15 seconds when running.

After the shader is complete, compiling with the build script, and I have confirmed both, ask if you should commit. When you commit, do not include any Claude info and look at other commit messages to make the format match. Make the style of the commit message match other rust shader commits. Include the lockfile.

If you have any doubts, ask me, but you should be able to iterate your way to a completely working shader without my help.

Tactics that helped AI

Giving AI complete control. I kept an eye on the code to make sure it was reasonable, but I made Claude do all edits. In the past, I have found that when both the AI and I make changes the AI gets confused. I now choose up front if I am using AI as an aid to my coding or if I am making AI code and I am reviewing instead.

Not compacting. I found that when Claude compacted the conversation, the output quality dropped until I gave Claude more context and corrections to recover. Eventually, my golden prompt became so effective that it was better to restart Claude with the prompt rather than continue a compacted session.

Putting scaffolding in place. In addition to the prompt, having scaffolding in place (directory layout, workspace setup, build commands, etc) meant the AI could focus just on the shader. This helped prevent confusion. Setting up scaffolding and working through a clear list is apparently good advice for both human programmers and AI!

Tightening the feedback loop. One thing I resisted was changing the build system to allow specifying a single shader instead of building them all. As more shaders were added, the build system slowed down. Claude would hit timeouts unless I told it to increase them and velocity took a hit. I eventually bit the bullet and had Claude add a feature to build a single shader at a time. After that, Claude could run fast edit-compile-fix loops without my involvement.

Using existing examples. As more shaders were implemented, Claude began referencing previously ported Rust shaders and correcting itself when it went off track. Errors and human intervention dropped significantly and velocity increased. Due to this the development curve was definitely not linear and accelerated up until the end. There’s a reason OpenAI and others suggest giving AI examples in prompts.

AI limitations

Even with my golden prompt, I could never get Claude to "one-shot" a shader port. The best I could get was a correct shader followed by manual reminders. The two main things I had to remind Claude were:

  1. "Look line by line in the GLSL, Slang, and Rust shaders and make sure they have equivalent semantics that will produce equivalent output."
  2. "When you commit, do not include any Claude info. Look at other commit messages and match the format. Match the style of other Rust shader commits. Include the lockfile."

I would just paste those lines back in from the golden prompt. So, Claude was effectively "three-shotting" the task. I think it was forgetting due to a small context window, or it was de-emphasizing those directions in the initial prompt. Interestingly, I only needed to repaste them in once per session. For multiple ports in the same session, Claude would remember and one-shot them correctly. Well, until compacting and then it was time to start fresh again.

When reviewing the generated code, I often caught Claude wallpapering over real or imagined Rust GPU limitations. For example, it assumed push constants were unsupported and hardcoded example values instead. Claude never told me this; it only showed up in the code. If I had only looked at the conversation I would have missed it. The code would have run and appeared functional but it wouldn’t have been correct.

I ❤️ AI

Even with all these issues, AI is wild. Despite being new to the project and inexperienced with shaders, I was able to implement everything 30x faster than the original expert author. Incredible!

I've been using AI to write code for a while with mixed results. I was impressed when it one-shotted Gscript automation or handled React and CSS cleanly. But in more complex domains it fell apart.

Lately I’ve been less frustrated. Partly because I use multiple AIs and have them interact, but mostly because I've learned how to manage their quirks and my expectations. Still, this shader porting experience really drove home just how far things have come. The fact that an AI could handle this volume of non-trivial, domain-specific code with minimal help is frankly insane.

Repos are forever

One issue that comes up repeatedly while using AI is that when Embark transferred the Rust GPU project to the community, they did not transfer the code repo. As a result, AI continues to treat the Embark repo as the source of truth and heavily weights the outdated APIs in that repository. I have to constantly teach AI the current Rust GPU repo url, APIs, and docs. The community anticipated problems like this and asked Embark to follow the standard repository transfer process, but they declined.

Rust GPU limitations

There were only a few issues hit while porting the shaders to Rust. The first I was able to fix with Claude's help, and the others are still outstanding. A huge benefit of using Rust for shaders is that the compiler and standard library are written in Rust too. When you hit an issue you can often fix it yourself, test it, and move on. No need to file bugs, wait for upstream, or switch to a different language like C++.

The fixed issue

In GLSL, querying the size of an image is done with textureSize or imageSize, depending on whether a level-of-detail (LOD) is required. In Rust GPU, these map to query_size_lod for sampled images using SampledImage, and query_size for multisampled or storage images. These functions return glam vector types like UVec2, UVec3, or scalars, depending on image dimensionality and array status.

There was a mismatch in the expected return shape, returning a UVec3 for a cubemap when SPIR-V expects a UVec2. Thankfully this showed up as a compile-time error rather than runtime error due to Rust's type system!

Missing APIs

Rust GPU does not currently support SPV_KHR_physical_storage_buffer and the PhysicalStorageBuffer64 addressing model, though there is a draft PR up by community member @jwollen. The shaders that use this API could not be ported.

Rust GPU does not support VK_EXT_sparse_residency, which enables partially resident textures. Support for sparse texture operations, including OpImageSparse* instructions and residency queries, is missing.

Rust GPU also lacks support for VK_KHR_fragment_shading_rate, which allows different regions of the framebuffer to be shaded at varying rates. This requires the FragmentShadingRateKHR built-in and associated decorations.

Come join us!

Porting the Vulkan shaders to Rust with Rust GPU was successful. Rust GPU is definitely ready to use in your Vulkan-based project.

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.