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

export const description = `
createPipelineLayout validation tests.

TODO: review existing tests, write descriptions, and make sure tests are complete.
`;

import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { getGPU } from '../../../common/util/navigator_gpu.js';
import { count, range, supportsImmediateData } from '../../../common/util/util.js';
import {
  bufferBindingTypeInfo,
  getBindingLimitForBindingType,
  kBufferBindingTypes,
} from '../../capability_info.js';
import { GPUConst } from '../../constants.js';

function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
  return JSON.parse(JSON.stringify(descriptor));
}

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

g.test('number_of_dynamic_buffers_exceeds_the_maximum_value')
  .desc(
    `
    Test that creating a pipeline layout fails with a validation error if the number of dynamic
    buffers exceeds the maximum value in the pipeline layout.
    - Test that creation of a pipeline using the maximum number of dynamic buffers added a dynamic
      buffer fails.

    TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.
  `
  )
  .paramsSubcasesOnly(u =>
    u //
      .combine('visibility', [0, 2, 4, 6])
      .combine('type', kBufferBindingTypes)
  )
  .fn(t => {
    const { type, visibility } = t.params;
    const info = bufferBindingTypeInfo({ type });
    const { maxDynamicLimit } = info.perPipelineLimitClass;
    const limit = getBindingLimitForBindingType(t.device, visibility, { buffer: { type } });
    const maxDynamic = Math.min(maxDynamicLimit ? t.device.limits[maxDynamicLimit]! : 0, limit);

    t.skipIf(limit === 0, `binding limit for ${type} === 0`);

    const maxDynamicBufferBindings: GPUBindGroupLayoutEntry[] = [];
    for (let binding = 0; binding < maxDynamic; binding++) {
      maxDynamicBufferBindings.push({
        binding,
        visibility,
        buffer: { type, hasDynamicOffset: true },
      });
    }

    const maxDynamicBufferBindGroupLayout = t.device.createBindGroupLayout({
      entries: maxDynamicBufferBindings,
    });

    const goodDescriptor = {
      entries: [{ binding: 0, visibility, buffer: { type, hasDynamicOffset: false } }],
    };

    if (limit > maxDynamic) {
      const goodPipelineLayoutDescriptor = {
        bindGroupLayouts: [
          maxDynamicBufferBindGroupLayout,
          t.device.createBindGroupLayout(goodDescriptor),
        ],
      };

      // Control case
      t.device.createPipelineLayout(goodPipelineLayoutDescriptor);
    }

    // Check dynamic buffers exceed maximum in pipeline layout.
    const badDescriptor = clone(goodDescriptor);
    badDescriptor.entries[0].buffer.hasDynamicOffset = true;

    const badPipelineLayoutDescriptor = {
      bindGroupLayouts: [
        maxDynamicBufferBindGroupLayout,
        t.device.createBindGroupLayout(badDescriptor),
      ],
    };

    t.expectValidationError(() => {
      t.device.createPipelineLayout(badPipelineLayoutDescriptor);
    });
  });

g.test('number_of_bind_group_layouts_exceeds_the_maximum_value')
  .desc(
    `
    Test that creating a pipeline layout fails with a validation error if the number of bind group
    layouts exceeds the maximum value in the pipeline layout.
    - Test that creation of a pipeline using the maximum number of bind groups added a bind group
      fails.
  `
  )
  .fn(t => {
    const bindGroupLayoutDescriptor: GPUBindGroupLayoutDescriptor = {
      entries: [],
    };

    const maxBindGroupLayouts = range(t.device.limits.maxBindGroups, () =>
      t.device.createBindGroupLayout(bindGroupLayoutDescriptor)
    );

    const goodPipelineLayoutDescriptor = {
      bindGroupLayouts: maxBindGroupLayouts,
    };

    // Control case
    t.device.createPipelineLayout(goodPipelineLayoutDescriptor);

    // Check bind group layouts exceed maximum in pipeline layout.
    const badPipelineLayoutDescriptor = {
      bindGroupLayouts: [
        ...maxBindGroupLayouts,
        t.device.createBindGroupLayout(bindGroupLayoutDescriptor),
      ],
    };

    t.expectValidationError(() => {
      t.device.createPipelineLayout(badPipelineLayoutDescriptor);
    });
  });

g.test('bind_group_layouts,device_mismatch')
  .desc(
    `
    Tests createPipelineLayout cannot be called with bind group layouts created from another device
    Test with two layouts to make sure all layouts can be validated:
    - layout0 and layout1 from same device
    - layout0 and layout1 from different device
    `
  )
  .paramsSubcasesOnly([
    { layout0Mismatched: false, layout1Mismatched: false }, // control case
    { layout0Mismatched: true, layout1Mismatched: false },
    { layout0Mismatched: false, layout1Mismatched: true },
  ])
  .beforeAllSubcases(t => t.usesMismatchedDevice())
  .fn(t => {
    const { layout0Mismatched, layout1Mismatched } = t.params;

    const mismatched = layout0Mismatched || layout1Mismatched;

    const bglDescriptor: GPUBindGroupLayoutDescriptor = {
      entries: [],
    };

    const layout0 = layout0Mismatched
      ? t.mismatchedDevice.createBindGroupLayout(bglDescriptor)
      : t.device.createBindGroupLayout(bglDescriptor);
    const layout1 = layout1Mismatched
      ? t.mismatchedDevice.createBindGroupLayout(bglDescriptor)
      : t.device.createBindGroupLayout(bglDescriptor);

    t.expectValidationError(() => {
      t.device.createPipelineLayout({ bindGroupLayouts: [layout0, layout1] });
    }, mismatched);
  });

const MaybeNullBindGroupLayoutTypes = ['Null', 'Undefined', 'Empty', 'NonEmpty'] as const;

g.test('bind_group_layouts,null_bind_group_layouts')
  .desc(
    `
    Tests that it is valid to create a pipeline layout with null bind group layouts.
    `
  )
  .paramsSubcasesOnly(u =>
    u //
      .combine('_bglCount', [1, 2, 3, 4] as const)
      .combine('_bgl0', MaybeNullBindGroupLayoutTypes)
      .expand('_bgl1', p => (p._bglCount > 1 ? MaybeNullBindGroupLayoutTypes : (['Null'] as const)))
      .expand('_bgl2', p => (p._bglCount > 2 ? MaybeNullBindGroupLayoutTypes : (['Null'] as const)))
      .expand('_bgl3', p => (p._bglCount > 3 ? MaybeNullBindGroupLayoutTypes : (['Null'] as const)))
      // Flatten the result down into a single subcase arg which is an array of BGL types.
      .expand('bindGroupLayouts', p => [[p._bgl0, p._bgl1, p._bgl2, p._bgl3].slice(0, p._bglCount)])
      // Only test combinations where exactly one of the BGLs is null|undefined|empty.
      .filter(p => count(p.bindGroupLayouts, x => x !== 'NonEmpty') === 1)
  )
  .fn(t => {
    const nonEmptyBindGroupLayout = t.device.createBindGroupLayout({
      entries: [
        {
          binding: 0,
          visibility: GPUConst.ShaderStage.COMPUTE,
          texture: {},
        },
      ],
    });

    const bindGroupLayouts = t.params.bindGroupLayouts.map(bindGroupLayoutType => {
      switch (bindGroupLayoutType) {
        case 'Null':
          return null;
        case 'Undefined':
          return undefined;
        case 'Empty':
          return t.device.createBindGroupLayout({ entries: [] });
        case 'NonEmpty':
          return nonEmptyBindGroupLayout;
      }
    });

    const kShouldError = false;
    t.expectValidationError(() => {
      t.device.createPipelineLayout({ bindGroupLayouts });
    }, kShouldError);
  });

g.test('bind_group_layouts,create_pipeline_with_null_bind_group_layouts')
  .desc(
    `
  Tests that it is valid to create a render pipeline or compute pipeline with a pipeline layout
  created with null bind group layouts as long as the pipeline layout matches the declarations in
  the shaders.
  `
  )
  .paramsSubcasesOnly(u =>
    u
      .combine('pipelineType', ['Render', 'Compute'] as const)
      .combine('emptyBindGroupLayoutType', ['Null', 'Undefined'] as const)
      .combine('emptyBindGroupLayoutIndex', [0, 1, 2, 3] as const)
      .combine('emptyBindGroupLayoutIndexMissedInShader', [true, false])
  )
  .fn(t => {
    const {
      pipelineType,
      emptyBindGroupLayoutType,
      emptyBindGroupLayoutIndex,
      emptyBindGroupLayoutIndexMissedInShader,
    } = t.params;

    const bindGroupLayouts: (GPUBindGroupLayout | null | undefined)[] = [];
    for (let i = 0; i < 4; ++i) {
      if (i === emptyBindGroupLayoutIndex) {
        switch (emptyBindGroupLayoutType) {
          case 'Null':
            bindGroupLayouts.push(null);
            break;
          case 'Undefined':
            bindGroupLayouts.push(undefined);
            break;
        }
      } else {
        const nonEmptyBindGroupLayout = t.device.createBindGroupLayout({
          entries: [
            {
              binding: 0,
              visibility: GPUConst.ShaderStage.COMPUTE | GPUConst.ShaderStage.FRAGMENT,
              buffer: {
                type: 'uniform',
              },
            },
          ],
        });
        bindGroupLayouts.push(nonEmptyBindGroupLayout);
      }
    }
    const layout = t.device.createPipelineLayout({ bindGroupLayouts });

    let declarations = '';
    let statement = '_ = 1';
    for (let i = 0; i < 4; ++i) {
      if (emptyBindGroupLayoutIndexMissedInShader && i === emptyBindGroupLayoutIndex) {
        continue;
      }
      declarations += `@group(${i}) @binding(0) var<uniform> input${i} : u32;\n`;
      statement += ` + input${i}`;
    }

    const shouldError = !emptyBindGroupLayoutIndexMissedInShader;

    switch (pipelineType) {
      case 'Render': {
        const code = `
        ${declarations}
        @vertex
        fn vert_main() -> @builtin(position) vec4f {
            return vec4f(0.0, 0.0, 0.0, 1.0);
        }

        @fragment
        fn frag_main() -> @location(0) vec4f {
            ${statement};
            return vec4f(0.0, 0.0, 0.0, 1.0);
        }
        `;
        const shaderModule = t.device.createShaderModule({
          code,
        });

        t.expectValidationError(() => {
          t.device.createRenderPipeline({
            layout,
            vertex: {
              module: shaderModule,
            },
            fragment: {
              module: shaderModule,
              targets: [
                {
                  format: 'rgba8unorm',
                },
              ],
            },
          });
        }, shouldError);
        break;
      }

      case 'Compute': {
        const code = `
        ${declarations}
        @compute @workgroup_size(1) fn cs_main() {
          ${statement};
        }
        `;
        const shaderModule = t.device.createShaderModule({
          code,
        });
        t.expectValidationError(() => {
          t.device.createComputePipeline({
            layout,
            compute: {
              module: shaderModule,
            },
          });
        }, shouldError);
        break;
      }
    }
  });

g.test('bind_group_layouts,set_pipeline_with_null_bind_group_layouts')
  .desc(
    `
  Tests that it is valid to use a render pipeline or compute pipeline with null bind group layouts
  used in the pipeline layout in a render pass encoder or compute pass encoder.
`
  )
  .paramsSubcasesOnly(u =>
    u
      .combine('pipelineType', ['Render', 'Compute'] as const)
      .combine('emptyBindGroupLayoutType', ['Null', 'Undefined'] as const)
      .combine('emptyBindGroupLayoutIndex', [0, 1, 2, 3] as const)
      .combine('setBindGroupOnEmptyBindGroupLayoutIndex', [true, false])
  )
  .fn(t => {
    const {
      pipelineType,
      emptyBindGroupLayoutType,
      emptyBindGroupLayoutIndex,
      setBindGroupOnEmptyBindGroupLayoutIndex,
    } = t.params;

    const nonEmptyBindGroupLayout = t.device.createBindGroupLayout({
      entries: [
        {
          binding: 0,
          visibility: GPUConst.ShaderStage.COMPUTE | GPUConst.ShaderStage.FRAGMENT,
          buffer: {
            type: 'uniform',
          },
        },
      ],
    });
    const bindGroups: GPUBindGroup[] = [];
    const bindGroupLayouts: (GPUBindGroupLayout | null | undefined)[] = [];
    let declarations = '';
    let statement = '_ = 1';
    for (let i = 0; i < 4; ++i) {
      if (i === emptyBindGroupLayoutIndex) {
        switch (emptyBindGroupLayoutType) {
          case 'Null':
            bindGroupLayouts.push(null);
            break;
          case 'Undefined':
            bindGroupLayouts.push(undefined);
            break;
        }
      } else {
        bindGroupLayouts.push(nonEmptyBindGroupLayout);
        declarations += `@group(${i}) @binding(0) var<uniform> input${i} : u32;\n`;
        statement += ` + input${i}`;
      }

      const buffer = t.createBufferTracked({
        usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
        size: 4,
      });
      const bindGroup = t.device.createBindGroup({
        layout: nonEmptyBindGroupLayout,
        entries: [
          {
            binding: 0,
            resource: {
              buffer,
            },
          },
        ],
      });
      bindGroups.push(bindGroup);
    }
    const layout = t.device.createPipelineLayout({ bindGroupLayouts });

    const shouldError = false;
    switch (pipelineType) {
      case 'Render': {
        const code = `
      ${declarations}
      @vertex
      fn vert_main() -> @builtin(position) vec4f {
          return vec4f(0.0, 0.0, 0.0, 1.0);
      }
      @fragment
      fn frag_main() -> @location(0) vec4f {
          ${statement};
          return vec4f(0.0, 0.0, 0.0, 1.0);
      }
      `;
        const shaderModule = t.device.createShaderModule({
          code,
        });

        const format = 'rgba8unorm';
        const renderPipeline = t.device.createRenderPipeline({
          layout,
          vertex: {
            module: shaderModule,
          },
          fragment: {
            module: shaderModule,
            targets: [
              {
                format,
              },
            ],
          },
        });

        const renderTarget = t.createTextureTracked({
          usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
          size: [1, 1, 1],
          format,
        });
        const commandEncoder = t.device.createCommandEncoder();
        const renderPassEncoder = commandEncoder.beginRenderPass({
          colorAttachments: [
            {
              view: renderTarget.createView(),
              loadOp: 'load',
              storeOp: 'store',
            },
          ],
        });
        for (let i = 0; i < 4; ++i) {
          if (!setBindGroupOnEmptyBindGroupLayoutIndex && i === emptyBindGroupLayoutIndex) {
            continue;
          }
          renderPassEncoder.setBindGroup(i, bindGroups[i]);
        }
        renderPassEncoder.setPipeline(renderPipeline);
        renderPassEncoder.draw(1);
        renderPassEncoder.end();

        t.expectValidationError(() => {
          t.queue.submit([commandEncoder.finish()]);
        }, shouldError);
        break;
      }

      case 'Compute': {
        const code = `
      ${declarations}
      @compute @workgroup_size(1) fn cs_main() {
        ${statement};
      }
      `;
        const shaderModule = t.device.createShaderModule({
          code,
        });

        const computePipeline = t.device.createComputePipeline({
          layout,
          compute: {
            module: shaderModule,
          },
        });
        const commandEncoder = t.device.createCommandEncoder();
        const computePassEncoder = commandEncoder.beginComputePass();
        for (let i = 0; i < 4; ++i) {
          if (!setBindGroupOnEmptyBindGroupLayoutIndex && i === emptyBindGroupLayoutIndex) {
            continue;
          }
          computePassEncoder.setBindGroup(i, bindGroups[i]);
        }
        computePassEncoder.setPipeline(computePipeline);
        computePassEncoder.dispatchWorkgroups(1);
        computePassEncoder.end();

        t.expectValidationError(() => {
          t.queue.submit([commandEncoder.finish()]);
        }, shouldError);
        break;
      }
    }
  });

g.test('immediate_data_size')
  .desc(
    `
    Test that creating a pipeline layout with immediateSize validates:
    - immediateSize must be a multiple of 4.
    - immediateSize must be <= device.limits.maxImmediateSize.
  `
  )
  .params(u => {
    const validImmediateSizes = [0, 4, 'max'] as const;
    const invalidAlignmentImmediateSizes = [1, 2, 3, 5] as const;
    const invalidRangeImmediateSizes = ['exceedLimit'] as const;
    return u.combine('immediateSize', [
      ...validImmediateSizes,
      ...invalidAlignmentImmediateSizes,
      ...invalidRangeImmediateSizes,
    ] as const);
  })
  .fn(t => {
    t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');

    const maxImmediateSize = t.device.limits.maxImmediateSize;

    const { immediateSize: sizeVariant } = t.params;
    let size: number;
    switch (sizeVariant) {
      case 'max':
        size = maxImmediateSize;
        break;
      case 'exceedLimit':
        size = maxImmediateSize + 4;
        break;
      default:
        size = sizeVariant;
        break;
    }

    const descriptor = {
      bindGroupLayouts: [],
      immediateSize: size,
    };

    const isMultipleOf4 = size % 4 === 0;
    const isWithinLimit = size <= maxImmediateSize;
    const shouldSucceed = isMultipleOf4 && isWithinLimit;

    t.expectValidationError(() => {
      t.device.createPipelineLayout(descriptor);
    }, !shouldSucceed);
  });