buffer_view CTS (#4652)
* Validation of buffer type and related builtin functions
* Execution tests for buffer_view
diff --git a/.gitignore b/.gitignore
index 09eb42e..1a359f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,9 @@
*.tmp.txt
/docs/tsdoc/
+# vim swap files
+*.swp*
+
# `pnpm` lockfile
pnpm-lock.yaml
diff --git a/src/common/util/util.ts b/src/common/util/util.ts
index 540a29f..4e19f9e 100644
--- a/src/common/util/util.ts
+++ b/src/common/util/util.ts
@@ -373,6 +373,7 @@
Uint8ClampedArray: Uint8ClampedArray;
Int32Array: Int32Array;
Uint32Array: Uint32Array;
+ Float16Array: Float16Array;
Float32Array: Float32Array;
Float64Array: Float64Array;
BigInt64Array: BigInt64Array;
diff --git a/src/resources/cache/hashes.json b/src/resources/cache/hashes.json
index 98e9043..c0bfe20 100644
--- a/src/resources/cache/hashes.json
+++ b/src/resources/cache/hashes.json
@@ -1,112 +1,112 @@
{
- "webgpu/shader/execution/binary/af_addition.bin": "6e41f81e",
- "webgpu/shader/execution/binary/af_logical.bin": "254d66ec",
- "webgpu/shader/execution/binary/af_division.bin": "e15d545b",
- "webgpu/shader/execution/binary/af_matrix_addition.bin": "4bfc497f",
- "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "2bbdd5e6",
- "webgpu/shader/execution/binary/af_multiplication.bin": "23c7c8ad",
- "webgpu/shader/execution/binary/af_remainder.bin": "9b84ecb1",
- "webgpu/shader/execution/binary/af_subtraction.bin": "bd4b2ec9",
- "webgpu/shader/execution/binary/f16_addition.bin": "5fe1fb4e",
- "webgpu/shader/execution/binary/f16_logical.bin": "225d12ee",
- "webgpu/shader/execution/binary/f16_division.bin": "dffabf5f",
- "webgpu/shader/execution/binary/f16_matrix_addition.bin": "acc779ee",
- "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "dcfbfff9",
- "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "4b73a544",
- "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "bfe9a25d",
- "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "a2222aec",
- "webgpu/shader/execution/binary/f16_multiplication.bin": "5d5265c9",
- "webgpu/shader/execution/binary/f16_remainder.bin": "5284a25",
- "webgpu/shader/execution/binary/f16_subtraction.bin": "b98fc99f",
- "webgpu/shader/execution/binary/f32_addition.bin": "13d7f835",
- "webgpu/shader/execution/binary/f32_logical.bin": "c1f1b636",
- "webgpu/shader/execution/binary/f32_division.bin": "80fee721",
- "webgpu/shader/execution/binary/f32_matrix_addition.bin": "a0d20c36",
- "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "3cabc829",
- "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "aed8659",
- "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "6063545d",
- "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "e36324e4",
- "webgpu/shader/execution/binary/f32_multiplication.bin": "40527d93",
- "webgpu/shader/execution/binary/f32_remainder.bin": "d8738967",
- "webgpu/shader/execution/binary/f32_subtraction.bin": "f5453a8",
- "webgpu/shader/execution/binary/i32_arithmetic.bin": "7fb5b26a",
- "webgpu/shader/execution/binary/i32_comparison.bin": "9679ab05",
- "webgpu/shader/execution/binary/u32_arithmetic.bin": "c9fe0078",
- "webgpu/shader/execution/binary/u32_comparison.bin": "7d0ba49",
- "webgpu/shader/execution/abs.bin": "320b5a02",
- "webgpu/shader/execution/acos.bin": "bb357762",
- "webgpu/shader/execution/acosh.bin": "1975ce7f",
- "webgpu/shader/execution/asin.bin": "74096576",
- "webgpu/shader/execution/asinh.bin": "cdb5dae3",
- "webgpu/shader/execution/atan.bin": "4d8cd242",
- "webgpu/shader/execution/atan2.bin": "213c0112",
- "webgpu/shader/execution/atanh.bin": "ca3cda5b",
- "webgpu/shader/execution/bitcast.bin": "2b639d22",
- "webgpu/shader/execution/ceil.bin": "9d91caf6",
- "webgpu/shader/execution/clamp.bin": "d7c7512f",
- "webgpu/shader/execution/cos.bin": "832af30d",
- "webgpu/shader/execution/cosh.bin": "1e9d346f",
- "webgpu/shader/execution/cross.bin": "8db02194",
- "webgpu/shader/execution/degrees.bin": "4e6926ca",
- "webgpu/shader/execution/determinant.bin": "46c05eb0",
- "webgpu/shader/execution/distance.bin": "d3d25ee0",
- "webgpu/shader/execution/dot.bin": "aac0064d",
- "webgpu/shader/execution/exp.bin": "4edc9995",
- "webgpu/shader/execution/exp2.bin": "71f9b9f1",
- "webgpu/shader/execution/faceForward.bin": "bbfa6f58",
- "webgpu/shader/execution/floor.bin": "46a8f6f6",
- "webgpu/shader/execution/fma.bin": "d8de76da",
- "webgpu/shader/execution/fract.bin": "ce59af66",
- "webgpu/shader/execution/frexp.bin": "2d856679",
- "webgpu/shader/execution/inverseSqrt.bin": "c2e19ec5",
- "webgpu/shader/execution/ldexp.bin": "85a3d6da",
- "webgpu/shader/execution/length.bin": "6462de73",
- "webgpu/shader/execution/log.bin": "9d3e5eac",
- "webgpu/shader/execution/log2.bin": "f0e6911",
- "webgpu/shader/execution/max.bin": "10d3b0f0",
- "webgpu/shader/execution/min.bin": "8150d732",
- "webgpu/shader/execution/mix.bin": "e80a07f6",
- "webgpu/shader/execution/modf.bin": "4ec56dfa",
- "webgpu/shader/execution/normalize.bin": "10f90ae0",
- "webgpu/shader/execution/pack2x16float.bin": "f238e86f",
- "webgpu/shader/execution/pow.bin": "342665d2",
- "webgpu/shader/execution/quantizeToF16.bin": "e2736816",
- "webgpu/shader/execution/radians.bin": "6520dcc1",
- "webgpu/shader/execution/reflect.bin": "4183175",
- "webgpu/shader/execution/refract.bin": "4915dbfe",
- "webgpu/shader/execution/round.bin": "99b75791",
- "webgpu/shader/execution/saturate.bin": "a14f35f0",
- "webgpu/shader/execution/sign.bin": "36761bf9",
- "webgpu/shader/execution/sin.bin": "74a2057b",
- "webgpu/shader/execution/sinh.bin": "8353f6ea",
- "webgpu/shader/execution/smoothstep.bin": "8122e6fb",
- "webgpu/shader/execution/sqrt.bin": "4b04e554",
- "webgpu/shader/execution/step.bin": "11ec30ed",
- "webgpu/shader/execution/tan.bin": "67da16db",
- "webgpu/shader/execution/tanh.bin": "2f62fb36",
- "webgpu/shader/execution/transpose.bin": "29a099da",
- "webgpu/shader/execution/trunc.bin": "ccf08bdc",
- "webgpu/shader/execution/unpack2x16float.bin": "7bfbbdf6",
- "webgpu/shader/execution/unpack2x16snorm.bin": "bed6a1e7",
- "webgpu/shader/execution/unpack2x16unorm.bin": "a26a06f0",
- "webgpu/shader/execution/unpack4x8snorm.bin": "756c46a0",
- "webgpu/shader/execution/unpack4x8unorm.bin": "575c7feb",
- "webgpu/shader/execution/unary/af_arithmetic.bin": "1176909d",
- "webgpu/shader/execution/unary/af_assignment.bin": "73927f22",
- "webgpu/shader/execution/unary/bool_conversion.bin": "e96f1762",
- "webgpu/shader/execution/unary/f16_arithmetic.bin": "e77cc4e8",
- "webgpu/shader/execution/unary/f16_conversion.bin": "4555bbc2",
- "webgpu/shader/execution/unary/f32_arithmetic.bin": "8e28dc17",
- "webgpu/shader/execution/unary/f32_conversion.bin": "1d219",
- "webgpu/shader/execution/unary/i32_arithmetic.bin": "ca6a70d",
- "webgpu/shader/execution/unary/i32_conversion.bin": "cf99c846",
- "webgpu/shader/execution/unary/u32_conversion.bin": "7f09c0ce",
- "webgpu/shader/execution/unary/ai_assignment.bin": "98de2fcc",
- "webgpu/shader/execution/binary/ai_arithmetic.bin": "6b88d962",
- "webgpu/shader/execution/unary/ai_arithmetic.bin": "e584bcb7",
- "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "c82b9644",
- "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "7ea675b6",
- "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "28f11a02",
- "webgpu/shader/execution/derivatives.bin": "93c7bf53",
- "webgpu/shader/execution/fwidth.bin": "e9668101"
+ "webgpu/shader/execution/binary/af_addition.bin": "aa14ec26",
+ "webgpu/shader/execution/binary/af_logical.bin": "8890f3f1",
+ "webgpu/shader/execution/binary/af_division.bin": "ad842076",
+ "webgpu/shader/execution/binary/af_matrix_addition.bin": "c6d8db63",
+ "webgpu/shader/execution/binary/af_matrix_subtraction.bin": "c09b18cc",
+ "webgpu/shader/execution/binary/af_multiplication.bin": "f0d2c49c",
+ "webgpu/shader/execution/binary/af_remainder.bin": "781a9ff6",
+ "webgpu/shader/execution/binary/af_subtraction.bin": "2cf4b54a",
+ "webgpu/shader/execution/binary/f16_addition.bin": "3efff4fc",
+ "webgpu/shader/execution/binary/f16_logical.bin": "de9424e7",
+ "webgpu/shader/execution/binary/f16_division.bin": "7cd144a1",
+ "webgpu/shader/execution/binary/f16_matrix_addition.bin": "3b986de1",
+ "webgpu/shader/execution/binary/f16_matrix_matrix_multiplication.bin": "4e135781",
+ "webgpu/shader/execution/binary/f16_matrix_scalar_multiplication.bin": "4c21775c",
+ "webgpu/shader/execution/binary/f16_matrix_subtraction.bin": "47e76662",
+ "webgpu/shader/execution/binary/f16_matrix_vector_multiplication.bin": "2b557015",
+ "webgpu/shader/execution/binary/f16_multiplication.bin": "6a3e21d7",
+ "webgpu/shader/execution/binary/f16_remainder.bin": "70c9358c",
+ "webgpu/shader/execution/binary/f16_subtraction.bin": "4563dd62",
+ "webgpu/shader/execution/binary/f32_addition.bin": "1f35a4dd",
+ "webgpu/shader/execution/binary/f32_logical.bin": "723abe00",
+ "webgpu/shader/execution/binary/f32_division.bin": "c95c68a",
+ "webgpu/shader/execution/binary/f32_matrix_addition.bin": "75957d7a",
+ "webgpu/shader/execution/binary/f32_matrix_matrix_multiplication.bin": "baf57d3c",
+ "webgpu/shader/execution/binary/f32_matrix_scalar_multiplication.bin": "cdbe1ae4",
+ "webgpu/shader/execution/binary/f32_matrix_subtraction.bin": "d9e7057",
+ "webgpu/shader/execution/binary/f32_matrix_vector_multiplication.bin": "634f59e6",
+ "webgpu/shader/execution/binary/f32_multiplication.bin": "a1b88039",
+ "webgpu/shader/execution/binary/f32_remainder.bin": "51048d79",
+ "webgpu/shader/execution/binary/f32_subtraction.bin": "aa74d81b",
+ "webgpu/shader/execution/binary/i32_arithmetic.bin": "78c2f699",
+ "webgpu/shader/execution/binary/i32_comparison.bin": "5c84159e",
+ "webgpu/shader/execution/binary/u32_arithmetic.bin": "99c3cdcf",
+ "webgpu/shader/execution/binary/u32_comparison.bin": "ee2050d9",
+ "webgpu/shader/execution/abs.bin": "d3a0c673",
+ "webgpu/shader/execution/acos.bin": "c6f18083",
+ "webgpu/shader/execution/acosh.bin": "2499b5b7",
+ "webgpu/shader/execution/asin.bin": "6b98a030",
+ "webgpu/shader/execution/asinh.bin": "129f4a25",
+ "webgpu/shader/execution/atan.bin": "6deed949",
+ "webgpu/shader/execution/atan2.bin": "395d1e72",
+ "webgpu/shader/execution/atanh.bin": "1a0f331a",
+ "webgpu/shader/execution/bitcast.bin": "ca478e97",
+ "webgpu/shader/execution/ceil.bin": "a002b286",
+ "webgpu/shader/execution/clamp.bin": "664e4e71",
+ "webgpu/shader/execution/cos.bin": "995ba625",
+ "webgpu/shader/execution/cosh.bin": "860e0f00",
+ "webgpu/shader/execution/cross.bin": "706065a0",
+ "webgpu/shader/execution/degrees.bin": "bacef53d",
+ "webgpu/shader/execution/determinant.bin": "a5fa820f",
+ "webgpu/shader/execution/distance.bin": "ae309025",
+ "webgpu/shader/execution/dot.bin": "6fe2eaad",
+ "webgpu/shader/execution/exp.bin": "16c78e4a",
+ "webgpu/shader/execution/exp2.bin": "aa3bd37a",
+ "webgpu/shader/execution/faceForward.bin": "6ec092ba",
+ "webgpu/shader/execution/floor.bin": "6c8c8506",
+ "webgpu/shader/execution/fma.bin": "d0240050",
+ "webgpu/shader/execution/fract.bin": "eedd46ad",
+ "webgpu/shader/execution/frexp.bin": "4f86736f",
+ "webgpu/shader/execution/inverseSqrt.bin": "c472a6d",
+ "webgpu/shader/execution/ldexp.bin": "f79e5c4c",
+ "webgpu/shader/execution/length.bin": "61c438dc",
+ "webgpu/shader/execution/log.bin": "602e67ab",
+ "webgpu/shader/execution/log2.bin": "59531d51",
+ "webgpu/shader/execution/max.bin": "315ad00",
+ "webgpu/shader/execution/min.bin": "5e67b47a",
+ "webgpu/shader/execution/mix.bin": "7b9c30fb",
+ "webgpu/shader/execution/modf.bin": "a926d2ab",
+ "webgpu/shader/execution/normalize.bin": "7a0f4894",
+ "webgpu/shader/execution/pack2x16float.bin": "5bda6ee2",
+ "webgpu/shader/execution/pow.bin": "d860633f",
+ "webgpu/shader/execution/quantizeToF16.bin": "e2cc5f7c",
+ "webgpu/shader/execution/radians.bin": "a564cae8",
+ "webgpu/shader/execution/reflect.bin": "ee6dda3f",
+ "webgpu/shader/execution/refract.bin": "769439d5",
+ "webgpu/shader/execution/round.bin": "e5c69d73",
+ "webgpu/shader/execution/saturate.bin": "a2baca59",
+ "webgpu/shader/execution/sign.bin": "c3031854",
+ "webgpu/shader/execution/sin.bin": "32cdc685",
+ "webgpu/shader/execution/sinh.bin": "807d1b08",
+ "webgpu/shader/execution/smoothstep.bin": "643c83a",
+ "webgpu/shader/execution/sqrt.bin": "ac7e5381",
+ "webgpu/shader/execution/step.bin": "ac99a50e",
+ "webgpu/shader/execution/tan.bin": "5efc9f6f",
+ "webgpu/shader/execution/tanh.bin": "8385aa65",
+ "webgpu/shader/execution/transpose.bin": "f3685a2e",
+ "webgpu/shader/execution/trunc.bin": "7eb127a7",
+ "webgpu/shader/execution/unpack2x16float.bin": "4eef663c",
+ "webgpu/shader/execution/unpack2x16snorm.bin": "c48522c4",
+ "webgpu/shader/execution/unpack2x16unorm.bin": "2af3269",
+ "webgpu/shader/execution/unpack4x8snorm.bin": "d4dbf395",
+ "webgpu/shader/execution/unpack4x8unorm.bin": "4c534802",
+ "webgpu/shader/execution/unary/af_arithmetic.bin": "1fde55fa",
+ "webgpu/shader/execution/unary/af_assignment.bin": "38273056",
+ "webgpu/shader/execution/unary/bool_conversion.bin": "3a8b1d94",
+ "webgpu/shader/execution/unary/f16_arithmetic.bin": "12ceb8ec",
+ "webgpu/shader/execution/unary/f16_conversion.bin": "585809f2",
+ "webgpu/shader/execution/unary/f32_arithmetic.bin": "8ebcea6b",
+ "webgpu/shader/execution/unary/f32_conversion.bin": "d6681b46",
+ "webgpu/shader/execution/unary/i32_arithmetic.bin": "b2cc8056",
+ "webgpu/shader/execution/unary/i32_conversion.bin": "bd484400",
+ "webgpu/shader/execution/unary/u32_conversion.bin": "82e99fb5",
+ "webgpu/shader/execution/unary/ai_assignment.bin": "c2c1d4e1",
+ "webgpu/shader/execution/binary/ai_arithmetic.bin": "b01bab0a",
+ "webgpu/shader/execution/unary/ai_arithmetic.bin": "dd916b48",
+ "webgpu/shader/execution/binary/af_matrix_matrix_multiplication.bin": "ad511107",
+ "webgpu/shader/execution/binary/af_matrix_scalar_multiplication.bin": "b54158a8",
+ "webgpu/shader/execution/binary/af_matrix_vector_multiplication.bin": "32c18616",
+ "webgpu/shader/execution/derivatives.bin": "a7a6547c",
+ "webgpu/shader/execution/fwidth.bin": "a4db2b2c"
}
\ No newline at end of file
diff --git a/src/webgpu/api/validation/dispatch.spec.ts b/src/webgpu/api/validation/dispatch.spec.ts
index 3b06a22..22059201 100644
--- a/src/webgpu/api/validation/dispatch.spec.ts
+++ b/src/webgpu/api/validation/dispatch.spec.ts
@@ -4,6 +4,8 @@
import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { keysOf } from '../../../common/util/data_tables.js';
+import { WGSLLanguageFeature } from '../../capability_info.js';
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
@@ -141,3 +143,428 @@
const expected = t.params.size === 'max' ? 0 : kMagic;
t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array([expected]));
});
+
+interface RequiredSizeCase {
+ code: string;
+ size: number; // Size must be greater than 4
+ binding_type: GPUBufferBindingType;
+ requires?: WGSLLanguageFeature[];
+}
+
+const kRequiredSizeCases: Record<string, RequiredSizeCase> = {
+ ro_storage_32bytes: {
+ code: `
+@group(0) @binding(0) var<storage> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ },
+ storage_32bytes: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'storage',
+ },
+ uniform_32bytes: {
+ code: `
+@group(0) @binding(0) var<uniform> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'uniform',
+ },
+ ro_storage_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ uniform_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'uniform',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferView1: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<u32, 16>>(&v, 0);
+}`,
+ size: 16 * 4,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferView2: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<u32, 16>>(&v, 56);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16 * 4,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView3: {
+ code: `
+struct S {
+ a: vec4u,
+ b: vec2u,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let x = 16u;
+ let p = bufferView<S>(&v, x);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView4: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<vec2u>>(&v, 0);
+}`,
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView5: {
+ code: `
+struct S {
+ a: array<u32, 4>
+}
+
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2u>(&v, 0);
+ let q = bufferView<S>(&v, 0);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView6: {
+ code: `
+struct S {
+ a: array<u32, 4>
+}
+
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<u32>(&v, 16);
+ let q = bufferView<S>(&v, 0);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView7: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2i>(&v, 16);
+}
+@fragment
+fn main2() {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView8: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2f>(&v, 16);
+}
+fn foo() {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView9: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<u32>(&v, 16);
+ foo(&v);
+}
+fn foo(p : ptr<storage, buffer, read_write>) {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view', 'unrestricted_pointer_parameters'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView1: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec2u>>(&v, 0, 32);
+}`,
+ // Size does not count toward min binding size.
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView2: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec2f>>(&v, 16, 32);
+}`,
+ // Offset and size do not count toward min binding size.
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView3: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 16;
+ let s = 32;
+ let p = bufferArrayView<array<vec2u>>(&v, o, s);
+}`,
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView4: {
+ code: `
+struct S {
+ a: vec4u,
+ b: u32,
+}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 16;
+ let s = 32;
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView5: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 0u;
+ let s = 64;
+ let p1 = bufferArrayView<array<u32>>(&v, o, s);
+ let p2 = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView6: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView7: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+@fragment
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView8: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bar();
+ foo();
+}
+fn bar() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+};
+
+g.test('shader_required_buffer_size')
+ .desc('Test that dispatch time validation occurs about the required buffer size')
+ .params(u =>
+ u
+ .combine('case', keysOf(kRequiredSizeCases))
+ .beginSubcases()
+ .combine('valid', [false, true] as const)
+ .combine('layout', ['auto', 'explicit'] as const)
+ )
+ .fn(t => {
+ const testcase = kRequiredSizeCases[t.params.case];
+ const features = testcase.requires ?? [];
+ features.forEach(f => {
+ t.skipIfLanguageFeatureNotSupported(f);
+ });
+
+ const buffer = t.createBufferTracked({
+ size: t.params.valid ? testcase.size : testcase.size - 4,
+ usage: testcase.binding_type === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: testcase.binding_type,
+ minBindingSize: 0,
+ },
+ },
+ ],
+ });
+ const layout = t.device.createPipelineLayout({ bindGroupLayouts: [bgLayout] });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: t.params.layout === 'auto' ? 'auto' : layout,
+ compute: {
+ module: t.device.createShaderModule({ code: testcase.code }),
+ },
+ });
+
+ if (t.params.layout === 'auto' && !t.params.valid) {
+ // 'auto' layout get minBindingSize from the shader.
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ ],
+ }),
+ true;
+ });
+ } else {
+ // Expect dispatch time validation.
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ ],
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const computePassEncoder = commandEncoder.beginComputePass();
+ computePassEncoder.setPipeline(pipeline);
+ computePassEncoder.setBindGroup(0, bg);
+ computePassEncoder.dispatchWorkgroups(1);
+ computePassEncoder.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ }, !t.params.valid);
+ }
+ });
diff --git a/src/webgpu/api/validation/min_buffer_binding_size.spec.ts b/src/webgpu/api/validation/min_buffer_binding_size.spec.ts
new file mode 100644
index 0000000..50b19c2
--- /dev/null
+++ b/src/webgpu/api/validation/min_buffer_binding_size.spec.ts
@@ -0,0 +1,658 @@
+export const description = `
+Minimum buffer binding size tests
+`;
+
+import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import { keysOf } from '../../../common/util/data_tables.js';
+import { WGSLLanguageFeature } from '../../capability_info.js';
+
+export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
+
+interface RequiredSizeCase {
+ code: string;
+ size: number; // Size must be greater than 4
+ binding_type: GPUBufferBindingType;
+ requires?: WGSLLanguageFeature[];
+}
+
+const kRequiredSizeCases: Record<string, RequiredSizeCase> = {
+ ro_storage_32bytes: {
+ code: `
+@group(0) @binding(0) var<storage> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ },
+ storage_32bytes: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'storage',
+ },
+ uniform_32bytes: {
+ code: `
+@group(0) @binding(0) var<uniform> v : array<vec4u, 2>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = v;
+}`,
+ size: 32,
+ binding_type: 'uniform',
+ },
+ ro_storage_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ uniform_sized_buffer: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<32>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = &v;
+}`,
+ size: 32,
+ binding_type: 'uniform',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferView1: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<u32, 16>>(&v, 0);
+}`,
+ size: 16 * 4,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferView2: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<u32, 16>>(&v, 56);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16 * 4,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView3: {
+ code: `
+struct S {
+ a: vec4u,
+ b: vec2u,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let x = 16u;
+ let p = bufferView<S>(&v, x);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView4: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<array<vec2u>>(&v, 0);
+}`,
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView5: {
+ code: `
+struct S {
+ a: array<u32, 4>
+}
+
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2u>(&v, 0);
+ let q = bufferView<S>(&v, 0);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView6: {
+ code: `
+struct S {
+ a: array<u32, 4>
+}
+
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<u32>(&v, 16);
+ let q = bufferView<S>(&v, 0);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView7: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2i>(&v, 16);
+}
+@fragment
+fn main2() {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView8: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<vec2f>(&v, 16);
+}
+fn foo() {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 8,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferView9: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<u32>(&v, 16);
+ foo(&v);
+}
+fn foo(p : ptr<storage, buffer, read_write>) {
+ let q = bufferView<vec4u>(&v, 32);
+}`,
+ // Offset does not count toward min binding size.
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view', 'unrestricted_pointer_parameters'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView1: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec2u>>(&v, 0, 32);
+}`,
+ // Size does not count toward min binding size.
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView2: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec2f>>(&v, 16, 32);
+}`,
+ // Offset and size do not count toward min binding size.
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView3: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 16;
+ let s = 32;
+ let p = bufferArrayView<array<vec2u>>(&v, o, s);
+}`,
+ size: 8,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ ro_storage_unsized_buffer_bufferArrayView4: {
+ code: `
+struct S {
+ a: vec4u,
+ b: u32,
+}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 16;
+ let s = 32;
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView5: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let o = 0u;
+ let s = 64;
+ let p1 = bufferArrayView<array<u32>>(&v, o, s);
+ let p2 = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView6: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView7: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+@fragment
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 16,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+ storage_unsized_buffer_bufferArrayView8: {
+ code: `
+struct S {
+ a : vec4f,
+ b : vec4f,
+}
+var<private> o : i32;
+var<private> s : i32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bar();
+ foo();
+}
+fn bar() {
+ let p = bufferArrayView<array<vec4u>>(&v, o, s);
+}
+fn foo() {
+ let p = bufferArrayView<array<S>>(&v, o, s);
+}`,
+ size: 32,
+ binding_type: 'storage',
+ requires: ['buffer_view'],
+ },
+};
+
+g.test('compute,shader_required_buffer_size')
+ .desc('Test that dispatch time validation occurs about the required buffer size')
+ .params(u =>
+ u
+ .combine('case', keysOf(kRequiredSizeCases))
+ .beginSubcases()
+ .combine('valid', [false, true] as const)
+ .combine('layout', ['auto', 'explicit'] as const)
+ )
+ .fn(t => {
+ const testcase = kRequiredSizeCases[t.params.case];
+ const features = testcase.requires ?? [];
+ features.forEach(f => {
+ t.skipIfLanguageFeatureNotSupported(f);
+ });
+
+ const buffer = t.createBufferTracked({
+ size: t.params.valid ? testcase.size : testcase.size - 4,
+ usage: testcase.binding_type === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: testcase.binding_type,
+ minBindingSize: 0,
+ },
+ },
+ ],
+ });
+ const layout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bgLayout],
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: t.params.layout === 'auto' ? 'auto' : layout,
+ compute: {
+ module: t.device.createShaderModule({ code: testcase.code }),
+ },
+ });
+
+ if (t.params.layout === 'auto' && !t.params.valid) {
+ // 'auto' layout get minBindingSize from the shader.
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ ],
+ }),
+ true;
+ });
+ } else {
+ // Expect dispatch time validation.
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ ],
+ });
+
+ const commandEncoder = t.device.createCommandEncoder();
+ const computePassEncoder = commandEncoder.beginComputePass();
+ computePassEncoder.setPipeline(pipeline);
+ computePassEncoder.setBindGroup(0, bg);
+ computePassEncoder.dispatchWorkgroups(1);
+ computePassEncoder.end();
+
+ t.expectValidationError(() => {
+ commandEncoder.finish();
+ }, !t.params.valid);
+ }
+ });
+
+const kRequiredRenderSizeCases: Record<string, RequiredSizeCase> = {
+ fragment_larger_buffer_view_ro_storage: {
+ code: `
+@group(0) @binding(0) var<storage> buf : buffer;
+@group(0) @binding(1) var<uniform> uni : u32;
+
+@vertex
+fn vs() -> @builtin(position) vec4f {
+ _ = uni;
+ _ = bufferView<array<u32, 2>>(&buf, 0);
+ return vec4f(0);
+}
+
+@fragment
+fn fs() -> @location(0) vec4f {
+ _ = uni;
+ _ = bufferView<array<u32, 4>>(&buf, 0);
+ return vec4f(0);
+}
+`,
+ size: 16,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ vertex_larger_buffer_view_ro_storage: {
+ code: `
+@group(0) @binding(0) var<storage> buf : buffer;
+@group(0) @binding(1) var<uniform> uni : u32;
+
+@vertex
+fn vs() -> @builtin(position) vec4f {
+ _ = uni;
+ _ = bufferView<array<vec4f, 2>>(&buf, 0);
+ return vec4f(0);
+}
+
+@fragment
+fn fs() -> @location(0) vec4f {
+ _ = uni;
+ _ = bufferView<array<u32, 4>>(&buf, 0);
+ return vec4f(0);
+}
+`,
+ size: 32,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ fragment_larger_buffer_array_view_ro_storage: {
+ code: `
+@group(0) @binding(0) var<storage> buf : buffer;
+@group(0) @binding(1) var<uniform> uni : u32;
+
+@vertex
+fn vs() -> @builtin(position) vec4f {
+ _ = bufferArrayView<array<u32>>(&buf, 0, uni);
+ return vec4f(0);
+}
+
+@fragment
+fn fs() -> @location(0) vec4f {
+ _ = bufferArrayView<array<vec4u>>(&buf, 0, uni);
+ return vec4f(0);
+}
+`,
+ size: 16,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+ vertex_larger_buffer_array_view_ro_storage: {
+ code: `
+@group(0) @binding(0) var<storage> buf : buffer;
+@group(0) @binding(1) var<uniform> uni : u32;
+
+@vertex
+fn vs() -> @builtin(position) vec4f {
+ _ = bufferArrayView<array<vec4f>>(&buf, 0, uni);
+ return vec4f(0);
+}
+
+@fragment
+fn fs() -> @location(0) vec4f {
+ _ = bufferArrayView<array<u32>>(&buf, 0, uni);
+ return vec4f(0);
+}
+`,
+ size: 16,
+ binding_type: 'read-only-storage',
+ requires: ['buffer_view'],
+ },
+};
+
+g.test('render,shader_required_buffer_size')
+ .params(u =>
+ u
+ .combine('case', keysOf(kRequiredRenderSizeCases))
+ .beginSubcases()
+ .combine('valid', [false, true] as const)
+ .combine('layout', ['auto', 'explicit'] as const)
+ )
+ .fn(t => {
+ const testcase = kRequiredRenderSizeCases[t.params.case];
+ const features = testcase.requires ?? [];
+ features.forEach(f => {
+ t.skipIfLanguageFeatureNotSupported(f);
+ });
+
+ const buffer = t.createBufferTracked({
+ size: t.params.valid ? testcase.size : testcase.size - 4,
+ usage: testcase.binding_type === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ });
+
+ const uniform = t.createBufferTracked({
+ size: 4,
+ usage: GPUBufferUsage.UNIFORM,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: testcase.binding_type,
+ minBindingSize: 0,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
+ buffer: {
+ type: 'uniform',
+ minBindingSize: 4,
+ },
+ },
+ ],
+ });
+ const layout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bgLayout],
+ });
+
+ const colorTexture = t.createTextureTracked({
+ size: { width: 1, height: 1 },
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const shaderModule = t.device.createShaderModule({ code: testcase.code });
+ const pipeline = t.device.createRenderPipeline({
+ layout: t.params.layout === 'auto' ? 'auto' : layout,
+ vertex: { module: shaderModule },
+ fragment: {
+ module: shaderModule,
+ targets: [{ format: 'rgba8unorm' }],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ if (t.params.layout === 'auto' && !t.params.valid) {
+ // 'auto' layout get minBindingSize from shaders
+ t.expectValidationError(() => {
+ t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: uniform,
+ },
+ },
+ ],
+ }),
+ true;
+ });
+ } else {
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: uniform,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view: colorTexture.createView(),
+ clearValue: [0, 0, 0, 0],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ });
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.draw(1), pass.end();
+
+ t.expectValidationError(() => {
+ encoder.finish();
+ }, !t.params.valid);
+ }
+ });
diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts
index 98ec040..871dcad 100644
--- a/src/webgpu/capability_info.ts
+++ b/src/webgpu/capability_info.ts
@@ -991,6 +991,7 @@
'linear_indexing',
'texture_formats_tier1',
'immediate_address_space',
+ 'buffer_view',
] as const;
export type WGSLLanguageFeature = (typeof kKnownWGSLLanguageFeatures)[number];
diff --git a/src/webgpu/shader/execution/expression/call/builtin/bufferArrayView.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/bufferArrayView.spec.ts
new file mode 100644
index 0000000..a8ec17d
--- /dev/null
+++ b/src/webgpu/shader/execution/expression/call/builtin/bufferArrayView.spec.ts
@@ -0,0 +1,618 @@
+export const description = `
+Execution tests for bufferArrayView
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { keysOf } from '../../../../../../common/util/data_tables.js';
+import { assert } from '../../../../../../common/util/util.js';
+import { AllFeaturesMaxLimitsGPUTest } from '../../../../../gpu_test.js';
+import { Type } from '../../../../../util/conversion.js';
+
+import {
+ kBufferSizes,
+ kArrayLengthTypes,
+ kOffsets,
+ kSizes,
+ kCalls,
+ kStructDecls,
+ isValidArrayLengthCase,
+ calculateArrayLength,
+ runLengthTest,
+ kLayoutCases,
+ runReadLayoutTest,
+ runWriteLayoutTest,
+ runReadWriteTest,
+} from './buffer_view_utils.js';
+
+export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
+
+g.test('array_length')
+ .desc('Tests arrayLength from a bufferView')
+ .params(u =>
+ u
+ .combine('type', keysOf(kArrayLengthTypes))
+ .combine('aspace', ['workgroup', 'storage', 'uniform'] as const)
+ .beginSubcases()
+ .combine('sized', [false, true] as const)
+ .combine('override', [false, true] as const)
+ .combine('bufferSize', kBufferSizes)
+ .combine('dynamic_offset', [0, 256] as const)
+ .filter(t => {
+ if (t.aspace === 'workgroup' && t.dynamic_offset !== 0) {
+ return false;
+ }
+ if (t.aspace !== 'workgroup' && t.override === true) {
+ return false;
+ }
+ return t.sized || t.aspace === 'storage';
+ })
+ )
+ .fn(t => {
+ const testcase = kArrayLengthTypes[t.params.type];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.aspace === 'uniform' && testcase.uniformStdLayout === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const bSize = t.params.override ? 'bufferSize' : `${t.params.bufferSize}`;
+ const buffer_ty = `buffer${t.params.sized ? `<${bSize}>` : ``}`;
+ let decl = '';
+ switch (t.params.aspace) {
+ case 'workgroup':
+ decl = `var<workgroup> v : ${buffer_ty};\n@group(0) @binding(0) var<storage> dummy : array<u32>;`;
+ break;
+ case 'uniform':
+ decl = `@group(0) @binding(0) var<uniform> v : ${buffer_ty};`;
+ break;
+ case 'storage':
+ decl = `@group(0) @binding(0) var<storage> v : ${buffer_ty};`;
+ break;
+ }
+
+ const values: number[] = [];
+ const offsets: number[] = [];
+ const sizes: number[] = [];
+ // Limit the number of assignments.
+ for (let o = 0; o < kOffsets.length; o += 2) {
+ for (const size of kSizes) {
+ const offset = kOffsets[o];
+
+ // Skip any case that results in an invalid memory reference.
+ // Split the writes up one per thread.
+ if (isValidArrayLengthCase(testcase, offset, size, t.params.bufferSize)) {
+ values.push(calculateArrayLength(testcase, offset, size, t.params.bufferSize));
+ // Setup the offset and size buffer values so they can be indexed by invocation id.
+ offsets.push(offset);
+ sizes.push(size);
+ }
+ }
+ }
+
+ assert(values.length > 0, 'no tests run');
+
+ const access = testcase.access ?? '';
+ const addrOf = access === '' ? '' : '&';
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+
+ const wgsl = `
+${enables}
+
+${kStructDecls}
+
+override bufferSize : u32 = 0u;
+${decl}
+@group(0) @binding(1) var<storage> offsets : array<u32>;
+@group(0) @binding(2) var<storage> sizes : array<u32>; // unused
+@group(0) @binding(3) var<storage, read_write> out : array<u32>;
+
+override wgx : u32;
+@compute @workgroup_size(wgx)
+fn main(@builtin(global_invocation_id) gid : vec3u,
+ @builtin(local_invocation_index) lid : u32) {
+ ${t.params.aspace === 'workgroup' ? '_ = dummy[0];' : ''}
+
+ if gid.x >= ${values.length} {
+ return;
+ }
+
+ out[gid.x] = arrayLength(${addrOf}bufferArrayView<${
+ testcase.type
+ }>(&v, offsets[gid.x], sizes[gid.x])${access});
+}`;
+
+ runLengthTest(
+ t,
+ wgsl,
+ t.params.aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ t.params.bufferSize,
+ t.params.dynamic_offset,
+ offsets,
+ sizes,
+ values
+ );
+ });
+
+g.test('array_length,functions')
+ .desc('Tests arrayLength from a bufferView')
+ .params(u =>
+ u
+ .combine('type', keysOf(kArrayLengthTypes))
+ .combine('aspace', ['workgroup', 'storage', 'uniform'] as const)
+ .beginSubcases()
+ .combine('call', kCalls)
+ .combine('bufferSize', kBufferSizes)
+ .combine('dynamic_offset', [0, 256] as const)
+ .filter(t => {
+ if (t.bufferSize === 128) {
+ return false;
+ }
+ return t.aspace !== 'workgroup' || t.dynamic_offset === 0;
+ })
+ )
+ .fn(t => {
+ const testcase = kArrayLengthTypes[t.params.type];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ if (t.params.aspace === 'uniform' && testcase.uniformStdLayout === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const buffer_ty = `buffer<${t.params.bufferSize}>`;
+ let decl = '';
+ switch (t.params.aspace) {
+ case 'workgroup':
+ decl = `var<workgroup> v : ${buffer_ty};\n@group(0) @binding(0) var<storage> dummy : array<u32>;`;
+ break;
+ case 'uniform':
+ decl = `@group(0) @binding(0) var<uniform> v : ${buffer_ty};`;
+ break;
+ case 'storage':
+ decl = `@group(0) @binding(0) var<storage> v : ${buffer_ty};`;
+ break;
+ }
+
+ const values: number[] = [];
+ const offsets: number[] = [];
+ const sizes: number[] = [];
+ // Limit the number of assignments.
+ for (let o = 0; o < kOffsets.length; o += 2) {
+ for (const size of kSizes) {
+ const offset = kOffsets[o];
+ const usedSize =
+ t.params.call === 'unsized' ? t.params.bufferSize : t.params.bufferSize / 2;
+
+ // Skip any case that results in an invalid memory reference.
+ // Split the writes up one per thread.
+ if (isValidArrayLengthCase(testcase, offset, size, usedSize)) {
+ values.push(calculateArrayLength(testcase, offset, size, usedSize));
+ // Setup the offset and size buffer values so they can be indexed by invocation id.
+ offsets.push(offset);
+ sizes.push(size);
+ }
+ }
+ }
+
+ assert(values.length > 0, 'no tests run');
+
+ const access = testcase.access ?? '';
+ const addrOf = access === '' ? '' : '&';
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+
+ const wgsl = `
+${enables}
+
+${kStructDecls}
+
+fn unsizedLength(p : ptr<${t.params.aspace}, buffer>, gidx : u32) -> u32 {
+ return arrayLength(${addrOf}bufferArrayView<${
+ testcase.type
+ }>(p, offsets[gidx], sizes[gidx])${access});
+}
+
+fn sizedLength(p : ptr<${t.params.aspace}, buffer<${t.params.bufferSize / 2}>>, gidx : u32) -> u32 {
+ return arrayLength(${addrOf}bufferArrayView<${
+ testcase.type
+ }>(p, offsets[gidx], sizes[gidx])${access});
+}
+
+fn sized_indirectLength(p : ptr<${t.params.aspace}, buffer<${
+ t.params.bufferSize / 2
+ }>>, gidx : u32) -> u32 {
+ return unsizedLength(p, gidx);
+}
+
+fn sized_sized_indirectLength(p : ptr<${t.params.aspace}, buffer<${
+ t.params.bufferSize
+ }>>, gidx : u32) -> u32 {
+ return sizedLength(p, gidx);
+}
+
+override bufferSize : u32 = 0u;
+${decl}
+@group(0) @binding(1) var<storage> offsets : array<u32>;
+@group(0) @binding(2) var<storage> sizes : array<u32>; // unused
+@group(0) @binding(3) var<storage, read_write> out : array<u32>;
+
+override wgx : u32;
+@compute @workgroup_size(wgx)
+fn main(@builtin(global_invocation_id) gid : vec3u,
+ @builtin(local_invocation_index) lid : u32) {
+ ${t.params.aspace === 'workgroup' ? '_ = dummy[0];' : ''}
+
+ if gid.x >= ${values.length} {
+ return;
+ }
+
+ out[gid.x] = ${t.params.call}Length(&v, gid.x);
+}`;
+
+ runLengthTest(
+ t,
+ wgsl,
+ t.params.aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ t.params.bufferSize,
+ t.params.dynamic_offset,
+ offsets,
+ sizes,
+ values
+ );
+ });
+
+g.test('read_layout')
+ .desc('Test reading memory layout from a bufferArrayView')
+ .params(u =>
+ u
+ .combine('case', keysOf(kLayoutCases))
+ .beginSubcases()
+ .combine('aspace', ['storage', 'ro_storage', 'uniform', 'workgroup'] as const)
+ .combine('offset', kOffsets)
+ .filter(t => {
+ const testcase = kLayoutCases[t.case];
+ if ((t.offset & (testcase.align - 1)) !== 0) {
+ return false;
+ }
+ return testcase.offset + t.offset < 252;
+ })
+ )
+ .fn(t => {
+ const testcase = kLayoutCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.aspace === 'uniform') {
+ if (testcase.uniformStdLayoutArrayView === true || testcase.uniformStdLayoutView === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'in';
+
+ const wgsl = `
+${enables}
+
+${testcase.decl ?? ''}
+
+@group(0) @binding(0)
+var<${t.params.aspace === 'uniform' ? 'uniform' : 'storage'}${
+ t.params.aspace === 'storage' ? ', read_write' : ''
+ }> in : buffer<256 * 4>;
+
+@group(0) @binding(1) var<storage, read_write> out : u32;
+
+var<workgroup> wg_var : buffer<256 * 4>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let in_ptr = bufferView<array<vec4u, 256 / 4>>(&in, 0);
+ let wg_ptr = bufferView<array<vec4u, 256 / 4>>(&wg_var, 0);
+ for (var i = 0; i < 256 / 4; i++) {
+ (*wg_ptr)[i] = (*in_ptr)[i];
+ }
+
+ workgroupBarrier();
+
+ let p = bufferArrayView<array<${testcase.type}>>(&${v}, ${
+ t.params.offset
+ }, bufferLength(&${v}) - ${t.params.offset});
+ out = u32((*p)[0]${testcase.access});
+}
+`;
+
+ runReadLayoutTest(t, testcase, wgsl, t.params.aspace, t.params.offset);
+ });
+
+g.test('write_layout')
+ .desc('Test writing memory layout via a bufferView')
+ .params(u =>
+ u
+ .combine('case', keysOf(kLayoutCases))
+ .beginSubcases()
+ .combine('assign', ['let', 'call'] as const)
+ .combine('aspace', ['storage', 'workgroup'] as const)
+ .combine('offset', kOffsets)
+ .filter(t => {
+ const testcase = kLayoutCases[t.case];
+ if ((t.offset & (testcase.align - 1)) !== 0) {
+ return false;
+ }
+ return testcase.offset + t.offset < 252;
+ })
+ )
+ .fn(t => {
+ const testcase = kLayoutCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'out';
+ const call_expr = `bufferArrayView<array<${testcase.type}>>(&${v}, ${t.params.offset}, bufferLength(&${v}) - ${t.params.offset})`;
+
+ let post_assign = '';
+ if (t.params.aspace === 'workgroup') {
+ post_assign = `
+ workgroupBarrier();
+
+ let out_ptr = bufferView<array<u32>>(&out, 0);
+ let wg_ptr = bufferView<array<u32>>(&wg_var, 0);
+ for (var i = 0; i < 256; i++) {
+ (*out_ptr)[i] = (*wg_ptr)[i];
+ }
+`;
+ }
+
+ const wgsl = `
+${enables}
+
+${testcase.decl ?? ''}
+
+@group(0) @binding(0) var<storage> in : u32;
+@group(0) @binding(1) var<storage, read_write> out : buffer<256 * 4>;
+
+
+var<workgroup> wg_var : buffer<256 * 4>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let val = ${testcase.f16 ? 'f16' : testcase.f32 ? 'f32' : 'u32'}(in);
+ let ptr = ${call_expr};
+ (*${t.params.assign === 'call' ? call_expr : 'ptr'})[0]${testcase.access} = val;
+
+ ${post_assign}
+}
+`;
+
+ runWriteLayoutTest(t, testcase, wgsl, t.params.offset);
+ });
+
+g.test('read')
+ .desc('Test reading various types from bufferArrayView')
+ .params(u =>
+ u
+ .combine('base_type', ['u32', 'i32', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .combine('wrap', ['none', 'vector', 'array', 'matrix'] as const)
+ .combine('width', [1, 2, 3, 4] as const)
+ .combine('aspace', ['workgroup', 'storage', 'ro_storage', 'uniform'] as const)
+ .combine('offset', [0, 4, 8, 12, 16, 32, 48, 64] as const)
+ .filter(t => {
+ if (t.wrap !== 'none' && t.width === 1) {
+ return false;
+ }
+ if (t.wrap === 'none' && t.width !== 1) {
+ return false;
+ }
+ if (t.wrap === 'matrix' && t.base_type !== 'f32' && t.base_type !== 'f16') {
+ return false;
+ }
+ if (t.aspace === 'uniform' && t.wrap === 'array') {
+ return false;
+ }
+ const ty = Type[t.base_type];
+ let align = ty.alignment;
+ switch (t.wrap) {
+ case 'vector':
+ align = Type.vec(t.width, ty).alignment;
+ break;
+ case 'array':
+ align = Type.array(2, ty).alignment;
+ break;
+ case 'matrix':
+ align = Type.mat(t.width, 2, ty).alignment;
+ break;
+ case 'none':
+ break;
+ }
+ if (t.offset % align !== 0) {
+ return false;
+ }
+ if (t.aspace === 'uniform' && align % 16 !== 0) {
+ return false;
+ }
+ return true;
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.base_type === 'f16') {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const ele_ty = Type[t.params.base_type];
+ let ty: Type = ele_ty;
+ switch (t.params.wrap) {
+ case 'vector':
+ ty = Type.vec(t.params.width, ele_ty);
+ break;
+ case 'array':
+ ty = Type.array(t.params.width, ele_ty);
+ break;
+ case 'matrix':
+ ty = Type.mat(t.params.width, 2, ele_ty);
+ break;
+ case 'none':
+ break;
+ }
+
+ const bufferSize = 128;
+ const enables = t.params.base_type === 'f16' ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'in';
+ const wgsl = `
+${enables}
+
+
+@group(0) @binding(0)
+var<${t.params.aspace === 'uniform' ? 'uniform' : 'storage'}${
+ t.params.aspace === 'storage' ? ', read_write' : ''
+ }> in : buffer<${bufferSize}>;
+
+@group(0) @binding(1) var<storage, read_write> out : ${ty.toString()};
+
+var<workgroup> wg_var : buffer<${bufferSize}>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let in_ptr = bufferView<array<vec4u, ${bufferSize} / 16>>(&in, 0);
+ let wg_ptr = bufferView<array<vec4u, ${bufferSize} / 16>>(&wg_var, 0);
+ for (var i = 0; i < 256 / 4; i++) {
+ (*wg_ptr)[i] = (*in_ptr)[i];
+ }
+
+ workgroupBarrier();
+
+ let p = bufferArrayView<array<${ty.toString()}>>(&${v}, ${
+ t.params.offset
+ }, bufferLength(&${v}) - ${t.params.offset});
+ out = (*p)[0];
+}
+`;
+
+ runReadWriteTest(true, t, wgsl, ele_ty, ty, t.params.aspace, t.params.offset, bufferSize);
+ });
+
+g.test('write')
+ .desc('Test writing various types via bufferArrayView')
+ .params(u =>
+ u
+ .combine('base_type', ['u32', 'i32', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .combine('wrap', ['none', 'vector', 'array', 'matrix'] as const)
+ .combine('width', [1, 2, 3, 4] as const)
+ .combine('aspace', ['workgroup', 'storage'] as const)
+ .combine('offset', [0, 4, 8, 12, 16, 32, 48, 64] as const)
+ .combine('assign', ['let', 'call'] as const)
+ .combine('swizzle', [false, true] as const)
+ .filter(t => {
+ if (t.wrap !== 'none' && t.width === 1) {
+ return false;
+ }
+ if (t.wrap === 'none' && t.width !== 1) {
+ return false;
+ }
+ if (t.wrap === 'matrix' && t.base_type !== 'f32' && t.base_type !== 'f16') {
+ return false;
+ }
+ if (t.wrap !== 'vector' && t.swizzle) {
+ return false;
+ }
+ const ty = Type[t.base_type];
+ let align = ty.alignment;
+ switch (t.wrap) {
+ case 'vector':
+ align = Type.vec(t.width, ty).alignment;
+ break;
+ case 'array':
+ align = Type.array(2, ty).alignment;
+ break;
+ case 'matrix':
+ align = Type.mat(t.width, 2, ty).alignment;
+ break;
+ case 'none':
+ break;
+ }
+ if (t.offset % align !== 0) {
+ return false;
+ }
+ return true;
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.base_type === 'f16') {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+ if (t.params.swizzle) {
+ t.skipIfLanguageFeatureNotSupported('swizzle_assignment');
+ }
+
+ let swizzle = '';
+ const ele_ty = Type[t.params.base_type];
+ let ty: Type = ele_ty;
+ switch (t.params.wrap) {
+ case 'vector':
+ ty = Type.vec(t.params.width, ele_ty);
+ if (t.params.swizzle) {
+ swizzle = '.xyzw'.substring(0, 1 + t.params.width);
+ }
+ break;
+ case 'array':
+ ty = Type.array(t.params.width, ele_ty);
+ break;
+ case 'matrix':
+ ty = Type.mat(t.params.width, 2, ele_ty);
+ break;
+ case 'none':
+ break;
+ }
+
+ const bufferSize = 128;
+ const enables = t.params.base_type === 'f16' ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'out';
+
+ let post_assign = '';
+ if (t.params.aspace === 'workgroup') {
+ post_assign = `
+ workgroupBarrier();
+
+ let out_ptr = bufferView<array<u32>>(&out, 0);
+ let wg_ptr = bufferView<array<u32>>(&wg_var, 0);
+ for (var i = 0; i < 256; i++) {
+ (*out_ptr)[i] = (*wg_ptr)[i];
+ }
+`;
+ }
+
+ const call_expr = `bufferArrayView<array<${ty.toString()}>>(&${v}, ${
+ t.params.offset
+ }, bufferLength(&${v}) - ${t.params.offset})`;
+ const wgsl = `
+${enables}
+
+@group(0) @binding(0) var<storage> in : ${ty.toString()};
+@group(0) @binding(1) var<storage, read_write> out : buffer<${bufferSize}>;
+
+
+var<workgroup> wg_var : buffer<${bufferSize}>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let ptr = ${call_expr};
+ (*${t.params.assign === 'call' ? call_expr : 'ptr'})[0]${swizzle} = in;
+
+ ${post_assign}
+}
+`;
+
+ runReadWriteTest(false, t, wgsl, ele_ty, ty, t.params.aspace, t.params.offset, bufferSize);
+ });
diff --git a/src/webgpu/shader/execution/expression/call/builtin/bufferLength.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/bufferLength.spec.ts
new file mode 100644
index 0000000..57754cd
--- /dev/null
+++ b/src/webgpu/shader/execution/expression/call/builtin/bufferLength.spec.ts
@@ -0,0 +1,485 @@
+export const description = `
+Execution tests for bufferLength
+
+Tests sized and unsized buffers across all address spaces (where applicable).
+Tested against buffers with static and dynamic offsets.
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+
+export const g = makeTestGroup(GPUTest);
+
+g.test('sized_buffer')
+ .desc('Tests bufferLength directly on sized buffer variables')
+ .params(u =>
+ u
+ .combine('size', [256, 512, 600, 1024, 2048] as const)
+ .beginSubcases()
+ .combine('param', ['none', 'unsized', 'sized'] as const)
+ .combine('padding', [false, true] as const)
+ .combine('dynamic_offset', [false, true] as const)
+ .combine('offset', [0, 256] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.param !== 'none') {
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ }
+
+ let fn_decls = '';
+ const assigns = ['', '', '', ''];
+ switch (t.params.param) {
+ case 'none':
+ assigns[0] = 'bufferLength(&uniform_buffer)';
+ assigns[1] = 'bufferLength(&ro_storage_buffer)';
+ assigns[2] = 'bufferLength(&storage_buffer)';
+ assigns[3] = 'bufferLength(&workgroup_buffer)';
+ break;
+ case 'unsized':
+ fn_decls = `
+fn uniformUnsized(p : ptr<uniform, buffer>) -> u32 {
+ return bufferLength(p);
+}
+fn roStorageUnsized(p : ptr<storage, buffer>) -> u32 {
+ return bufferLength(p);
+}
+fn storageUnsized(p : ptr<storage, buffer, read_write>) -> u32 {
+ return bufferLength(p);
+}
+fn workgroupUnsized(p : ptr<workgroup, buffer>) -> u32 {
+ return bufferLength(p);
+}`;
+ assigns[0] = 'uniformUnsized(&uniform_buffer)';
+ assigns[1] = 'roStorageUnsized(&ro_storage_buffer)';
+ assigns[2] = 'storageUnsized(&storage_buffer)';
+ assigns[3] = 'workgroupUnsized(&workgroup_buffer)';
+ break;
+ case 'sized':
+ fn_decls = `
+fn uniformSized(p : ptr<uniform, buffer<${t.params.size / 2}>>) -> u32 {
+ return bufferLength(p);
+}
+fn roStorageSized(p : ptr<storage, buffer<${t.params.size / 2}>>) -> u32 {
+ return bufferLength(p);
+}
+fn storageSized(p : ptr<storage, buffer<${t.params.size / 2}>, read_write>) -> u32 {
+ return bufferLength(p);
+}
+fn workgroupSized(p : ptr<workgroup, buffer<${t.params.size / 2}>>) -> u32 {
+ return bufferLength(p);
+}`;
+ assigns[0] = 'uniformSized(&uniform_buffer)';
+ assigns[1] = 'roStorageSized(&ro_storage_buffer)';
+ assigns[2] = 'storageSized(&storage_buffer)';
+ assigns[3] = 'workgroupSized(&workgroup_buffer)';
+ break;
+ }
+
+ const wgsl = `
+@group(0) @binding(0) var<uniform> uniform_buffer : buffer<${t.params.size}>;
+@group(0) @binding(1) var<storage> ro_storage_buffer : buffer<${t.params.size}>;
+@group(0) @binding(2) var<storage, read_write> storage_buffer : buffer<${t.params.size}>;
+var<workgroup> workgroup_buffer : buffer<${t.params.size}>;
+
+@group(0) @binding(3) var<storage, read_write> out : array<u32, 4>;
+
+${fn_decls}
+
+@compute @workgroup_size(4)
+fn main(@builtin(local_invocation_index) lid : u32) {
+ out[0] = ${assigns[0]};
+ out[1] = ${assigns[1]};
+ out[2] = ${assigns[2]};
+ out[3] = ${assigns[3]};
+}`;
+
+ const padding = t.params.padding ? 256 : 0;
+ const offset = t.params.offset;
+ const dynOffset = t.params.dynamic_offset ? 256 : 0;
+ const bufferSize = t.params.size + padding + offset + dynOffset;
+ const noOffsetSize = t.params.size + padding;
+
+ const uniformBuffer = t.createBufferTracked({
+ size: bufferSize,
+ usage: GPUBufferUsage.UNIFORM,
+ });
+ const roStorageBuffer = t.createBufferTracked({
+ size: bufferSize,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const storageBuffer = t.createBufferTracked({
+ size: bufferSize,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const outputBuffer = t.createBufferTracked({
+ size: 4 * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'uniform',
+ hasDynamicOffset: t.params.dynamic_offset,
+ minBindingSize: t.params.size,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'read-only-storage',
+ hasDynamicOffset: t.params.dynamic_offset,
+ minBindingSize: t.params.size,
+ },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ hasDynamicOffset: t.params.dynamic_offset,
+ minBindingSize: t.params.size,
+ },
+ },
+ {
+ binding: 3,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ minBindingSize: 4 * 4,
+ },
+ },
+ ],
+ });
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bgLayout],
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: uniformBuffer,
+ offset,
+ size: noOffsetSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: roStorageBuffer,
+ offset,
+ size: noOffsetSize,
+ },
+ },
+ {
+ binding: 2,
+ resource: {
+ buffer: storageBuffer,
+ offset,
+ size: noOffsetSize,
+ },
+ },
+ {
+ binding: 3,
+ resource: {
+ buffer: outputBuffer,
+ size: 4 * 4,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ if (t.params.dynamic_offset) {
+ pass.setBindGroup(0, bg, [dynOffset, dynOffset, dynOffset]);
+ } else {
+ pass.setBindGroup(0, bg);
+ }
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ const expectedSize = t.params.param === 'sized' ? t.params.size / 2 : t.params.size;
+ const expected = new Uint32Array([expectedSize, expectedSize, expectedSize, expectedSize]);
+ t.expectGPUBufferValuesEqual(outputBuffer, expected);
+ });
+
+g.test('unsized_buffer')
+ .desc('Tests bufferLength directly on unsized buffer variables')
+ .params(u =>
+ u
+ .combine('size', [256, 512, 600, 1024, 2048] as const)
+ .beginSubcases()
+ .combine('param', ['none', 'unsized'] as const)
+ .combine('padding', [false, true] as const)
+ .combine('dynamic_offset', [false, true] as const)
+ .combine('offset', [0, 256] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.param !== 'none') {
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ }
+
+ let fn_decls = '';
+ const assigns = ['', '', '', ''];
+ switch (t.params.param) {
+ case 'none':
+ assigns[0] = 'bufferLength(&ro_storage_buffer)';
+ assigns[1] = 'bufferLength(&storage_buffer)';
+ break;
+ case 'unsized':
+ fn_decls = `
+fn roStorageUnsized(p : ptr<storage, buffer>) -> u32 {
+ return bufferLength(p);
+}
+fn storageUnsized(p : ptr<storage, buffer, read_write>) -> u32 {
+ return bufferLength(p);
+}`;
+ assigns[0] = 'roStorageUnsized(&ro_storage_buffer)';
+ assigns[1] = 'storageUnsized(&storage_buffer)';
+ break;
+ }
+
+ const wgsl = `
+@group(0) @binding(0) var<storage> ro_storage_buffer : buffer;
+@group(0) @binding(1) var<storage, read_write> storage_buffer : buffer;
+
+@group(0) @binding(2) var<storage, read_write> out : array<u32, 4>;
+
+${fn_decls}
+
+@compute @workgroup_size(4)
+fn main(@builtin(local_invocation_index) lid : u32) {
+ out[0] = ${assigns[0]};
+ out[1] = ${assigns[1]};
+}`;
+
+ const padding = t.params.padding ? 256 : 0;
+ const offset = t.params.offset;
+ const dynOffset = t.params.dynamic_offset ? 256 : 0;
+ const bufferSize = t.params.size + padding + offset + dynOffset;
+ const noOffsetSize = t.params.size + padding;
+
+ const roStorageBuffer = t.createBufferTracked({
+ size: bufferSize,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const storageBuffer = t.createBufferTracked({
+ size: bufferSize,
+ usage: GPUBufferUsage.STORAGE,
+ });
+ const outputBuffer = t.createBufferTracked({
+ size: 4 * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'read-only-storage',
+ hasDynamicOffset: t.params.dynamic_offset,
+ minBindingSize: t.params.size,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ hasDynamicOffset: t.params.dynamic_offset,
+ minBindingSize: t.params.size,
+ },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ minBindingSize: 4 * 4,
+ },
+ },
+ ],
+ });
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bgLayout],
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: roStorageBuffer,
+ offset,
+ size: noOffsetSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: storageBuffer,
+ offset,
+ size: noOffsetSize,
+ },
+ },
+ {
+ binding: 2,
+ resource: {
+ buffer: outputBuffer,
+ size: 4 * 4,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ if (t.params.dynamic_offset) {
+ pass.setBindGroup(0, bg, [dynOffset, dynOffset]);
+ } else {
+ pass.setBindGroup(0, bg);
+ }
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ const expected = new Uint32Array([noOffsetSize, noOffsetSize]);
+ t.expectGPUBufferValuesEqual(outputBuffer, expected);
+ });
+
+g.test('max_size_buffer')
+ .desc('Test with a maximum sized buffer')
+ .params(u =>
+ u
+ .combine('aspace', ['workgroup', 'uniform', 'storage', 'ro_storage'] as const)
+ .combine('sized', [false, true] as const)
+ .filter(t => {
+ return t.sized === true || t.aspace === 'storage' || t.aspace === 'ro_storage';
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+
+ const {
+ maxUniformBufferBindingSize,
+ maxStorageBufferBindingSize,
+ maxComputeWorkgroupStorageSize,
+ } = t.device.limits;
+
+ let size = 16;
+ switch (t.params.aspace) {
+ case 'workgroup':
+ size = maxComputeWorkgroupStorageSize;
+ break;
+ case 'uniform':
+ size = maxUniformBufferBindingSize;
+ break;
+ case 'storage':
+ case 'ro_storage':
+ size = maxStorageBufferBindingSize;
+ break;
+ }
+
+ const type = `buffer${t.params.sized ? `<${size}u>` : ``}`;
+ let decl = '';
+ switch (t.params.aspace) {
+ case 'workgroup':
+ decl = `var<workgroup> v : ${type};\n@group(0) @binding(1) var<storage> dummy : u32;`;
+ break;
+ case 'uniform':
+ decl = `@group(0) @binding(1) var<uniform> v: ${type};`;
+ break;
+ case 'storage':
+ decl = `@group(0) @binding(1) var<storage, read_write> v : ${type};`;
+ break;
+ case 'ro_storage':
+ decl = `@group(0) @binding(1) var<storage> v : ${type};`;
+ break;
+ }
+
+ const wgsl = `
+@group(0) @binding(0) var<storage, read_write> out : u32;
+${decl}
+
+@compute @workgroup_size(1)
+fn main() {
+ out = bufferLength(&v);
+ ${t.params.aspace === 'workgroup' ? '_ = dummy;' : ''}
+}`;
+
+ const outputBuffer = t.createBufferTracked({
+ size: 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+ // For the workgroup case we create a dummy storage buffer to simplify bindings.
+ const inputBuffer = t.createBufferTracked({
+ size,
+ usage: t.params.aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: inputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ const expected = new Uint32Array([size]);
+ t.expectGPUBufferValuesEqual(outputBuffer, expected);
+ });
diff --git a/src/webgpu/shader/execution/expression/call/builtin/bufferView.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/bufferView.spec.ts
new file mode 100644
index 0000000..6c6442d
--- /dev/null
+++ b/src/webgpu/shader/execution/expression/call/builtin/bufferView.spec.ts
@@ -0,0 +1,584 @@
+export const description = `
+Execution tests for bufferView
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { keysOf } from '../../../../../../common/util/data_tables.js';
+import { assert } from '../../../../../../common/util/util.js';
+import { AllFeaturesMaxLimitsGPUTest } from '../../../../../gpu_test.js';
+import { Type } from '../../../../../util/conversion.js';
+
+import {
+ kBufferSizes,
+ kArrayLengthTypes,
+ kOffsets,
+ kCalls,
+ kStructDecls,
+ isValidArrayLengthCase,
+ calculateArrayLength,
+ runLengthTest,
+ kLayoutCases,
+ runReadLayoutTest,
+ runWriteLayoutTest,
+ runReadWriteTest,
+} from './buffer_view_utils.js';
+
+export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
+
+g.test('array_length')
+ .desc('Tests arrayLength from a bufferView')
+ .params(u =>
+ u
+ .combine('type', keysOf(kArrayLengthTypes))
+ .combine('aspace', ['workgroup', 'storage', 'uniform'] as const)
+ .beginSubcases()
+ .combine('sized', [false, true] as const)
+ .combine('override', [false, true] as const)
+ .combine('bufferSize', kBufferSizes)
+ .combine('dynamic_offset', [0, 256] as const)
+ .filter(t => {
+ if (t.aspace === 'workgroup' && t.dynamic_offset !== 0) {
+ return false;
+ }
+ if (t.aspace !== 'workgroup' && t.override === true) {
+ return false;
+ }
+ return t.sized || t.aspace === 'storage';
+ })
+ )
+ .fn(t => {
+ const testcase = kArrayLengthTypes[t.params.type];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.aspace === 'uniform' && testcase.uniformStdLayout === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const bSize = t.params.override ? 'bufferSize' : `${t.params.bufferSize}`;
+ const buffer_ty = `buffer${t.params.sized ? `<${bSize}>` : ``}`;
+ let decl = '';
+ switch (t.params.aspace) {
+ case 'workgroup':
+ decl = `var<workgroup> v : ${buffer_ty};\n@group(0) @binding(0) var<storage> dummy : array<u32>;`;
+ break;
+ case 'uniform':
+ decl = `@group(0) @binding(0) var<uniform> v : ${buffer_ty};`;
+ break;
+ case 'storage':
+ decl = `@group(0) @binding(0) var<storage> v : ${buffer_ty};`;
+ break;
+ }
+
+ const values: number[] = [];
+ const offsets: number[] = [];
+ const sizes: number[] = [0];
+ for (const offset of kOffsets) {
+ // Skip any case that results in an invalid memory reference.
+ // Split the writes up one per thread.
+ if (isValidArrayLengthCase(testcase, offset, 0, t.params.bufferSize)) {
+ values.push(calculateArrayLength(testcase, offset, 0, t.params.bufferSize));
+ // Setup the offset and size buffer values so they can be indexed by invocation id.
+ offsets.push(offset);
+ }
+ }
+ assert(values.length > 0, 'no tests run');
+
+ const access = testcase.access ?? '';
+ const addrOf = access === '' ? '' : '&';
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+
+ const wgsl = `
+${enables}
+
+${kStructDecls}
+
+override bufferSize : u32 = 0u;
+${decl}
+@group(0) @binding(1) var<storage> offsets : array<u32>;
+@group(0) @binding(2) var<storage> sizes : array<u32>; // unused
+@group(0) @binding(3) var<storage, read_write> out : array<u32>;
+
+override wgx : u32;
+@compute @workgroup_size(wgx)
+fn main(@builtin(global_invocation_id) gid : vec3u) {
+ ${t.params.aspace === 'workgroup' ? '_ = dummy[0];' : ''}
+
+ if gid.x >= ${values.length} {
+ return;
+ }
+
+ out[gid.x] = arrayLength(${addrOf}bufferView<${testcase.type}>(&v, offsets[gid.x])${access});
+}`;
+
+ runLengthTest(
+ t,
+ wgsl,
+ t.params.aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ t.params.bufferSize,
+ t.params.dynamic_offset,
+ offsets,
+ sizes,
+ values
+ );
+ });
+
+g.test('array_length,functions')
+ .desc('Tests arrayLength from a bufferView through various function calls')
+ .params(u =>
+ u
+ .combine('type', keysOf(kArrayLengthTypes))
+ .combine('aspace', ['workgroup', 'storage', 'uniform'] as const)
+ .beginSubcases()
+ .combine('call', kCalls)
+ .combine('bufferSize', kBufferSizes)
+ .combine('dynamic_offset', [0, 256] as const)
+ .filter(t => {
+ if (t.type === 'array_S_2' || t.type === 'mat4x4f' || t.type === 'mat4x3f') {
+ return t.bufferSize > 128;
+ }
+ return t.aspace !== 'workgroup' || t.dynamic_offset === 0;
+ })
+ )
+ .fn(t => {
+ const testcase = kArrayLengthTypes[t.params.type];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ if (t.params.aspace === 'uniform' && testcase.uniformStdLayout === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const buffer_ty = `buffer<${t.params.bufferSize}>`;
+ let decl = '';
+ switch (t.params.aspace) {
+ case 'workgroup':
+ decl = `var<workgroup> v : ${buffer_ty};\n@group(0) @binding(0) var<storage> dummy : array<u32>;`;
+ break;
+ case 'uniform':
+ decl = `@group(0) @binding(0) var<uniform> v : ${buffer_ty};`;
+ break;
+ case 'storage':
+ decl = `@group(0) @binding(0) var<storage> v : ${buffer_ty};`;
+ break;
+ }
+
+ const values: number[] = [];
+ const offsets: number[] = [];
+ const sizes: number[] = [0];
+ for (const offset of kOffsets) {
+ const usedSize = t.params.call === 'unsized' ? t.params.bufferSize : t.params.bufferSize / 2;
+ // Skip any case that results in an invalid memory reference.
+ // Split the writes up one per thread.
+ if (isValidArrayLengthCase(testcase, offset, 0, usedSize)) {
+ values.push(calculateArrayLength(testcase, offset, 0, usedSize));
+ // Setup the offset and size buffer values so they can be indexed by invocation id.
+ offsets.push(offset);
+ }
+ }
+
+ assert(values.length > 0, 'no tests run');
+
+ const access = testcase.access ?? '';
+ const addrOf = access === '' ? '' : '&';
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+
+ const wgsl = `
+${enables}
+
+${kStructDecls}
+
+fn unsizedLength(p : ptr<${t.params.aspace}, buffer>, gidx : u32) -> u32 {
+ return arrayLength(${addrOf}bufferView<${testcase.type}>(p, offsets[gidx])${access});
+}
+
+fn sizedLength(p : ptr<${t.params.aspace}, buffer<${t.params.bufferSize / 2}>>, gidx : u32) -> u32 {
+ return arrayLength(${addrOf}bufferView<${testcase.type}>(p, offsets[gidx])${access});
+}
+
+fn sized_indirectLength(p : ptr<${t.params.aspace}, buffer<${
+ t.params.bufferSize / 2
+ }>>, gidx : u32) -> u32 {
+ return unsizedLength(p, gidx);
+}
+
+fn sized_sized_indirectLength(p : ptr<${t.params.aspace}, buffer<${
+ t.params.bufferSize
+ }>>, gidx : u32) -> u32 {
+ return sizedLength(p, gidx);
+}
+
+override bufferSize : u32 = 0u;
+${decl}
+@group(0) @binding(1) var<storage> offsets : array<u32>;
+@group(0) @binding(2) var<storage> sizes : array<u32>; // unused
+@group(0) @binding(3) var<storage, read_write> out : array<u32>;
+
+override wgx : u32;
+@compute @workgroup_size(wgx)
+fn main(@builtin(global_invocation_id) gid : vec3u) {
+ ${t.params.aspace === 'workgroup' ? '_ = dummy[0];' : ''}
+
+ if gid.x >= ${values.length} {
+ return;
+ }
+
+ out[gid.x] = ${t.params.call}Length(&v, gid.x);
+}`;
+
+ runLengthTest(
+ t,
+ wgsl,
+ t.params.aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE,
+ t.params.bufferSize,
+ t.params.dynamic_offset,
+ offsets,
+ sizes,
+ values
+ );
+ });
+
+g.test('read_layout')
+ .desc('Test reading memory layout from a bufferView')
+ .params(u =>
+ u
+ .combine('case', keysOf(kLayoutCases))
+ .beginSubcases()
+ .combine('aspace', ['storage', 'ro_storage', 'uniform', 'workgroup'] as const)
+ .combine('offset', kOffsets)
+ .filter(t => {
+ const testcase = kLayoutCases[t.case];
+ if ((t.offset & (testcase.align - 1)) !== 0) {
+ return false;
+ }
+ return testcase.offset + t.offset < 252;
+ })
+ )
+ .fn(t => {
+ const testcase = kLayoutCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.aspace === 'uniform' && testcase.uniformStdLayoutView === true) {
+ t.skipIfLanguageFeatureNotSupported('uniform_buffer_standard_layout');
+ }
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'in';
+
+ const wgsl = `
+${enables}
+
+${testcase.decl ?? ''}
+
+@group(0) @binding(0)
+var<${t.params.aspace === 'uniform' ? 'uniform' : 'storage'}${
+ t.params.aspace === 'storage' ? ', read_write' : ''
+ }> in : buffer<256 * 4>;
+
+@group(0) @binding(1) var<storage, read_write> out : u32;
+
+var<workgroup> wg_var : buffer<256 * 4>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let in_ptr = bufferView<array<vec4u, 256 / 4>>(&in, 0);
+ let wg_ptr = bufferView<array<vec4u, 256 / 4>>(&wg_var, 0);
+ for (var i = 0; i < 256 / 4; i++) {
+ (*wg_ptr)[i] = (*in_ptr)[i];
+ }
+
+ workgroupBarrier();
+
+ let p = bufferView<${testcase.type}>(&${v}, ${t.params.offset});
+ out = u32((*p)${testcase.access});
+}
+`;
+
+ runReadLayoutTest(t, testcase, wgsl, t.params.aspace, t.params.offset);
+ });
+
+g.test('write_layout')
+ .desc('Test writing memory layout via a bufferView')
+ .params(u =>
+ u
+ .combine('case', keysOf(kLayoutCases))
+ .beginSubcases()
+ .combine('assign', ['let', 'call'] as const)
+ .combine('aspace', ['storage', 'workgroup'] as const)
+ .combine('offset', kOffsets)
+ .filter(t => {
+ const testcase = kLayoutCases[t.case];
+ if ((t.offset & (testcase.align - 1)) !== 0) {
+ return false;
+ }
+ return testcase.offset + t.offset < 252;
+ })
+ )
+ .fn(t => {
+ const testcase = kLayoutCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (testcase.f16) {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const enables = testcase.f16 === true ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'out';
+ const call_expr = `bufferView<${testcase.type}>(&${v}, ${t.params.offset})`;
+
+ let post_assign = '';
+ if (t.params.aspace === 'workgroup') {
+ post_assign = `
+ workgroupBarrier();
+
+ let out_ptr = bufferView<array<u32>>(&out, 0);
+ let wg_ptr = bufferView<array<u32>>(&wg_var, 0);
+ for (var i = 0; i < 256; i++) {
+ (*out_ptr)[i] = (*wg_ptr)[i];
+ }
+`;
+ }
+
+ const wgsl = `
+${enables}
+
+${testcase.decl ?? ''}
+
+@group(0) @binding(0) var<storage> in : u32;
+@group(0) @binding(1) var<storage, read_write> out : buffer<256 * 4>;
+
+
+var<workgroup> wg_var : buffer<256 * 4>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let val = ${testcase.f16 ? 'f16' : testcase.f32 ? 'f32' : 'u32'}(in);
+ let ptr = ${call_expr};
+ (*${t.params.assign === 'call' ? call_expr : 'ptr'})${testcase.access} = val;
+
+ ${post_assign}
+}
+`;
+
+ runWriteLayoutTest(t, testcase, wgsl, t.params.offset);
+ });
+
+g.test('read')
+ .desc('Test reading various types from bufferView')
+ .params(u =>
+ u
+ .combine('base_type', ['u32', 'i32', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .combine('wrap', ['none', 'vector', 'array', 'matrix'] as const)
+ .combine('width', [1, 2, 3, 4] as const)
+ .combine('aspace', ['workgroup', 'storage', 'ro_storage', 'uniform'] as const)
+ .combine('offset', [0, 4, 8, 12, 16, 32, 48, 64] as const)
+ .filter(t => {
+ if (t.wrap !== 'none' && t.width === 1) {
+ return false;
+ }
+ if (t.wrap === 'none' && t.width !== 1) {
+ return false;
+ }
+ if (t.wrap === 'matrix' && t.base_type !== 'f32' && t.base_type !== 'f16') {
+ return false;
+ }
+ if (t.aspace === 'uniform' && t.wrap === 'array') {
+ return false;
+ }
+ const ty = Type[t.base_type];
+ let align = ty.alignment;
+ switch (t.wrap) {
+ case 'vector':
+ align = Type.vec(t.width, ty).alignment;
+ break;
+ case 'array':
+ align = Type.array(2, ty).alignment;
+ break;
+ case 'matrix':
+ align = Type.mat(t.width, 2, ty).alignment;
+ break;
+ case 'none':
+ break;
+ }
+ if (t.offset % align !== 0) {
+ return false;
+ }
+ return true;
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.base_type === 'f16') {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+
+ const ele_ty = Type[t.params.base_type];
+ let ty: Type = ele_ty;
+ switch (t.params.wrap) {
+ case 'vector':
+ ty = Type.vec(t.params.width, ele_ty);
+ break;
+ case 'array':
+ ty = Type.array(t.params.width, ele_ty);
+ break;
+ case 'matrix':
+ ty = Type.mat(t.params.width, 2, ele_ty);
+ break;
+ case 'none':
+ break;
+ }
+
+ const bufferSize = 128;
+ const enables = t.params.base_type === 'f16' ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'in';
+ const wgsl = `
+${enables}
+
+
+@group(0) @binding(0)
+var<${t.params.aspace === 'uniform' ? 'uniform' : 'storage'}${
+ t.params.aspace === 'storage' ? ', read_write' : ''
+ }> in : buffer<${bufferSize}>;
+
+@group(0) @binding(1) var<storage, read_write> out : ${ty.toString()};
+
+var<workgroup> wg_var : buffer<${bufferSize}>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let in_ptr = bufferView<array<vec4u, ${bufferSize} / 16>>(&in, 0);
+ let wg_ptr = bufferView<array<vec4u, ${bufferSize} / 16>>(&wg_var, 0);
+ for (var i = 0; i < 256 / 4; i++) {
+ (*wg_ptr)[i] = (*in_ptr)[i];
+ }
+
+ workgroupBarrier();
+
+ let p = bufferView<${ty.toString()}>(&${v}, ${t.params.offset});
+ out = (*p);
+}
+`;
+
+ runReadWriteTest(true, t, wgsl, ele_ty, ty, t.params.aspace, t.params.offset, bufferSize);
+ });
+
+g.test('write')
+ .desc('Test writing various types via bufferView')
+ .params(u =>
+ u
+ .combine('base_type', ['u32', 'i32', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .combine('wrap', ['none', 'vector', 'array', 'matrix'] as const)
+ .combine('width', [1, 2, 3, 4] as const)
+ .combine('aspace', ['workgroup', 'storage'] as const)
+ .combine('offset', [0, 4, 8, 12, 16, 32, 48, 64] as const)
+ .combine('assign', ['let', 'call'] as const)
+ .combine('swizzle', [false, true] as const)
+ .filter(t => {
+ if (t.wrap !== 'none' && t.width === 1) {
+ return false;
+ }
+ if (t.wrap === 'none' && t.width !== 1) {
+ return false;
+ }
+ if (t.wrap === 'matrix' && t.base_type !== 'f32' && t.base_type !== 'f16') {
+ return false;
+ }
+ if (t.wrap !== 'vector' && t.swizzle) {
+ return false;
+ }
+ const ty = Type[t.base_type];
+ let align = ty.alignment;
+ switch (t.wrap) {
+ case 'vector':
+ align = Type.vec(t.width, ty).alignment;
+ break;
+ case 'array':
+ align = Type.array(2, ty).alignment;
+ break;
+ case 'matrix':
+ align = Type.mat(t.width, 2, ty).alignment;
+ break;
+ case 'none':
+ break;
+ }
+ if (t.offset % align !== 0) {
+ return false;
+ }
+ return true;
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (t.params.base_type === 'f16') {
+ t.skipIfDeviceDoesNotHaveFeature('shader-f16');
+ }
+ if (t.params.swizzle) {
+ t.skipIfLanguageFeatureNotSupported('swizzle_assignment');
+ }
+
+ let swizzle = '';
+ const ele_ty = Type[t.params.base_type];
+ let ty: Type = ele_ty;
+ switch (t.params.wrap) {
+ case 'vector':
+ ty = Type.vec(t.params.width, ele_ty);
+ if (t.params.swizzle) {
+ swizzle = '.xyzw'.substring(0, 1 + t.params.width);
+ }
+ break;
+ case 'array':
+ ty = Type.array(t.params.width, ele_ty);
+ break;
+ case 'matrix':
+ ty = Type.mat(t.params.width, 2, ele_ty);
+ break;
+ case 'none':
+ break;
+ }
+
+ const bufferSize = 128;
+ const enables = t.params.base_type === 'f16' ? 'enable f16;' : '';
+ const v = t.params.aspace === 'workgroup' ? 'wg_var' : 'out';
+
+ let post_assign = '';
+ if (t.params.aspace === 'workgroup') {
+ post_assign = `
+ workgroupBarrier();
+
+ let out_ptr = bufferView<array<u32>>(&out, 0);
+ let wg_ptr = bufferView<array<u32>>(&wg_var, 0);
+ for (var i = 0; i < 256; i++) {
+ (*out_ptr)[i] = (*wg_ptr)[i];
+ }
+`;
+ }
+
+ const call_expr = `bufferView<${ty.toString()}>(&${v}, ${t.params.offset})`;
+ const wgsl = `
+${enables}
+
+@group(0) @binding(0) var<storage> in : ${ty.toString()};
+@group(0) @binding(1) var<storage, read_write> out : buffer<${bufferSize}>;
+
+
+var<workgroup> wg_var : buffer<${bufferSize}>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let ptr = ${call_expr};
+ (*${t.params.assign === 'call' ? call_expr : 'ptr'})${swizzle} = in;
+
+ ${post_assign}
+}
+`;
+
+ runReadWriteTest(false, t, wgsl, ele_ty, ty, t.params.aspace, t.params.offset, bufferSize);
+ });
diff --git a/src/webgpu/shader/execution/expression/call/builtin/buffer_view_utils.ts b/src/webgpu/shader/execution/expression/call/builtin/buffer_view_utils.ts
new file mode 100644
index 0000000..8622b98
--- /dev/null
+++ b/src/webgpu/shader/execution/expression/call/builtin/buffer_view_utils.ts
@@ -0,0 +1,975 @@
+import {
+ iterRange,
+ typedArrayParam,
+ typedArrayFromParam,
+ TypedArrayBufferView,
+} from '../../../../../../common/util/util.js';
+import { GPUTest } from '../../../../../gpu_test.js';
+import {
+ Type,
+ ScalarType,
+ ArrayType,
+ MatrixType,
+ VectorType,
+} from '../../../../../util/conversion.js';
+
+export const kBufferSizes = [128, 256, 512, 1024] as const;
+export const kOffsets = [0, 1, 2, 3, 4, 8, 12, 16, 32, 100, 156, 480, 768] as const;
+export const kSizes = [...kBufferSizes, 32, 48, 64] as const;
+export const kCalls = ['unsized', 'sized', 'sized_indirect', 'sized_sized_indirect'] as const;
+
+interface ArrayLengthType {
+ type: string;
+ access?: string;
+ align: number;
+ stride: number;
+ arrayOffset?: number;
+ f16?: boolean;
+ uniformStdLayout?: boolean;
+}
+
+/**
+ * Return true if the case is valid
+ *
+ * @param type The testcase
+ * @param offset The offset to the view call
+ * @param size The size to the view call
+ * @param bufferSize The size of the buffer
+ * @returns true if case is valid.
+ */
+export function isValidArrayLengthCase(
+ type: ArrayLengthType,
+ offset: number,
+ size: number,
+ bufferSize: number
+): boolean {
+ if (size === 0) {
+ // bufferView case
+ return offset + (type.arrayOffset ?? 0) + type.stride <= bufferSize;
+ } else {
+ // bufferArrayView case
+ if ((type.arrayOffset ?? 0) + type.stride > size) {
+ return false;
+ }
+ return offset + size <= bufferSize;
+ }
+}
+
+/**
+ * Calculates the return length in elements
+ *
+ * @param type The testcase
+ * @param offset The offset to the view call
+ * @param size The size to the view call
+ * @param bufferSize The buffer size
+ * @returns The number of elements in the arrayLength call
+ */
+export function calculateArrayLength(
+ type: ArrayLengthType,
+ offset: number,
+ size: number,
+ bufferSize: number
+): number {
+ const alignOffset = offset & ~(type.align - 1);
+ if (size === 0) {
+ // bufferView case
+ return Math.floor((bufferSize - alignOffset - (type.arrayOffset ?? 0)) / type.stride);
+ } else {
+ // bufferArrayView case
+ return Math.floor((size - (type.arrayOffset ?? 0)) / type.stride);
+ }
+}
+
+export const kStructDecls = `
+struct S {
+ a: vec4u,
+ b: u32,
+}
+struct T {
+ a : u32,
+ b : array<u32, 5>
+}
+struct S_arr {
+ a : vec4u,
+ b : array<u32>,
+}
+struct T_arr {
+ a : S,
+ b : array<T>,
+}
+`;
+
+export const kArrayLengthTypes: Record<string, ArrayLengthType> = {
+ // Scalars and vectors
+ u32: {
+ type: 'array<u32>',
+ align: 4,
+ stride: 4,
+ uniformStdLayout: true,
+ },
+ vec2f: {
+ type: 'array<vec2f>',
+ align: 8,
+ stride: 8,
+ uniformStdLayout: true,
+ },
+ vec3i: {
+ type: 'array<vec3i>',
+ align: 16,
+ stride: 16,
+ },
+ vec4u: {
+ type: 'array<vec4u>',
+ align: 16,
+ stride: 16,
+ },
+ f16: {
+ type: 'array<f16>',
+ align: 2,
+ stride: 2,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ vec2h: {
+ type: 'array<vec2h>',
+ align: 4,
+ stride: 4,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ vec3h: {
+ type: 'array<vec3h>',
+ align: 8,
+ stride: 8,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ vec4h: {
+ type: 'array<vec4h>',
+ align: 8,
+ stride: 8,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ // Matrices
+ mat2x2f: {
+ type: 'array<mat2x2f>',
+ align: 8,
+ stride: 16,
+ },
+ mat2x3f: {
+ type: 'array<mat2x3f>',
+ align: 16,
+ stride: 32,
+ },
+ mat2x4f: {
+ type: 'array<mat2x4f>',
+ align: 16,
+ stride: 32,
+ },
+ mat4x2f: {
+ type: 'array<mat4x2f>',
+ align: 8,
+ stride: 32,
+ },
+ mat4x3f: {
+ type: 'array<mat4x3f>',
+ align: 16,
+ stride: 64,
+ },
+ mat4x4f: {
+ type: 'array<mat4x4f>',
+ align: 16,
+ stride: 64,
+ },
+ mat3x2h: {
+ type: 'array<mat3x2h>',
+ align: 4,
+ stride: 12,
+ f16: true,
+ },
+ mat3x3h: {
+ type: 'array<mat3x3h>',
+ align: 8,
+ stride: 24,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ mat3x4h: {
+ type: 'array<mat3x4h>',
+ align: 8,
+ stride: 24,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ // Structs without arrays
+ S: {
+ type: 'array<S>',
+ align: 16,
+ stride: 32,
+ },
+ T: {
+ type: 'array<T>',
+ align: 4,
+ stride: 24,
+ uniformStdLayout: true,
+ },
+ // Sized arrays
+ array_u32_4: {
+ type: 'array<array<u32, 4>>',
+ align: 4,
+ stride: 16,
+ uniformStdLayout: true,
+ },
+ array_f16_6: {
+ type: 'array<array<f16, 6>>',
+ align: 2,
+ stride: 12,
+ f16: true,
+ uniformStdLayout: true,
+ },
+ array_vec2f_2: {
+ type: 'array<array<vec2f, 2>>',
+ align: 8,
+ stride: 16,
+ uniformStdLayout: true,
+ },
+ array_S_2: {
+ type: 'array<array<S, 2>>',
+ align: 16,
+ stride: 64,
+ },
+ array_T_2: {
+ type: 'array<array<T, 2>>',
+ align: 4,
+ stride: 48,
+ },
+ // Structs with runtime arrays
+ S_arr: {
+ type: 'S_arr',
+ access: '.b',
+ align: 16,
+ stride: 4,
+ arrayOffset: 16,
+ },
+ T_arr: {
+ type: 'T_arr',
+ access: '.b',
+ align: 16,
+ stride: 24,
+ arrayOffset: 32,
+ },
+};
+
+/**
+ * Run arrayLength tests for bufferView and bufferArrayView
+ *
+ * @param t The test
+ * @param wgsl The shader code. Interface requirements:
+ * * Overrides:
+ * * wgx: workgroup size x
+ * * bufferSize: size of the buffer
+ * * (0, 0): input buffer with 'usage' usage
+ * * (0, 1): read-only-storage-buffer for offsets
+ * * (0, 2): read-only-storage-buffer for sizes
+ * * (0, 3): storage-buffer for output
+ * @param usage The test buffer usage
+ * @param bufferSize The size of the test buffer
+ * @param dynOffset The size of the dynamic offset for the test buffer
+ * @param offsets The values for the offset buffer
+ * @param sizes The values for the size buffer
+ * @param values The expected results
+ */
+export function runLengthTest(
+ t: GPUTest,
+ wgsl: string,
+ usage: GPUBufferUsageFlags,
+ bufferSize: number,
+ dynOffset: number,
+ offsets: number[],
+ sizes: number[],
+ values: number[]
+) {
+ const fullBufferSize = bufferSize + dynOffset;
+ const inputBuffer = t.createBufferTracked({
+ size: fullBufferSize,
+ usage,
+ });
+ const offsetBuffer = t.makeBufferWithContents(
+ new Uint32Array(offsets),
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
+ );
+ const sizeBuffer = t.makeBufferWithContents(
+ new Uint32Array(sizes),
+ GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
+ );
+ const outputBuffer = t.createBufferTracked({
+ size: values.length * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const bgLayout = t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: usage === GPUBufferUsage.UNIFORM ? 'uniform' : 'read-only-storage',
+ hasDynamicOffset: dynOffset !== 0,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'read-only-storage',
+ },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'read-only-storage',
+ },
+ },
+ {
+ binding: 3,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage',
+ },
+ },
+ ],
+ });
+
+ // Limit the number of invocations per workgroup to 128 to fit the default compat limits. Launch extra workgroups to fit all the required invocations.
+ const wgx = Math.min(values.length, 128);
+ const num_wgs = Math.ceil(values.length / wgx);
+
+ const pipelineLayout = t.device.createPipelineLayout({
+ bindGroupLayouts: [bgLayout],
+ });
+ const pipeline = t.device.createComputePipeline({
+ layout: pipelineLayout,
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ constants: { wgx, bufferSize },
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: inputBuffer,
+ size: bufferSize,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: offsetBuffer,
+ },
+ },
+ {
+ binding: 2,
+ resource: {
+ buffer: sizeBuffer,
+ },
+ },
+ {
+ binding: 3,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg, dynOffset === 0 ? [] : [dynOffset]);
+ pass.dispatchWorkgroups(num_wgs, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array(values));
+}
+
+interface LayoutCase {
+ type: string;
+ decl?: string;
+ access: string;
+ align: number;
+ offset: number;
+ f16?: boolean;
+ f32?: boolean;
+ uniformStdLayoutView?: boolean;
+ uniformStdLayoutArrayView?: boolean;
+}
+
+export const kLayoutCases: Record<string, LayoutCase> = {
+ vec2u_align8: {
+ type: 'S_vec2u_align',
+ decl: 'struct S_vec2u_align { x : u32, y : vec2u, }',
+ access: '.y[1]',
+ align: 8,
+ offset: 12,
+ uniformStdLayoutArrayView: true,
+ },
+ vec3u_align16: {
+ type: 'S_vec3u_align',
+ decl: 'struct S_vec3u_align { x : u32, y : vec3u, }',
+ access: '.y[2]',
+ align: 16,
+ offset: 24,
+ },
+ vec4u_align16: {
+ type: 'S_vec4u_align',
+ decl: 'struct S_vec4u_align { x : u32, y : vec4u, }',
+ access: '.y[0]',
+ align: 16,
+ offset: 16,
+ },
+ struct_align32: {
+ type: 'S_align32',
+ decl: 'struct S_align32 { x : u32, @align(32) y : u32, }',
+ access: '.y',
+ align: 32,
+ offset: 32,
+ },
+ vec2h_align4: {
+ type: 'S_vec2h_align',
+ decl: 'struct S_vec2h_align { x : f16, y : vec2h }',
+ access: '.y[0]',
+ align: 4,
+ offset: 4,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ vec3h_align8: {
+ type: 'S_vec3h_align',
+ decl: 'struct S_vec3h_align { x : f16, y : vec3h }',
+ access: '.y.z',
+ align: 8,
+ offset: 12,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ vec4h_align8: {
+ type: 'S_vec4h_align',
+ decl: 'struct S_vec4h_align { x : f16, y : vec4h }',
+ access: '.y.z',
+ align: 8,
+ offset: 12,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ vec3i_size12: {
+ type: 'S_vec3i_size',
+ decl: 'struct S_vec3i_size { x : vec3i, y : u32 }',
+ access: '.y',
+ align: 16,
+ offset: 12,
+ },
+ vec3h_size6: {
+ type: 'S_vec3h_size',
+ decl: 'struct S_vec3h_size { x : vec3h, y : f16, z : f16 }',
+ access: '.z',
+ align: 8,
+ offset: 8,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ size80: {
+ type: 'S_size80',
+ decl: 'struct S_size80 { @size(80) x : u32, y : u32 }',
+ access: '.y',
+ align: 4,
+ offset: 80,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x2f_align8: {
+ type: 'S_mat2x2f_align',
+ decl: 'struct S_mat2x2f_align { x : u32, y : mat2x2f }',
+ access: '.y[0][0]',
+ align: 8,
+ offset: 8,
+ f32: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat3x3f_align16: {
+ type: 'S_mat3x3f_align',
+ decl: 'struct S_mat3x3f_align { x : u32, y : mat3x3f }',
+ access: '.y[0][0]',
+ align: 16,
+ offset: 16,
+ f32: true,
+ },
+ mat4x4f_align16: {
+ type: 'S_mat4x4f_align',
+ decl: 'struct S_mat4x4f_align { x : u32, y : mat4x4f }',
+ access: '.y[1][0]',
+ align: 16,
+ offset: 32,
+ f32: true,
+ },
+ mat3x2h_align4: {
+ type: 'S_mat3x2h_align',
+ decl: 'struct S_mat3x2h_align { x : f16, y : mat3x2h }',
+ access: '.y[0][0]',
+ align: 4,
+ offset: 4,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat4x3h_align8: {
+ type: 'S_mat4x3h_align',
+ decl: 'struct S_mat4x3h_align { x : f16, y : mat4x3h }',
+ access: '.y[0][0]',
+ align: 8,
+ offset: 8,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x4h_align8: {
+ type: 'S_mat2x4h_align',
+ decl: 'struct S_mat2x4h_align { x : f16, y : mat2x4h }',
+ access: '.y[0][0]',
+ align: 8,
+ offset: 8,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x2f_size: {
+ type: 'S_mat2x2f_size',
+ decl: 'struct S_mat2x2f_size { x : mat2x2f, y : u32 }',
+ access: '.y',
+ align: 8,
+ offset: 16,
+ uniformStdLayoutArrayView: true,
+ },
+ mat3x2f_size: {
+ type: 'S_mat3x2f_size',
+ decl: 'struct S_mat3x2f_size { x : mat3x2f, y : u32 }',
+ access: '.y',
+ align: 8,
+ offset: 24,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x3f_size: {
+ type: 'S_mat2x3f_size',
+ decl: 'struct S_mat2x3f_size { x : mat2x3f, y : u32 }',
+ access: '.y',
+ align: 16,
+ offset: 32,
+ },
+ mat3x3f_size: {
+ type: 'S_mat3x3f_size',
+ decl: 'struct S_mat3x3f_size { x : mat3x3f, y : u32 }',
+ access: '.y',
+ align: 16,
+ offset: 48,
+ },
+ mat2x4f_size: {
+ type: 'S_mat2x4f_size',
+ decl: 'struct S_mat2x4f_size { x : mat2x4f, y : u32 }',
+ access: '.y',
+ align: 16,
+ offset: 32,
+ },
+ mat3x4f_size: {
+ type: 'S_mat3x4f_size',
+ decl: 'struct S_mat3x4f_size { x : mat3x4f, y : u32 }',
+ access: '.y',
+ align: 16,
+ offset: 48,
+ },
+ mat2x2h_size: {
+ type: 'S_mat2x2h_size',
+ decl: 'struct S_mat2x2h_size { x : mat2x2h, y : f16 }',
+ access: '.y',
+ align: 4,
+ offset: 8,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat4x2h_size: {
+ type: 'S_mat4x2h_size',
+ decl: 'struct S_mat4x2h_size { x : mat4x2h, y : f16 }',
+ access: '.y',
+ align: 4,
+ offset: 16,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x3h_size: {
+ type: 'S_mat2x3h_size',
+ decl: 'struct S_mat2x3h_size { x : mat2x3h, y : f16 }',
+ access: '.y',
+ align: 8,
+ offset: 16,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat4x3h_size: {
+ type: 'S_mat4x3h_size',
+ decl: 'struct S_mat4x3h_size { x : mat4x3h, y : f16 }',
+ access: '.y',
+ align: 8,
+ offset: 32,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat2x4h_size: {
+ type: 'S_mat2x4h_size',
+ decl: 'struct S_mat2x4h_size { x : mat2x4h, y : f16 }',
+ access: '.y',
+ align: 8,
+ offset: 16,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ mat4x4h_size: {
+ type: 'S_mat4x4h_size',
+ decl: 'struct S_mat4x4h_size { x : mat4x4h, y : f16 }',
+ access: '.y',
+ align: 8,
+ offset: 32,
+ f16: true,
+ uniformStdLayoutArrayView: true,
+ },
+ struct_size_roundup: {
+ type: 'S_struct_size_roundup',
+ decl: `struct Inner { x : vec3u }
+ struct S_struct_size_roundup { x : Inner, y : u32 }`,
+ access: '.y',
+ align: 16,
+ offset: 16,
+ },
+ struct_inner_size: {
+ type: 'S_struct_inner_size',
+ decl: `struct Inner { @size(112) x : u32 }
+ struct S_struct_inner_size { x : Inner, y : u32 }`,
+ access: '.y',
+ align: 4,
+ offset: 112,
+ uniformStdLayoutArrayView: true,
+ },
+ struct_inner_align: {
+ type: 'S_struct_inner_align',
+ decl: `struct Inner { @align(64) x : u32 }
+ struct S_struct_inner_align { x : Inner, y : u32 }`,
+ access: '.y',
+ align: 64,
+ offset: 64,
+ },
+ struct_inner_size_and_align: {
+ type: 'S_struct_inner_size_and_align',
+ decl: `struct Inner { @align(32) @size(33) x : u32 }
+ struct S_struct_inner_size_and_align { x : Inner, y : u32 }`,
+ access: '.y',
+ align: 32,
+ offset: 64,
+ },
+ struct_override_size: {
+ type: 'S_struct_override_size',
+ decl: `struct Inner { @size(32) x : u32 }
+ struct S_struct_override_size { @size(64) x : Inner, y : u32 }`,
+ access: '.y',
+ align: 4,
+ offset: 64,
+ uniformStdLayoutArrayView: true,
+ },
+ struct_double_align: {
+ type: 'S_struct_double_align',
+ decl: `struct Inner { x : u32, @align(32) y : u32 }
+ struct S_struct_double_align { x : u32, @align(64) y : Inner }`,
+ access: '.y.y',
+ align: 64,
+ offset: 96,
+ },
+ array_stride_size: {
+ type: 'array<S_stride, 4>',
+ decl: 'struct S_stride { @size(16) x : u32 }',
+ access: '[2].x',
+ align: 4,
+ offset: 32,
+ },
+};
+
+/**
+ * Runs a read test for memory layouts from bufferView and bufferArrayView
+ *
+ * @param t The GPUTest
+ * @param testcase the LayoutCase
+ * @param wgsl the shader code
+ * * (0, 0) - uniform or storage buffer
+ * * (0, 1) - storage buffer
+ * @param aspace the address space being tested
+ * @param offset the offset to the view call
+ */
+export function runReadLayoutTest(
+ t: GPUTest,
+ testcase: LayoutCase,
+ wgsl: string,
+ aspace: string,
+ offset: number
+) {
+ let usage = GPUBufferUsage.COPY_DST;
+ if (aspace === 'uniform') {
+ usage |= GPUBufferUsage.UNIFORM;
+ } else {
+ usage |= GPUBufferUsage.STORAGE;
+ }
+
+ const kBufferLength = 256;
+ // Magic number is 42 in various representations.
+ const inMagicNumber = testcase.f16 ? 0x5140 : testcase.f32 ? 0x42280000 : 42;
+ const inputBuffer = t.makeBufferWithContents(
+ new Uint32Array([
+ ...iterRange(kBufferLength, x => {
+ if (x * 4 === testcase.offset + offset) {
+ return inMagicNumber;
+ } else {
+ return 0;
+ }
+ }),
+ ]),
+ usage
+ );
+
+ const outputBuffer = t.createBufferTracked({
+ size: 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: inputBuffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(outputBuffer, new Uint32Array([42]));
+}
+
+/**
+ * Runs a write test for memory layout from bufferView and bufferArrayView
+ *
+ * @param t The GPUTest
+ * @param testcase the LayoutCase
+ * @param wgsl The shader code
+ * * (0, 0) - a read-only storage buffer
+ * * (0, 1) - a read-write storage buffer
+ * @param offset The offset for the view call
+ */
+export function runWriteLayoutTest(t: GPUTest, testcase: LayoutCase, wgsl: string, offset: number) {
+ const inputBuffer = t.makeBufferWithContents(
+ new Uint32Array([42]),
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.STORAGE
+ );
+
+ const outputBuffer = t.createBufferTracked({
+ size: 256 * 4,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: inputBuffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ // Magic number is 42 in various representations.
+ const outMagicNumber = testcase.f16 ? 0x5140 : testcase.f32 ? 0x42280000 : 42;
+ const expect = new Uint32Array([
+ ...iterRange(128, x => {
+ if (x * 4 === testcase.offset + offset) {
+ return outMagicNumber;
+ } else {
+ return 0;
+ }
+ }),
+ ]);
+ t.expectGPUBufferValuesEqual(outputBuffer, expect);
+}
+
+/**
+ * Performs a type-based read or write test for bufferView and bufferArrayView
+ *
+ * Note: This only covers types with no padding. Padding is covered by the layout tests.
+ * @param read Test read if true
+ * @param t The GPUTest
+ * @param wgsl The shader code
+ * @param eleTy The base element type
+ * @param ty The test type
+ * @param aspace The address space to test
+ * @param offset The offset for the call
+ * @param bufferSize The test buffer size
+ */
+export function runReadWriteTest(
+ read: boolean,
+ t: GPUTest,
+ wgsl: string,
+ eleTy: ScalarType,
+ ty: ScalarType | VectorType | MatrixType | ArrayType,
+ aspace: string,
+ offset: number,
+ bufferSize: number
+) {
+ let num_eles = 1;
+ if (ty instanceof VectorType) {
+ num_eles *= ty.width;
+ } else if (ty instanceof ArrayType) {
+ num_eles *= ty.count;
+ } else if (ty instanceof MatrixType) {
+ num_eles *= ty.rows * ty.cols;
+ }
+
+ const bufferElements = bufferSize / eleTy.size;
+
+ const start = offset / eleTy.size;
+ const end = start + num_eles;
+ const ins: number[] = [];
+ const outs: number[] = [];
+ for (let i = 0; i < bufferElements; i++) {
+ if (i >= start && i < end) {
+ ins.push(i + 4);
+ outs.push(i + 4);
+ } else {
+ if (read) {
+ ins.push(0);
+ } else {
+ outs.push(0);
+ }
+ }
+ }
+ if (!read && eleTy === Type.f16 && ins.length % 2 === 1) {
+ ins.push(0);
+ }
+ if (read && eleTy === Type.f16 && outs.length % 2 === 1) {
+ outs.push(0);
+ }
+
+ const inMap = {
+ u32: typedArrayParam('Uint32Array', ins),
+ i32: typedArrayParam('Int32Array', ins),
+ f32: typedArrayParam('Float32Array', ins),
+ f16: typedArrayParam('Float16Array', ins),
+ };
+ const inputData: TypedArrayBufferView = typedArrayFromParam(
+ inMap[eleTy.toString() as 'u32' | 'i32' | 'f32' | 'f16']
+ );
+
+ const outMap = {
+ u32: typedArrayParam('Uint32Array', outs),
+ i32: typedArrayParam('Int32Array', outs),
+ f32: typedArrayParam('Float32Array', outs),
+ f16: typedArrayParam('Float16Array', outs),
+ };
+ const outputData: TypedArrayBufferView = typedArrayFromParam(
+ outMap[eleTy.toString() as 'u32' | 'i32' | 'f32' | 'f16']
+ );
+
+ const inputBuffer = t.makeBufferWithContents(
+ inputData,
+ GPUBufferUsage.COPY_SRC |
+ (aspace === 'uniform' ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE)
+ );
+ const outputBuffer = t.createBufferTracked({
+ size: outs.length * eleTy.size,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
+ });
+
+ const pipeline = t.device.createComputePipeline({
+ layout: 'auto',
+ compute: {
+ module: t.device.createShaderModule({ code: wgsl }),
+ },
+ });
+
+ const bg = t.device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: inputBuffer,
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: outputBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = t.device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bg);
+ pass.dispatchWorkgroups(1, 1, 1);
+ pass.end();
+ t.queue.submit([encoder.finish()]);
+
+ t.expectGPUBufferValuesEqual(outputBuffer, outputData);
+}
diff --git a/src/webgpu/shader/validation/decl/let.spec.ts b/src/webgpu/shader/validation/decl/let.spec.ts
index 3c1ec06..58a86ef 100644
--- a/src/webgpu/shader/validation/decl/let.spec.ts
+++ b/src/webgpu/shader/validation/decl/let.spec.ts
@@ -10,7 +10,7 @@
interface Case {
code: string;
- valid: boolean | 'texture_and_sampler_let';
+ valid: boolean | 'texture_and_sampler_let' | 'buffer_view';
decls?: string;
}
@@ -109,6 +109,36 @@
valid: 'texture_and_sampler_let',
decls: `@group(0) @binding(0) var samp_comp : sampler_comparison;`,
},
+ buffer: {
+ code: `let x : buffer = b;`,
+ valid: false,
+ decls: `@group(0) @binding(0) var<storage> b : buffer;`,
+ },
+ buffer_sized: {
+ code: `let x : buffer<128> = b;`,
+ valid: false,
+ decls: `@group(0) @binding(0) var<uniform> b : buffer<128>;`,
+ },
+ buffer_override_sized: {
+ code: `let x : buffer<o> = b;`,
+ valid: false,
+ decls: `override o = 16u; var<workgroup> b : buffer<o>;`,
+ },
+ ptr_buffer: {
+ code: `let x : ptr<storage, buffer> = &b;`,
+ valid: 'buffer_view',
+ decls: `@group(0) @binding(0) var<storage> b : buffer;`,
+ },
+ ptr_buffer_sized: {
+ code: `let x : ptr<uniform, buffer<128>> = &b;`,
+ valid: 'buffer_view',
+ decls: `@group(0) @binding(0) var<uniform> b : buffer<128>;`,
+ },
+ ptr_buffer_override_sized: {
+ code: `let x : ptr<workgroup, buffer<o>> = &b;`,
+ valid: 'buffer_view',
+ decls: `override o = 16u; var<workgroup> b : buffer<o>;`,
+ },
};
g.test('type')
@@ -129,6 +159,8 @@
let expect: boolean = testcase.valid === true;
if (testcase.valid === 'texture_and_sampler_let') {
expect = t.hasLanguageFeature('texture_and_sampler_let');
+ } else if (testcase.valid === 'buffer_view') {
+ expect = t.hasLanguageFeature('buffer_view');
}
t.expectCompileResult(expect, code);
});
@@ -178,6 +210,21 @@
code: `var x = 1;\nlet y = x << 1;`,
valid: true,
},
+ buffer_mismatch: {
+ code: `let x : ptr<uniform, buffer> = &b;`,
+ valid: false,
+ decls: `@group(0) @binding(0) var<uniform> b : buffer<128>;`,
+ },
+ buffer_smaller_size: {
+ code: `let x : ptr<uniform, buffer<64>> = &b;`,
+ valid: false,
+ decls: `@group(0) @binding(0) var<uniform> b : buffer<128>;`,
+ },
+ buffer_mismatched_overrides: {
+ code: `let p : ptr<workgroup, buffer<o1>> = &b;`,
+ valid: false,
+ decls: `override o1 : u32;\noverride o2 : u32;\nvar<workgroup> b : buffer<o2>;`,
+ },
};
g.test('initializer')
diff --git a/src/webgpu/shader/validation/decl/var.spec.ts b/src/webgpu/shader/validation/decl/var.spec.ts
index 5d5c091..7f23f3d 100644
--- a/src/webgpu/shader/validation/decl/var.spec.ts
+++ b/src/webgpu/shader/validation/decl/var.spec.ts
@@ -29,30 +29,35 @@
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
i32: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
u32: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
f32: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
f16: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: true,
+ requiresBufferView: false,
},
// Vectors.
@@ -61,30 +66,35 @@
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
vec3i: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
vec4u: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
vec2f: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
vec3h: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: true,
+ requiresBufferView: false,
},
// Matrices.
@@ -93,12 +103,14 @@
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
mat3x4h: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: true,
+ requiresBufferView: false,
},
// Atomics.
@@ -107,12 +119,14 @@
isConstructible: false,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
'atomic<u32>': {
isHostShareable: true,
isConstructible: false,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
// Arrays.
@@ -121,36 +135,42 @@
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
},
'array<vec4<bool>, 4>': {
isHostShareable: false,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
'array<vec4u>': {
isHostShareable: true,
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
},
'array<vec4u, 4>': {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
'array<vec4u, array_size_const>': {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
'array<vec4u, array_size_override>': {
isHostShareable: false,
isConstructible: false,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
// Structures.
@@ -159,36 +179,42 @@
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
S_bool: {
isHostShareable: false,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
S_S_bool: {
isHostShareable: false,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
S_array_vec4u: {
isHostShareable: true,
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
},
S_array_vec4u_4: {
isHostShareable: true,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
S_array_bool_4: {
isHostShareable: false,
isConstructible: true,
isFixedFootprint: true,
requiresF16: false,
+ requiresBufferView: false,
},
// Misc.
@@ -197,18 +223,44 @@
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
},
sampler: {
isHostShareable: false,
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
},
'texture_2d<f32>': {
isHostShareable: false,
isConstructible: false,
isFixedFootprint: false,
requiresF16: false,
+ requiresBufferView: false,
+ },
+
+ // Buffers
+ buffer: {
+ isHostShareable: true,
+ isConstructible: false,
+ isFixedFootprint: false,
+ requiresF16: false,
+ requiresBufferView: true,
+ },
+ 'buffer<128>': {
+ isHostShareable: true,
+ isConstructible: false,
+ isFixedFootprint: true,
+ requiresF16: false,
+ requiresBufferView: true,
+ },
+ 'buffer<array_size_override>': {
+ isHostShareable: false,
+ isConstructible: false,
+ isFixedFootprint: true,
+ requiresF16: false,
+ requiresBufferView: true,
},
};
@@ -288,7 +340,8 @@
break;
case 'uniform':
decl = '@group(0) @binding(0) var<uniform> foo : ';
- shouldPass = type.isHostShareable && type.isConstructible;
+ shouldPass =
+ type.isHostShareable && (type.isConstructible || t.params.type === 'buffer<128>');
break;
case 'workgroup':
decl = 'var<workgroup> foo : ';
@@ -319,6 +372,7 @@
${decl} ${t.params.via_alias ? 'MyType' : t.params.type};
`;
+ shouldPass &&= !kTypes[t.params.type].requiresBufferView || t.hasLanguageFeature('buffer_view');
t.expectCompileResult(shouldPass, wgsl);
});
@@ -368,6 +422,7 @@
${decl} ${t.params.via_alias ? 'MyType' : t.params.type};
}`;
+ shouldPass &&= !kTypes[t.params.type].requiresBufferView || t.hasLanguageFeature('buffer_view');
t.expectCompileResult(shouldPass, wgsl);
});
diff --git a/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts b/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts
index 59b26b5..879a08b 100644
--- a/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts
+++ b/src/webgpu/shader/validation/expression/call/builtin/arrayLength.spec.ts
@@ -25,6 +25,20 @@
t.expectCompileResult(false, code);
});
+g.test('buffer_type')
+ .desc('Validates buffers are rejected')
+ .params(u => u.combine('sized', [false, true] as const))
+ .fn(t => {
+ const code = `
+@group(0) @binding(0) var<storage> b : buffer${t.params.sized ? '<1024>' : ''};
+@compute @workgroup_size(1)
+fn main() {
+ _ = arrayLength(&b);
+}`;
+
+ t.expectCompileResult(false, code);
+ });
+
const atomic_types = ['u32', 'i32'].map(j => `atomic<${j}>`);
const vec_types = [2, 3, 4]
.map(i => ['i32', 'u32', 'f32', 'f16'].map(j => `vec${i}<${j}>`))
diff --git a/src/webgpu/shader/validation/expression/call/builtin/bufferArrayView.spec.ts b/src/webgpu/shader/validation/expression/call/builtin/bufferArrayView.spec.ts
new file mode 100644
index 0000000..aa6ee20
--- /dev/null
+++ b/src/webgpu/shader/validation/expression/call/builtin/bufferArrayView.spec.ts
@@ -0,0 +1,540 @@
+export const description = `
+Validation tests for bufferArrayView
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js';
+import { WGSLLanguageFeature } from '../../../../../capability_info.js';
+import { Type, elementTypeOf, kAllScalarsAndVectors } from '../../../../../util/conversion.js';
+import { ShaderValidationTest } from '../../../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('must_use')
+ .desc('Tests that the builtin has the @must_use attribute')
+ .params(u => u.combine('must_use', [true, false] as const))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const wgsl = `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ ${t.params.must_use ? 'let p = ' : ''}bufferArrayView<array<u32>>(&v, 0, 64);
+}`;
+
+ t.expectCompileResult(t.params.must_use, wgsl);
+ });
+
+const kTypes = objectsToRecord(kAllScalarsAndVectors);
+
+g.test('return_store_type')
+ .desc('Validates return type')
+ .params(u =>
+ u
+ .combine('template_type', keysOf(kTypes))
+ .filter(t => {
+ const type = kTypes[t.template_type];
+ const eleType = elementTypeOf(type);
+ return (
+ eleType !== Type.abstractInt && eleType !== Type.abstractFloat && eleType !== Type.bool
+ );
+ })
+ .combine('return_type', keysOf(kTypes))
+ .filter(t => {
+ const type = kTypes[t.return_type];
+ const eleType = elementTypeOf(type);
+ return (
+ eleType !== Type.abstractInt && eleType !== Type.abstractFloat && eleType !== Type.bool
+ );
+ })
+ .beginSubcases()
+ .combine('ptr', [false, true] as const)
+ .combine('array', [false, true] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const template_type = kTypes[t.params.template_type];
+ const return_type = kTypes[t.params.return_type];
+ let ret_type = return_type.toString();
+ let temp_type = template_type.toString();
+ if (t.params.array) {
+ ret_type = `array<${return_type.toString()}>`;
+ temp_type = `array<${temp_type}>`;
+ }
+ if (t.params.ptr) {
+ ret_type = `ptr<storage, ${ret_type}>`;
+ }
+ let enables = ``;
+ if (template_type.requiresF16() || return_type.requiresF16()) {
+ enables = `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let res : ${ret_type} = bufferArrayView<${temp_type}>(&v, 0, 128);
+}`;
+
+ t.expectCompileResult(t.params.ptr && t.params.array && template_type === return_type, wgsl);
+ });
+
+g.test('buffer_type')
+ .desc('Validates the buffer parameter type')
+ .params(u =>
+ u
+ .combine('type', [
+ 'unsized_ro_storage',
+ 'unsized_storage',
+ 'sized_ro_storage',
+ 'sized_storage',
+ 'sized_uniform',
+ 'sized_workgroup',
+ 'override_workgroup',
+ ] as const)
+ .beginSubcases()
+ .combine('ptr', [false, true] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const aspace = t.params.type.substr(t.params.type.lastIndexOf('_') + 1);
+ let access = '';
+ switch (t.params.type) {
+ case 'unsized_ro_storage':
+ case 'sized_ro_storage':
+ access = ', read';
+ break;
+ case 'unsized_storage':
+ case 'sized_storage':
+ access = ', read_write';
+ break;
+ default:
+ break;
+ }
+ const wgsl = `
+@group(0) @binding(0) var<storage> unsized_ro_storage : buffer;
+@group(0) @binding(1) var<storage, read_write> unsized_storage : buffer;
+@group(0) @binding(2) var<storage> sized_ro_storage : buffer<128>;
+@group(0) @binding(3) var<storage, read_write> sized_storage : buffer<128>;
+@group(0) @binding(4) var<uniform> sized_uniform : buffer<128>;
+var<workgroup> sized_workgroup : buffer<128>;
+override o : u32;
+var<workgroup> override_workgroup : buffer<o>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let p : ptr<${aspace}, array<u32>${access}> = bufferArrayView<array<u32>>(${
+ t.params.ptr ? '&' : ''
+ }${t.params.type}, 0, 128);
+}`;
+
+ t.expectCompileResult(t.params.ptr, wgsl);
+ });
+
+g.test('offset_type')
+ .desc('Validates the offset parameter type')
+ .params(u => u.combine('type', keysOf(kTypes)))
+ .fn(t => {
+ const type = kTypes[t.params.type];
+ let enables = `enable subgroups;\n`;
+ if (type.requiresF16()) {
+ enables += `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<u32>>(&v, ${type.create(0).wgsl()}, 128);
+}`;
+
+ t.expectCompileResult(
+ type === Type.abstractInt || type === Type.u32 || type === Type.i32,
+ wgsl
+ );
+ });
+
+g.test('size_type')
+ .desc('Validates the offset parameter type')
+ .params(u => u.combine('type', keysOf(kTypes)))
+ .fn(t => {
+ const type = kTypes[t.params.type];
+ let enables = `enable subgroups;\n`;
+ if (type.requiresF16()) {
+ enables += `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferArrayView<array<u32>>(&v, 0, ${type.create(128).wgsl()});
+}`;
+
+ t.expectCompileResult(
+ type === Type.abstractInt || type === Type.u32 || type === Type.i32,
+ wgsl
+ );
+ });
+
+interface EarlyEvalCase {
+ code: string;
+ valid: boolean | 'pipeline';
+ constants?: Record<string, GPUPipelineConstantValue>;
+ ptr_param?: boolean;
+}
+
+const kEarlyEvalCases: Record<string, EarlyEvalCase> = {
+ ro_storage_buffer: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<16>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 16);
+}`,
+ valid: true,
+ },
+ storage_buffer: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer<16>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 16);
+}`,
+ valid: true,
+ },
+ uniform_buffer: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<16>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 16);
+}`,
+ valid: true,
+ },
+ workgroup_buffer: {
+ code: `
+var<workgroup> v : buffer<16>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 16);
+}`,
+ valid: true,
+ },
+ const_size_too_small_for_type: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 12);
+}`,
+ valid: false,
+ },
+ buffer_too_small_for_const_size: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<12>;
+fn foo() {
+ let p = bufferArrayView<array<u32>>(&v, 0, 16);
+}`,
+ valid: false,
+ },
+ buffer_too_small_for_type: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<12>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, 16);
+}`,
+ valid: false,
+ },
+ const_size_too_small_for_const_offset_and_type: {
+ code: `
+var<workgroup> v : buffer<128>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 16, 30);
+}`,
+ valid: false,
+ },
+ buffer_size_too_small_for_const_offset_and_type: {
+ code: `
+var<workgroup> v : buffer<30>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 16, 128);
+}`,
+ valid: false,
+ },
+ override_size_too_small_for_type: {
+ code: `
+override size : u32;
+@group(0) @binding(0) var<storage> v : buffer;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, size);
+}`,
+ valid: 'pipeline',
+ constants: { size: 12 },
+ },
+ buffer_too_small_for_override_size: {
+ code: `
+override buffer_size : u32;
+override size : u32;
+var<workgroup> v : buffer<buffer_size>;
+fn foo() {
+ let p = bufferArrayView<array<u32>>(&v, 0, size);
+}`,
+ valid: 'pipeline',
+ constants: { buffer_size: 12, size: 16 },
+ },
+ override_buffer_too_small_for_type: {
+ code: `
+override buffer_size : u32;
+override size : u32;
+var<workgroup> v : buffer<buffer_size>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, 0, size);
+}`,
+ valid: 'pipeline',
+ constants: { buffer_size: 12, size: 16 },
+ },
+ override_size_too_small_for_override_offset_and_type: {
+ code: `
+override offset : u32;
+override size : u32;
+@group(0) @binding(0) var<uniform> v : buffer<128>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, offset, size);
+}`,
+ valid: 'pipeline',
+ constants: { offset: 16, size: 30 },
+ },
+ buffer_size_too_small_for_override_offset_and_type: {
+ code: `
+override buffer_size : u32;
+override offset : u32;
+var<workgroup> v : buffer<buffer_size>;
+fn foo() {
+ let p = bufferArrayView<array<vec4u>>(&v, offset, 128);
+}`,
+ valid: 'pipeline',
+ constants: { buffer_size: 30, offset: 16 },
+ },
+ const_offset_not_aligned: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+fn foo() {
+ let p = bufferarrayview<array<vec4u>>(&v, 12, 128);
+}`,
+ valid: false,
+ },
+ override_offset_not_aligned: {
+ code: `
+override offset : u32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+fn foo() {
+ let p = bufferarrayview<array<vec4u>>(&v, offset, 128);
+}`,
+ valid: false,
+ constants: { offset: 12 },
+ },
+ const_offset_plus_type_size_out_of_range: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+struct S {
+ a: vec4u,
+ b: vec4f,
+}
+fn foo() {
+ let p = buffer<array<S>>(&v, 4294967279, 128);
+}`,
+ valid: false,
+ },
+ override_offset_plus_type_size_out_of_range: {
+ code: `
+override offset : u32;
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+struct S {
+ a: vec4u,
+ b: vec4f,
+}
+fn foo() {
+ let p = buffer<array<S>>(&v, offset, 128);
+}`,
+ valid: false,
+ constants: { offset: 4294967279 },
+ },
+ const_multiple_of_stride: {
+ code: `
+struct S {
+ a : vec4u,
+ b : array<vec2u>,
+}
+var<workgroup> v : buffer<256>;
+fn foo() {
+ let p = bufferArrayView<S>(&v, 0, 24);
+}`,
+ valid: true,
+ },
+ const_not_multiple_of_stride: {
+ code: `
+struct S {
+ a : vec4u,
+ b : array<vec2u>,
+}
+var<workgroup> v : buffer<256>;
+fn foo() {
+ let p = bufferArrayView<S>(&v, 0, 26);
+}`,
+ valid: false,
+ },
+ override_not_multiple_of_stride: {
+ code: `
+override size : u32;
+struct S {
+ a : vec4u,
+ b : array<vec2u>,
+}
+var<workgroup> v : buffer<256>;
+fn foo() {
+ let p = bufferArrayView<S>(&v, 0, size);
+}`,
+ valid: 'pipeline',
+ constants: { size: 26 },
+ },
+ const_buffer_too_small_through_unsized_function: {
+ code: `
+@group(0) @binding(0) var<storage> buffer<16>;
+fn bar(p : ptr<storage, buffer>) {
+ let q = bufferArrayView<array<u32>>(p, 0, 32);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: false,
+ ptr_param: true,
+ },
+ override_buffer_too_small_through_unsized_function: {
+ code: `
+override size : u32;
+@group(0) @binding(0) var<storage> v : buffer<16>;
+fn bar(p : ptr<storage, buffer>) {
+ let q = bufferArrayView<array<u32>>(p, 0, size);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: 'pipeline',
+ constants: { size: 32 },
+ ptr_param: true,
+ },
+ sized_buffer_to_smaller_buffer_too_small: {
+ code: `
+var<workgroup> v : buffer<256>;
+fn bar(p : ptr<workgroup, buffer<16>>) {
+ let q = bufferArrayView<array<u32>>(q, 16, 4);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: false,
+ ptr_param: true,
+ },
+};
+
+g.test('early_eval_errors')
+ .desc('Test shader-creation and pipeline-creation errors')
+ .params(u => u.combine('case', keysOf(kEarlyEvalCases)))
+ .fn(t => {
+ const testcase = kEarlyEvalCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (testcase.ptr_param === true) {
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ }
+
+ t.expectCompileResult(testcase.valid !== false, testcase.code);
+ if (testcase.valid !== false) {
+ t.expectPipelineResult({
+ expectedResult: testcase.valid === true,
+ code: testcase.code,
+ constants: testcase.constants,
+ statements: ['foo();'],
+ });
+ }
+ });
+
+interface LHSCallCase {
+ code: string;
+ requires?: WGSLLanguageFeature[];
+}
+
+const kLHSCallCases: Record<string, LHSCallCase> = {
+ u32_array: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferArrayView<array<u32>>(&v, 0, 128))[0] = 123u;
+}`,
+ },
+ f32_vector_letter: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferArrayView<array<vec4f>>(&v, 0, 128))[0].x = 13.3f;
+}`,
+ },
+ f32_struct_access: {
+ code: `
+struct S {
+ member_a: f32,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferArrayView<array<S>>(&v, 0, 128))[0].member_a = 13.3f;
+}`,
+ },
+ u32_array_ptr_composite: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bufferArrayView<array<u32>>(&v, 0, 128)[0] = 123u;
+}`,
+ requires: ['pointer_composite_access'],
+ },
+ vec4u_swizzle_assign: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferArrayView<array<vec4u>>(&v, 0, 128))[0].zxy = vec3u(42);
+}`,
+ requires: ['swizzle_assignment'],
+ },
+ vec4u_ptr_composite_swizzle_assign: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bufferArrayView<array<vec4u>>(&v, 0, 128)[0].zxy = vec3u(42);
+}`,
+ requires: ['pointer_composite_access', 'swizzle_assignment'],
+ },
+ compound_assign: {
+ code: `
+var<workgroup> v : buffer<256>;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferArrayView<array<u32>>(&v, 0, 128))[0] += 123u;
+}`,
+ },
+};
+
+g.test('lhs_call')
+ .desc('Validate that bufferView can be on the LHS of an assignment')
+ .params(u => u.combine('case', keysOf(kLHSCallCases)))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+
+ const testcase = kLHSCallCases[t.params.case];
+ let valid = true;
+ const features = testcase.requires ?? [];
+ features.forEach(f => {
+ valid &&= t.hasLanguageFeature(f);
+ });
+ t.expectCompileResult(valid, testcase.code);
+ });
diff --git a/src/webgpu/shader/validation/expression/call/builtin/bufferLength.spec.ts b/src/webgpu/shader/validation/expression/call/builtin/bufferLength.spec.ts
new file mode 100644
index 0000000..be3a3c6
--- /dev/null
+++ b/src/webgpu/shader/validation/expression/call/builtin/bufferLength.spec.ts
@@ -0,0 +1,115 @@
+export const description = `
+Validation tests for bufferLength
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js';
+import { Type, elementTypeOf, kAllScalarsAndVectors } from '../../../../../util/conversion.js';
+import { ShaderValidationTest } from '../../../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('must_use')
+ .desc('Tests that the builtin has the @must_use attribute')
+ .params(u => u.combine('must_use', [true, false] as const))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const wgsl = `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ ${t.params.must_use ? '_ = ' : ''}bufferLength(&v);
+}`;
+
+ t.expectCompileResult(t.params.must_use, wgsl);
+ });
+
+const kTypes = objectsToRecord(kAllScalarsAndVectors);
+
+g.test('return_type')
+ .desc('Validates return type')
+ .params(u =>
+ u.combine('type', keysOf(kTypes)).filter(t => {
+ const type = kTypes[t.type];
+ const eleType = elementTypeOf(type);
+ return eleType !== Type.abstractInt && eleType !== Type.abstractFloat;
+ })
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const type = kTypes[t.params.type];
+ let enables = ``;
+ if (type.requiresF16()) {
+ enables = `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let res : ${type.toString()} = bufferLength(&v);
+}`;
+
+ t.expectCompileResult(type === Type.u32, wgsl);
+ });
+
+g.test('data_type')
+ .desc('Validates the input parameter type')
+ .params(u =>
+ u
+ .combine('type', [
+ 'unsized_ro_storage',
+ 'unsized_storage',
+ 'sized_ro_storage',
+ 'sized_storage',
+ 'sized_uniform',
+ 'sized_workgroup',
+ 'override_workgroup',
+ ] as const)
+ .beginSubcases()
+ .combine('ptr', [false, true] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const wgsl = `
+@group(0) @binding(0) var<storage> unsized_ro_storage : buffer;
+@group(0) @binding(1) var<storage, read_write> unsized_storage : buffer;
+@group(0) @binding(2) var<storage> sized_ro_storage : buffer<128>;
+@group(0) @binding(3) var<storage, read_write> sized_storage : buffer<128>;
+@group(0) @binding(4) var<uniform> sized_uniform : buffer<128>;
+var<workgroup> sized_workgroup : buffer<128>;
+override o : u32;
+var<workgroup> override_workgroup : buffer<o>;
+
+@compute @workgroup_size(1)
+fn main() {
+ _ = bufferLength(${t.params.ptr ? '&' : ''}${t.params.type});
+}`;
+
+ t.expectCompileResult(t.params.ptr, wgsl);
+ });
+
+const kInvalidCases = {
+ baseline: `&b`,
+ no_args: `bufferLength()`,
+ two_args: `bufferLength(&b, 0)`,
+ scalar: `bufferLength(0u)`,
+ ptr_to_scalar: `bufferLength(&v[0])`,
+ ptr_to_array: `bufferLength(&v)`,
+};
+
+g.test('invalid_cases')
+ .desc('Test a smattering of invalid bufferLength calls')
+ .params(u => u.combine('test', keysOf(kInvalidCases)))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const code = `
+var<workgroup> v : array<u32, 4>;
+var<workgroup> b : buffer<128>;
+@compute @workgroup_size(1)
+fn main() {
+ _ = ${kInvalidCases[t.params.test]};
+}`;
+
+ t.expectCompileResult(t.params.test === 'baseline', code);
+ });
diff --git a/src/webgpu/shader/validation/expression/call/builtin/bufferView.spec.ts b/src/webgpu/shader/validation/expression/call/builtin/bufferView.spec.ts
new file mode 100644
index 0000000..50dbf12
--- /dev/null
+++ b/src/webgpu/shader/validation/expression/call/builtin/bufferView.spec.ts
@@ -0,0 +1,451 @@
+export const description = `
+Validation tests for bufferView
+`;
+
+import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
+import { keysOf, objectsToRecord } from '../../../../../../common/util/data_tables.js';
+import { WGSLLanguageFeature } from '../../../../../capability_info.js';
+import { Type, elementTypeOf, kAllScalarsAndVectors } from '../../../../../util/conversion.js';
+import { ShaderValidationTest } from '../../../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+g.test('must_use')
+ .desc('Tests that the builtin has the @must_use attribute')
+ .params(u => u.combine('must_use', [true, false] as const))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const wgsl = `
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ ${t.params.must_use ? 'let p = ' : ''}bufferView<u32>(&v, 0);
+}`;
+
+ t.expectCompileResult(t.params.must_use, wgsl);
+ });
+
+const kTypes = objectsToRecord(kAllScalarsAndVectors);
+
+g.test('return_store_type')
+ .desc('Validates return type')
+ .params(u =>
+ u
+ .combine('template_type', keysOf(kTypes))
+ .filter(t => {
+ const type = kTypes[t.template_type];
+ const eleType = elementTypeOf(type);
+ return (
+ eleType !== Type.abstractInt && eleType !== Type.abstractFloat && eleType !== Type.bool
+ );
+ })
+ .combine('return_type', keysOf(kTypes))
+ .filter(t => {
+ const type = kTypes[t.return_type];
+ const eleType = elementTypeOf(type);
+ return (
+ eleType !== Type.abstractInt && eleType !== Type.abstractFloat && eleType !== Type.bool
+ );
+ })
+ .beginSubcases()
+ .combine('ptr', [false, true] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const template_type = kTypes[t.params.template_type];
+ const return_type = kTypes[t.params.return_type];
+ let ret_type = return_type.toString();
+ if (t.params.ptr) {
+ ret_type = `ptr<storage, ${ret_type}>`;
+ }
+ let enables = ``;
+ if (template_type.requiresF16() || return_type.requiresF16()) {
+ enables = `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let res : ${ret_type} = bufferView<${template_type.toString()}>(&v, 0);
+}`;
+
+ t.expectCompileResult(t.params.ptr && template_type === return_type, wgsl);
+ });
+
+g.test('buffer_type')
+ .desc('Validates the buffer parameter type')
+ .params(u =>
+ u
+ .combine('type', [
+ 'unsized_ro_storage',
+ 'unsized_storage',
+ 'sized_ro_storage',
+ 'sized_storage',
+ 'sized_uniform',
+ 'sized_workgroup',
+ 'override_workgroup',
+ ] as const)
+ .beginSubcases()
+ .combine('ptr', [false, true] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const aspace = t.params.type.substr(t.params.type.lastIndexOf('_') + 1);
+ let access = '';
+ switch (t.params.type) {
+ case 'unsized_ro_storage':
+ case 'sized_ro_storage':
+ access = ', read';
+ break;
+ case 'unsized_storage':
+ case 'sized_storage':
+ access = ', read_write';
+ break;
+ default:
+ break;
+ }
+ const wgsl = `
+@group(0) @binding(0) var<storage> unsized_ro_storage : buffer;
+@group(0) @binding(1) var<storage, read_write> unsized_storage : buffer;
+@group(0) @binding(2) var<storage> sized_ro_storage : buffer<128>;
+@group(0) @binding(3) var<storage, read_write> sized_storage : buffer<128>;
+@group(0) @binding(4) var<uniform> sized_uniform : buffer<128>;
+var<workgroup> sized_workgroup : buffer<128>;
+override o : u32;
+var<workgroup> override_workgroup : buffer<o>;
+
+@compute @workgroup_size(1)
+fn main() {
+ let p : ptr<${aspace}, u32${access}> = bufferView<u32>(${t.params.ptr ? '&' : ''}${
+ t.params.type
+ }, 0);
+}`;
+
+ t.expectCompileResult(t.params.ptr, wgsl);
+ });
+
+g.test('offset_type')
+ .desc('Validates the offset parameter type')
+ .params(u => u.combine('type', keysOf(kTypes)))
+ .fn(t => {
+ const type = kTypes[t.params.type];
+ let enables = `enable subgroups;\n`;
+ if (type.requiresF16()) {
+ enables += `enable f16;`;
+ }
+ const wgsl = `
+${enables}
+@group(0) @binding(0) var<storage> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ let p = bufferView<u32>(&v, ${type.create(0).wgsl()});
+}`;
+
+ t.expectCompileResult(
+ type === Type.abstractInt || type === Type.u32 || type === Type.i32,
+ wgsl
+ );
+ });
+
+interface EarlyEvalCase {
+ code: string;
+ valid: boolean | 'pipeline';
+ constants?: Record<string, GPUPipelineConstantValue>;
+ ptr_param?: boolean;
+}
+
+const kEarlyEvalCases: Record<string, EarlyEvalCase> = {
+ ro_storage_buffer: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<16>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: true,
+ },
+ storage_buffer: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer<16>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: true,
+ },
+ uniform_buffer: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<16>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: true,
+ },
+ workgroup_buffer: {
+ code: `
+var<workgroup> v : buffer<16>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: true,
+ },
+ ro_storage_buffer_too_small_for_type: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer<8>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: false,
+ },
+ uniform_buffer_too_small_with_const_offset: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<24>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 16);
+}`,
+ valid: false,
+ },
+ uniform_buffer_too_small_with_override_offset: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<24>;
+override offset : u32;
+fn foo() {
+ let p = bufferView<vec4u>(&v, offset);
+}`,
+ valid: 'pipeline',
+ constants: { offset: 16 },
+ },
+ workgroup_buffer_too_small_with_override: {
+ code: `
+override size : u32;
+var<workgroup> v : buffer<size>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 0);
+}`,
+ valid: 'pipeline',
+ constants: { size: 12 },
+ },
+ storage_buffer_too_small_for_one_array_element: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer<28>;
+struct S {
+ a: vec4u,
+ b: vec4f,
+}
+fn foo() {
+ let p = bufferView<array<S>>(&v, 0);
+}`,
+ valid: false,
+ },
+ storage_buffer_out_of_range_u32: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+struct S {
+ a: vec4u,
+ b: vec4f,
+}
+fn foo() {
+ let p = buffer<S>(&v, 4294967279);
+}`,
+ valid: false,
+ },
+ uniform_buffer_too_small_through_unsized_function: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<28>;
+fn bar(p : ptr<uniform, buffer>) {
+ let q = bufferView<vec4u>(p, 16);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: false,
+ ptr_param: true,
+ },
+ uniform_buffer_too_small_through_unsized_function_override: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<28>;
+override offset : u32;
+fn bar(p : ptr<uniform, buffer>) {
+ let q = bufferView<vec4u>(p, offset);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: 'pipeline',
+ constants: { offset: 16 },
+ ptr_param: true,
+ },
+ workgroup_to_smaller_size_param: {
+ code: `
+var<workgroup> v : buffer<128>;
+fn bar(p : ptr<workgroup, buffer<16>) {
+ let q = bufferView<array<vec2u, 3>(p, 0);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: false,
+ ptr_param: true,
+ },
+ ro_storage_unsized_buffer_to_sized_param: {
+ code: `
+@group(0) @binding(0) var<storage> v : buffer;
+fn bar(p : ptr<storage, buffer<16>) {
+ let q = bufferView<array<vec2u, 3>(p, 0);
+}
+fn foo() {
+ bar(&v);
+}`,
+ valid: false,
+ ptr_param: true,
+ },
+ offset_not_aligned: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<128>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, 12);
+}`,
+ valid: false,
+ },
+ offset_not_aligned_override: {
+ code: `
+override offset : u32;
+@group(0) @binding(0) var<uniform> v : buffer<128>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, offset);
+}`,
+ valid: 'pipeline',
+ constants: { offset: 12 },
+ },
+ offset_negative: {
+ code: `
+@group(0) @binding(0) var<uniform> v : buffer<128>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, -16);
+}`,
+ valid: false,
+ },
+ offset_negative_override: {
+ code: `
+override offset : u32;
+@group(0) @binding(0) var<uniform> v : buffer<128>;
+fn foo() {
+ let p = bufferView<vec4u>(&v, offset);
+}`,
+ valid: 'pipeline',
+ constants: { offset: -16 },
+ },
+};
+
+g.test('early_eval_errors')
+ .desc('Test shader-creation and pipeline-creation errors')
+ .params(u => u.combine('case', keysOf(kEarlyEvalCases)))
+ .fn(t => {
+ const testcase = kEarlyEvalCases[t.params.case];
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ if (testcase.ptr_param === true) {
+ t.skipIfLanguageFeatureNotSupported('unrestricted_pointer_parameters');
+ }
+
+ t.expectCompileResult(testcase.valid !== false, testcase.code);
+ if (testcase.valid !== false) {
+ t.expectPipelineResult({
+ expectedResult: testcase.valid === true,
+ code: testcase.code,
+ constants: testcase.constants,
+ statements: ['foo();'],
+ });
+ }
+ });
+
+interface LHSCallCase {
+ code: string;
+ requires?: WGSLLanguageFeature[];
+}
+
+const kLHSCallCases: Record<string, LHSCallCase> = {
+ u32: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ *bufferView<u32>(&v, 0) = 123u;
+}`,
+ },
+ u32_array: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferView<array<u32, 4>>(&v, 0))[0] = 123u;
+}`,
+ },
+ f32_vector_letter: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferView<vec4f>(&v, 0)).x = 13.3f;
+}`,
+ },
+ f32_struct_access: {
+ code: `
+struct S {
+ member_a: f32,
+}
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferView<S>(&v, 0)).member_a = 13.3f;
+}`,
+ },
+ u32_array_ptr_composite: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bufferView<array<u32, 4>>(&v, 0)[0] = 123u;
+}`,
+ requires: ['pointer_composite_access'],
+ },
+ vec4u_swizzle_assign: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ (*bufferView<vec4u>(&v, 0)).zxy = vec3u(42);
+}`,
+ requires: ['swizzle_assignment'],
+ },
+ vec4u_ptr_composite_swizzle_assign: {
+ code: `
+@group(0) @binding(0) var<storage, read_write> v : buffer;
+@compute @workgroup_size(1)
+fn main() {
+ bufferView<vec4u>(&v, 0).zxy = vec3u(42);
+}`,
+ requires: ['pointer_composite_access', 'swizzle_assignment'],
+ },
+ compound_assign: {
+ code: `
+var<workgroup> v : buffer<256>;
+@compute @workgroup_size(1)
+fn main() {
+ *bufferView<u32>(&v, 0) += 123u;
+}`,
+ },
+};
+
+g.test('lhs_call')
+ .desc('Validate that bufferView can be on the LHS of an assignment')
+ .params(u => u.combine('case', keysOf(kLHSCallCases)))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+
+ const testcase = kLHSCallCases[t.params.case];
+ let valid = true;
+ const features = testcase.requires ?? [];
+ features.forEach(f => {
+ valid &&= t.hasLanguageFeature(f);
+ });
+ t.expectCompileResult(valid, testcase.code);
+ });
diff --git a/src/webgpu/shader/validation/functions/restrictions.spec.ts b/src/webgpu/shader/validation/functions/restrictions.spec.ts
index 73c3a37..5cec4c3 100644
--- a/src/webgpu/shader/validation/functions/restrictions.spec.ts
+++ b/src/webgpu/shader/validation/functions/restrictions.spec.ts
@@ -202,7 +202,7 @@
interface ParamTypeCase {
name: string;
- valid: boolean | 'with_unrestricted_pointer_parameters';
+ valid: boolean | 'with_unrestricted_pointer_parameters' | 'with_buffer_view';
}
const kFunctionParamTypeCases: Record<string, ParamTypeCase> = {
@@ -304,6 +304,68 @@
invalid_ptr6: { name: `ptr<private,u32,read_write>`, valid: false }, // Can't specify access mode
invalid_ptr7: { name: `ptr<private,clamp>`, valid: false }, // Invalid store type
invalid_ptr8: { name: `ptr<function, texture_external>`, valid: false }, // non-constructible pointer type
+
+ // Buffers (need buffer_view and unrestricted_pointer_parameters)
+ ptrBufferUnsized_storage: {
+ name: `ptr<storage, buffer, read_write>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSized_storage: {
+ name: `ptr<storage, buffer<128>, read_write>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSizedSmall_storage: {
+ name: `ptr<storage, buffer<64>, read_write>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferUnsized_ro_storage: {
+ name: `ptr<storage, buffer>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSized_ro_storage: {
+ name: `ptr<storage, buffer<128>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSizedSmall_ro_storage: {
+ name: `ptr<storage, buffer<64>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferUnsized_uniform: {
+ name: `ptr<uniform, buffer>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSized_uniform: {
+ name: `ptr<uniform, buffer<128>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSizedSmall_uniform: {
+ name: `ptr<uniform, buffer<64>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferUnsized_workgroup: {
+ name: `ptr<workgroup, buffer>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSized_workgroup: {
+ name: `ptr<workgroup, buffer<128>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferSizedSmall_workgroup: {
+ name: `ptr<workgroup, buffer<64>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferOverrideNoDefault_workgroup: {
+ name: `ptr<workgroup, buffer<override_no_default>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferOverrideDefault_workgroup: {
+ name: `ptr<workgroup, buffer<override_default>>`,
+ valid: 'with_buffer_view',
+ },
+ ptrBufferOverrideExpr_workgroup: {
+ name: `ptr<workgroup, buffer<override_expr>>`,
+ valid: 'with_buffer_view',
+ },
};
g.test('function_parameter_types')
@@ -324,6 +386,10 @@
let isValid = testcase.valid;
if (isValid === 'with_unrestricted_pointer_parameters') {
isValid = t.hasLanguageFeature('unrestricted_pointer_parameters');
+ } else if (isValid === 'with_buffer_view') {
+ isValid =
+ t.hasLanguageFeature('unrestricted_pointer_parameters') &&
+ t.hasLanguageFeature('buffer_view');
}
t.expectCompileResult(isValid, code);
@@ -333,6 +399,7 @@
value: string;
matches: string[];
needsUnrestrictedPointerParameters?: boolean;
+ needsBufferView?: boolean;
}
const kFunctionParamValueCases: Record<string, ParamValueCase> = {
@@ -519,6 +586,81 @@
matches: ['ptrWorkgroupOverrideExpr'],
needsUnrestrictedPointerParameters: true,
},
+
+ // Buffer view
+ ptrStorageBufferUnsized: {
+ value: `&storage_buffer_unsized`,
+ matches: ['ptrBufferUnsized_storage'],
+ needsBufferView: true,
+ },
+ ptrStorageBufferSized: {
+ value: `&storage_buffer_sized`,
+ matches: ['ptrBufferUnsized_storage', 'ptrBufferSized_storage', 'ptrBufferSizedSmall_storage'],
+ needsBufferView: true,
+ },
+ ptrStorageBufferSizedSmall: {
+ value: `&storage_buffer_sized_small`,
+ matches: ['ptrBufferUnsized_storage', 'ptrBufferSizedSmall_storage'],
+ needsBufferView: true,
+ },
+ ptrROStorageBufferUnsized: {
+ value: `&ro_storage_buffer_unsized`,
+ matches: ['ptrBufferUnsized_ro_storage'],
+ needsBufferView: true,
+ },
+ ptrROStorageBufferSized: {
+ value: `&ro_storage_buffer_sized`,
+ matches: [
+ 'ptrBufferUnsized_ro_storage',
+ 'ptrBufferSized_ro_storage',
+ 'ptrBufferSizedSmall_ro_storage',
+ ],
+ needsBufferView: true,
+ },
+ ptrROStorageBufferSizedSmall: {
+ value: `&ro_storage_buffer_sized_small`,
+ matches: ['ptrBufferUnsized_ro_storage', 'ptrBufferSizedSmall_ro_storage'],
+ needsBufferView: true,
+ },
+ ptrUniformBufferSized: {
+ value: `&uniform_buffer_sized`,
+ matches: ['ptrBufferUnsized_uniform', 'ptrBufferSized_uniform', 'ptrBufferSizedSmall_uniform'],
+ needsBufferView: true,
+ },
+ ptrUniformBufferSizedSmall: {
+ value: `&uniform_buffer_sized_small`,
+ matches: ['ptrBufferUnsized_uniform', 'ptrBufferSizedSmall_uniform'],
+ needsBufferView: true,
+ },
+ ptrWorkgroupBufferSized: {
+ value: `&wg_buffer_sized`,
+ matches: [
+ 'ptrBufferUnsized_workgroup',
+ 'ptrBufferSized_workgroup',
+ 'ptrBufferSizedSmall_workgroup',
+ ],
+ needsBufferView: true,
+ },
+ ptrWorkgroupBufferSizedSmall: {
+ value: `&wg_buffer_sized_small`,
+ matches: ['ptrBufferUnsized_workgroup', 'ptrBufferSizedSmall_workgroup'],
+ needsBufferView: true,
+ },
+ ptrWorkgroupBufferOverrideNoDefault: {
+ value: `&wg_buffer_override_no_default`,
+ matches: ['ptrBufferUnsized_workgroup', 'ptrBufferOverrideNoDefault_workgroup'],
+ needsBufferView: true,
+ },
+ ptrWorkgroupBufferOverrideDefault: {
+ value: `&wg_buffer_override_default`,
+ matches: ['ptrBufferUnsized_workgroup', 'ptrBufferOverrideDefault_workgroup'],
+ needsBufferView: true,
+ },
+ ptrWorkgroupBufferOverrideExpr: {
+ value: `&wg_buffer_override_expr`,
+ matches: ['ptrBufferUnsized_workgroup', 'ptrBufferOverrideExpr_workgroup'],
+ needsBufferView: true,
+ },
};
function parameterMatches(decl: string, matches: string[]): boolean {
@@ -548,6 +690,26 @@
const param = kFunctionParamTypeCases[t.params.decl];
const arg = kFunctionParamValueCases[t.params.arg];
const enable = param.name === 'f16' ? 'enable f16;' : '';
+ let buffer_decls = ``;
+ if (param.valid === 'with_buffer_view') {
+ buffer_decls = `
+@group(2) @binding(0) var<storage, read_write> storage_buffer_unsized : buffer;
+@group(2) @binding(1) var<storage, read_write> storage_buffer_sized : buffer<128>;
+@group(2) @binding(2) var<storage, read_write> storage_buffer_sized_small : buffer<64>;
+@group(2) @binding(3) var<storage> ro_storage_buffer_unsized : buffer;
+@group(2) @binding(4) var<storage> ro_storage_buffer_sized : buffer<128>;
+@group(2) @binding(5) var<storage> ro_storage_buffer_sized_small : buffer<64>;
+@group(2) @binding(6) var<uniform> uniform_buffer_sized : buffer<128>;
+@group(2) @binding(7) var<uniform> uniform_buffer_sized_small : buffer<64>;
+
+var<workgroup> wg_buffer_sized : buffer<128>;
+var<workgroup> wg_buffer_sized_small : buffer<64>;
+var<workgroup> wg_buffer_override_no_default : buffer<override_no_default>;
+var<workgroup> wg_buffer_override_default : buffer<override_default>;
+var<workgroup> wg_buffer_override_expr : buffer<override_expr>;
+`;
+ }
+
const code = `
${enable}
@@ -574,6 +736,8 @@
@group(1) @binding(2)
var<uniform> uniform_host_shareable : host_shareable;
+${buffer_decls}
+
fn bar(param : ${param.name}) { }
var<private> g_u32 : u32;
@@ -642,11 +806,20 @@
(kFunctionParamTypeCases[t.params.decl].valid === 'with_unrestricted_pointer_parameters' ||
arg.needsUnrestrictedPointerParameters) ??
false;
+ const needsBufferView =
+ (kFunctionParamTypeCases[t.params.decl].valid === 'with_buffer_view' ||
+ arg.needsBufferView) ??
+ false;
let isValid = parameterMatches(t.params.decl, arg.matches);
if (isValid && needsUnrestrictedPointerParameters) {
isValid = t.hasLanguageFeature('unrestricted_pointer_parameters');
}
+ if (isValid && needsBufferView) {
+ isValid =
+ t.hasLanguageFeature('unrestricted_pointer_parameters') &&
+ t.hasLanguageFeature('buffer_view');
+ }
t.expectCompileResult(isValid, code);
});
diff --git a/src/webgpu/shader/validation/shader_io/locations.spec.ts b/src/webgpu/shader/validation/shader_io/locations.spec.ts
index 5641b78..c569e28 100644
--- a/src/webgpu/shader/validation/shader_io/locations.spec.ts
+++ b/src/webgpu/shader/validation/shader_io/locations.spec.ts
@@ -95,6 +95,8 @@
'texture_depth_multisampled_2d',
'sampler',
'sampler_comparison',
+ 'buffer',
+ 'buffer<16>',
]);
g.test('stage_inout')
diff --git a/src/webgpu/shader/validation/types/buffer.spec.ts b/src/webgpu/shader/validation/types/buffer.spec.ts
new file mode 100644
index 0000000..ca4be62
--- /dev/null
+++ b/src/webgpu/shader/validation/types/buffer.spec.ts
@@ -0,0 +1,188 @@
+export const description = `
+Validation tests for buffer types
+`;
+
+import { makeTestGroup } from '../../../../common/framework/test_group.js';
+import { keysOf } from '../../../../common/util/data_tables.js';
+import { ShaderValidationTest } from '../shader_validation_test.js';
+
+export const g = makeTestGroup(ShaderValidationTest);
+
+const kParseCases = {
+ unsized: {
+ code: `alias T = buffer;`,
+ valid: true,
+ },
+ literal: {
+ code: `alias T = buffer<16>;`,
+ valid: true,
+ },
+ literal_negative: {
+ code: `alias T = buffer<-1>;`,
+ valid: false,
+ },
+ literal_zero: {
+ code: `alias T = buffer<0>;`,
+ valid: false,
+ },
+ const: {
+ code: `const x = 16; alias T = buffer<x>;`,
+ valid: true,
+ },
+ const_negative: {
+ code: `const x = -1; alias T = buffer<x>;`,
+ valid: false,
+ },
+ const_zero: {
+ code: `const x = 0; alias T = buffer<x>;`,
+ valid: false,
+ },
+ const_function: {
+ code: `const x = 16; const y = 32; alias T = buffer<min(x, y)>;`,
+ valid: true,
+ },
+ override: {
+ code: `override x = 16; alias T = buffer<x>;`,
+ valid: true,
+ },
+ override_negative: {
+ code: `override x = -1; alias T = buffer<x>;`,
+ valid: true,
+ },
+ override_zero: {
+ code: `override x = 0; alias T = buffer<x>;`,
+ valid: true,
+ },
+ override_function: {
+ code: `override x = 16; override y = 32; alias T = buffer<max(x, y)>;`,
+ valid: true,
+ },
+ empty_template: {
+ code: `alias T = buffer<>;`,
+ valid: false,
+ },
+ missing_rparen: {
+ code: `alias T = buffer<16;`,
+ valid: false,
+ },
+ subtype: {
+ code: `alias T = buffer<u32>;`,
+ valid: false,
+ },
+ pointer_workgroup_unsized_buffer: {
+ code: `alias T = buffer; alias PT = ptr<workgroup, T>;`,
+ valid: true,
+ },
+ pointer_uniform_unsized_buffer: {
+ code: `alias T = buffer; alias PT = ptr<uniform, T>;`,
+ valid: true,
+ },
+ pointer_storage_unsized_buffer: {
+ code: `alias T = buffer; alias PT = ptr<storage, T>;`,
+ valid: true,
+ },
+ pointer_workgroup_sized_buffer: {
+ code: `alias T = buffer<32>; alias PT = ptr<workgroup, T>;`,
+ valid: true,
+ },
+ pointer_uniform_sized_buffer: {
+ code: `alias T = buffer<32>; alias PT = ptr<uniform, T>;`,
+ valid: true,
+ },
+ pointer_storage_sized_buffer: {
+ code: `alias T = buffer<32>; alias PT = ptr<storage, T>;`,
+ valid: true,
+ },
+ pointer_workgroup_override_sized_buffer: {
+ code: `override o : i32; alias T = buffer<o>; alias PT = ptr<workgroup, T>;`,
+ valid: true,
+ },
+ pointer_uniform_override_sized_buffer: {
+ code: `override o : i32; alias T = buffer<o>; alias PT = ptr<uniform, T>;`,
+ valid: false,
+ },
+ pointer_storage_override_sized_buffer: {
+ code: `override o : i32; alias T = buffer<o>; alias PT = ptr<storage, T>;`,
+ valid: false,
+ },
+ buffer_3bytes: {
+ code: `alias T = buffer<3>;`,
+ valid: false,
+ },
+ buffer_2bytes_f16: {
+ code: `enable f16;\nalias T = buffer<2>;`,
+ valid: true,
+ },
+ buffer_2bytes_no_f16: {
+ code: `alias T = buffer<2>;`,
+ valid: false,
+ },
+};
+
+g.test('parse')
+ .desc('Test buffer type parsing')
+ .params(u => u.combine('case', keysOf(kParseCases)))
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const testcase = kParseCases[t.params.case];
+ t.expectCompileResult(testcase.valid, testcase.code);
+ });
+
+g.test('address_space')
+ .desc('Test buffer type validity for each address space')
+ .params(u =>
+ u
+ .combine('case', keysOf(kParseCases))
+ .filter(t => {
+ return kParseCases[t.case].valid && !t.case.includes('pointer');
+ })
+ .beginSubcases()
+ .combine('aspace', [
+ 'function',
+ 'private',
+ 'storage',
+ 'uniform',
+ 'workgroup',
+ 'immediate',
+ ] as const)
+ )
+ .fn(t => {
+ t.skipIfLanguageFeatureNotSupported('buffer_view');
+ const testcase = kParseCases[t.params.case];
+
+ let mvar = '';
+ let fvar = '';
+ switch (t.params.aspace) {
+ case 'function':
+ fvar = `var v : T;`;
+ break;
+ case 'private':
+ case 'workgroup':
+ case 'immediate':
+ mvar = `var<${t.params.aspace}> v : T;`;
+ break;
+ case 'storage':
+ case 'uniform':
+ mvar = `@group(0) @binding(0) var<${t.params.aspace}> v : T;`;
+ break;
+ }
+
+ const wgsl = `
+${testcase.code}
+${mvar}
+@compute @workgroup_size(1)
+fn main() {
+ ${fvar}
+}`;
+
+ let expected =
+ t.params.aspace !== 'function' &&
+ t.params.aspace !== 'private' &&
+ t.params.aspace !== 'immediate';
+ if (t.params.case === 'unsized') {
+ expected = t.params.aspace === 'storage';
+ } else if (t.params.case.includes('override')) {
+ expected = t.params.aspace === 'workgroup';
+ }
+ t.expectCompileResult(expected, wgsl);
+ });