Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2aedf7e
init
reczkok Feb 3, 2026
31949f1
cleanup
reczkok Feb 3, 2026
c1ee033
move to runners
reczkok Feb 3, 2026
5d4080f
remove useless comment
reczkok Feb 3, 2026
2733e82
Merge branch 'main' into feat/tgpu-sort
reczkok Feb 3, 2026
cb1ae64
cleanup example more
reczkok Feb 3, 2026
355c073
smol
reczkok Feb 3, 2026
15b863c
thumbnail and cleanup
reczkok Feb 9, 2026
7993706
add test
reczkok Feb 9, 2026
a37a8af
Merge branch 'main' into feat/tgpu-sort
reczkok Feb 9, 2026
9fc46ad
fix lockfil
reczkok Feb 9, 2026
f64584d
Merge branch 'main' into feat/tgpu-sort
reczkok Feb 9, 2026
d95317d
Merge branch 'main' into feat/tgpu-sort
reczkok Feb 11, 2026
f7d1ec3
fixes
reczkok Feb 11, 2026
c43b383
lighter on the old ALU
reczkok Feb 11, 2026
366cfc4
fix
reczkok Feb 11, 2026
d14a742
update test
reczkok Feb 13, 2026
3b43ae0
Merge branch 'main' into feat/tgpu-sort
reczkok Feb 13, 2026
a728984
better workgroup sizes and example rework
reczkok Feb 13, 2026
57b6e07
fixes
reczkok Feb 13, 2026
fa324ad
fix test
reczkok Feb 13, 2026
63794d3
modern controls
reczkok Feb 13, 2026
c15fa83
restore faulty snapshot
reczkok Feb 13, 2026
313d1ef
Merge branch 'main' into feat/tgpu-sort
reczkok Mar 4, 2026
c39bc77
EAT THE CONCURRENT SUM
reczkok Mar 4, 2026
3b27efb
lock
reczkok Mar 4, 2026
390e507
Apply suggestions from code review
reczkok Mar 4, 2026
eaa4538
dedeprecate
reczkok Mar 4, 2026
4d16af0
further dedeprecate
reczkok Mar 4, 2026
ab986e4
some fixes
reczkok Mar 4, 2026
96edde4
Merge branch 'main' into feat/tgpu-sort
reczkok Mar 4, 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
2 changes: 1 addition & 1 deletion apps/typegpu-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
"@stackblitz/sdk": "^1.11.0",
"@tailwindcss/vite": "^4.1.18",
"@typegpu/color": "workspace:*",
"@typegpu/concurrent-scan": "workspace:*",
"@typegpu/geometry": "workspace:*",
"@typegpu/noise": "workspace:*",
"@typegpu/sdf": "workspace:*",
"@typegpu/sort": "workspace:*",
"@typegpu/three": "workspace:*",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
Expand Down
48 changes: 48 additions & 0 deletions apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<canvas></canvas>
<div id="sort-overlay" hidden>
<div id="sort-spinner"></div>
<span id="sort-status"></span>
</div>
<style>
#sort-overlay {
position: absolute;
inset: 0;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
backdrop-filter: blur(8px);
background: rgba(23, 23, 36, 0.5);
opacity: 0;
transition: opacity 0.3s ease-in-out;
}

#sort-overlay:not([hidden]) {
display: flex;
}

#sort-overlay.visible {
opacity: 1;
}

#sort-spinner {
width: 40px;
height: 40px;
border: 3px solid #2e295f;
border-top-color: #6453d2;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

#sort-status {
color: #c3c4f1;
font-size: 0.875rem;
font-family: 'Aeonik', sans-serif;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
282 changes: 282 additions & 0 deletions apps/typegpu-docs/src/examples/algorithms/bitonic-sort/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import tgpu, { d, std } from 'typegpu';
import {
type BitonicSorter,
type BitonicSorterOptions,
createBitonicSorter,
decomposeWorkgroups,
} from '@typegpu/sort';
import { randf } from '@typegpu/noise';
import { fullScreenTriangle } from 'typegpu/common';
import { defineControls } from '../../common/defineControls.ts';

const maxBufferSize = await navigator.gpu.requestAdapter().then((adapter) => {
if (!adapter) {
throw new Error('No GPU adapter found');
}
const limits = adapter.limits;
return Math.min(limits.maxStorageBufferBindingSize, limits.maxBufferSize);
});

const root = await tgpu.init({
device: {
optionalFeatures: ['timestamp-query'],
requiredLimits: {
maxStorageBufferBindingSize: maxBufferSize,
maxBufferSize: maxBufferSize,
},
},
});
Comment on lines +12 to +28
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example calls navigator.gpu.requestAdapter() to compute limits, but then tgpu.init() will call navigator.gpu.requestAdapter() again internally. If the browser returns a different adapter the second time, the requiredLimits computed from the first adapter may exceed the second adapter’s limits and cause initialization to fail. Prefer requesting the device once (using the adapter you queried) and passing it to tgpu.initFromDevice, or otherwise ensuring the same adapter/device is used for both limit discovery and initialization.

Suggested change
const maxBufferSize = await navigator.gpu.requestAdapter().then((adapter) => {
if (!adapter) {
throw new Error('No GPU adapter found');
}
const limits = adapter.limits;
return Math.min(limits.maxStorageBufferBindingSize, limits.maxBufferSize);
});
const root = await tgpu.init({
device: {
optionalFeatures: ['timestamp-query'],
requiredLimits: {
maxStorageBufferBindingSize: maxBufferSize,
maxBufferSize: maxBufferSize,
},
},
});
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('No GPU adapter found');
}
const limits = adapter.limits;
const maxBufferSize = Math.min(
limits.maxStorageBufferBindingSize,
limits.maxBufferSize,
);
const requiredFeatures: GPUFeatureName[] = [];
if (adapter.features.has('timestamp-query' as GPUFeatureName)) {
requiredFeatures.push('timestamp-query');
}
const device = await adapter.requestDevice({
requiredLimits: {
maxStorageBufferBindingSize: maxBufferSize,
maxBufferSize: maxBufferSize,
},
requiredFeatures,
});
const root = await tgpu.initFromDevice(device);

Copilot uses AI. Check for mistakes.
const hasTimestampQuery = root.enabledFeatures.has('timestamp-query');
const querySet = hasTimestampQuery ? root.createQuerySet('timestamp', 2) : null;

const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const context = root.configureContext({ canvas });

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

const maxSide = Math.floor(Math.sqrt(maxBufferSize / 4));
const minLog = 2; // log_2(4)
const maxLog = Math.floor(Math.log2(maxSide));
const arraySizeOptions = Array.from({ length: 8 }, (_, i) => {
const side = Math.round(2 ** (minLog + (i * (maxLog - minLog)) / 7));
return side * side;
});

type SortOrderKey = 'ascending' | 'descending' | 'bit-reversed' | 'xor-scatter';

const sortOrders: Record<SortOrderKey, BitonicSorterOptions> = {
ascending: {},
descending: {
compare: (a, b) => {
'use gpu';
return a > b;
},
paddingValue: 0,
},
'bit-reversed': {
compare: (a, b) => {
'use gpu';
return std.reverseBits(a) < std.reverseBits(b);
},
},
'xor-scatter': {
compare: (a, b) => {
'use gpu';
return (a ^ 0xaa) < (b ^ 0xaa);
},
},
};

const state = {
arraySize: arraySizeOptions[2],
sortOrder: 'ascending' as SortOrderKey,
};

const WORKGROUP_SIZE = 256;

const renderLayout = tgpu.bindGroupLayout({
data: {
storage: d.arrayOf(d.u32),
access: 'readonly',
},
});

const initLayout = tgpu.bindGroupLayout({
data: {
storage: d.arrayOf(d.u32),
access: 'mutable',
},
});

const initLength = root.createUniform(d.u32, state.arraySize);
const initSeed = root.createUniform(d.f32, 0);

const fragmentFn = tgpu.fragmentFn({
in: { uv: d.vec2f },
out: d.vec4f,
})((input) => {
const arrayLength = initLength.$;

const cols = d.u32(std.round(std.sqrt(d.f32(arrayLength))));
const rows = d.u32(std.round(arrayLength / cols));

const col = d.u32(std.floor(input.uv.x * d.f32(cols)));
const row = d.u32(std.floor(input.uv.y * d.f32(rows)));
const idx = row * cols + col;

if (idx >= arrayLength) {
return d.vec4f(0.1, 0.1, 0.1, 1);
}

const value = renderLayout.$.data[idx];
const normalized = value / 255;

return d.vec4f(normalized, normalized, normalized, 1);
});

const initKernel = tgpu.computeFn({
workgroupSize: [WORKGROUP_SIZE],
in: {
gid: d.builtin.globalInvocationId,
numWorkgroups: d.builtin.numWorkgroups,
},
})((input) => {
const spanX = input.numWorkgroups.x * WORKGROUP_SIZE;
const spanY = input.numWorkgroups.y * spanX;
const idx = input.gid.x + input.gid.y * spanX + input.gid.z * spanY;

if (idx >= initLength.$) {
return;
}

randf.seed3(d.vec3f(d.f32(idx & 0xffff), d.f32(idx >> 16), initSeed.$));
const n = randf.sample();
initLayout.$.data[idx] = d.u32(std.floor(n * 256.0));
});

const renderPipeline = root.createRenderPipeline({
vertex: fullScreenTriangle,
fragment: fragmentFn,
targets: { format: presentationFormat },
});

const initPipeline = root.createComputePipeline({ compute: initKernel });

let buffer = root.createBuffer(d.arrayOf(d.u32, state.arraySize)).$usage('storage');

let bindGroup = root.createBindGroup(renderLayout, {
data: buffer,
});
let initBindGroup = root.createBindGroup(initLayout, {
data: buffer,
});

function createSorters(buf: typeof buffer) {
return Object.fromEntries(
Object.entries(sortOrders).map(([key, opts]) => [key, createBitonicSorter(root, buf, opts)]),
) as Record<SortOrderKey, BitonicSorter>;
}

let sorters = createSorters(buffer);

function recreateBuffer() {
for (const s of Object.values(sorters)) {
s.destroy();
}
buffer.destroy();

buffer = root.createBuffer(d.arrayOf(d.u32, state.arraySize)).$usage('storage');

bindGroup = root.createBindGroup(renderLayout, {
data: buffer,
});

initBindGroup = root.createBindGroup(initLayout, {
data: buffer,
});

sorters = createSorters(buffer);
}

function generateRandomArray() {
const workgroupsTotal = Math.ceil(state.arraySize / WORKGROUP_SIZE);
const [workgroupsX, workgroupsY, workgroupsZ] = decomposeWorkgroups(workgroupsTotal);

initLength.write(state.arraySize);
initSeed.write(Math.random() * 1000);

initPipeline.with(initBindGroup).dispatchWorkgroups(workgroupsX, workgroupsY, workgroupsZ);

render();
}

function render() {
renderPipeline
.withColorAttachment({
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
storeOp: 'store',
})
.with(bindGroup)
.draw(3);
}

const overlay = document.getElementById('sort-overlay') as HTMLDivElement;
const spinnerEl = document.getElementById('sort-spinner') as HTMLDivElement;
const statusEl = document.getElementById('sort-status') as HTMLSpanElement;
canvas.parentElement?.appendChild(overlay);

function showOverlay(text: string, showSpinner = true) {
spinnerEl.hidden = !showSpinner;
statusEl.textContent = text;
overlay.hidden = false;
overlay.classList.add('visible');
}

function hideOverlay(delayMs = 1500) {
setTimeout(() => {
overlay.classList.remove('visible');
overlay.addEventListener('transitionend', () => (overlay.hidden = true), {
once: true,
});
}, delayMs);
}

async function sort() {
const sorter = sorters[state.sortOrder];

showOverlay('Sorting...');
sorter.run({ querySet: querySet ?? undefined });

let gpuTimeMs: number | null = null;
if (querySet?.available) {
querySet.resolve();
const timestamps = await querySet.read();
gpuTimeMs = Number(timestamps[1] - timestamps[0]) / 1_000_000;
}

render();

const timeStr =
gpuTimeMs !== null
? ` in ${
gpuTimeMs >= 1000 ? `${(gpuTimeMs / 1000).toFixed(2)}s` : `${gpuTimeMs.toFixed(2)}ms`
}`
: '';
showOverlay(`\u2714 Sorted${timeStr}`, false);
hideOverlay();
}

// #region Example controls & Cleanup

const sortOrderKeys = Object.keys(sortOrders) as SortOrderKey[];

export const controls = defineControls({
'Array Size': {
initial: arraySizeOptions[2],
options: arraySizeOptions,
onSelectChange: (value) => {
state.arraySize = isNaN(value) ? 64 : value;
recreateBuffer();
generateRandomArray();
},
},
'Sort Order': {
initial: 'ascending',
options: sortOrderKeys,
onSelectChange: (value) => {
state.sortOrder = value;
},
},
Reshuffle: { onButtonClick: generateRandomArray },
Sort: { onButtonClick: sort },
});

export function onCleanup() {
for (const s of Object.values(sorters)) {
s.destroy();
}
root.destroy();
}

// #endregion
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Bitonic Sort",
"category": "algorithms",
"tags": ["experimental", "compute"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initCache, prefixScan } from '@typegpu/concurrent-scan';
import { createPrefixScanComputer, prefixScan } from '@typegpu/sort';
import type { TgpuRoot } from 'typegpu';
import { d, std } from 'typegpu';

Expand Down Expand Up @@ -45,7 +45,7 @@ export async function performCalculationsWithTime(
const jsTime = performance.now() - jsStartTime;

// GPU version
initCache(root, { operation: std.add, identityElement: 0 });
createPrefixScanComputer(root, { operation: std.add, identityElement: 0 });
const querySet = root.createQuerySet('timestamp', 2);
const gpuStartTime = performance.now();
const calcResult = prefixScan(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import * as std from 'typegpu/std';
import type { BinaryOp } from '@typegpu/concurrent-scan';
import type { BinaryOp } from '@typegpu/sort';

// tgpu functions

Expand Down
2 changes: 1 addition & 1 deletion apps/typegpu-docs/src/examples/tests/prefix-scan/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import { type BinaryOp, prefixScan, scan } from '@typegpu/concurrent-scan';
import { type BinaryOp, prefixScan, scan } from '@typegpu/sort';
import * as std from 'typegpu/std';
import { addFn, concat10, isArrayEqual, mulFn, prefixScanJS, scanJS } from './functions.ts';

Expand Down
8 changes: 4 additions & 4 deletions apps/typegpu-docs/src/utils/examples/sandboxModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ export const SANDBOX_MODULES: Record<string, SandboxModuleDefinition> = {
import: { reroute: 'typegpu-color/src/index.ts' },
typeDef: { reroute: 'typegpu-color/src/index.ts' },
},
'@typegpu/concurrent-scan': {
import: { reroute: 'typegpu-concurrent-scan/src/index.ts' },
typeDef: { reroute: 'typegpu-concurrent-scan/src/index.ts' },
},
'@typegpu/three': {
typeDef: { reroute: 'typegpu-three/src/index.ts' },
},
'@typegpu/sort': {
import: { reroute: 'typegpu-sort/src/index.ts' },
typeDef: { reroute: 'typegpu-sort/src/index.ts' },
},
};
Loading
Loading