From 96ad697ba815e26573cd9f0d5fbbeb0e4d8edd6b Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 27 Apr 2025 16:17:20 -0500 Subject: [PATCH] Start writing the words --- .../bounding box.png | Bin 0 -> 3371 bytes blog/2025-03-30-draw-compute-shader/index.mdx | 107 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 blog/2025-03-30-draw-compute-shader/bounding box.png create mode 100644 blog/2025-03-30-draw-compute-shader/index.mdx diff --git a/blog/2025-03-30-draw-compute-shader/bounding box.png b/blog/2025-03-30-draw-compute-shader/bounding box.png new file mode 100644 index 0000000000000000000000000000000000000000..63f1d731b8ef79af074351679f86e8e974fea995 GIT binary patch literal 3371 zcmeAS@N?(olHy`uVBq!ia0y~yUage(c!@6@aFM%AEbVpxD z28NCO+M1MG8ZUIm= z1A~oyML}Y6c4~=2Qfhi;o~`=(-TT!QGE;2T!%ck+d<&dYGcrA@ic*8C{6dnevXd=S z)a~tfxoj$|fF|UomLw`vv0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`<*Em(=3qqRfJl%=|nB zkb#Lw`K2YcN~NWlDOPETX{JdjDWqhPo-n#wq5eh89L?hDl0L z>q|1z5ccO4LxTYrAo?H~eM3D1kdr}dEB~U*RG=$BR@xcbfaOudZ1h16L2{TKL+Hvl+1+81Nj}@Z!7tjk%Zp%~WR5 z*vXh){+*G5;fL|=-Nwb`3=9tzpR+vwz7H5%Za`s<#Xv?&Adonu0VEPd7#JE1fQiIG z0vP`aJxmM?0;9@C17tLXjOK;W!eO+u9IY2dE6UL(!Dt(42sRqeyn83h$k32q%~)F` WoVeub`CC9wFnGH9xvX + +## GUIs in Rust with `egui` + +:::note +This section focuses on creating an application that displays the GPU results. If that's not your style, +you can [skip ahead to the shaders](#shaders-in-rust). +::: + +## Shaders in Rust + +All the code in this post is written in Rust - including the parts that run on a GPU. Rust GPU +compiles Rust code into a shader that runs on a graphics card. There are three primary shader types used: + +- [Compute shader](https://www.khronos.org/opengl/wiki/Compute_Shader); can run arbitrary computation, but can't + display to a screen +- [Vertex shader](https://www.khronos.org/opengl/wiki/Vertex_Shader); used to define the output image area +- [Fragment shader](https://www.khronos.org/opengl/wiki/Fragment_Shader); draws an image by returning + a color for each pixel + +The application will output pixel information into an image buffer using a compute shader, then display that image +using a combination of vertex and fragment shaders. + +### Compute shader + +
+ Why the focus on compute shaders if they can't display an image on screen? Why not use a fragment shader? + + The answer is related to how fragment shaders run on a graphics card. Fragment shaders are functions that receive + an input coordinate and return the color of that coordinate in the output. By running the fragment shader once per + pixel, we build up an image to display. + + This "run once per pixel" mode maps poorly to the fractal flame algorithm. Specifically, the "[chaos game](https://en.wikipedia.org/wiki/Chaos_game)" + jumps around to random locations; only by iterating many times can we figure out what the color of a pixel + should be. However, once the fragment shader ends, we'd lose all the information gathered about the other pixels. + + Using a compute shader avoids this "discarded information" problem. Because it can run arbitrary computations, + we can record information about the whole image and make it available later. Compute shaders are overkill + for the examples in this blog post, but are important to a GPU implementation of the fractal flame algorithm. +
+ +The first compute shader example is nothing special; it draws a white rectangle at the edges of an image: + +```rust +const BLACK: glam::Vec4 = glam::vec4(0.0, 0.0, 0.0, 1.0); +const WHITE: glam::Vec4 = glam::vec4(1.0, 1.0, 1.0, 1.0); + +pub struct DrawSized { + pub image_size: glam::UVec2, + pub viewport_size: glam::UVec2, +} + +// The `#[spirv]` annotations describe some details of how this code should run +// on the GPU; they can be ignored for now + +#[spirv(compute(threads(1)))] +pub fn main_cs_bounding( + #[spirv(uniform, descriptor_set = 0, binding = 0)] viewport: &DrawSized, + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] image: &mut [glam::Vec4], +) { + let width = viewport.image_size.x as usize; + let height = viewport.image_size.y as usize; + + for x in 0..width { + for y in 0..height { + image[image_index(x, y, width)] = + if x == 0 || x == width - 1 || y == 0 || y == height - 1 { + WHITE + } else { + BLACK + } + } + } +} +``` + +After running this code, the contents of the `image` buffer should look something like this: + +![White square with black interior](./bounding%20box.png) + +## Vertex/Fragment shaders + +