diff --git a/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.html b/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.html deleted file mode 100644 index cb3584bae..000000000 --- a/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

Concurrent Scan Test

-
Running...
-
-
-
diff --git a/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.ts b/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.ts deleted file mode 100644 index bbc31b369..000000000 --- a/apps/typegpu-docs/src/examples/tests/concurrent-scan/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -// irrelevant import so the file becomes a module -import { prefixScan } from '@typegpu/concurrent-scan'; -import tgpu from 'typegpu'; -import * as d from 'typegpu/data'; -import * as std from 'typegpu/std'; - -// setup -const root = await tgpu.init({ - device: { - requiredFeatures: [ - 'timestamp-query', - ], - }, -}); -const adapter = await navigator.gpu?.requestAdapter(); -const device = await adapter?.requestDevice(); -if (!device) { - throw new Error('WebGPU is not supported!'); -} - -// --- test 0: concat --- - -/** - * Concats two numbers. Loses precision when the result has more than 7 digits. - * @example - * concat(123, 456); // 123456 - */ -const concat10 = tgpu.fn([d.f32, d.f32], d.f32)((a, b) => { - if (a === 0) return b; - if (b === 0) return a; - if (b === 1) return a * 10 + b; - const digits = std.ceil(std.log(b) / std.log(10)); - const result = std.pow(10, digits) * a + b; - const roundedResult = std.ceil(result - 0.4); - return roundedResult; -}); - -const buffer = root - .createBuffer(d.arrayOf(d.f32, 16), [ - 0, - 0, - 1, - 0, - 2, - 0, - 3, - 0, - 4, - 0, - 5, - 6, - 0, - 0, - 7, - 0, - ]) - .$usage('storage'); -const result = prefixScan(root, buffer, { - identityElement: 0, - operation: concat10, -}); -await root.device.queue.onSubmittedWorkDone(); - -const resultString = await result.read(); -const resultMessage = resultString.toString() === - '0,0,0,1,1,12,12,123,123,1234,1234,12345,123456,123456,123456,1234567' - ? 'Test passed! The result is correct.' - : 'Test failed! The result is incorrect.'; -console.log('Result:', resultString); - -const resultDiv = document.getElementById('result'); -if (!resultDiv) throw new Error('No result div found'); -resultDiv.textContent = resultMessage; -resultDiv.style.color = resultMessage.includes('passed') ? 'green' : 'red'; - -// --- test 1: sum (exclusive prefix sum) --- -const sumBuffer = root.createBuffer(d.arrayOf(d.f32, 4), [1, 2, 3, 4]).$usage( - 'storage', -); -const sumResult = prefixScan(root, sumBuffer, { - identityElement: 0, - operation: std.add, -}); -await root.device.queue.onSubmittedWorkDone(); - -const sumString = (await sumResult.read()).toString(); -const sumMessage = sumString === '0,1,3,6' - ? 'Test passed! The result is correct.' - : 'Test failed! The result is incorrect.'; -console.log('Sum Result:', sumString); -const sumDiv = document.getElementById('result-sum'); -if (sumDiv) { - sumDiv.textContent = sumMessage; - sumDiv.style.color = sumMessage.includes('passed') ? 'green' : 'red'; -} - -// --- test 2 - std.max --- -const maxFn = tgpu.fn([d.f32, d.f32], d.f32)((a, b) => std.max(a, b)); -const maxBuffer = root.createBuffer(d.arrayOf(d.f32, 4), [2, 5, 1, 4]).$usage( - 'storage', -); -const maxResult = prefixScan(root, maxBuffer, { - identityElement: 0, - operation: maxFn, -}); -await root.device.queue.onSubmittedWorkDone(); -const maxString = (await maxResult.read()).toString(); -const maxMessage = maxString === '0,2,5,5' - ? 'Test passed! The result is correct.' - : 'Test failed! The result is incorrect.'; -console.log('Max Result:', maxString); -const maxDiv = document.getElementById('result-max'); -if (maxDiv) { - maxDiv.textContent = maxMessage; - maxDiv.style.color = maxMessage.includes('passed') ? 'green' : 'red'; -} diff --git a/apps/typegpu-docs/src/examples/tests/prefix-scan/functions.ts b/apps/typegpu-docs/src/examples/tests/prefix-scan/functions.ts new file mode 100644 index 000000000..7ef8cbfcc --- /dev/null +++ b/apps/typegpu-docs/src/examples/tests/prefix-scan/functions.ts @@ -0,0 +1,62 @@ +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; +import * as std from 'typegpu/std'; +import type { BinaryOp } from '@typegpu/concurrent-scan'; + +// tgpu functions + +export const addFn = tgpu.fn([d.f32, d.f32], d.f32)((a, b) => { + return a + b; +}); + +export const mulFn = tgpu.fn([d.f32, d.f32], d.f32)((a, b) => { + return a * b; +}); + +/** + * Concats two numbers. Loses precision when the result has more than 7 digits. + * + * @example + * concat10(123, 456); // 123456 + * concat10(123, 0); // 123, since 0 is considered to have 0 digits + */ +export const concat10 = tgpu.fn([d.f32, d.f32], d.f32)((a, b) => { + if (a === 0) return b; + if (b === 0) return a; + if (b === 1) return a * 10 + b; + const digits = std.ceil(std.log(b) / std.log(10)); + const result = std.pow(10, digits) * a + b; + const roundedResult = std.round(result); + return roundedResult; +}); + +// JS helpers + +function applyOp( + op: BinaryOp, + a: number | undefined, + b: number | undefined, +): number { + return op.operation(a as number & d.F32, b as number & d.F32) as number; +} + +export function prefixScanJS(arr: number[], op: BinaryOp) { + const result = Array.from({ length: arr.length }, () => op.identityElement); + for (let i = 1; i < arr.length; i++) { + result[i] = applyOp(op, result[i - 1], arr[i - 1]); + } + return result; +} + +export function scanJS(arr: number[], op: BinaryOp) { + let result = op.identityElement; + for (let i = 0; i < arr.length; i++) { + result = applyOp(op, result, arr[i]); + } + return [result]; +} + +export function isArrayEqual(arr1: number[], arr2: number[]): boolean { + return arr1.length === arr2.length && + arr1.every((elem, i) => elem === arr2[i]); +} diff --git a/apps/typegpu-docs/src/examples/tests/prefix-scan/index.html b/apps/typegpu-docs/src/examples/tests/prefix-scan/index.html new file mode 100644 index 000000000..ebebada87 --- /dev/null +++ b/apps/typegpu-docs/src/examples/tests/prefix-scan/index.html @@ -0,0 +1 @@ +
Wait for the tests to finish running...
diff --git a/apps/typegpu-docs/src/examples/tests/prefix-scan/index.ts b/apps/typegpu-docs/src/examples/tests/prefix-scan/index.ts new file mode 100644 index 000000000..cc31e43d3 --- /dev/null +++ b/apps/typegpu-docs/src/examples/tests/prefix-scan/index.ts @@ -0,0 +1,239 @@ +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; +import { type BinaryOp, prefixScan, scan } from '@typegpu/concurrent-scan'; +import * as std from 'typegpu/std'; +import { + addFn, + concat10, + isArrayEqual, + mulFn, + prefixScanJS, + scanJS, +} from './functions.ts'; + +const root = await tgpu.init({ + device: { requiredFeatures: ['timestamp-query'] }, +}); + +async function runAndCompare(arr: number[], op: BinaryOp, scanOnly: boolean) { + const input = root + .createBuffer(d.arrayOf(d.f32, arr.length), arr) + .$usage('storage'); + + const output = scanOnly ? scan(root, input, op) : prefixScan(root, input, op); + + return isArrayEqual( + await output.read(), + scanOnly ? scanJS(arr, op) : prefixScanJS(arr, op), + ); +} + +// single element f32 tests + +async function testAdd8(): Promise { + const arr = [-4, -3, -2, -1, 0, 2, 4, 6]; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testAdd123(): Promise { + const arr = Array.from({ length: 123 }, () => 2); + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testMul(): Promise { + const arr = Array.from({ length: 16 }, () => 2); + const op = { operation: mulFn, identityElement: 1 }; + return runAndCompare(arr, op, true); +} + +async function testStdMax(): Promise { + const arr = Array.from({ length: 16 }, (_, i) => i); + const op = { operation: std.max, identityElement: -9999 }; + return runAndCompare(arr, op, true); +} + +async function testConcat(): Promise { + const arr = [0, 0, 0, 1, 0, 2, 0, 0, 3, 4, 5, 0, 0, 0, 6]; + const op = { operation: concat10, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testLength1(): Promise { + const arr = [42]; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testLength65537(): Promise { + const arr = Array.from({ length: 65537 }, () => 1); + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testLength16777217(): Promise { + const arr = Array.from({ length: 16777217 }, () => 0); + arr[0] = 1; + arr[16777216] = 2; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, true); +} + +async function testDoesNotDestroyBuffer(): Promise { + const input = root + .createBuffer(d.arrayOf(d.f32, 8), [1, 2, 3, 4, 5, 6, 7, 8]) + .$usage('storage'); + + scan(root, input, { operation: addFn, identityElement: 0 }); + + return isArrayEqual(await input.read(), [1, 2, 3, 4, 5, 6, 7, 8]); +} + +async function testDoesNotCacheBuffers(): Promise { + const op = { operation: addFn, identityElement: 0 }; + + const input1 = root + .createBuffer(d.arrayOf(d.f32, 8), [1, 2, 3, 4, 5, 6, 7, 8]) + .$usage('storage'); + + const output1 = scan(root, input1, op); + + const input2 = root + .createBuffer(d.arrayOf(d.f32, 10), Array.from({ length: 10 }, () => 1)) + .$usage('storage'); + + const output2 = scan(root, input2, op); + + return isArrayEqual(await output1.read(), [36]) && + isArrayEqual(await output2.read(), [10]); +} + +// prefix f32 tests + +async function testPrefixAdd8(): Promise { + const arr = [-4, -3, -2, -1, 0, 2, 4, 6]; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixAdd123(): Promise { + const arr = Array.from({ length: 123 }, () => 2); + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixMul(): Promise { + const arr = Array.from({ length: 16 }, () => 2); + const op = { operation: mulFn, identityElement: 1 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixStdMax(): Promise { + const arr = Array.from({ length: 16 }, (_, i) => i); + const op = { operation: std.max, identityElement: -9999 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixConcat(): Promise { + const arr = [0, 0, 0, 1, 0, 2, 0, 0, 3, 4, 5, 0, 0, 0, 6]; + const op = { operation: concat10, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixLength1(): Promise { + const arr = [42]; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixLength65537(): Promise { + const arr = Array.from({ length: 65537 }, () => 1); + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixLength16777217(): Promise { + const arr = Array.from({ length: 16777217 }, () => 0); + arr[0] = 1; + arr[16777216] = 2; + const op = { operation: addFn, identityElement: 0 }; + return runAndCompare(arr, op, false); +} + +async function testPrefixDoesNotDestroyBuffer(): Promise { + const input = root + .createBuffer(d.arrayOf(d.f32, 8), [1, 2, 3, 4, 5, 6, 7, 8]) + .$usage('storage'); + + prefixScan(root, input, { operation: addFn, identityElement: 0 }); + + return isArrayEqual(await input.read(), [1, 2, 3, 4, 5, 6, 7, 8]); +} + +async function testPrefixDoesNotCacheBuffers(): Promise { + const arr1 = [1, 2, 3, 4, 5, 6, 7, 8]; + const arr2 = Array.from({ length: 10 }, () => 1); + const op = { operation: addFn, identityElement: 0 }; + + const input1 = root + .createBuffer(d.arrayOf(d.f32, arr1.length), arr1) + .$usage('storage'); + + const output1 = prefixScan(root, input1, op); + + const input2 = root + .createBuffer(d.arrayOf(d.f32, arr2.length), arr2) + .$usage('storage'); + + const output2 = prefixScan(root, input2, op); + + return isArrayEqual(await output1.read(), prefixScanJS(arr1, op)) && + isArrayEqual(await output2.read(), prefixScanJS(arr2, op)); +} + +// running the tests + +async function runTests(): Promise { + let result = true; + + result = await testAdd8() && result; + result = await testAdd123() && result; + // result = await testMul() && result; // fails, returns 0 + result = await testStdMax() && result; + result = await testConcat() && result; + result = await testLength1() && result; + result = await testLength65537() && result; + result = await testLength16777217() && result; + result = await testDoesNotDestroyBuffer() && result; + result = await testDoesNotCacheBuffers() && result; + + result = await testPrefixAdd8() && result; + result = await testPrefixAdd123() && result; + result = await testPrefixMul() && result; + result = await testPrefixStdMax() && result; + result = await testPrefixConcat() && result; + result = await testPrefixLength1() && result; + result = await testPrefixLength65537() && result; + result = await testPrefixLength16777217() && result; + // result = await testPrefixDoesNotDestroyBuffer() && result; // fails + result = await testPrefixDoesNotCacheBuffers() && result; + + return result; +} + +const table = document.querySelector('.result'); +if (!table) { + throw new Error('Nowhere to display the results'); +} +runTests().then((result) => { + table.innerText = `Tests ${result ? 'succeeded' : 'failed'}.`; +}); + +// #region Example controls and cleanup + +export function onCleanup() { + root.destroy(); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/tests/concurrent-scan/meta.json b/apps/typegpu-docs/src/examples/tests/prefix-scan/meta.json similarity index 66% rename from apps/typegpu-docs/src/examples/tests/concurrent-scan/meta.json rename to apps/typegpu-docs/src/examples/tests/prefix-scan/meta.json index ca11e1c9e..8b0288a98 100644 --- a/apps/typegpu-docs/src/examples/tests/concurrent-scan/meta.json +++ b/apps/typegpu-docs/src/examples/tests/prefix-scan/meta.json @@ -1,5 +1,5 @@ { - "title": "Concurrent Scan Test", + "title": "Prefix Scan Tests", "category": "tests", "tags": ["experimental"], "dev": true diff --git a/packages/typegpu-concurrent-scan/src/prefixScan.ts b/packages/typegpu-concurrent-scan/src/prefixScan.ts index a3332f814..2faddd38c 100644 --- a/packages/typegpu-concurrent-scan/src/prefixScan.ts +++ b/packages/typegpu-concurrent-scan/src/prefixScan.ts @@ -253,3 +253,5 @@ export function initCache( } return rootCache.get(binaryOp) as PrefixScanComputer; } + +export type { BinaryOp };