Skip to content

Commit 5d76bb2

Browse files
lyonsnokainino0x
andauthored
Add packed integer dot product sample (#572)
Adds a small `dot4I8Packed` compute example toward #350. The sample packs four signed 8-bit components into each `u32`, computes four example dot products, and reads the results back from the GPU. A dat.gui selector displays the input vectors and result for each example. It requires `packed_4x8_integer_dot_product` and displays a requirement message when that WGSL language feature is unavailable. There is one packed compute pipeline, with no scalar fallback or in-sample result validation. ### Testing - Lint, source typecheck, and the Node 20.20.2 build pass. The build emits existing typed-array warnings in unrelated samples. - Chrome 153.0.8010.48, headless with `--enable-unsafe-webgpu`, on an Apple Metal adapter: all four examples returned the expected results, and the dat.gui selector was exercised. - Desktop and narrow-screen layouts inspected. Injecting feature unavailability displayed the requirement message without requesting an adapter or compiling a shader. No new dependencies or external assets. --------- Co-authored-by: Kai Ninomiya <kainino@chromium.org>
1 parent 4944c70 commit 5d76bb2

5 files changed

Lines changed: 172 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>webgpu-samples: packedIntegerDotProduct</title>
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
}
11+
body {
12+
width: calc(100vw - 280px);
13+
height: 400px;
14+
overflow-y: hidden;
15+
}
16+
pre {
17+
white-space: pre-wrap;
18+
height: 370px;
19+
}
20+
</style>
21+
<script defer src="main.js" type="module"></script>
22+
<script defer type="module" src="../../js/iframe-helper.js"></script>
23+
</head>
24+
<body>
25+
<pre id="output">Computing packed integer dot products…</pre>
26+
</body>
27+
</html>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { GUI } from 'dat.gui';
2+
import packedWGSL from './packed.wgsl';
3+
import { quitIfWebGPUNotAvailableOrMissingFeatures } from '../util';
4+
5+
type vec4i = readonly [number, number, number, number];
6+
// Pack four signed 8-bit components into a u32, low byte first.
7+
function pack4xI8([x, y, z, w]: vec4i): number {
8+
// `&` operator applies sign extension to i32 before operating.
9+
// `>>> 0` converts the final i32 to u32.
10+
/*prettier-ignore*/
11+
return ((x & 0xff) |
12+
((y & 0xff) << 8) |
13+
((z & 0xff) << 16) |
14+
((w & 0xff) << 24)) >>> 0;
15+
}
16+
17+
const outputElement = document.querySelector('#output') as HTMLElement;
18+
if (
19+
!navigator.gpu?.wgslLanguageFeatures.has('packed_4x8_integer_dot_product')
20+
) {
21+
result.textContent =
22+
"This sample requires the WGSL language feature 'packed_4x8_integer_dot_product'.";
23+
} else {
24+
const adapter = await navigator.gpu.requestAdapter({
25+
featureLevel: 'compatibility',
26+
});
27+
const device = await adapter?.requestDevice();
28+
quitIfWebGPUNotAvailableOrMissingFeatures(adapter, device);
29+
30+
const kInputSize = 2 * Uint32Array.BYTES_PER_ELEMENT;
31+
const inputBuffer = device.createBuffer({
32+
size: kInputSize,
33+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
34+
});
35+
36+
const kOutputSize = Int32Array.BYTES_PER_ELEMENT;
37+
const outputBuffer = device.createBuffer({
38+
size: kOutputSize,
39+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
40+
});
41+
const readbackBuffer = device.createBuffer({
42+
size: kOutputSize,
43+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
44+
});
45+
46+
const pipeline = await device.createComputePipelineAsync({
47+
layout: 'auto',
48+
compute: { module: device.createShaderModule({ code: packedWGSL }) },
49+
});
50+
const bindGroup = device.createBindGroup({
51+
layout: pipeline.getBindGroupLayout(0),
52+
entries: [
53+
{ binding: 0, resource: { buffer: inputBuffer } },
54+
{ binding: 1, resource: { buffer: outputBuffer } },
55+
],
56+
});
57+
58+
async function updateResult() {
59+
// If an update is still in progress just wait until it's done.
60+
if (readbackBuffer.mapState !== 'unmapped') {
61+
setTimeout(updateResult, 0);
62+
return;
63+
}
64+
65+
const lhs = [settings.lhs0, settings.lhs1, settings.lhs2, settings.lhs3];
66+
const rhs = [settings.rhs0, settings.rhs1, settings.rhs2, settings.rhs3];
67+
68+
device.queue.writeBuffer(
69+
inputBuffer,
70+
0,
71+
new Uint32Array([lhs, rhs].map(pack4xI8))
72+
);
73+
const encoder = device.createCommandEncoder();
74+
const pass = encoder.beginComputePass();
75+
pass.setPipeline(pipeline);
76+
pass.setBindGroup(0, bindGroup);
77+
pass.dispatchWorkgroups(1);
78+
pass.end();
79+
encoder.copyBufferToBuffer(outputBuffer, 0, readbackBuffer, 0, kOutputSize);
80+
device.queue.submit([encoder.finish()]);
81+
82+
await readbackBuffer.mapAsync(GPUMapMode.READ);
83+
const result = new Int32Array(readbackBuffer.getMappedRange())[0];
84+
85+
// Result should be the same in JS, show that for comparison.
86+
const expected =
87+
lhs[0] * rhs[0] + lhs[1] * rhs[1] + lhs[2] * rhs[2] + lhs[3] * rhs[3];
88+
89+
const lhsStr = `[${lhs
90+
.map((x) => x.toString().padStart(4))
91+
.join(', ')}] (0x${pack4xI8(lhs).toString(16).padStart(8, '0')})`;
92+
const rhsStr = `[${rhs
93+
.map((x) => x.toString().padStart(4))
94+
.join(', ')}] (0x${pack4xI8(rhs).toString(16).padStart(8, '0')})`;
95+
const outStr = result.toString().padStart(6);
96+
const expStr = expected.toString().padStart(6);
97+
outputElement.textContent = `
98+
99+
WGSL dot4I8Packed of ${lhsStr}
100+
by ${rhsStr} gave ${outStr} (JS gave ${expStr})`;
101+
102+
readbackBuffer.unmap();
103+
}
104+
105+
const settings = {
106+
lhs0: 1,
107+
lhs1: -2,
108+
lhs2: 3,
109+
lhs3: -4,
110+
rhs0: -5,
111+
rhs1: 6,
112+
rhs2: -7,
113+
rhs3: 8,
114+
};
115+
const gui = new GUI();
116+
gui.add(settings, 'lhs0', -127, 128, 1).onChange(updateResult);
117+
gui.add(settings, 'lhs1', -127, 128, 1).onChange(updateResult);
118+
gui.add(settings, 'lhs2', -127, 128, 1).onChange(updateResult);
119+
gui.add(settings, 'lhs3', -127, 128, 1).onChange(updateResult);
120+
gui.add(settings, 'rhs0', -127, 128, 1).onChange(updateResult);
121+
gui.add(settings, 'rhs1', -127, 128, 1).onChange(updateResult);
122+
gui.add(settings, 'rhs2', -127, 128, 1).onChange(updateResult);
123+
gui.add(settings, 'rhs3', -127, 128, 1).onChange(updateResult);
124+
updateResult();
125+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
name: 'Packed Integer Dot Product',
3+
description:
4+
'Packs four signed 8-bit integers into each u32, computes their dot product with dot4I8Packed, and reads back the results.',
5+
filename: __DIRNAME__,
6+
sources: [{ path: 'main.ts' }, { path: 'packed.wgsl' }],
7+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
requires packed_4x8_integer_dot_product;
2+
3+
struct Input { lhs: u32, rhs: u32 }
4+
5+
@group(0) @binding(0) var<storage, read> input: Input;
6+
@group(0) @binding(1) var<storage, read_write> output: i32;
7+
8+
@compute @workgroup_size(1)
9+
fn main() {
10+
output = dot4I8Packed(input.lhs, input.rhs);
11+
}

‎src/samples.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import normalMap from '../sample/normalMap/meta';
2424
import occlusionQuery from '../sample/occlusionQuery/meta';
2525
import particleLife from '../sample/particleLife/meta';
2626
import particles from '../sample/particles/meta';
27+
import packedIntegerDotProduct from '../sample/packedIntegerDotProduct/meta';
2728
import points from '../sample/points/meta';
2829
import primitivePicking from '../sample/primitivePicking/meta';
2930
import pristineGrid from '../sample/pristineGrid/meta';
@@ -116,6 +117,7 @@ export const pageCategories: PageCategory[] = [
116117
computeBoids,
117118
gameOfLife,
118119
bitonicSort,
120+
packedIntegerDotProduct,
119121
},
120122
},
121123

0 commit comments

Comments
 (0)