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

export const description = `
API validation tests for dynamic state commands (setViewport/ScissorRect/BlendColor...).

TODO: ensure existing tests cover these notes. Note many of these may be operation tests instead.
> - setViewport
>     - {x, y} = {0, invalid values if any}
>     - {width, height, minDepth, maxDepth} = {
>         - least possible value that's valid
>         - greatest possible negative value that's invalid
>         - greatest possible positive value that's valid
>         - least possible positive value that's invalid if any
>         - }
>     - minDepth {<, =, >} maxDepth
> - setScissorRect
>     - {width, height} = 0
>     - {x+width, y+height} = attachment size + 1
> - setBlendConstant
>     - color {slightly, very} out of range
>     - used with a simple pipeline that {does, doesn't} use it
> - setStencilReference
>     - {0, max}
>     - used with a simple pipeline that {does, doesn't} use it
`;

import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../../gpu_test.js';
import { nextAfterF32 } from '../../../../../util/math.js';

interface ViewportCall {
  x: number;
  y: number;
  w: number;
  h: number;
  minDepth: number;
  maxDepth: number;
}

interface ScissorCall {
  x: number;
  y: number;
  w: number;
  h: number;
}

class F extends AllFeaturesMaxLimitsGPUTest {
  testViewportCall(
    success: boolean,
    v: ViewportCall,
    attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
  ) {
    const attachment = this.createTextureTracked({
      format: 'rgba8unorm',
      size: attachmentSize,
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const encoder = this.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: attachment.createView(),
          loadOp: 'load',
          storeOp: 'store',
        },
      ],
    });
    pass.setViewport(v.x, v.y, v.w, v.h, v.minDepth, v.maxDepth);
    pass.end();

    this.expectValidationError(() => {
      encoder.finish();
    }, !success);
  }

  testScissorCall(
    success: boolean | 'type-error',
    s: ScissorCall,
    attachmentSize: GPUExtent3D = { width: 1, height: 1, depthOrArrayLayers: 1 }
  ) {
    const attachment = this.createTextureTracked({
      format: 'rgba8unorm',
      size: attachmentSize,
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const encoder = this.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: attachment.createView(),
          loadOp: 'load',
          storeOp: 'store',
        },
      ],
    });
    if (success === 'type-error') {
      this.shouldThrow('TypeError', () => {
        pass.setScissorRect(s.x, s.y, s.w, s.h);
      });
    } else {
      pass.setScissorRect(s.x, s.y, s.w, s.h);
      pass.end();

      this.expectValidationError(() => {
        encoder.finish();
      }, !success);
    }
  }

  createDummyRenderPassEncoder(): { encoder: GPUCommandEncoder; pass: GPURenderPassEncoder } {
    const attachment = this.createTextureTracked({
      format: 'rgba8unorm',
      size: [1, 1, 1],
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const encoder = this.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: attachment.createView(),
          loadOp: 'load',
          storeOp: 'store',
        },
      ],
    });

    return { encoder, pass };
  }
}

export const g = makeTestGroup(F);

g.test('setViewport,width_height_nonnegative')
  .desc(
    `Test that the width and height parameters of setViewport must be non-negative.

TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
TODO Test the first value smaller than -0`
  )
  .paramsSubcasesOnly([
    // Control case: everything to 0 is ok, covers the empty viewport case.
    { x: 0, y: 0, w: 0, h: 0 },

    // Negative width/height is invalid
    { x: 0, y: 0, w: -1, h: 0 },
    { x: 0, y: 0, w: 0, h: -1 },

    // Negative width/height is invalid even if the resulting bounds are positive
    { x: 1, y: 0, w: -1, h: 0 },
    { x: 0, y: 1, w: 0, h: -1 },
  ])
  .fn(t => {
    const { x, y, w, h } = t.params;
    const success = w >= 0 && h >= 0;
    t.testViewportCall(success, { x, y, w, h, minDepth: 0, maxDepth: 1 });
  });

g.test('setViewport,exceeds_attachment_size')
  .desc(`Test that the viewport can exceed the attachment size`)
  .paramsSubcasesOnly([
    { attachmentWidth: 3, attachmentHeight: 3 },
    { attachmentWidth: 1024, attachmentHeight: 1024 },
  ])
  .fn(t => {
    const { attachmentWidth, attachmentHeight } = t.params;
    t.testViewportCall(
      true,
      { x: 0, y: 0, w: attachmentWidth + 1, h: attachmentHeight + 1, minDepth: 0, maxDepth: 1 },
      { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
    );
  });

g.test('setViewport,xy_rect_contained_in_bounds')
  .desc(
    `Test that the rectangle defined by x, y, width, height must be contained in the maximum viewport bounds
and that the viewport size cannot exceed the maximum.`
  )
  .paramsSubcasesOnly(u =>
    u.combine('dimension', [0, 1]).combineWithParams([
      // Control case: max viewport is valid.
      { om: 0, od: 0, sd: 0, _success: true },

      // Other valid cases
      { om: -1, od: 0, sd: 0, _success: true },
      { om: -2, od: 0, sd: 0, _success: true },
      { om: 1, od: -1, sd: 0, _success: true },
      { om: 0, od: -1, sd: 0, _success: true },
      { om: 0, od: 1, sd: 0, _success: true },
      { om: 1, od: 0, sd: -1, _success: true },

      // Cases that go outside the allowed bounds
      { om: -2, od: -1, sd: 0, _success: false },
      { om: 1, od: 0, sd: 0, _success: false },
      { om: 1, od: 1, sd: -1, _success: false },
      { om: 1, od: 'negative', sd: 0, _success: false },

      // Case that exceeds the max viewport size
      { om: 0, od: 0, sd: 1, _success: false },
      { om: 0, od: 0, sd: 'positive', _success: false },
    ])
  )
  .fn(t => {
    const { dimension, om, od, sd, _success } = t.params;

    const maxViewportSize = t.device.limits.maxTextureDimension2D;

    const xy = [0, 0];
    const wh = [maxViewportSize, maxViewportSize];

    xy[dimension] = maxViewportSize * om;

    if (od === 'negative' || od === 'positive') {
      xy[dimension] = nextAfterF32(xy[dimension], od, 'no-flush');
    } else {
      xy[dimension] += od as number;
    }

    if (sd === 'negative' || sd === 'positive') {
      wh[dimension] = nextAfterF32(wh[dimension], sd, 'no-flush');
    } else {
      wh[dimension] += sd as number;
    }

    const x = xy[0];
    const y = xy[1];
    const w = wh[0];
    const h = wh[1];

    t.testViewportCall(_success, { x, y, w, h, minDepth: 0, maxDepth: 1 });
  });

g.test('setViewport,depth_rangeAndOrder')
  .desc('Test that 0 <= minDepth <= maxDepth <= 1')
  .paramsSubcasesOnly([
    // Success cases
    { minDepth: 0, maxDepth: 1 },
    { minDepth: -0, maxDepth: -0 },
    { minDepth: 1, maxDepth: 1 },
    { minDepth: 0.3, maxDepth: 0.7 },
    { minDepth: 0.7, maxDepth: 0.7 },
    { minDepth: 0.3, maxDepth: 0.3 },

    // Invalid cases
    { minDepth: -0.1, maxDepth: 1 },
    { minDepth: 0, maxDepth: 1.1 },
    { minDepth: 0.5, maxDepth: 0.49999 },
  ])
  .fn(t => {
    const { minDepth, maxDepth } = t.params;
    const success =
      0 <= minDepth && minDepth <= 1 && 0 <= maxDepth && maxDepth <= 1 && minDepth <= maxDepth;
    t.testViewportCall(success, { x: 0, y: 0, w: 1, h: 1, minDepth, maxDepth });
  });

g.test('setScissorRect,x_y_width_height_nonnegative')
  .desc(
    `Test that the parameters of setScissorRect to define the box must be non-negative or a TypeError is thrown.

TODO Test -0 (it should be valid) but can't be tested because the harness complains about duplicate parameters.
TODO Test the first value smaller than -0`
  )
  .paramsSubcasesOnly([
    // Control case: everything to 0 is ok, covers the empty scissor case.
    { x: 0, y: 0, w: 0, h: 0 },

    // Test -1
    { x: -1, y: 0, w: 0, h: 0 },
    { x: 0, y: -1, w: 0, h: 0 },
    { x: 0, y: 0, w: -1, h: 0 },
    { x: 0, y: 0, w: 0, h: -1 },
  ])
  .fn(t => {
    const { x, y, w, h } = t.params;
    const success = x >= 0 && y >= 0 && w >= 0 && h >= 0;
    t.testScissorCall(success ? true : 'type-error', { x, y, w, h });
  });

g.test('setScissorRect,xy_rect_contained_in_attachment')
  .desc(
    'Test that the rectangle defined by x, y, width, height must be contained in the attachments'
  )
  .paramsSubcasesOnly(u =>
    u
      .combineWithParams([
        { attachmentWidth: 3, attachmentHeight: 5 },
        { attachmentWidth: 5, attachmentHeight: 3 },
        { attachmentWidth: 1024, attachmentHeight: 1 },
        { attachmentWidth: 1, attachmentHeight: 1024 },
      ])
      .combineWithParams([
        // Control case: a full scissor is valid.
        { dx: 0, dy: 0, dw: 0, dh: 0 },

        // Other valid cases with a partial scissor.
        { dx: 1, dy: 0, dw: -1, dh: 0 },
        { dx: 0, dy: 1, dw: 0, dh: -1 },
        { dx: 0, dy: 0, dw: -1, dh: 0 },
        { dx: 0, dy: 0, dw: 0, dh: -1 },

        // Test with a small value that causes the scissor to go outside the attachment.
        { dx: 1, dy: 0, dw: 0, dh: 0 },
        { dx: 0, dy: 1, dw: 0, dh: 0 },
        { dx: 0, dy: 0, dw: 1, dh: 0 },
        { dx: 0, dy: 0, dw: 0, dh: 1 },
      ])
  )
  .fn(t => {
    const { attachmentWidth, attachmentHeight, dx, dy, dw, dh } = t.params;
    const x = dx;
    const y = dy;
    const w = attachmentWidth + dw;
    const h = attachmentWidth + dh;

    const success = x + w <= attachmentWidth && y + h <= attachmentHeight;
    t.testScissorCall(
      success,
      { x, y, w, h },
      { width: attachmentWidth, height: attachmentHeight, depthOrArrayLayers: 1 }
    );
  });

g.test('setBlendConstant')
  .desc('Test that almost any color value is valid for setBlendConstant')
  .paramsSubcasesOnly([
    { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
    { r: -1.0, g: -1.0, b: -1.0, a: -1.0 },
    { r: Number.MAX_SAFE_INTEGER, g: Number.MIN_SAFE_INTEGER, b: -0, a: 100000 },
  ])
  .fn(t => {
    const { r, g, b, a } = t.params;
    const encoders = t.createDummyRenderPassEncoder();
    encoders.pass.setBlendConstant({ r, g, b, a });
    encoders.pass.end();
    encoders.encoder.finish();
  });

g.test('setStencilReference')
  .desc('Test that almost any stencil reference value is valid for setStencilReference')
  .paramsSubcasesOnly([
    { value: 1 }, //
    { value: 0 },
    { value: 1000 },
    { value: 0xffffffff },
  ])
  .fn(t => {
    const { value } = t.params;
    const encoders = t.createDummyRenderPassEncoder();
    encoders.pass.setStencilReference(value);
    encoders.pass.end();
    encoders.encoder.finish();
  });