|
| 1 | +--- |
| 2 | +title: TypeGPU 0.11 |
| 3 | +date: 2026-04-14 |
| 4 | +tags: |
| 5 | + - Release notes |
| 6 | +authors: |
| 7 | + - name: Iwo Plaza |
| 8 | + title: TypeGPU developer |
| 9 | + picture: https://avatars.githubusercontent.com/u/7166752?s=200 |
| 10 | +cover: |
| 11 | + alt: A collage of examples introduced alongside TypeGPU 0.11 |
| 12 | + image: banner.webp |
| 13 | +--- |
| 14 | + |
| 15 | +Hello fellow GPU enthusiast! |
| 16 | + |
| 17 | +Over the past 2 months, my team and I have been pulling on a few threads that we thought would improve TypeGPU in terms of efficiency, and as a byproduct, we actually made the APIs more convenient. We are also introducing a _lint plugin_ to further improve the diagnostics and feedback you receive while writing TypeGPU shaders, on top of the type safety we already provide. |
| 18 | + |
| 19 | +- [New examples](#new-examples) |
| 20 | +- [Migration guide](#migration-guide) |
| 21 | +- [New and improved Write APIs](#efficient-write-apis) |
| 22 | +- [Efficient write APIs](#efficient-write-apis) |
| 23 | +- [A better partial write](#a-better-partial-write) |
| 24 | + |
| 25 | + |
| 26 | +We have been pulling a few more threads than I mentioned here.. but for those, you'll have to wait for the next blog post 🤐. |
| 27 | + |
| 28 | +## New examples |
| 29 | + |
| 30 | +My teammate Konrad Reczko [(@reczkok)](https://github.com/reczkok) has outdone himself again, and delivered 3 new examples that push TypeGPU APIs to their limits: |
| 31 | + |
| 32 | +* ["Genetic Racing"](https://typegpu.com/examples/#example=algorithms--genetic-racing) - watch a swarm of cars learn to traverse a procedurally generated race track. |
| 33 | +* ["Mesh Skinning"](https://typegpu.com/examples/#example=simple--mesh-skinning) - an animation and skinning system built from scratch in TypeGPU. |
| 34 | +* ["Parallax Occlusion Mapping"](https://typegpu.com/examples/#example=rendering--pom) - squeezing amazing depth out of just two triangles and a set of textures. |
| 35 | + |
| 36 | +## Migration guide |
| 37 | + |
| 38 | +### Deprecated APIs |
| 39 | + |
| 40 | +The `buffer.writePartial` API is being deprecated in favor of `buffer.patch` [(and here are the reasons why)](#a-better-partial-write). |
| 41 | +To migrate, simply replace any partial write of arrays in the form of `[{ idx: 2, value: foo }, /* ... */]` with `{ 2: foo, /* ... */ }`. |
| 42 | + |
| 43 | +```diff lang=ts |
| 44 | +const buffer = root.createBuffer(d.arrayOf(d.vec3f, 5)).$usage('storage'); |
| 45 | + |
| 46 | +- buffer.writePartial([{ idx: 2, value: d.vec3f(1, 2, 3) }]); |
| 47 | ++ buffer.patch({ 2: d.vec3f(1, 2, 3) }); |
| 48 | +``` |
| 49 | + |
| 50 | +### Stabilizing textures and samplers |
| 51 | + |
| 52 | +One by one, we're making our APIs available without the `['~unstable']` prefix, and this time around, it's **textures** and **samplers**. |
| 53 | +Just drop the unstable prefix, and you're good to go. |
| 54 | + |
| 55 | +```diff lang=ts |
| 56 | +- const sampler = root['~unstable'].createSampler({ |
| 57 | ++ const sampler = root.createSampler({ |
| 58 | + magFilter: 'linear', |
| 59 | + minFilter: 'linear', |
| 60 | +}); |
| 61 | + |
| 62 | +- const texture = root['~unstable'].createTexture({ |
| 63 | ++ const texture = root.createTexture({ |
| 64 | + size: [256, 256], |
| 65 | + format: 'rgba8unorm' as const, |
| 66 | +}).$usage('sampled'); |
| 67 | +``` |
| 68 | + |
| 69 | +## New and improved write APIs |
| 70 | + |
| 71 | +### Efficient data |
| 72 | + |
| 73 | +When writing to a buffer with an array of vectors, it's no longer required to create vector instances (e.g. `d.vec3f()`). |
| 74 | +```ts |
| 75 | +const positionsMutable = root.createMutable(d.arrayOf(d.vec3f, 3)); |
| 76 | + |
| 77 | +// existing overload |
| 78 | +positionsMutable.write([d.vec3f(0, 1, 2), d.vec3f(3, 4, 5), d.vec3f(6, 7, 8)]); |
| 79 | +// new overloads ⚡ |
| 80 | +positionsMutable.write([[0, 1, 2], [3, 4, 5], [6, 7, 8]]); // tuples |
| 81 | +positionsMutable.write(new Float32Array([0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0])); // typed arrays (mind the padding) |
| 82 | +// and more... |
| 83 | +``` |
| 84 | +Each one is more efficient than the previous, so you can choose the appropriate API for your efficiency needs. |
| 85 | +[More about these new APIs here](https://docs.swmansion.com/TypeGPU/fundamentals/buffers/#writing-to-a-buffer). |
| 86 | + |
| 87 | +### A better partial write |
| 88 | + |
| 89 | +When writing to a buffer, we require the passed in value to exactly match the schema. This specifically means that updating a single field of a single array item was very costly. The `buffer.partialWrite` API remedied that by accepting partial records |
| 90 | +for structs, and a list of indices and values to update in arrays. This works fine, but doesn't compose well with more complex data structures: |
| 91 | + |
| 92 | +```ts |
| 93 | +const Node = d.struct({ |
| 94 | + color: d.vec3f, |
| 95 | + // Indices of neighboring nodes |
| 96 | + neighbors: d.arrayOf(d.u32, 4), |
| 97 | +}); |
| 98 | + |
| 99 | +const nodes = root.createUniform(d.arrayOf(Node, 100)); |
| 100 | + |
| 101 | +// Updating the 50th node |
| 102 | +nodes.writePartial([ |
| 103 | + { |
| 104 | + idx: 50, |
| 105 | + value: { |
| 106 | + color: d.vec3f(1, 0, 1), |
| 107 | + // We cannot pass [48, 49, 51, 52], as we could with nodes.write() |
| 108 | + neighbors: [{ idx: 0, value: 48 }, { idx: 0, value: 49 }, { idx: 0, value: 51 }, { idx: 0, value: 52 }], |
| 109 | + }, |
| 110 | + } |
| 111 | +]); |
| 112 | +``` |
| 113 | + |
| 114 | +If we loosened the type to accept either partial arrays or full arrays, then we would reach an ambiguity in the following case: |
| 115 | + |
| 116 | +```ts |
| 117 | +const Foo = d.struct({ |
| 118 | + idx: d.u32, |
| 119 | + value: d.f32, |
| 120 | +}); |
| 121 | + |
| 122 | +const foos = root.createUniform(d.arrayOf(Foo, 2)); |
| 123 | + |
| 124 | +foos.writePartial([{ idx: 1, value: /* ... */ }, { idx: 0, value: /* ... */ }]); |
| 125 | +``` |
| 126 | + |
| 127 | +We could traverse the value deeper to disambiguate, but for the sake of efficiency and being able to reuse optimizations added to `buffer.write` by [Konrad](https://github.com/reczkok), we chose to add a new API: |
| 128 | + |
| 129 | +```diff lang=ts |
| 130 | +- foos.writePartial([{ idx: 1, value: /* ... */ }, { idx: 0, value: /* ... */ }]); |
| 131 | ++ foos.patch({ 1: /* ... */, 0: /* ... */ }); |
| 132 | +``` |
| 133 | + |
| 134 | +[You can read more about .patch in the Buffers guide](/TypeGPU/fundamentals/buffers/#patching-buffers). |
| 135 | + |
| 136 | +### Writing struct-of-arrays (SoA) data |
| 137 | + |
| 138 | +When the buffer schema is an `array<struct<...>>`, you can write the data in a struct-of-arrays form with `writeSoA` from `typegpu/common`. This is useful when your CPU-side data is already stored per-field, such as simulation attributes kept in separate typed arrays. |
| 139 | + |
| 140 | +```ts |
| 141 | +const Particle = d.struct({ |
| 142 | + pos: d.vec3f, |
| 143 | + vel: d.f32, |
| 144 | +}); |
| 145 | + |
| 146 | +const particleBuffer = root.createBuffer(d.arrayOf(Particle, 2)); |
| 147 | + |
| 148 | +common.writeSoA(particleBuffer, { |
| 149 | + pos: new Float32Array([ |
| 150 | + 1, 2, 3, |
| 151 | + 4, 5, 6, |
| 152 | + ]), |
| 153 | + vel: new Float32Array([10, 20]), |
| 154 | +}); |
| 155 | +``` |
| 156 | + |
| 157 | +[More about this API can be found in the Buffers guide.](/TypeGPU/fundamentals/buffers/#writing-struct-of-arrays-soa-data) |
| 158 | + |
| 159 | +## Shader code ergonomics |
| 160 | + |
| 161 | +There's been a lot of improvements to our shader generation, mainly in regards to compile-time execution and pruning of unreachable branches. |
| 162 | +I will highlight some of them in the following sections. |
| 163 | + |
| 164 | +### std.range |
| 165 | + |
| 166 | +The new `std.range` function works similarly to `range()` in Python, and returns an array that can be iterated over. |
| 167 | +When combined with `tgpu.unroll`, it's now very easy to produce a set amount of code blocks. |
| 168 | + |
| 169 | +```ts |
| 170 | +let result = d.u32(); |
| 171 | +for (const i of tgpu.unroll(std.range(3))) { |
| 172 | + // this block will be inlined 3 times |
| 173 | + result += i * 10; |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +Generates: |
| 178 | +```wgsl |
| 179 | +var result = 0u; |
| 180 | +// unrolled iteration #0 |
| 181 | +{ |
| 182 | + result += 0u; |
| 183 | +} |
| 184 | +// unrolled iteration #1 |
| 185 | +{ |
| 186 | + result += 10u; |
| 187 | +} |
| 188 | +// unrolled iteration #2 |
| 189 | +{ |
| 190 | + result += 20u; |
| 191 | +} |
| 192 | +```` |
| 193 | +
|
| 194 | +Because `i` is known at compile-time, the `i * 10` get evaluated and injected into the generated code in each block. |
| 195 | +For more, refer to [tgpu.unroll documentation.](/TypeGPU/fundamentals/utils/#tgpuunroll). |
| 196 | +
|
| 197 | +### Boolean logic |
| 198 | +
|
| 199 | +Logical expressions are now short-circuited if we can determine the result early. |
| 200 | +```ts |
| 201 | +const clampingEnabled = tgpu.accessor(d.bool); |
| 202 | +
|
| 203 | +function lerp(a: number, b: number, t: number) { |
| 204 | + 'use gpu'; |
| 205 | + let value = a + (b - a) * t; |
| 206 | + if (clampingEnabled.$ && (value > 1 || value < 0)) { |
| 207 | + value = std.saturate(value); |
| 208 | + } |
| 209 | + return value; |
| 210 | +} |
| 211 | +``` |
| 212 | + |
| 213 | +Generated WGSL depending on the value of `clampingEnabled`: |
| 214 | + |
| 215 | +```wgsl |
| 216 | +// clampingEnabled.$ === false |
| 217 | +fn lerp(a: f32, b: f32, t: f32) -> f32 { |
| 218 | + var value = a + (b - a) * t; |
| 219 | + return value; |
| 220 | +} |
| 221 | +
|
| 222 | +// clampingEnabled.$ === true |
| 223 | +fn lerp(a: f32, b: f32, t: f32) -> f32 { |
| 224 | + var value = a + (b - a) * t; |
| 225 | + if (value > 1 || value < 0) { |
| 226 | + value = std.saturate(value); |
| 227 | + } |
| 228 | + return value; |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### Convenience overload for `tgpu.const` API |
| 233 | + |
| 234 | +If you're defining a WGSL constant using an array schema, you no longer have to duplicate the array length both in the value and in the schema. |
| 235 | +The `tgpu.const` function now accepts dynamically-sized schemas. |
| 236 | + |
| 237 | +```diff lang=ts |
| 238 | +const ColorStops = d.arrayOf(d.vec3f); |
| 239 | + |
| 240 | +const colorStops = tgpu.const( |
| 241 | +- ColorStops(3), |
| 242 | ++ ColorStops, |
| 243 | + [d.vec3f(1, 0, 0), d.vec3f(0, 1, 0), d.vec3f(0, 0, 1)], |
| 244 | +); |
| 245 | +``` |
| 246 | + |
| 247 | +## Ecosystem updates |
| 248 | + |
| 249 | +There have been a lot of work outside of the `typegpu` package, both internally and from the community. |
| 250 | + |
| 251 | +### Lint plugin |
| 252 | + |
| 253 | +Aleksander Katan ([@aleksanderkatan](https://github.com/aleksanderkatan)) has been working behind the scenes on an ESLint/Oxlint plugin, capable of catching user errors that types cannot. |
| 254 | + |
| 255 | +```ts |
| 256 | +import tgpu, { d } from 'typegpu'; |
| 257 | + |
| 258 | +function increment(n: number) { |
| 259 | + 'use gpu'; |
| 260 | + return n++; |
| 261 | + // ^^^ |
| 262 | + // Cannot assign to 'n' since WGSL parameters are immutable. |
| 263 | + // If you're using d.ref, please either use '.$' or disable this rule |
| 264 | +} |
| 265 | + |
| 266 | +function createBoid() { |
| 267 | + 'use gpu'; |
| 268 | + const boid = { pos: d.vec2f(), size: 1 }; |
| 269 | + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 270 | + // { pos: d.vec2f(), size: 1 } must be wrapped in a schema call |
| 271 | + return boid; |
| 272 | +} |
| 273 | + |
| 274 | +function clampTo0(n: number) { |
| 275 | + 'use gpu'; |
| 276 | + let result; |
| 277 | + // ^^^^^^ |
| 278 | + // 'result' must have an initial value |
| 279 | + if (n < 0) { |
| 280 | + result = 0; |
| 281 | + } else { |
| 282 | + result = n; |
| 283 | + } |
| 284 | + return result; |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +[For setup instructions and available rules, refer to the documentation](/TypeGPU/tooling/eslint-plugin-typegpu/) |
| 289 | + |
| 290 | +### New @typegpu/color helpers |
| 291 | + |
| 292 | +There are three new helper functions importable from `@typegpu/color` which can be called at compile-time to create |
| 293 | +color vectors from hexadecimal strings: `hexToRgb`, `hexToRgba` and `hexToOklab`. |
| 294 | + |
| 295 | +```ts |
| 296 | +import { hexToRgb } from '@typegpu/color'; |
| 297 | + |
| 298 | +function getGradientColor(t: number) { |
| 299 | + 'use gpu'; |
| 300 | + const from = hexToRgb('#FF00FF'); |
| 301 | + const to = hexToRgb('#00FF00'); |
| 302 | + return std.mix(from, to, t); |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | +Generated WGSL: |
| 307 | +```wgsl |
| 308 | +fn getGradientColor(t: f32) -> f32 { |
| 309 | + var from = d.vec3f(1, 0, 1); |
| 310 | + var to = d.vec3f(0, 1, 0); |
| 311 | + return mix(from, to, t); |
| 312 | +} |
| 313 | +```` |
| 314 | +
|
| 315 | +### Bundler plugin rewrite |
| 316 | +
|
| 317 | +The `unplugin-typegpu` package is what enables TypeScript shaders, and to support its continued development, we rewrote it from |
| 318 | +the ground up. It should now support more bundlers than ever before, out of the box, including `esbuild`. |
| 319 | +
|
| 320 | +### Motion GPU |
| 321 | +
|
| 322 | +A minimalist WebGPU framework called [Motion GPU](https://motion-gpu.dev/) introduced a way to integrate with TypeGPU, and wrote about it |
| 323 | +in their documentation [(Integrations / TypeGPU)](https://motion-gpu.dev/docs/integrations-typegpu). It's awesome to see the continued adoption |
| 324 | +of TypeGPU in other ecosystems and communities 🎉 |
| 325 | +
|
| 326 | +## What's next? |
| 327 | +
|
| 328 | +There are many more things introduced in TypeGPU 0.11 that I haven't mentioned. If you're curious, you can |
| 329 | +read [the full 0.11.0 changelog](https://github.com/software-mansion/TypeGPU/compare/v0.10.2...v0.11.0). |
0 commit comments