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

export const description = `
Tests that createComputePipeline(async), and createRenderPipeline(async)
reject pipelines that are invalid in compat mode

- test that depth textures can not be used with non-comparison samplers
- test that dpdxFine, dpdyFine, fwidthFine are disallowed

TODO:
- test that a shader that has more than min(maxSamplersPerShaderStage, maxSampledTexturesPerShaderStage)
  texture+sampler combinations generates a validation error.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { keysOf } from '../../../../common/util/data_tables.js';
import * as vtu from '../../../api/validation/validation_test_utils.js';
import {
  kShortShaderStages,
  kShortShaderStageToShaderStage,
} from '../../../shader/execution/expression/call/builtin/texture_utils.js';
import { CompatibilityTest } from '../../compatibility_test.js';

export const g = makeTestGroup(CompatibilityTest);

const kDepthCases = {
  textureSampleWith2DF32: {
    sampleWGSL: 'textureSample(t, s, vec2f(0))', // should pass
    textureType: 'texture_2d<f32>',
    viewDimension: '2d',
  },
  textureSampleWithDepth2D: {
    sampleWGSL: 'textureSample(t, s, vec2f(0))',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureSampleWithDepthCube: {
    sampleWGSL: 'textureSample(t, s, vec3f(0))',
    textureType: 'texture_depth_cube',
    viewDimension: 'cube',
  },
  textureSampleWithDepth2DArray: {
    sampleWGSL: 'textureSample(t, s, vec2f(0), 0)',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
  textureSampleWithOffsetWithDepth2D: {
    sampleWGSL: 'textureSample(t, s, vec2f(0), vec2i(0, 0))',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureSampleWithOffsetWithDepth2DArray: {
    sampleWGSL: 'textureSample(t, s, vec2f(0), 0, vec2i(0, 0))',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
  textureSampleLevelWithDepth2D: {
    sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0)',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureSampleLevelWithDepthCube: {
    sampleWGSL: 'textureSampleLevel(t, s, vec3f(0), 0)',
    textureType: 'texture_depth_cube',
    viewDimension: 'cube',
  },
  textureSampleLevelWithDepth2DArray: {
    sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, 0)',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
  textureSampleLevelWithOffsetWithDepth2D: {
    sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, vec2i(0, 0))',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureSampleLevelWithOffsetWithDepth2DArray: {
    sampleWGSL: 'textureSampleLevel(t, s, vec2f(0), 0, 0, vec2i(0, 0))',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
  textureGatherWithDepth2D: {
    sampleWGSL: 'textureGather(t, s, vec2f(0))',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureGatherWithDepthCube: {
    sampleWGSL: 'textureGather(t, s, vec3f(0))',
    textureType: 'texture_depth_cube',
    viewDimension: 'cube',
  },
  textureGatherWithDepth2DArray: {
    sampleWGSL: 'textureGather(t, s, vec2f(0), 0)',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
  textureGatherWithOffsetWithDepth2D: {
    sampleWGSL: 'textureGather(t, s, vec2f(0), vec2i(0, 0))',
    textureType: 'texture_depth_2d',
    viewDimension: '2d',
  },
  textureGatherWithOffsetDepth2DArray: {
    sampleWGSL: 'textureGather(t, s, vec2f(0), 0, vec2i(0, 0))',
    textureType: 'texture_depth_2d_array',
    viewDimension: '2d-array',
  },
} as const;

g.test('depth_textures')
  .desc('Tests that depth textures can not be used with non-comparison samplers in compat mode.')
  .params(u =>
    u //
      .combine('depthCase', keysOf(kDepthCases))
      .combine('stage', kShortShaderStages)
      .filter(
        t => kDepthCases[t.depthCase].sampleWGSL.startsWith('textureGather') || t.stage === 'f'
      )
      .combine('async', [false, true] as const)
  )
  .fn(t => {
    const { depthCase, stage: shortStage, async } = t.params;
    const { sampleWGSL, textureType, viewDimension } = kDepthCases[depthCase];
    const stage = kShortShaderStageToShaderStage[shortStage];

    const usageWGSL = `_ = ${sampleWGSL};`;
    const module = t.device.createShaderModule({
      code: `
        @group(0) @binding(0) var t: ${textureType};
        @group(1) @binding(0) var s: sampler;

        // make sure it's fine such a combination exists but it's not used.
        fn unused() {
          ${usageWGSL};
        }

        @vertex fn vs() -> @builtin(position) vec4f {
            ${stage === 'vertex' ? usageWGSL : ''}
            return vec4f(0);
        }

        @fragment fn fs() -> @location(0) vec4f {
            ${stage === 'fragment' ? usageWGSL : ''}
            return vec4f(0);
        }

        @compute @workgroup_size(1) fn cs() {
            ${stage === 'compute' ? usageWGSL : ''};
        }
      `,
    });

    const bindGroupLayout0 = t.device.createBindGroupLayout({
      entries: [
        {
          binding: 0,
          visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
          texture: {
            sampleType: textureType.includes('depth') ? 'depth' : 'float',
            viewDimension,
            multisampled: false,
          },
        },
      ],
    });

    const bindGroupLayout1 = t.device.createBindGroupLayout({
      entries: [
        {
          binding: 0,
          visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
          sampler: {
            type: 'non-filtering',
          },
        },
      ],
    });

    const layout = t.device.createPipelineLayout({
      bindGroupLayouts: [bindGroupLayout0, bindGroupLayout1],
    });

    const success = !t.isCompatibility || textureType === 'texture_2d<f32>';
    switch (stage) {
      case 'compute':
        vtu.doCreateComputePipelineTest(t, async, success, {
          layout,
          compute: {
            module,
          },
        });
        break;
      case 'fragment':
      case 'vertex':
        vtu.doCreateRenderPipelineTest(t, async, success, {
          layout,
          vertex: {
            module,
          },
          fragment: {
            module,
            targets: [{ format: 'rgba8unorm' }],
          },
        });
        break;
    }
  });

function numCombosToNumber(maxCombos: number, numCombos: string): number {
  const m = /^max([+-]?)(\d+)$/.exec(numCombos);
  if (m) {
    if (m[1] === '+') {
      return maxCombos + parseInt(m[2]);
    } else if (m[1] === '-') {
      return maxCombos - parseInt(m[2]);
    } else {
      return maxCombos;
    }
  }
  return parseInt(numCombos);
}

function numNonSampledToNumber(maxTextures: number, numNonSampled: '0' | '1' | 'max') {
  switch (numNonSampled) {
    case '0':
      return 0;
    case '1':
      return 1;
    case 'max':
      return maxTextures;
  }
}

g.test('texture_sampler_combos')
  .desc(
    `
Test that you can not use more texture+sampler combos than
min(maxSamplersPerShaderStage, maxSampledTexturesPerShaderStage)
in compatibility mode.

The spec, copy and pasted here:

maxCombinationsPerStage = min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage)
for each stage of the pipeline:
  sum = 0
  for each texture binding in the pipeline layout which is visible to that stage:
    sum += max(1, number of texture sampler combos for that texture binding)
  for each external texture binding in the pipeline layout which is visible to that stage:
    sum += 1 // for LUT texture + LUT sampler
    sum += 3 * max(1, number of external_texture sampler combos) // for Y+U+V
  if sum > maxCombinationsPerStage
    generate a validation error.

`
  )
  .params(u =>
    // prettier-ignore
    u
      .combineWithParams([
        ...([
          { pass: true,  numCombos: 'max',   numNonSampled: '1',   numExternal: 0, useSame: false },
          { pass: false, numCombos: 'max+1', numNonSampled: '1',   numExternal: 0, useSame: false },
          { pass: true,  numCombos: '1',     numNonSampled: 'max', numExternal: 0, useSame: false },
          { pass: false, numCombos: '2',     numNonSampled: 'max', numExternal: 0, useSame: false },
          { pass: true,  numCombos: 'max-4', numNonSampled: '0',   numExternal: 1, useSame: false },
          { pass: false, numCombos: 'max-3', numNonSampled: '0',   numExternal: 1, useSame: false },
          { pass: true,  numCombos: 'max-8', numNonSampled: '0',   numExternal: 2, useSame: false },
          { pass: false, numCombos: 'max-7', numNonSampled: '0',   numExternal: 2, useSame: false },
          { pass: true,  numCombos: 'max-7', numNonSampled: '0',   numExternal: 2, useSame: true },
          { pass: false, numCombos: 'max-6', numNonSampled: '0',   numExternal: 2, useSame: true },
        ] as const).map(p => ([{ ...p, stages: 'vertex' }, { ...p, stages: 'fragment' }, { ...p, stages: 'compute' }] as const)).flat(),
        {   pass: true,  numCombos: 'max',   numNonSampled: '1',   numExternal: 0, useSame: false, stages: 'vertex,fragment' },
      ] as const)
      .combine('async', [false, true] as const)
  )
  .fn(t => {
    const { device } = t;
    const { pass, stages, numCombos, numNonSampled, numExternal, useSame, async } = t.params;
    const { maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage } = device.limits;

    const maxTexturesPerStage = maxSampledTexturesPerShaderStage - numExternal * 3;
    const maxCombos = Math.min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage);
    const numCombinations = numCombosToNumber(maxCombos, numCombos);
    const numNonSampledTextures = numNonSampledToNumber(
      maxSampledTexturesPerShaderStage,
      numNonSampled
    );

    const textureDeclarations: string[][] = [[], []];
    const samplerDeclarations: string[][] = [[], []];
    const usages: string[][] = [[], []];
    const bindGroupLayoutEntries: GPUBindGroupLayoutEntry[][] = [[], [], [], []];
    const numStages = stages === 'compute' ? 1 : 2;
    const visibilityByStage =
      stages === 'compute'
        ? [GPUShaderStage.COMPUTE]
        : [GPUShaderStage.VERTEX, GPUShaderStage.FRAGMENT];

    const addTextureDeclaration = (stage: number, binding: number) => {
      textureDeclarations[stage].push(
        `@group(${stage * 2}) @binding(${binding}) var t${stage}_${binding}: texture_2d<f32>;`
      );
      bindGroupLayoutEntries[stage * 2].push({
        binding,
        visibility: visibilityByStage[stage],
        texture: {},
      });
    };

    const addSamplerDeclaration = (stage: number, binding: number) => {
      samplerDeclarations[stage].push(
        `@group(${stage * 2 + 1}) @binding(${binding}) var s${stage}_${binding}: sampler;`
      );
      bindGroupLayoutEntries[stage * 2 + 1].push({
        binding,
        visibility: visibilityByStage[stage],
        sampler: {},
      });
    };

    const addExternalTextureDeclaration = (stage: number, binding: number, id: number) => {
      textureDeclarations[stage].push(
        `@group(${stage * 2}) @binding(${binding}) var e${stage}_${id}: texture_external;`
      );
      bindGroupLayoutEntries[stage * 2].push({
        binding,
        visibility: visibilityByStage[stage],
        externalTexture: {},
      });
    };

    for (let stage = 0; stage < numStages; ++stage) {
      let count = 0;
      for (let t = 0; count < numCombinations && t < maxTexturesPerStage; ++t) {
        addTextureDeclaration(stage, t);
        for (let s = 0; count < numCombinations && s < maxSamplersPerShaderStage; ++s) {
          if (t === 0) {
            addSamplerDeclaration(stage, s);
          }
          usages[stage].push(
            `  c += textureSampleLevel(t${stage}_${t}, s${stage}_${s}, vec2f(0), 0);`
          );
          ++count;
        }
      }

      for (let t = 0; t < numNonSampledTextures; ++t) {
        if (t >= textureDeclarations[stage].length) {
          addTextureDeclaration(stage, t);
        }
        usages[stage].push(`  c += textureLoad(t${stage}_${t}, vec2u(0), 0);`);
      }

      for (let t = 0; t < numExternal; ++t) {
        if (t === 0 || !useSame) {
          const et = textureDeclarations[stage].length + t;
          addExternalTextureDeclaration(stage, et, t);
        }
        const eBinding = useSame ? 0 : t;
        const sBinding = useSame ? t : 0;
        usages[stage].push(
          `  c += textureSampleBaseClampToEdge(e${stage}_${eBinding}, s${stage}_${sBinding}, vec2f(0));`
        );
      }
    }

    const code = `
${textureDeclarations[0].join('\n')}
${textureDeclarations[1].join('\n')}

${samplerDeclarations[0].join('\n')}
${samplerDeclarations[1].join('\n')}

fn usage0() -> vec4f {
  var c: vec4f;
  ${usages[0].join('\n')}
  return c;
}

fn usage1() -> vec4f {
  var c: vec4f;
  ${usages[1].join('\n')}
  return c;
}

@vertex fn vs() -> @builtin(position) vec4f {
  _ = ${stages.includes('vertex') ? 'usage0()' : 'vec4f(0)'};
  return vec4f(0);
}

@fragment fn fs() -> @location(0) vec4f {
  return ${stages.includes('fragment') ? 'usage1()' : 'vec4f(0)'};
}

@compute @workgroup_size(1) fn cs() {
  _ = usage0();
}

`;

    // MAINTENANCE_TODO: remove this. It's only needed because of a bug in dawn
    // with auto layouts.
    const layout = device.createPipelineLayout({
      bindGroupLayouts: bindGroupLayoutEntries.map(entries =>
        device.createBindGroupLayout({ entries })
      ),
    });

    const module = device.createShaderModule({ code });
    if (stages === 'compute') {
      vtu.doCreateComputePipelineTest(t, async, pass || !t.isCompatibility, {
        layout,
        compute: { module },
      });
    } else {
      vtu.doCreateRenderPipelineTest(t, async, pass || !t.isCompatibility, {
        layout,
        vertex: { module },
        fragment: { module, targets: [{ format: 'rgba8unorm' }] },
      });
    }
  });

g.test('fine_derivatives')
  .desc(
    `
Test that dpdxFine, dpdyFine, fwidthFine are disallowed in compatibility mode.
`
  )
  .params(u =>
    u
      .combine('builtin', [
        'dpdxCoarse', // to check the test itself, should pass always
        'dpdxFine',
        'dpdyFine',
        'fwidthFine',
      ] as const)
      .combine('async', [false, true] as const)
      .beginSubcases()
      .combine('type', ['f32', 'vec2f', 'vec3f', 'vec4f'])
  )
  .fn(t => {
    const { builtin, async, type } = t.params;

    const code = `
    struct VOut {
      @builtin(position) pos: vec4f,
      @location(0) v: ${type},
    };

    @vertex fn vs(@builtin(vertex_index) vNdx: u32) -> VOut {
      let pos = array(vec2f(-1, 3), vec2f(3, -1), vec2f(-1, -1));
      return VOut(vec4f(pos[vNdx], 0, 1), ${type}(pos[vNdx].x));
    }

    @fragment fn fs(v: VOut) -> @location(0) vec4f {
      _ = ${builtin}(v.v);
      return vec4f(0);
    }
    `;

    const module = t.device.createShaderModule({ code });

    const success = !t.isCompatibility || builtin === 'dpdxCoarse';
    vtu.doCreateRenderPipelineTest(t, async, success, {
      layout: 'auto',
      vertex: { module },
      fragment: { module, targets: [{ format: 'rgba8unorm' }] },
    });
  });