Adds CTS test coverage for the `snorm10-10-10-2` vertex format. (#4695)

* This PR adds CTS test coverage for the `snorm10-10-10-2` vertex format.

* Adds `snorm10-10-10-2` to `kVertexFormatInfo` in `capability_info.ts` (with an explicit cast until `@webgpu/types` is updated).
* Adds signed 10-10-10-2 bit-packing (`makeRgb10a2Signed`) and normalization (`normalizeRgb10a2Signed`) test helpers.
* Adds packed format test data in `correctness.spec.ts` to verify conversion from raw vertex data to `vec4<f32>` in the vertex shader.

Issue: https://github.com/gpuweb/gpuweb/issues/6292

<hr>

**Requirements for PR author:**

- [x] All missing test coverage is tracked with "TODO" or `.unimplemented()`.
- [x] New helpers are `/** documented */` and new helper files are found in `helper_index.txt`.
- [x] Test behaves as expected in a WebGPU implementation. (If not passing, explain above.)
- [x] Test have be tested with compatibility mode validation enabled and behave as expected. (If not passing, explain above.)

**Requirements for [reviewer sign-off](https://github.com/gpuweb/cts/blob/main/docs/reviews.md):**

- [ ] Tests are properly located.
- [ ] [Test descriptions](https://github.com/gpuweb/cts/blob/main/docs/intro/plans.md) are accurate and complete.
- [ ] Tests provide complete coverage (including validation control cases). **Missing coverage MUST be covered by TODOs.**
- [ ] Tests avoid [over-parameterization](https://github.com/gpuweb/cts/blob/main/docs/organization.md#parameterization) (see case count report).

When landing this PR, be sure to make any necessary issue status updates.

* Roll @webgpu/types to 0.1.72

* Address review comments on makeRgb10a2Signed

---------

Co-authored-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/package-lock.json b/package-lock.json
index fb65120..d61298a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
         "@types/w3c-image-capture": "^1.0.10",
         "@typescript-eslint/eslint-plugin": "^6.9.1",
         "@typescript-eslint/parser": "^6.9.1",
-        "@webgpu/types": "^0.1.71",
+        "@webgpu/types": "^0.1.72",
         "ansi-colors": "4.1.3",
         "babel-plugin-add-header-comment": "^1.0.3",
         "babel-plugin-const-enum": "^1.2.0",
@@ -148,7 +148,6 @@
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
       "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.22.13",
@@ -1042,7 +1041,6 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
       "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -1218,7 +1216,6 @@
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
       "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "6.9.1",
         "@typescript-eslint/types": "6.9.1",
@@ -1542,11 +1539,10 @@
       "dev": true
     },
     "node_modules/@webgpu/types": {
-      "version": "0.1.71",
-      "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz",
-      "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
-      "dev": true,
-      "license": "BSD-3-Clause"
+      "version": "0.1.72",
+      "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz",
+      "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==",
+      "dev": true
     },
     "node_modules/abbrev": {
       "version": "1.1.1",
@@ -1572,7 +1568,6 @@
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
       "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
       "dev": true,
-      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -2042,7 +2037,6 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
-      "peer": true,
       "dependencies": {
         "caniuse-lite": "^1.0.30001541",
         "electron-to-chromium": "^1.4.535",
@@ -2903,7 +2897,6 @@
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
       "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -4356,7 +4349,6 @@
       "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz",
       "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "dateformat": "~4.6.2",
         "eventemitter2": "~0.4.13",
@@ -4834,7 +4826,6 @@
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
       "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "5.62.0",
         "@typescript-eslint/types": "5.62.0",
@@ -5042,7 +5033,6 @@
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
       "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -7220,7 +7210,6 @@
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
       "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
       "dev": true,
-      "peer": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
       },
@@ -8645,7 +8634,6 @@
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
       "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
       "dev": true,
-      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -9054,7 +9042,6 @@
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
       "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.22.13",
@@ -9760,7 +9747,6 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
       "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
       "dev": true,
-      "peer": true,
       "requires": {
         "undici-types": "~5.26.4"
       }
@@ -9901,7 +9887,6 @@
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
       "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@typescript-eslint/scope-manager": "6.9.1",
         "@typescript-eslint/types": "6.9.1",
@@ -10091,9 +10076,9 @@
       "dev": true
     },
     "@webgpu/types": {
-      "version": "0.1.71",
-      "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz",
-      "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
+      "version": "0.1.72",
+      "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz",
+      "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==",
       "dev": true
     },
     "abbrev": {
@@ -10116,8 +10101,7 @@
       "version": "8.11.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
       "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "acorn-jsx": {
       "version": "5.3.2",
@@ -10463,7 +10447,6 @@
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
       "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
       "dev": true,
-      "peer": true,
       "requires": {
         "caniuse-lite": "^1.0.30001541",
         "electron-to-chromium": "^1.4.535",
@@ -11092,7 +11075,6 @@
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
       "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
       "dev": true,
-      "peer": true,
       "requires": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -12160,7 +12142,6 @@
       "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz",
       "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
       "dev": true,
-      "peer": true,
       "requires": {
         "dateformat": "~4.6.2",
         "eventemitter2": "~0.4.13",
@@ -12513,7 +12494,6 @@
           "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
           "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
           "dev": true,
-          "peer": true,
           "requires": {
             "@typescript-eslint/scope-manager": "5.62.0",
             "@typescript-eslint/types": "5.62.0",
@@ -12641,7 +12621,6 @@
           "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
           "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
           "dev": true,
-          "peer": true,
           "requires": {
             "@eslint-community/eslint-utils": "^4.2.0",
             "@eslint-community/regexpp": "^4.6.1",
@@ -14230,8 +14209,7 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
       "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "prettier-linter-helpers": {
       "version": "1.0.0",
@@ -15307,8 +15285,7 @@
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
       "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "unbox-primitive": {
       "version": "1.0.2",
diff --git a/package.json b/package.json
index 7493b84..ec9e302 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
     "@types/w3c-image-capture": "^1.0.10",
     "@typescript-eslint/eslint-plugin": "^6.9.1",
     "@typescript-eslint/parser": "^6.9.1",
-    "@webgpu/types": "^0.1.71",
+    "@webgpu/types": "^0.1.72",
     "ansi-colors": "4.1.3",
     "babel-plugin-add-header-comment": "^1.0.3",
     "babel-plugin-const-enum": "^1.2.0",
diff --git a/src/webgpu/api/operation/vertex_state/correctness.spec.ts b/src/webgpu/api/operation/vertex_state/correctness.spec.ts
index 8ddeaa4..44baebd 100644
--- a/src/webgpu/api/operation/vertex_state/correctness.spec.ts
+++ b/src/webgpu/api/operation/vertex_state/correctness.spec.ts
@@ -71,6 +71,26 @@
   return rgba / normalizationFactor;
 }
 
+function makeRgb10a2Signed(rgba: Array<number>): number {
+  const [r, g, b, a] = rgba;
+  // Check the input fits in i32, then check it's in range for i10 or i2.
+  assert((r | 0) === r && r >= -512 && r <= 511);
+  assert((g | 0) === g && g >= -512 && g <= 511);
+  assert((b | 0) === b && b >= -512 && b <= 511);
+  assert((a | 0) === a && a >= -2 && a <= 1);
+  const r_bits = r & 0x3ff;
+  const g_bits = g & 0x3ff;
+  const b_bits = b & 0x3ff;
+  const a_bits = a & 0x3;
+  return r_bits | (g_bits << 10) | (b_bits << 20) | (a_bits << 30);
+}
+
+function normalizeRgb10a2Signed(val: number, index: number): number {
+  const isAlpha = index % 4 === 3;
+  const maxVal = isAlpha ? 1 : 511;
+  return Math.max(val / maxVal, -1.0);
+}
+
 type TestData = {
   shaderBaseType: string;
   floatTolerance?: number;
@@ -379,6 +399,32 @@
       }
 
       case 'snorm': {
+        if (formatInfo.bytesPerComponent === 'packed') {
+          assert(bitSize === 0);
+          switch (format as string) {
+            case 'snorm10-10-10-2': {
+              /* prettier-ignore */
+              const data = [
+                [   0,    0,    0,  0],
+                [ 511,  511,  511,  1],
+                [-512, -512, -512, -2],
+                [ 243, -123,  342, -1],
+              ];
+              const vertexData = new Uint32Array(data.map(makeRgb10a2Signed)).buffer;
+              const expectedData = new Float32Array(data.flat().map(normalizeRgb10a2Signed)).buffer;
+
+              return {
+                shaderBaseType: 'f32',
+                testComponentCount: data.flat().length,
+                expectedData,
+                vertexData,
+                floatTolerance: 0.1 / 511,
+              };
+            }
+            default:
+              unreachable();
+          }
+        }
         /* prettier-ignore */
         const data = [
           42,
diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts
index b13e310..017227c 100644
--- a/src/webgpu/capability_info.ts
+++ b/src/webgpu/capability_info.ts
@@ -371,6 +371,7 @@
   // 32 bit packed
   'unorm10-10-10-2': [           'packed',  'unorm',                4,          4, 'vec4<f32>'],
   'unorm8x4-bgra':   [           'packed',  'unorm',                4,          4, 'vec4<f32>'],
+  'snorm10-10-10-2': [           'packed',  'snorm',                4,          4, 'vec4<f32>'],
 } as const);
 /** List of all GPUVertexFormat values. */
 export const kVertexFormats = keysOf(kVertexFormatInfo);