Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,784 words · 1 segments analyzed
The other night I wanted to implement a small Voronoi-diagram shader to use as a background to a music video, to show up the music project I recently finished. Voronoi Noise is type of algorithm I never implemented before, and I was always mesmerized by the geometric and often organic-ish way it looks. Without much direction in mind, I started implementing it and experimenting. I got it looking pretty cool and I was about to call it a day, but then next morning I found out that, in one of my computers only, there was a weird stutter to the animation. So I decided to dig into it to find out what the problem was. This is the write-up of my whole adventure of a week debugging and disassemblying shaders. There are a few plot twists to the story, and hopefully a lot of interesting information too. This is the final shader. You can also check it on Shadertoy. I still haven’t worked on the video, but you can check the music on all digital platforms: stuffy knows. This shader is not too complicated. In fact, it’s basically just a few concepts put together: Voronoi Diagram A variation of what’s called a Worley Noise, or a Voronoi Noise, that makes the cells distinct between themselves. In this case, I divide the space into equal tiles, then I get a random point within these tiles (the Voronoi centers), and lastly, I calculate the distance of each one of the pixels to the closest 9 Voronoi centers. The closest one defines which Voronoi cell that points belong to. That generates this, in monochrome: voronoi diagrams in black and white There’s a really nice introduction to Voronoi noise in The Book Of Shaders. I think that was the first resource on that I’ve seen back when I was learning shaders. There’s also a lot of other really cool resources in there. UV Wrapping It’s a space distortion. Before I divide the space equally, I distort the space using a noise. Applying it to the Voronoi diagram, I get this: distorting the space Palette Lookup I colored the Voronoi cells with basically the inverted distance to the center. So now, finally, I get the most appropriate color from the palette (considering they’re in the order I’d like), and apply a little bit of the shading, since I only have 7 colors in the palette: const vec3 palette[7] = vec3[7]( vec3(0.008, 0.451, 0.325), // #027353 vec3(0.090, 0.275, 0.090), // #174617 vec3(0.000, 0.455, 0.545), // #00748B vec3(0.949, 0.361, 0.745), // #F25CBE vec3(0.659, 0.580, 0.949), // #A894F2 vec3(1.000, 0.725, 0.820), // #FFB9D1 vec3(0.788, 0.949, 0.675) // #C9F2AC ); /// ... col = palette[i] + vec3(val - .5) * .8; That generates this: the final look for the shader I also move through the palette, making this popping, moving effect that I really enjoy. The final shader is available here. The Problem Next morning, I was working on a different computer than the one I was building the shader initially, so when I opened it to keep tweaking it, I noticed one problem: There’s this weird stutter that wasn’t visible before. At first I was trying it on Firefox, on Windows, then I opened it on Chrome, then Edge. All the browsers displayed the same issue. So I started stripping out the effects to find where the issue was lying. Removing the UV warping, the palette cycling and making the cells bigger made the issue clearer: Just as a comparison, that’s how it’s supposed to look like: It seemed that the issue was in the part of the code that generated the Voronoi centers: // get distance to all points for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { vec2 o = origin + (vec2(i, j) * tile_size); vec2 v = o * 398.0 + vec2(iTime * 1.3, iTime * 1.4); vec2 c = o + noise2(v) / TILES; float d = distance(uv, c); if (d < dist) { dist = d; point = o; } } } More specifically in the lines where I define v and c. The rule to procedurally compose the noise call, and the call itself. Somehow, something within that noise call was acting different in this computer that worked on my other computer. So I tested with these different devices: Two different Linux Laptops with Integrated Intel GPU: Normal Google Pixel 9 Pro phone: Normal Linux PC with an RTX 2070: Normal Windows PC with an RTX 4070: Broken Out of those 5 different devices, the only one in which the stutter happened, was the last one. I even tried different browsers in pretty much all of them. I asked some graphics programmer friends, but they were too busy to help me. So I decided to start debugging it with the tools I had in hand. The Noise Function Just a quick simplified introduction for those who know nothing about shader programming. A shader is a program that runs on the GPU. There are several types of shaders, depending on which part of the graphics pipeline you’re in. Shadertoy takes a Fragment Shader (otherwise known as a Pixel Shader). That’s the stage right after the GPU rasterizes the vertices of an object and then it’s this program that’s responsible for generating the final pixel colors, in summary. The programs essentially runs once per pixel, per frame, in the Shadertoy viewport. You can’t store state, so it has no side-effects. Think of functional programming: it’s like the whole shader is a pure function, it takes some inputs and will always generate the same output based on them. That’s part of what makes Shadertoy so fun to play with. The only two parameter that ever changes in my shader are: 1. the coordinate of the current pixel; 2. the time variable. The former is passed in the form of a 2d vector, in which the components range from 0.0f to 1.0f; the latter is a float, and it only changes from frame to frame. In order to get a random value, I have to rely on hash functions, then possibly make a procedural noise to smooth it out. The noise function I use in this shader was taken from this article on Procedural Noises, by Inigo Quilez, the creator of Shadertoy and one of the most influential graphics programmers I know. It’s slightly different from the article, but I’ve been using this same code in pretty much every shader I wrote since 2019. Here’s the full noise code used in this shader. No need to really understand it, but it basically gets hashes of different values and interpolates them, effectively smoothing out the output. Check Inigo Quilez article if you want to understand better. float hash1(float n) { return fract(n * 17.0 * fract(n * 0.3183099)); } float noisev(in vec3 x) { vec3 p = floor(x); vec3 w = fract(x); vec3 u = w * w * w * (w * (w * 6.0 - 15.0) + 10.0); float n = p.x + 317.0 * p.y + 157.0 * p.z; float a = hash1(n + 0.0); float b = hash1(n + 1.0); float c = hash1(n + 317.0); float d = hash1(n + 318.0); float e = hash1(n + 157.0); float f = hash1(n + 158.0); float g = hash1(n + 474.0); float h = hash1(n + 475.0); float k0 = a; float k1 = b - a; float k2 = c - a; float k3 = e - a; float k4 = a - b - c + d; float k5 = a - c - e + g; float k6 = a - b - e + f; float k7 = -a + b + c - d + e - f - g + h; return (k0 + k1 * u.x + k2 * u.y + k3 * u.z + k4 * u.x * u.y + k5 * u.y * u.z + k6 * u.z * u.x + k7 * u.x * u.y * u.z); } First Attempt to Find the Issue Since my friends couldn’t give me directions on to debug this, I fired off an LLM and explained the problem, along with the full code. At first it started analyzing the hash function to find some continuity problem, but I thought it was not a good idea, since this code is used in thousands of Shadertoy shaders. So I steered it away from it. Then it decided that it could be some issue with a call to fract, right in the beginning of the noise function. Apparently it found some people online complaining about a problem with it in Nvidia 40xx drivers. fract gets the fractional part of a float. Reading through the thinking section, I noticed that the LLM (Kimi K3) was assuming that the stuttering started after some time of the code running, which can indeed degrade the precision of the float, causing visible stutters. Pretty common thing, but in this case, it was stuttering since the very first second, but I actually decided to try the first suggestion it gave: substitute fract(x) by x - floor(x), which is mathematically equivalent. And it worked. It fixed the issue. I didn’t see the Voronoi vertices snapping anymore. I asked for an explanation, and it gave me this: “Root cause: Nvidia driver 595.79 (RTX 4070) miscompiles GLSL fract() usage in this shader’s context for large-magnitude operands (~100–1000): the returned fractional part is temporally discontinuous (moves smoothly, then jumps). Proof: replacing fract(x) with the mathematically identical x - floor(x) (already computed as p — an exact fp32 subtraction) eliminates the snapping on the affected machine while producing bit-identical output on healthy GPUs. A compiler that computes the same equation differently depending on which instructions it emits is, by definition, a miscompile. Effect on the shader: fract(v) is the interpolation weight of the value noise; when it lies, the noise output teleports instead of gliding — and the Voronoi centers (c = o + noise2(v)/TILES) teleport with it.” At this point I could have moved on with my life. Shader was working, I could go back to tweaking it, improving the code, then publish it. But I’m too curious and I wasn’t very convinced, so I decided to dig further. Reproducing the Issue The natural next step is finding the minimum possible code that will reproduce the issue. So I asked the LLM, since it already had the hypothesis that generated the fix. It failed. I changed the model a couple of times, even to proprietary models like Opus, but none of them was able to create a single program that reproduced the issue.