Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
d2d8a83
basic for ... of ... loop compilation
cieplypolar Dec 14, 2025
a2bb186
naming convention fix
cieplypolar Dec 15, 2025
edfaea7
refactor, var vs let, support for vectors
cieplypolar Dec 16, 2025
4909740
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 16, 2025
f8ac4b8
biome fixes
cieplypolar Dec 16, 2025
8be4dd7
for of in some examples
cieplypolar Dec 16, 2025
d74b998
updated wgsl tests
cieplypolar Dec 16, 2025
934bacf
cleanup
cieplypolar Dec 16, 2025
38bee47
minor test cleanup
cieplypolar Dec 17, 2025
581f7d2
Taking a reference to the iterated value if it's a non-ephemeral element
iwoplaza Dec 18, 2025
5e8cfe0
Revert example change
iwoplaza Dec 18, 2025
c386c45
Free test!
Dec 18, 2025
9b0da3d
more review changes + vector type guard impr
cieplypolar Dec 18, 2025
17c34dc
review changes
cieplypolar Dec 20, 2025
70d515d
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 20, 2025
41a0165
nested forOf test
cieplypolar Dec 21, 2025
3990078
Merge branch 'feat/for-of-loop' of github.com:software-mansion/TypeGP…
cieplypolar Dec 21, 2025
e47a2cc
the last test, I promise
cieplypolar Dec 21, 2025
80e60bd
test cleanup
cieplypolar Dec 22, 2025
26e0f98
I lied
cieplypolar Dec 22, 2025
3b90675
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 23, 2025
46ef92a
merge main
cieplypolar Dec 23, 2025
a1addca
intermediate representation of array expression
cieplypolar Jan 8, 2026
807b881
missing origin comment
cieplypolar Jan 8, 2026
a374551
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 8, 2026
b2c7209
Merge branch 'main' into fix/arrayExpr-resolve-delay
cieplypolar Jan 8, 2026
243d9e6
unused import
cieplypolar Jan 8, 2026
923bf2c
Merge branch 'fix/arrayExpr-resolve-delay' of github.com:software-man…
cieplypolar Jan 8, 2026
cd6c194
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 9, 2026
81e8e64
intermediate representation of array expression
cieplypolar Jan 8, 2026
d6c6823
unroll initial commit
cieplypolar Jan 8, 2026
0f54edd
very minor cleanup
cieplypolar Jan 8, 2026
d65a6c1
trying to make it readable
cieplypolar Jan 9, 2026
57bfbae
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 12, 2026
182a8d0
fix: array expression resolves references correctly
cieplypolar Jan 13, 2026
babc641
Merge branch 'main' into fix/arrayExpr-resolve-delay
cieplypolar Jan 13, 2026
e7c3f25
Merge branch 'fix/arrayExpr-resolve-delay' into feat/unroll
cieplypolar Jan 13, 2026
7f20601
Merge branch 'feat/for-of-loop' into feat/unroll
cieplypolar Jan 13, 2026
e30a98f
more work on unroll
cieplypolar Jan 13, 2026
d3a19ea
block with externals
cieplypolar Jan 16, 2026
d64a71f
nr fix
cieplypolar Jan 16, 2026
91715f5
nested block test
cieplypolar Jan 16, 2026
e21cda1
no ts-expect-error
cieplypolar Jan 16, 2026
1cd9c43
Merge branch 'main' into feat/unroll
cieplypolar Jan 16, 2026
0ed7d32
wip
cieplypolar Jan 20, 2026
43e4f42
new name registry
cieplypolar Feb 11, 2026
7dc3af9
Merge branch 'main' into impr/better-nameRegistry
cieplypolar Feb 18, 2026
03aaab5
test align
cieplypolar Feb 18, 2026
f231c19
Merge branch 'main' into feat/unroll
cieplypolar Feb 18, 2026
ad27b43
align with new apis
cieplypolar Feb 18, 2026
5f34483
use vector components field instead of regex match
cieplypolar Feb 18, 2026
a89d840
we don't need allTimeUsedNames
cieplypolar Feb 19, 2026
da9c104
lost game of life
cieplypolar Feb 19, 2026
c90cdeb
block externals with tests + more elegant for of
cieplypolar Feb 19, 2026
c656ab0
Merge branch 'main' into impr/better-nameRegistry
cieplypolar Feb 19, 2026
70f3070
for ... of ... fixes + tests
cieplypolar Feb 19, 2026
b445d7e
cleanup
cieplypolar Feb 19, 2026
3067577
rolldown tests
cieplypolar Feb 20, 2026
49b57d6
more cleanup
cieplypolar Feb 20, 2026
8edf9b2
rename
cieplypolar Feb 20, 2026
06d058e
Merge branch 'main' into impr/block-externals
cieplypolar Feb 20, 2026
40bad85
Merge branch 'main' into feat/unroll
cieplypolar Feb 20, 2026
2efe773
Merge branch 'impr/block-externals' into feat/unroll
cieplypolar Feb 20, 2026
45df1df
Merge branch 'impr/better-nameRegistry' into feat/unroll
cieplypolar Feb 20, 2026
1657f9d
circular deps
cieplypolar Feb 20, 2026
368062f
unrolling names of struct fields
cieplypolar Feb 20, 2026
235b546
unroll is stable :)
cieplypolar Feb 20, 2026
c765aba
working unroll (I hope)
cieplypolar Feb 20, 2026
2795d9c
Merge branch 'main' into feat/unroll
cieplypolar Feb 20, 2026
971cd5d
Merge branch 'main' into impr/better-nameRegistry
cieplypolar Feb 22, 2026
7a2901c
regular for header in new block scope
cieplypolar Feb 23, 2026
14b694a
Merge branch 'impr/better-nameRegistry' of github.com:software-mansio…
cieplypolar Feb 23, 2026
47e4b64
Merge branch 'main' into impr/block-externals
cieplypolar Feb 23, 2026
10e6552
Merge branch 'main' into impr/block-externals
cieplypolar Feb 23, 2026
71800b6
Merge branch 'main' into impr/better-nameRegistry
cieplypolar Feb 23, 2026
24d9403
Merge branch 'main' into feat/unroll
cieplypolar Feb 23, 2026
12533be
Merge branch 'impr/better-nameRegistry' into feat/unroll
cieplypolar Feb 23, 2026
7606387
Merge branch 'impr/block-externals' into feat/unroll
cieplypolar Feb 23, 2026
4a08433
forbid pointers in AE
cieplypolar Feb 23, 2026
42da806
cleanup
cieplypolar Feb 23, 2026
060d517
unroll in examples
cieplypolar Feb 23, 2026
5fbd3d8
docs
cieplypolar Feb 23, 2026
a28c26c
Merge branch 'main' into impr/better-nameRegistry
cieplypolar Feb 23, 2026
994c57c
review changes
cieplypolar Feb 23, 2026
92e57a7
Merge branch 'main' into impr/block-externals
cieplypolar Feb 23, 2026
d1b08e4
Merge branch 'impr/block-externals' into feat/unroll
cieplypolar Feb 23, 2026
afdd668
Merge branch 'impr/better-nameRegistry' into feat/unroll
cieplypolar Feb 23, 2026
5980198
review changes
cieplypolar Feb 23, 2026
4c40905
Merge branch 'main' into feat/unroll
cieplypolar Feb 23, 2026
8b38ee6
partial review changes
cieplypolar Feb 23, 2026
82d274a
Merge branch 'feat/unroll' of github.com:software-mansion/TypeGPU int…
cieplypolar Feb 23, 2026
253b2ae
Merge branch 'main' into feat/unroll
cieplypolar Feb 23, 2026
6b45504
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
89d9c83
cleanup
cieplypolar Feb 24, 2026
a09feb0
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
73ec870
more cleanup
cieplypolar Feb 24, 2026
15ab264
Merge branch 'feat/unroll' of github.com:software-mansion/TypeGPU int…
cieplypolar Feb 24, 2026
e88c10a
continue and break detection
cieplypolar Feb 24, 2026
65b04d9
oxlint
cieplypolar Feb 24, 2026
9cf95f1
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
552f505
oxlint
cieplypolar Feb 24, 2026
e2a23e3
Merge branch 'feat/unroll' of github.com:software-mansion/TypeGPU int…
cieplypolar Feb 24, 2026
819f3e8
oxlint
cieplypolar Feb 24, 2026
dcbb268
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
ab119e6
cleanup
cieplypolar Feb 24, 2026
7dca2e7
deleted test
cieplypolar Feb 24, 2026
6384fea
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
7040af8
review changes
cieplypolar Feb 24, 2026
0a94c84
review changes
cieplypolar Feb 24, 2026
fe72540
Merge branch 'main' into feat/unroll
cieplypolar Feb 24, 2026
f2fca4e
more review changes
cieplypolar Feb 24, 2026
205ce8c
Merge branch 'feat/unroll' of github.com:software-mansion/TypeGPU int…
cieplypolar Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,59 @@ Otherwise, for example when using `tgpu.resolve` on a WGSL template, logs are ig
- `console.log` only works in fragment and compute shaders.
This is due to a [WebGPU limitation](https://www.w3.org/TR/WGSL/#address-space) that does not allow modifying buffers during the vertex shader stage.
- `console.log` currently does not support template literals (but you can use [string substitutions](https://developer.mozilla.org/en-US/docs/Web/API/console#using_string_substitutions), or just pass multiple arguments instead).

## *for...of...* loops

TypeGPU supports `for...of...` loops in shader functions. The only constraints are that the loop variable must be declared with `const` and the iterable must be stored in a variable.

```ts twoslash
import tgpu, { d } from 'typegpu';

const processNeighbor = (cell: d.v2i) => {};

// ---cut---
const processNeighbors = (cell: d.v2i) => {
'use gpu';

const offsets = [
d.vec2i(0, 1),
d.vec2i(0, -1),
d.vec2i(1, 0),
d.vec2i(-1, 0),
];

for (const offset of offsets) {
processNeighbor(cell.add(offset));
}
};
```

## *tgpu.unroll*

For code with small, fixed iteration counts, you can use `tgpu.unroll` to unroll loops at compile time. This eliminates branch prediction overhead and can significantly improve performance.

### Usage

Wrap your iterable with `tgpu.unroll()`:

```ts twoslash
import tgpu, { d } from 'typegpu';

const processNeighbor = (cell: d.v2i) => {};

// ---cut---
const processNeighbors = (cell: d.v2i) => {
'use gpu';

for (const dy of tgpu.unroll([-1, 0, 1])) {
for (const dx of tgpu.unroll([-1, 0, 1])) {
processNeighbor(cell.add(d.vec2i(dx, dy)));
}
}
};
```

:::note
- There are no constraints on how large a loop can be for unrolling. We will always try to unroll it, and if we can't, you'll receive an error.
- You cannot use `continue` or `break` inside loop that you intend to unroll later.
:::
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ const jumpFlood = root.createGuardedComputePipeline((x, y) => {
let bestInsideDist = 1e20;
let bestOutsideDist = 1e20;

for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
for (const dx of tgpu.unroll([-1, 0, 1])) {
for (const dy of tgpu.unroll([-1, 0, 1])) {
const sample = sampleWithOffset(
pingPongLayout.$.readView,
d.vec2i(x, y),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,22 +165,23 @@ const jumpFlood = root.createGuardedComputePipeline((x, y) => {
let minDist = 1e20;
let bestSample = SampleResult({ color: d.vec4f(), coord: d.vec2f(-1) });

for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
for (const dy of tgpu.unroll([-1, 0, 1])) {
for (const dx of tgpu.unroll([-1, 0, 1])) {
const sample = sampleWithOffset(
pingPongLayout.$.readView,
d.vec2i(x, y),
d.vec2i(dx * offset, dy * offset),
);

if (sample.coord.x < 0) {
continue;
}

const dist = std.distance(d.vec2f(x, y), sample.coord.mul(d.vec2f(size)));
if (dist < minDist) {
minDist = dist;
bestSample = SampleResult(sample);
if (sample.coord.x >= 0) {
const dist = std.distance(
d.vec2f(x, y),
sample.coord.mul(d.vec2f(size)),
);
if (dist < minDist) {
minDist = dist;
bestSample = SampleResult(sample);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export const computeFn = tgpu.computeFn({
).sub(d.vec2i(filterOffset, 0));

// Load a tile of pixels into shared memory
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
for (const r of tgpu.unroll([0, 1, 2, 3])) {
for (const c of tgpu.unroll([0, 1, 2, 3])) {
let loadIndex = baseIndex.add(d.vec2i(c, r));
if (flipAccess.$) {
loadIndex = loadIndex.yx;
Expand All @@ -75,8 +75,8 @@ export const computeFn = tgpu.computeFn({
std.workgroupBarrier();

// Apply the horizontal blur filter and write to the output texture
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
for (const r of tgpu.unroll([0, 1, 2, 3])) {
for (const c of tgpu.unroll([0, 1, 2, 3])) {
let writeIndex = baseIndex.add(d.vec2i(c, r));
if (flipAccess.$) {
writeIndex = writeIndex.yx;
Expand Down
8 changes: 4 additions & 4 deletions apps/typegpu-docs/src/examples/image-processing/blur/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ const computeFn = tgpu.computeFn({
).sub(d.vec2i(filterOffset, 0));

// Load a tile of pixels into shared memory
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
for (const r of tgpu.unroll([0, 1, 2, 3])) {
for (const c of tgpu.unroll([0, 1, 2, 3])) {
let loadIndex = baseIndex.add(d.vec2i(c, r));
if (ioLayout.$.flip !== 0) {
loadIndex = loadIndex.yx;
Expand All @@ -92,8 +92,8 @@ const computeFn = tgpu.computeFn({
std.workgroupBarrier();

// Apply the horizontal blur filter and write to the output texture
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
for (const r of tgpu.unroll([0, 1, 2, 3])) {
for (const c of tgpu.unroll([0, 1, 2, 3])) {
let writeIndex = baseIndex.add(d.vec2i(c, r));
if (ioLayout.$.flip !== 0) {
writeIndex = writeIndex.yx;
Expand Down
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/rendering/3d-fish/compute.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { d, std } from 'typegpu';
import tgpu, { d, std } from 'typegpu';
import * as p from './params.ts';
import { computeBindGroupLayout as layout } from './schemas.ts';
import { projectPointOnLine } from './tgsl-helpers.ts';
Expand Down Expand Up @@ -39,7 +39,7 @@ export const simulate = (fishIndex: number) => {
if (cohesionCount > 0) {
cohesion = cohesion / cohesionCount - fishData.position;
}
for (let i = 0; i < 3; i += 1) {
for (const i of tgpu.unroll([0, 1, 2])) {
const repulsion = d.vec3f();
repulsion[i] = 1;

Expand Down
3 changes: 2 additions & 1 deletion apps/typegpu-docs/src/examples/rendering/clouds/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ export const raymarch = tgpu.fn([d.vec3f, d.vec3f, d.vec3f], d.vec4f)(
},
);

const iterations = Array.from({ length: FBM_OCTAVES }, (_, i) => i);
const fbm = tgpu.fn([d.vec3f], d.f32)((pos) => {
let sum = d.f32();
let amp = d.f32(CLOUD_AMPLITUDE);
let freq = d.f32(CLOUD_FREQUENCY);

for (let i = 0; i < FBM_OCTAVES; i++) {
for (const _i of tgpu.unroll(iterations)) {
sum += noise3d(std.mul(pos, freq)) * amp;
amp *= FBM_PERSISTENCE;
freq *= FBM_LACUNARITY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class IcosphereGenerator {
]);

const baseIndexNext = triangleIndex * 12;
for (let i = d.u32(0); i < 12; i++) {
for (const i of tgpu.unroll([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])) {
const reprojectedVertex = newVertices[i];

const triBase = i - (i % 3);
Expand Down
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/rendering/jelly-slider/taa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const taaResolveFn = tgpu.computeFn({

const dimensions = std.textureDimensions(taaResolveLayout.$.currentTexture);

for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (const x of tgpu.unroll([-1, 0, 1])) {
for (const y of tgpu.unroll([-1, 0, 1])) {
const sampleCoord = d.vec2i(gid.xy).add(d.vec2i(x, y));
const clampedCoord = std.clamp(
sampleCoord,
Expand Down
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/rendering/jelly-switch/taa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const taaResolveFn = tgpu.computeFn({

const dimensions = std.textureDimensions(taaResolveLayout.$.currentTexture);

for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (const x of tgpu.unroll([-1, 0, 1])) {
for (const y of tgpu.unroll([-1, 0, 1])) {
const sampleCoord = d.vec2i(gid.xy).add(d.vec2i(x, y));
const clampedCoord = std.clamp(
sampleCoord,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const sampleWithChromaticAberration = (
) => {
'use gpu';
const samples = d.arrayOf(d.vec3f, 3)();
for (let i = 0; i < 3; i++) {
for (const i of tgpu.unroll([0, 1, 2])) {
const channelOffset = dir.mul((d.f32(i) - 1.0) * offset);
samples[i] =
std.textureSampleBias(tex, sampler, uv.sub(channelOffset), blur).rgb;
Expand Down
3 changes: 2 additions & 1 deletion apps/typegpu-docs/src/examples/simple/ripple-cube/pbr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ export const evaluateLight = (
.mul(ndotl);
};

const lightCountIterations = Array.from({ length: LIGHT_COUNT }, (_, i) => i);
export const shade = (p: d.v3f, n: d.v3f, v: d.v3f): d.v3f => {
'use gpu';
const material = materialAccess.$;
const f0 = std.mix(d.vec3f(0.04), material.albedo, material.metallic);

let lo = d.vec3f(0);
for (let i = 0; i < LIGHT_COUNT; i++) {
for (const i of tgpu.unroll(lightCountIterations)) {
lo = lo.add(evaluateLight(p, n, v, lightsAccess.$[i], material, f0));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export function createPostProcessingPipelines(
let minColor = d.vec3f(9999);
let maxColor = d.vec3f(-9999);

for (let ox = -1; ox <= 1; ox++) {
for (let oy = -1; oy <= 1; oy++) {
for (const ox of tgpu.unroll([-1, 0, 1])) {
for (const oy of tgpu.unroll([-1, 0, 1])) {
const sampleCoord = coord.add(d.vec2i(ox, oy));
const clampedCoord = std.clamp(
sampleCoord,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,21 +160,19 @@ const computeVelocity = (x: number, y: number): d.v2f => {
];
let dirChoiceCount = 1;

for (const offset of neighborOffsets) {
for (const offset of tgpu.unroll(neighborOffsets)) {
const neighborDensity = getCell(x + offset.x, y + offset.y);
const cost = neighborDensity.z + d.f32(offset.y) * gravityCost;

if (!isValidFlowOut(x + offset.x, y + offset.y)) {
continue;
}

if (cost === leastCost) {
dirChoices[dirChoiceCount] = d.vec2f(d.f32(offset.x), d.f32(offset.y));
dirChoiceCount++;
} else if (cost < leastCost) {
leastCost = cost;
dirChoices[0] = d.vec2f(d.f32(offset.x), d.f32(offset.y));
dirChoiceCount = 1;
if (isValidFlowOut(x + offset.x, y + offset.y)) {
if (cost === leastCost) {
dirChoices[dirChoiceCount] = d.vec2f(d.f32(offset.x), d.f32(offset.y));
dirChoiceCount++;
} else if (cost < leastCost) {
leastCost = cost;
dirChoices[0] = d.vec2f(d.f32(offset.x), d.f32(offset.y));
dirChoiceCount = 1;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ const getPerpendicular = (dir: d.v3f) => {
return std.normalize(std.cross(dir, axis));
};

const numSamples = 8;
const samplesIterations = Array.from({ length: numSamples }, (_, i) => i);
const sense3D = (pos: d.v3f, direction: d.v3f) => {
'use gpu';
const dims = std.textureDimensions(computeLayout.$.oldState);
Expand All @@ -187,8 +189,7 @@ const sense3D = (pos: d.v3f, direction: d.v3f) => {
const perp1 = getPerpendicular(direction);
const perp2 = std.cross(direction, perp1);

const numSamples = 8;
for (let i = 0; i < numSamples; i++) {
for (const i of tgpu.unroll(samplesIterations)) {
const theta = (i / numSamples) * 2 * Math.PI;

const coneOffset = perp1.mul(std.cos(theta)).add(perp2.mul(std.sin(theta)));
Expand Down
4 changes: 2 additions & 2 deletions apps/typegpu-docs/src/examples/simulation/slime-mold/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ const blur = tgpu.computeFn({
let count = d.f32();

// 3x3 blur kernel
for (let offsetY = -1; offsetY <= 1; offsetY++) {
for (let offsetX = -1; offsetX <= 1; offsetX++) {
for (const offsetY of tgpu.unroll([-1, 0, 1])) {
for (const offsetX of tgpu.unroll([-1, 0, 1])) {
const samplePos = d.vec2i(gid.xy).add(d.vec2i(offsetX, offsetY));
const dimsi = d.vec2i(dims);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const getNeighbors = tgpu.fn([d.vec2i, d.vec2i], d.arrayOf(d.vec2i, 4))(
d.vec2i(1, 0),
d.vec2i(0, 1),
];
for (let i = 0; i < 4; i++) {
for (const i of tgpu.unroll([0, 1, 2, 3])) {
adjacentOffsets[i] = std.clamp(
std.add(coords, adjacentOffsets[i]),
d.vec2i(),
Expand Down
54 changes: 54 additions & 0 deletions packages/typegpu/src/core/unroll/tgpuUnroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { stitch } from '../resolve/stitch.ts';
import {
$gpuCallable,
$internal,
$resolve,
} from '../../../src/shared/symbols.ts';
import { setName } from '../../../src/shared/meta.ts';
import type { DualFn } from '../../../src/types.ts';
import type { AnyData } from '../../../src/data/dataTypes.ts';
import {
type ResolvedSnippet,
snip,
type Snippet,
} from '../../../src/data/snippet.ts';
import type { ResolutionCtx, SelfResolvable } from '../../../src/types.ts';

/**
* The result of calling `tgpu.unroll(...)`. The code responsible for
* generating shader code can check if the value of a snippet is
* an instance of `UnrollableIterable`, and act accordingly.
*/
export class UnrollableIterable implements SelfResolvable {
readonly [$internal] = true;

constructor(public readonly snippet: Snippet) {}

[$resolve](_ctx: ResolutionCtx): ResolvedSnippet {
return snip(
stitch`${this.snippet}`,
this.snippet.dataType as AnyData,
this.snippet.origin,
);
}
}

/**
* Marks an iterable to be unrolled by the wgslGenerator.
*/
export const unroll = (() => {
const impl = (<T extends Iterable<unknown>>(value: T) => value) as unknown as
& DualFn<(<T extends Iterable<unknown>>(value: T) => T)>
& { [$internal]: true };

setName(impl, 'unroll');
impl.toString = () => 'unroll';
impl[$internal] = true;
impl[$gpuCallable] = {
call(_ctx, [value]) {
return snip(new UnrollableIterable(value), value.dataType, value.origin);
},
};

return impl;
})();
1 change: 1 addition & 0 deletions packages/typegpu/src/tgpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export { bindGroupLayout } from './tgpuBindGroupLayout.ts';
export { computeFn } from './core/function/tgpuComputeFn.ts';
export { fragmentFn } from './core/function/tgpuFragmentFn.ts';
export { vertexFn } from './core/function/tgpuVertexFn.ts';
export { unroll } from './core/unroll/tgpuUnroll.ts';

export * as '~unstable' from './tgpuUnstable.ts';
Loading
Loading