Add readonly and readwrite storage textures as bindable resources (#3219)

* Add readonly, writeonly and readwrite storage texture as bindable resources

This patch adds `readonlyStorageTex`, `writeonlyStorageTex` and
`readwriteStorageTex` as `ValidBinableResource` and updates all the
related tests to support them.

* Small fix

* Use r32float for all storage textures
diff --git a/src/webgpu/api/validation/createBindGroup.spec.ts b/src/webgpu/api/validation/createBindGroup.spec.ts
index c195134..75a545a 100644
--- a/src/webgpu/api/validation/createBindGroup.spec.ts
+++ b/src/webgpu/api/validation/createBindGroup.spec.ts
@@ -8,6 +8,7 @@
 import { assert, makeValueTestVariant, unreachable } from '../../../common/util/util.js';
 import {
   allBindingEntries,
+  BindableResource,
   bindingTypeInfo,
   bufferBindingEntries,
   bufferBindingTypeInfo,
@@ -106,7 +107,7 @@
   .desc(
     'Test that only compatible resource types specified in the BindGroupLayout are allowed for each entry.'
   )
-  .paramsSubcasesOnly(u =>
+  .params(u =>
     u //
       .combine('resourceType', kBindableResources)
       .combine('entry', allBindingEntries(false))
@@ -121,6 +122,17 @@
 
     const resource = t.getBindingResource(resourceType);
 
+    const IsStorageTextureResourceType = (resourceType: BindableResource) => {
+      switch (resourceType) {
+        case 'readonlyStorageTex':
+        case 'readwriteStorageTex':
+        case 'writeonlyStorageTex':
+          return true;
+        default:
+          return false;
+      }
+    };
+
     let resourceBindingIsCompatible;
     switch (info.resource) {
       // Either type of sampler may be bound to a filtering sampler binding.
@@ -131,6 +143,11 @@
       case 'nonFiltSamp':
         resourceBindingIsCompatible = resourceType === 'nonFiltSamp';
         break;
+      case 'readonlyStorageTex':
+      case 'readwriteStorageTex':
+      case 'writeonlyStorageTex':
+        resourceBindingIsCompatible = IsStorageTextureResourceType(resourceType);
+        break;
       default:
         resourceBindingIsCompatible = info.resource === resourceType;
         break;
@@ -166,7 +183,7 @@
 
     const descriptor = {
       size: { width: 16, height: 16, depthOrArrayLayers: 1 },
-      format: 'rgba8unorm' as const,
+      format: 'r32float' as const,
       usage: appliedUsage,
       sampleCount: info.resource === 'sampledTexMS' ? 4 : 1,
     };
@@ -539,9 +556,7 @@
 g.test('texture,resource_state')
   .desc('Test bind group creation with various texture resource states')
   .paramsSubcasesOnly(u =>
-    u
-      .combine('state', kResourceStates)
-      .combine('entry', sampledAndStorageBindingEntries(true, 'rgba8unorm'))
+    u.combine('state', kResourceStates).combine('entry', sampledAndStorageBindingEntries(true))
   )
   .fn(t => {
     const { state, entry } = t.params;
@@ -561,10 +576,11 @@
     const usage = entry.texture?.multisampled
       ? info.usage | GPUConst.TextureUsage.RENDER_ATTACHMENT
       : info.usage;
+    const format = entry.storageTexture !== undefined ? 'r32float' : 'rgba8unorm';
     const texture = t.createTextureWithState(state, {
       usage,
       size: [1, 1],
-      format: 'rgba8unorm',
+      format,
       sampleCount: entry.texture?.multisampled ? 4 : 1,
     });
 
@@ -639,7 +655,9 @@
         { buffer: { type: 'storage' } },
         { sampler: { type: 'filtering' } },
         { texture: { multisampled: false } },
-        { storageTexture: { access: 'write-only', format: 'rgba8unorm' } },
+        { storageTexture: { access: 'write-only', format: 'r32float' } },
+        { storageTexture: { access: 'read-only', format: 'r32float' } },
+        { storageTexture: { access: 'read-write', format: 'r32float' } },
       ] as const)
       .beginSubcases()
       .combineWithParams([
diff --git a/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts b/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts
index 163c20c..bef3396 100644
--- a/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts
+++ b/src/webgpu/api/validation/encoding/programmable/pipeline_bind_group_compat.spec.ts
@@ -38,7 +38,9 @@
   'uniformBuf',
   'filtSamp',
   'sampledTex',
-  'storageTex',
+  'readonlyStorageTex',
+  'writeonlyStorageTex',
+  'readwriteStorageTex',
 ];
 
 function getTestCmds(
@@ -75,7 +77,17 @@
     if (entry.buffer !== undefined) return 'uniformBuf';
     if (entry.sampler !== undefined) return 'filtSamp';
     if (entry.texture !== undefined) return 'sampledTex';
-    if (entry.storageTexture !== undefined) return 'storageTex';
+    if (entry.storageTexture !== undefined) {
+      switch (entry.storageTexture.access) {
+        case undefined:
+        case 'write-only':
+          return 'writeonlyStorageTex';
+        case 'read-only':
+          return 'readonlyStorageTex';
+        case 'read-write':
+          return 'readwriteStorageTex';
+      }
+    }
     unreachable();
   }
 
@@ -208,8 +220,14 @@
       case 'sampledTex':
         entry.texture = {}; // default sampleType: float
         break;
-      case 'storageTex':
-        entry.storageTexture = { access: 'write-only', format: 'rgba8unorm' };
+      case 'readonlyStorageTex':
+        entry.storageTexture = { access: 'read-only', format: 'r32float' };
+        break;
+      case 'writeonlyStorageTex':
+        entry.storageTexture = { access: 'write-only', format: 'r32float' };
+        break;
+      case 'readwriteStorageTex':
+        entry.storageTexture = { access: 'read-write', format: 'r32float' };
         break;
     }
 
diff --git a/src/webgpu/api/validation/validation_test.ts b/src/webgpu/api/validation/validation_test.ts
index 1be0866..6e6802d 100644
--- a/src/webgpu/api/validation/validation_test.ts
+++ b/src/webgpu/api/validation/validation_test.ts
@@ -152,11 +152,11 @@
   }
 
   /** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */
-  getStorageTexture(): GPUTexture {
+  getStorageTexture(format: GPUTextureFormat): GPUTexture {
     return this.trackForCleanup(
       this.device.createTexture({
         size: { width: 16, height: 16, depthOrArrayLayers: 1 },
-        format: 'rgba8unorm',
+        format,
         usage: GPUTextureUsage.STORAGE_BINDING,
       })
     );
@@ -220,8 +220,10 @@
         return this.getSampledTexture(1).createView();
       case 'sampledTexMS':
         return this.getSampledTexture(4).createView();
-      case 'storageTex':
-        return this.getStorageTexture().createView();
+      case 'readonlyStorageTex':
+      case 'writeonlyStorageTex':
+      case 'readwriteStorageTex':
+        return this.getStorageTexture('r32float').createView();
     }
   }
 
@@ -255,10 +257,10 @@
   }
 
   /** Return an arbitrarily-configured GPUTexture with the `STORAGE` usage from mismatched device. */
-  getDeviceMismatchedStorageTexture(): GPUTexture {
+  getDeviceMismatchedStorageTexture(format: GPUTextureFormat): GPUTexture {
     return this.getDeviceMismatchedTexture({
       size: { width: 4, height: 4, depthOrArrayLayers: 1 },
-      format: 'rgba8unorm',
+      format,
       usage: GPUTextureUsage.STORAGE_BINDING,
     });
   }
@@ -289,8 +291,10 @@
         return this.getDeviceMismatchedSampledTexture(1).createView();
       case 'sampledTexMS':
         return this.getDeviceMismatchedSampledTexture(4).createView();
-      case 'storageTex':
-        return this.getDeviceMismatchedStorageTexture().createView();
+      case 'readonlyStorageTex':
+      case 'writeonlyStorageTex':
+      case 'readwriteStorageTex':
+        return this.getDeviceMismatchedStorageTexture('r32float').createView();
     }
   }
 
diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts
index d7fe718..1bd5d3b 100644
--- a/src/webgpu/capability_info.ts
+++ b/src/webgpu/capability_info.ts
@@ -322,7 +322,9 @@
   | 'storageBuf'
   | 'sampler'
   | 'sampledTex'
-  | 'storageTex';
+  | 'readonlyStorageTex'
+  | 'writeonlyStorageTex'
+  | 'readwriteStorageTex';
 /**
  * Classes of `PerPipelineLayout` binding limits. Two bindings with the same class
  * count toward the same `PerPipelineLayout` limit(s) in the spec (if any).
@@ -337,7 +339,9 @@
   | 'compareSamp'
   | 'sampledTex'
   | 'sampledTexMS'
-  | 'storageTex';
+  | 'readonlyStorageTex'
+  | 'writeonlyStorageTex'
+  | 'readwriteStorageTex';
 type ErrorBindableResource = 'errorBuf' | 'errorSamp' | 'errorTex';
 
 /**
@@ -353,7 +357,9 @@
   'compareSamp',
   'sampledTex',
   'sampledTexMS',
-  'storageTex',
+  'readonlyStorageTex',
+  'writeonlyStorageTex',
+  'readwriteStorageTex',
   'errorBuf',
   'errorSamp',
   'errorTex',
@@ -376,11 +382,13 @@
   };
 } =
   /* prettier-ignore */ {
-  'uniformBuf': { class: 'uniformBuf', maxLimit: 'maxUniformBuffersPerShaderStage', },
-  'storageBuf': { class: 'storageBuf', maxLimit: 'maxStorageBuffersPerShaderStage', },
-  'sampler':    { class: 'sampler',    maxLimit: 'maxSamplersPerShaderStage', },
-  'sampledTex': { class: 'sampledTex', maxLimit: 'maxSampledTexturesPerShaderStage', },
-  'storageTex': { class: 'storageTex', maxLimit: 'maxStorageTexturesPerShaderStage', },
+  'uniformBuf':          { class: 'uniformBuf', maxLimit: 'maxUniformBuffersPerShaderStage', },
+  'storageBuf':          { class: 'storageBuf', maxLimit: 'maxStorageBuffersPerShaderStage', },
+  'sampler':             { class: 'sampler',    maxLimit: 'maxSamplersPerShaderStage', },
+  'sampledTex':          { class: 'sampledTex', maxLimit: 'maxSampledTexturesPerShaderStage', },
+  'readonlyStorageTex':  { class: 'readonlyStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', },
+  'writeonlyStorageTex': { class: 'writeonlyStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', },
+  'readwriteStorageTex': { class: 'readwriteStorageTex', maxLimit: 'maxStorageTexturesPerShaderStage', },
 };
 
 /**
@@ -398,11 +406,13 @@
   };
 } =
   /* prettier-ignore */ {
-  'uniformBuf': { class: 'uniformBuf', maxDynamicLimit: 'maxDynamicUniformBuffersPerPipelineLayout', },
-  'storageBuf': { class: 'storageBuf', maxDynamicLimit: 'maxDynamicStorageBuffersPerPipelineLayout', },
-  'sampler':    { class: 'sampler',    maxDynamicLimit: '', },
-  'sampledTex': { class: 'sampledTex', maxDynamicLimit: '', },
-  'storageTex': { class: 'storageTex', maxDynamicLimit: '', },
+  'uniformBuf':          { class: 'uniformBuf', maxDynamicLimit: 'maxDynamicUniformBuffersPerPipelineLayout', },
+  'storageBuf':          { class: 'storageBuf', maxDynamicLimit: 'maxDynamicStorageBuffersPerPipelineLayout', },
+  'sampler':             { class: 'sampler',    maxDynamicLimit: '', },
+  'sampledTex':          { class: 'sampledTex', maxDynamicLimit: '', },
+  'readonlyStorageTex':  { class: 'readonlyStorageTex', maxDynamicLimit: '', },
+  'writeonlyStorageTex': { class: 'writeonlyStorageTex', maxDynamicLimit: '', },
+  'readwriteStorageTex': { class: 'readwriteStorageTex', maxDynamicLimit: '', },
 };
 
 interface BindingKindInfo {
@@ -416,14 +426,16 @@
   readonly [k in ValidBindableResource]: BindingKindInfo;
 } =
   /* prettier-ignore */ {
-  uniformBuf:   { resource: 'uniformBuf',   perStageLimitClass: kPerStageBindingLimits.uniformBuf, perPipelineLimitClass: kPerPipelineBindingLimits.uniformBuf, },
-  storageBuf:   { resource: 'storageBuf',   perStageLimitClass: kPerStageBindingLimits.storageBuf, perPipelineLimitClass: kPerPipelineBindingLimits.storageBuf, },
-  filtSamp:     { resource: 'filtSamp',     perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
-  nonFiltSamp:  { resource: 'nonFiltSamp',  perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
-  compareSamp:  { resource: 'compareSamp',  perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
-  sampledTex:   { resource: 'sampledTex',   perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
-  sampledTexMS: { resource: 'sampledTexMS', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
-  storageTex:   { resource: 'storageTex',   perStageLimitClass: kPerStageBindingLimits.storageTex, perPipelineLimitClass: kPerPipelineBindingLimits.storageTex, },
+  uniformBuf:          { resource: 'uniformBuf',   perStageLimitClass: kPerStageBindingLimits.uniformBuf, perPipelineLimitClass: kPerPipelineBindingLimits.uniformBuf, },
+  storageBuf:          { resource: 'storageBuf',   perStageLimitClass: kPerStageBindingLimits.storageBuf, perPipelineLimitClass: kPerPipelineBindingLimits.storageBuf, },
+  filtSamp:            { resource: 'filtSamp',     perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
+  nonFiltSamp:         { resource: 'nonFiltSamp',  perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
+  compareSamp:         { resource: 'compareSamp',  perStageLimitClass: kPerStageBindingLimits.sampler,    perPipelineLimitClass: kPerPipelineBindingLimits.sampler,    },
+  sampledTex:          { resource: 'sampledTex',   perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
+  sampledTexMS:        { resource: 'sampledTexMS', perStageLimitClass: kPerStageBindingLimits.sampledTex, perPipelineLimitClass: kPerPipelineBindingLimits.sampledTex, },
+  readonlyStorageTex:  { resource: 'readonlyStorageTex',   perStageLimitClass: kPerStageBindingLimits.readonlyStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.readonlyStorageTex, },
+  writeonlyStorageTex: { resource: 'writeonlyStorageTex',   perStageLimitClass: kPerStageBindingLimits.writeonlyStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.writeonlyStorageTex, },
+  readwriteStorageTex: { resource: 'readwriteStorageTex',   perStageLimitClass: kPerStageBindingLimits.readwriteStorageTex, perPipelineLimitClass: kPerPipelineBindingLimits.readwriteStorageTex, },
 };
 
 // Binding type info
@@ -483,11 +495,27 @@
 
 /** Binding type info (including class limits) for the specified GPUStorageTextureBindingLayout. */
 export function storageTextureBindingTypeInfo(d: GPUStorageTextureBindingLayout) {
-  return {
-    usage: GPUConst.TextureUsage.STORAGE_BINDING,
-    ...kBindingKind.storageTex,
-    ...kValidStagesStorageWrite,
-  };
+  switch (d.access) {
+    case undefined:
+    case 'write-only':
+      return {
+        usage: GPUConst.TextureUsage.STORAGE_BINDING,
+        ...kBindingKind.writeonlyStorageTex,
+        ...kValidStagesStorageWrite,
+      };
+    case 'read-only':
+      return {
+        usage: GPUConst.TextureUsage.STORAGE_BINDING,
+        ...kBindingKind.readonlyStorageTex,
+        ...kValidStagesAll,
+      };
+    case 'read-write':
+      return {
+        usage: GPUConst.TextureUsage.STORAGE_BINDING,
+        ...kBindingKind.readwriteStorageTex,
+        ...kValidStagesStorageWrite,
+      };
+  }
 }
 /** List of all GPUStorageTextureAccess values. */
 export const kStorageTextureAccessValues = ['read-only', 'read-write', 'write-only'] as const;
@@ -539,8 +567,10 @@
  */
 export function textureBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
   return [
-    ...(includeUndefined ? [{ texture: { multisampled: undefined } }] : []),
-    { texture: { multisampled: false } },
+    ...(includeUndefined
+      ? [{ texture: { multisampled: undefined, sampleType: 'unfilterable-float' } } as const]
+      : []),
+    { texture: { multisampled: false, sampleType: 'unfilterable-float' } },
     { texture: { multisampled: true, sampleType: 'unfilterable-float' } },
   ] as const;
 }
@@ -549,18 +579,16 @@
  *
  * Note: Generates different `access` options, but not `format` or `viewDimension` options.
  */
-export function storageTextureBindingEntries(format: GPUTextureFormat): readonly BGLEntry[] {
-  return [{ storageTexture: { access: 'write-only', format } }] as const;
+export function storageTextureBindingEntries(): readonly BGLEntry[] {
+  return [
+    { storageTexture: { access: 'write-only', format: 'r32float' } },
+    { storageTexture: { access: 'read-only', format: 'r32float' } },
+    { storageTexture: { access: 'read-write', format: 'r32float' } },
+  ] as const;
 }
 /** Generate a list of possible texture-or-storageTexture-typed BGLEntry values. */
-export function sampledAndStorageBindingEntries(
-  includeUndefined: boolean,
-  storageTextureFormat: GPUTextureFormat = 'rgba8unorm'
-): readonly BGLEntry[] {
-  return [
-    ...textureBindingEntries(includeUndefined),
-    ...storageTextureBindingEntries(storageTextureFormat),
-  ] as const;
+export function sampledAndStorageBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
+  return [...textureBindingEntries(includeUndefined), ...storageTextureBindingEntries()] as const;
 }
 /**
  * Generate a list of possible BGLEntry values of every type, but not variants with different:
@@ -569,14 +597,11 @@
  * - texture.viewDimension
  * - storageTexture.viewDimension
  */
-export function allBindingEntries(
-  includeUndefined: boolean,
-  storageTextureFormat: GPUTextureFormat = 'rgba8unorm'
-): readonly BGLEntry[] {
+export function allBindingEntries(includeUndefined: boolean): readonly BGLEntry[] {
   return [
     ...bufferBindingEntries(includeUndefined),
     ...samplerBindingEntries(includeUndefined),
-    ...sampledAndStorageBindingEntries(includeUndefined, storageTextureFormat),
+    ...sampledAndStorageBindingEntries(includeUndefined),
   ] as const;
 }