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

export const description = `
- Test pipeline outputs with different color attachment number, formats, component counts, etc.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { range } from '../../../../common/util/util.js';
import {
  computeBytesPerSampleFromFormats,
  getColorRenderByteCost,
  getTextureFormatType,
  isSintOrUintFormat,
  kPossibleColorRenderableTextureFormats,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';
import { getFragmentShaderCodeWithOutput, getPlainTypeInfo } from '../../../util/shader.js';
import { kTexelRepresentationInfo } from '../../../util/texture/texel_data.js';

const kVertexShader = `
@vertex fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
  var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
      vec2<f32>(-1.0, -3.0),
      vec2<f32>(3.0, 1.0),
      vec2<f32>(-1.0, 1.0));
  return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
`;

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

// Values to write into each attachment
// We make values different for each attachment index and each channel
// to make sure they didn't get mixed up

// Clamp alpha to 3 to avoid comparing a large expected value with a max 3 value for rgb10a2uint
// MAINTENANCE_TODO: Make TexelRepresentation.numericRange per-component and use that.
const attachmentsIntWriteValues = [
  { R: 1, G: 2, B: 3, A: 1 },
  { R: 5, G: 6, B: 7, A: 2 },
  { R: 9, G: 10, B: 11, A: 3 },
  { R: 13, G: 14, B: 15, A: 0 },
];
const attachmentsFloatWriteValues = [
  { R: 0.12, G: 0.34, B: 0.56, A: 0 },
  { R: 0.78, G: 0.9, B: 0.19, A: 1 },
  { R: 0.28, G: 0.37, B: 0.46, A: 0.3 },
  { R: 0.55, G: 0.64, B: 0.73, A: 1 },
];

g.test('color,attachments')
  .desc(`Test that pipeline with sparse color attachments write values correctly.`)
  .params(u =>
    u
      .combine('format', kPossibleColorRenderableTextureFormats)
      .beginSubcases()
      .combine('attachmentCount', [2, 3, 4])
      .expand('emptyAttachmentId', p => range(p.attachmentCount, i => i))
  )
  .fn(t => {
    const { format, attachmentCount, emptyAttachmentId } = t.params;

    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatNotUsableAsRenderAttachment(format);

    const componentCount = kTexelRepresentationInfo[format].componentOrder.length;

    // We only need to test formats that have a valid color attachment bytes per sample.
    const pixelByteCost = getColorRenderByteCost(format);
    t.skipIf(
      pixelByteCost === undefined ||
        computeBytesPerSampleFromFormats(range(attachmentCount, () => format)) >
          t.device.limits.maxColorAttachmentBytesPerSample
    );

    const writeValues = isSintOrUintFormat(format)
      ? attachmentsIntWriteValues
      : attachmentsFloatWriteValues;

    const renderTargets = range(attachmentCount, () =>
      t.createTextureTracked({
        format,
        size: { width: 1, height: 1, depthOrArrayLayers: 1 },
        usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
      })
    );
    const pipeline = t.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: kVertexShader,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: t.device.createShaderModule({
          code: getFragmentShaderCodeWithOutput(
            range(attachmentCount, i =>
              i === emptyAttachmentId
                ? null
                : {
                    values: [
                      writeValues[i].R,
                      writeValues[i].G,
                      writeValues[i].B,
                      writeValues[i].A,
                    ],
                    plainType: getPlainTypeInfo(getTextureFormatType(format)!),
                    componentCount,
                  }
            )
          ),
        }),
        entryPoint: 'main',
        targets: range(attachmentCount, i => (i === emptyAttachmentId ? null : { format })),
      },
      primitive: { topology: 'triangle-list' },
    });

    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: range(attachmentCount, i =>
        i === emptyAttachmentId
          ? null
          : {
              view: renderTargets[i].createView(),
              clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 0.5 },
              loadOp: 'clear',
              storeOp: 'store',
            }
      ),
    });
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    for (let i = 0; i < attachmentCount; i++) {
      if (i === emptyAttachmentId) {
        continue;
      }
      ttu.expectSinglePixelComparisonsAreOkInTexture(t, { texture: renderTargets[i] }, [
        { coord: { x: 0, y: 0 }, exp: writeValues[i] },
      ]);
    }
  });

g.test('color,component_count')
  .desc(
    `Test that extra components of the output (e.g. f32, vec2<f32>, vec3<f32>, vec4<f32>) are discarded.`
  )
  .params(u =>
    u
      .combine('format', kPossibleColorRenderableTextureFormats)
      .beginSubcases()
      .combine('componentCount', [1, 2, 3, 4])
      .filter(x => x.componentCount >= kTexelRepresentationInfo[x.format].componentOrder.length)
  )
  .fn(t => {
    const { format, componentCount } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatNotUsableAsRenderAttachment(format);

    // expected RGBA values
    // extra channels are discarded
    const values = [0, 1, 0, 1];

    const renderTarget = t.createTextureTracked({
      format,
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const pipeline = t.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: kVertexShader,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: t.device.createShaderModule({
          code: getFragmentShaderCodeWithOutput([
            {
              values,
              plainType: getPlainTypeInfo(getTextureFormatType(format)!),
              componentCount,
            },
          ]),
        }),
        entryPoint: 'main',
        targets: [{ format }],
      },
      primitive: { topology: 'triangle-list' },
    });

    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: renderTarget.createView(),
          clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    t.expectSingleColor(renderTarget, format, {
      size: [1, 1, 1],
      exp: { R: values[0], G: values[1], B: values[2], A: values[3] },
    });
  });

g.test('color,component_count,blend')
  .desc(
    `Test that blending behaves correctly when:
- fragment output has no alpha, but the src alpha is not used for the blend operation indicated by blend factors
- attachment format has no alpha, and the dst alpha should be assumed as 1

The attachment has a load value of [1, 0, 0, 1]
`
  )
  .params(u =>
    u
      .combine('format', ['r8unorm', 'rg8unorm', 'rgba8unorm', 'bgra8unorm'] as const)
      .beginSubcases()
      // _result is expected values in the color attachment (extra channels are discarded)
      // output is the fragment shader output vector
      // 0.498 -> 0x7f, 0.502 -> 0x80
      .combineWithParams([
        // fragment output has no alpha
        {
          _result: [0, 0, 0, 0],
          output: [0],
          colorSrcFactor: 'one',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0, 0, 0, 0],
          output: [0],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [1, 0, 0, 0],
          output: [0],
          colorSrcFactor: 'one-minus-dst-alpha',
          colorDstFactor: 'dst-alpha',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'one',
        },
        {
          _result: [0.498, 0, 0, 0],
          output: [0.498],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'one',
        },
        {
          _result: [0, 1, 0, 0],
          output: [0, 1],
          colorSrcFactor: 'one',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0, 1, 0, 0],
          output: [0, 1],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [1, 0, 0, 0],
          output: [0, 1],
          colorSrcFactor: 'one-minus-dst-alpha',
          colorDstFactor: 'dst-alpha',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'one',
        },
        {
          _result: [0, 1, 0, 0],
          output: [0, 1, 0],
          colorSrcFactor: 'one',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0, 1, 0, 0],
          output: [0, 1, 0],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'zero',
        },
        {
          _result: [1, 0, 0, 0],
          output: [0, 1, 0],
          colorSrcFactor: 'one-minus-dst-alpha',
          colorDstFactor: 'dst-alpha',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'one',
        },
        // fragment output has alpha
        {
          _result: [0.502, 1, 0, 0.498],
          output: [0, 1, 0, 0.498],
          colorSrcFactor: 'one',
          colorDstFactor: 'one-minus-src-alpha',
          alphaSrcFactor: 'one',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0.502, 0.498, 0, 0.498],
          output: [0, 1, 0, 0.498],
          colorSrcFactor: 'src-alpha',
          colorDstFactor: 'one-minus-src-alpha',
          alphaSrcFactor: 'one',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0, 1, 0, 0.498],
          output: [0, 1, 0, 0.498],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'one',
          alphaDstFactor: 'zero',
        },
        {
          _result: [0, 1, 0, 0.498],
          output: [0, 1, 0, 0.498],
          colorSrcFactor: 'dst-alpha',
          colorDstFactor: 'zero',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'src',
        },
        {
          _result: [1, 0, 0, 1],
          output: [0, 1, 0, 0.498],
          colorSrcFactor: 'one-minus-dst-alpha',
          colorDstFactor: 'dst-alpha',
          alphaSrcFactor: 'zero',
          alphaDstFactor: 'dst-alpha',
        },
      ] as const)
      .filter(x => x.output.length >= kTexelRepresentationInfo[x.format].componentOrder.length)
  )
  .fn(t => {
    const {
      format,
      _result,
      output,
      colorSrcFactor,
      colorDstFactor,
      alphaSrcFactor,
      alphaDstFactor,
    } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatNotBlendable(format);

    const componentCount = output.length;

    const renderTarget = t.createTextureTracked({
      format,
      size: { width: 1, height: 1, depthOrArrayLayers: 1 },
      usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const pipeline = t.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: t.device.createShaderModule({
          code: kVertexShader,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: t.device.createShaderModule({
          code: getFragmentShaderCodeWithOutput([
            {
              values: output,
              plainType: getPlainTypeInfo(getTextureFormatType(format)!),
              componentCount,
            },
          ]),
        }),
        entryPoint: 'main',
        targets: [
          {
            format,
            blend: {
              color: {
                srcFactor: colorSrcFactor,
                dstFactor: colorDstFactor,
                operation: 'add',
              },
              alpha: {
                srcFactor: alphaSrcFactor,
                dstFactor: alphaDstFactor,
                operation: 'add',
              },
            },
          },
        ],
      },
      primitive: { topology: 'triangle-list' },
    });

    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: renderTarget.createView(),
          clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
    t.device.queue.submit([encoder.finish()]);

    t.expectSingleColor(renderTarget, format, {
      size: [1, 1, 1],
      exp: { R: _result[0], G: _result[1], B: _result[2], A: _result[3] },
    });
  });