🏡 index : cold-air-networks/mirror-webgpu-cts.git

export const description = `
TODO:
- interface matching between pipeline layout and shader
    - x= bind group index values, binding index values, multiple bindings
    - x= {superset, subset}
`;

import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import {
  kShaderStageCombinations,
  kShaderStages,
  ValidBindableResource,
} from '../../capability_info.js';
import { GPUConst } from '../../constants.js';

import * as vtu from './validation_test_utils.js';

type BindableResourceType = ValidBindableResource | 'readonlyStorageBuf';
const kBindableResources = [
  'uniformBuf',
  'storageBuf',
  'readonlyStorageBuf',
  'filtSamp',
  'nonFiltSamp',
  'compareSamp',
  'sampledTex',
  'sampledTexMS',
  'readonlyStorageTex',
  'writeonlyStorageTex',
  'readwriteStorageTex',
] as const;

const bindGroupLayoutEntryContents = {
  compareSamp: {
    sampler: {
      type: 'comparison',
    },
  },
  filtSamp: {
    sampler: {
      type: 'filtering',
    },
  },
  nonFiltSamp: {
    sampler: {
      type: 'non-filtering',
    },
  },
  sampledTex: {
    texture: {
      sampleType: 'unfilterable-float',
    },
  },
  sampledTexMS: {
    texture: {
      sampleType: 'unfilterable-float',
      multisampled: true,
    },
  },
  storageBuf: {
    buffer: {
      type: 'storage',
    },
  },
  readonlyStorageBuf: {
    buffer: {
      type: 'read-only-storage',
    },
  },
  uniformBuf: {
    buffer: {
      type: 'uniform',
    },
  },
  readonlyStorageTex: {
    storageTexture: {
      format: 'r32float',
      access: 'read-only',
    },
  },
  writeonlyStorageTex: {
    storageTexture: {
      format: 'r32float',
      access: 'write-only',
    },
  },
  readwriteStorageTex: {
    storageTexture: {
      format: 'r32float',
      access: 'read-write',
    },
  },
} as const;

class F extends AllFeaturesMaxLimitsGPUTest {
  createPipelineLayout(
    bindingInPipelineLayout: BindableResourceType,
    visibility: number
  ): GPUPipelineLayout {
    return this.device.createPipelineLayout({
      bindGroupLayouts: [
        this.device.createBindGroupLayout({
          entries: [
            {
              binding: 0,
              visibility,
              ...bindGroupLayoutEntryContents[bindingInPipelineLayout],
            },
          ],
        }),
      ],
    });
  }

  getBindableResourceShaderDeclaration(bindableResource: BindableResourceType): string {
    switch (bindableResource) {
      case 'compareSamp':
        return 'var tmp : sampler_comparison';
      case 'filtSamp':
      case 'nonFiltSamp':
        return 'var tmp : sampler';
      case 'sampledTex':
        return 'var tmp : texture_2d<f32>';
      case 'sampledTexMS':
        return 'var tmp : texture_multisampled_2d<f32>';
      case 'storageBuf':
        return 'var<storage, read_write> tmp : vec4u';
      case 'readonlyStorageBuf':
        return 'var<storage, read> tmp : vec4u';
      case 'uniformBuf':
        return 'var<uniform> tmp : vec4u;';
      case 'readonlyStorageTex':
        return 'var tmp : texture_storage_2d<r32float, read>';
      case 'writeonlyStorageTex':
        return 'var tmp : texture_storage_2d<r32float, write>';
      case 'readwriteStorageTex':
        return 'var tmp : texture_storage_2d<r32float, read_write>';
    }
  }
}

const bindingResourceCompatibleWithShaderStages = function (
  bindingResource: BindableResourceType,
  shaderStages: number
): boolean {
  if ((shaderStages & GPUConst.ShaderStage.VERTEX) > 0) {
    switch (bindingResource) {
      case 'writeonlyStorageTex':
      case 'readwriteStorageTex':
      case 'storageBuf':
        return false;
      default:
        break;
    }
  }
  return true;
};

export const g = makeTestGroup(F);

g.test('pipeline_layout_shader_exact_match')
  .desc(
    `
  Test that the binding type in the pipeline layout must match the related declaration in shader.
  Note that read-write storage textures in the pipeline layout can match write-only storage textures
  in the shader.
  `
  )
  .params(u =>
    u
      .combine('bindingInPipelineLayout', kBindableResources)
      .combine('bindingInShader', kBindableResources)
      .beginSubcases()
      .combine('pipelineLayoutVisibility', kShaderStageCombinations)
      .combine('shaderStageWithBinding', kShaderStages)
      .combine('isBindingStaticallyUsed', [true, false] as const)
      .unless(
        p =>
          // We don't test using non-filtering sampler in shader because it has the same declaration
          // as filtering sampler.
          p.bindingInShader === 'nonFiltSamp' ||
          !bindingResourceCompatibleWithShaderStages(
            p.bindingInPipelineLayout,
            p.pipelineLayoutVisibility
          ) ||
          !bindingResourceCompatibleWithShaderStages(p.bindingInShader, p.shaderStageWithBinding)
      )
  )
  .fn(t => {
    const {
      bindingInPipelineLayout,
      bindingInShader,
      pipelineLayoutVisibility,
      shaderStageWithBinding,
      isBindingStaticallyUsed,
    } = t.params;

    if (t.isCompatibility) {
      const bindingUsedWithVertexStage =
        (shaderStageWithBinding & GPUShaderStage.VERTEX) !== 0 ||
        (pipelineLayoutVisibility & GPUShaderStage.VERTEX) !== 0;
      const bindingUsedWithFragmentStage =
        (shaderStageWithBinding & GPUShaderStage.FRAGMENT) !== 0 ||
        (pipelineLayoutVisibility & GPUShaderStage.FRAGMENT) !== 0;
      const bindingIsStorageBuffer =
        bindingInPipelineLayout === 'readonlyStorageBuf' ||
        bindingInPipelineLayout === 'storageBuf';
      const bindingIsStorageTexture =
        bindingInPipelineLayout === 'readonlyStorageTex' ||
        bindingInPipelineLayout === 'readwriteStorageTex' ||
        bindingInPipelineLayout === 'writeonlyStorageTex';
      t.skipIf(
        bindingUsedWithVertexStage &&
          bindingIsStorageBuffer &&
          t.device.limits.maxStorageBuffersInVertexStage === 0,
        'Storage buffers can not be used in vertex shaders because maxStorageBuffersInVertexStage === 0'
      );
      t.skipIf(
        bindingUsedWithVertexStage &&
          bindingIsStorageTexture &&
          t.device.limits.maxStorageTexturesInVertexStage === 0,
        'Storage textures can not be used in vertex shaders because maxStorageTexturesInVertexStage === 0'
      );
      t.skipIf(
        bindingUsedWithFragmentStage &&
          bindingIsStorageBuffer &&
          t.device.limits.maxStorageBuffersInFragmentStage === 0,
        'Storage buffers can not be used in fragment shaders because maxStorageBuffersInFragmentStage === 0'
      );
      t.skipIf(
        bindingUsedWithFragmentStage &&
          bindingIsStorageTexture &&
          t.device.limits.maxStorageTexturesInFragmentStage === 0,
        'Storage textures can not be used in fragment shaders because maxStorageTexturesInFragmentStage === 0'
      );
    }

    const layout = t.createPipelineLayout(bindingInPipelineLayout, pipelineLayoutVisibility);
    const bindResourceDeclaration = `@group(0) @binding(0) ${t.getBindableResourceShaderDeclaration(
      bindingInShader
    )}`;
    const staticallyUseBinding = isBindingStaticallyUsed ? '_ = tmp; ' : '';
    const isAsync = false;

    let success = true;
    if (isBindingStaticallyUsed) {
      success = bindingInPipelineLayout === bindingInShader;

      // Filtering and non-filtering both have the same shader declaration.
      success ||= bindingInPipelineLayout === 'nonFiltSamp' && bindingInShader === 'filtSamp';

      // Promoting storage textures that are read-write in the layout can be readonly in the shader.
      success ||=
        bindingInPipelineLayout === 'readwriteStorageTex' &&
        bindingInShader === 'writeonlyStorageTex';

      // The shader using the resource must be included in the visibility in the layout.
      success &&= (pipelineLayoutVisibility & shaderStageWithBinding) > 0;
    }

    switch (shaderStageWithBinding) {
      case GPUConst.ShaderStage.COMPUTE: {
        const computeShader = `
        ${bindResourceDeclaration};
        @compute @workgroup_size(1)
        fn main() {
          ${staticallyUseBinding}
        }
        `;
        vtu.doCreateComputePipelineTest(t, isAsync, success, {
          layout,
          compute: {
            module: t.device.createShaderModule({
              code: computeShader,
            }),
          },
        });
        break;
      }
      case GPUConst.ShaderStage.VERTEX: {
        const vertexShader = `
        ${bindResourceDeclaration};
        @vertex
        fn main() -> @builtin(position) vec4f {
          ${staticallyUseBinding}
          return vec4f();
        }
        `;
        vtu.doCreateRenderPipelineTest(t, isAsync, success, {
          layout,
          vertex: {
            module: t.device.createShaderModule({
              code: vertexShader,
            }),
          },
          depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'always' },
        });
        break;
      }
      case GPUConst.ShaderStage.FRAGMENT: {
        const fragmentShader = `
        ${bindResourceDeclaration};
        @fragment
        fn main() -> @location(0) vec4f {
          ${staticallyUseBinding}
          return vec4f();
        }
        `;
        vtu.doCreateRenderPipelineTest(t, isAsync, success, {
          layout,
          vertex: {
            module: t.device.createShaderModule({
              code: `
                @vertex
                fn main() -> @builtin(position) vec4f {
                  return vec4f();
                }`,
            }),
          },
          fragment: {
            module: t.device.createShaderModule({
              code: fragmentShader,
            }),
            targets: [
              {
                format: 'rgba8unorm',
              },
            ],
          },
        });
        break;
      }
    }
  });