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

export const description = `
Execution tests for discard.

The discard statement converts invocations into helpers.
This results in the following conditions:
  * No outputs are written
  * No resources are written
  * Atomics are undefined

Conditions that still occur:
  * Derivative calculations are correct
  * Reads
  * Writes to non-external memory
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { iterRange } from '../../../../common/util/util.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTestBase } from '../../../gpu_test.js';
import { checkElementsPassPredicate } from '../../../util/check_contents.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

// Framebuffer dimensions
const kWidth = 64;
const kHeight = 64;

const kSharedCode = `
@group(0) @binding(0) var<storage, read_write> output: array<vec2f>;
@group(0) @binding(1) var<storage, read_write> atomicIndex : atomic<u32>;
@group(0) @binding(2) var<uniform> uniformValues : array<vec4u, 5>;

@vertex
fn vsMain(@builtin(vertex_index) index : u32) -> @builtin(position) vec4f {
  const vertices = array(
    vec2(-1, -1), vec2(-1,  0), vec2( 0, -1),
    vec2(-1,  0), vec2( 0,  0), vec2( 0, -1),

    vec2( 0, -1), vec2( 0,  0), vec2( 1, -1),
    vec2( 0,  0), vec2( 1,  0), vec2( 1, -1),

    vec2(-1,  0), vec2(-1,  1), vec2( 0,  0),
    vec2(-1,  1), vec2( 0,  1), vec2( 0,  0),

    vec2( 0,  0), vec2( 0,  1), vec2( 1,  0),
    vec2( 0,  1), vec2( 1,  1), vec2( 1,  0),
  );
  return vec4f(vec2f(vertices[index]), 0, 1);
}
`;

function drawFullScreen(
  t: GPUTestBase,
  code: string,
  useStorageBuffers: boolean,
  dataChecker: (a: Float32Array) => Error | undefined,
  framebufferChecker: (a: Uint32Array) => Error | undefined
) {
  t.skipIf(
    useStorageBuffers &&
      t.isCompatibility &&
      !(t.device.limits.maxStorageBuffersInFragmentStage! >= 2),
    `maxStorageBuffersInFragmentStage${t.device.limits.maxStorageBuffersInFragmentStage} is less than 2`
  );

  const pipeline = t.device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: t.device.createShaderModule({ code }),
      entryPoint: 'vsMain',
    },
    fragment: {
      module: t.device.createShaderModule({ code }),
      entryPoint: 'fsMain',
      targets: [{ format: 'rg32uint' }],
    },
    primitive: {
      topology: 'triangle-list',
    },
  });

  const bytesPerWord = 4;
  const framebuffer = t.createTextureTracked({
    size: [kWidth, kHeight],
    usage:
      GPUTextureUsage.COPY_SRC |
      GPUTextureUsage.COPY_DST |
      GPUTextureUsage.RENDER_ATTACHMENT |
      GPUTextureUsage.TEXTURE_BINDING,
    format: 'rg32uint',
  });

  // Create a buffer to copy the framebuffer contents into.
  // Initialize with a sentinel value and load this buffer to detect unintended writes.
  const fbBuffer = t.makeBufferWithContents(
    new Uint32Array([...iterRange(kWidth * kHeight * 2, x => kWidth * kHeight)]),
    GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
  );

  // Create a buffer to hold the storage shader resources.
  // (0,0) = vec2u width * height
  // (0,1) = u32
  const dataSize = 2 * kWidth * kHeight * bytesPerWord;
  const dataBufferSize = dataSize + bytesPerWord;
  const dataBuffer = t.createBufferTracked({
    size: dataBufferSize,
    usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
  });

  const uniformSize = bytesPerWord * 5 * 4;
  const uniformBuffer = t.makeBufferWithContents(
    // Loop bound, [derivative constants].
    // prettier-ignore
    new Uint32Array([
      4, 0, 0, 0,
      1, 0, 0, 0,
      4, 0, 0, 0,
      4, 0, 0, 0,
      7, 0, 0, 0,
    ]),
    GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM
  );

  // 'atomicIndex' packed at the end of the buffer.
  const bg = t.device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      ...(useStorageBuffers
        ? [
            {
              binding: 0,
              resource: {
                buffer: dataBuffer,
                offset: 0,
                size: dataSize,
              },
            },
            {
              binding: 1,
              resource: {
                buffer: dataBuffer,
                offset: dataSize,
                size: bytesPerWord,
              },
            },
          ]
        : []),
      {
        binding: 2,
        resource: {
          buffer: uniformBuffer,
          offset: 0,
          size: uniformSize,
        },
      },
    ],
  });

  const encoder = t.device.createCommandEncoder();
  encoder.copyBufferToTexture(
    {
      buffer: fbBuffer,
      offset: 0,
      bytesPerRow: kWidth * bytesPerWord * 2,
      rowsPerImage: kHeight,
    },
    { texture: framebuffer },
    { width: kWidth, height: kHeight }
  );
  const pass = encoder.beginRenderPass({
    colorAttachments: [
      {
        view: framebuffer.createView(),
        loadOp: 'load',
        storeOp: 'store',
      },
    ],
  });
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bg);
  pass.draw(24);
  pass.end();
  encoder.copyTextureToBuffer(
    { texture: framebuffer },
    {
      buffer: fbBuffer,
      offset: 0,
      bytesPerRow: kWidth * bytesPerWord * 2,
      rowsPerImage: kHeight,
    },
    { width: kWidth, height: kHeight }
  );
  t.queue.submit([encoder.finish()]);

  if (useStorageBuffers) {
    t.expectGPUBufferValuesPassCheck(dataBuffer, dataChecker, {
      type: Float32Array,
      typedLength: dataSize / bytesPerWord,
    });
  }

  t.expectGPUBufferValuesPassCheck(fbBuffer, framebufferChecker, {
    type: Uint32Array,
    typedLength: kWidth * kHeight * 2,
  });
}

g.test('all')
  .desc('Test a shader that discards all fragments')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  _ = uniformValues[0];
  discard;
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  `
      : ''
  }
  return vec2u(1);
}
`;

    // No storage writes occur.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === 0;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number) => {
                return 0;
              },
            },
          ],
        }
      );
    };

    // No fragment outputs occur.
    const fbChecker = (a: Uint32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === kWidth * kHeight;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                return 0;
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('three_quarters')
  .desc('Test a shader that discards all but the upper-left quadrant fragments')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  _ = uniformValues[0];
  if (pos.x >= 0.5 * ${kWidth} || pos.y >= 0.5 * ${kHeight}) {
    discard;
  }
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  return vec2u(idx);
  `
      : `
  return vec2(u32(pos.x), u32(pos.y));

  `
  }
}
`;

    // Only the the upper left quadrant is kept.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          const is_x = idx % 2 === 0;
          if (is_x) {
            return value < 0.5 * kWidth;
          } else {
            return value < 0.5 * kHeight;
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number): number | string => {
                const is_x = idx % 2 === 0;
                if (is_x) {
                  const x = Math.floor(idx / 2) % kWidth;
                  if (x >= kWidth / 2) {
                    return 0;
                  }
                } else {
                  const y = Math.floor((idx - 1) / kWidth);
                  if (y >= kHeight / 2) {
                    return 0;
                  }
                }
                if (is_x) {
                  return `< ${0.5 * kWidth}`;
                } else {
                  return `< ${0.5 * kHeight}`;
                }
              },
            },
          ],
        }
      );
    };
    const fbChecker = (a: Uint32Array) => {
      const discarded = (x: number, y: number) => x >= kWidth / 2 || y >= kHeight / 2;
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          const fragId = (idx / 2) | 0;
          const x = fragId % kWidth;
          const y = (fragId / kWidth) | 0;
          if (discarded(x, y)) {
            return value === kWidth * kHeight;
          } else {
            if (useStorageBuffers) {
              return value < (kWidth * kHeight) / 4;
            } else {
              return value === (idx % 2 ? y : x);
            }
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                const x = idx % kWidth;
                const y = Math.floor(idx / kWidth);
                if (discarded(x, y)) {
                  return 0;
                } else {
                  return useStorageBuffers ? 'any' : idx % 2 ? y : x;
                }
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('function_call')
  .desc('Test discards happening in a function call')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

fn foo(pos : vec2f) {
  let p = vec2i(pos);
  if p.x <= ${kWidth} / 2 && p.y <= ${kHeight} / 2 {
    discard;
  }
  if p.x >= ${kWidth} / 2 && p.y >= ${kHeight} / 2 {
    discard;
  }
}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  _ = uniformValues[0];
  foo(pos.xy);
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  return vec2u(idx);
  `
      : `
  return vec2u(u32(pos.x), u32(pos.y));
  `
  }
}
`;

    // Only the upper right and bottom left quadrants are kept.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          const is_x = idx % 2 === 0;
          if (value === 0.0) {
            return is_x ? a[idx + 1] === 0 : a[idx - 1] === 0;
          }

          let expect = is_x ? kWidth : kHeight;
          expect = 0.5 * expect;
          if (value < expect) {
            return is_x ? a[idx + 1] > 0.5 * kWidth : a[idx - 1] > 0.5 * kHeight;
          } else {
            return is_x ? a[idx + 1] < 0.5 * kWidth : a[idx - 1] < 0.5 * kHeight;
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number): number | string => {
                if (idx < (kWidth * kHeight) / 2) {
                  return 'any';
                } else {
                  return 0;
                }
              },
            },
          ],
        }
      );
    };
    const fbChecker = (a: Uint32Array) => {
      const discarded = (x: number, y: number) =>
        (x >= kWidth / 2 && y >= kHeight / 2) || (x <= kWidth / 2 && y <= kHeight / 2);
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          const fragId = (idx / 2) | 0;
          const x = fragId % kWidth;
          const y = (fragId / kWidth) | 0;
          if (discarded(x, y)) {
            return value === kWidth * kHeight;
          } else {
            if (useStorageBuffers) {
              return value < (kWidth * kHeight) / 2;
            } else {
              return value === (idx % 2 ? y : x);
            }
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                const x = idx % kWidth;
                const y = Math.floor(idx / kWidth);
                if (discarded(x, y)) {
                  return kWidth * kHeight;
                }
                return useStorageBuffers ? 'any' : idx % 2 ? y : x;
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('loop')
  .desc('Test discards in a loop')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  _ = uniformValues[0];
  for (var i = 0; i < 2; i++) {
    if i > 0 {
      discard;
    }
  }
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  `
      : ''
  }
  return vec2u(1);
}
`;

    // No storage writes occur.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === 0;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number) => {
                return 0;
              },
            },
          ],
        }
      );
    };

    // No fragment outputs occur.
    const fbChecker = (a: Uint32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === kWidth * kHeight;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                return kWidth * kHeight;
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('continuing')
  .desc('Test discards in a loop')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  _ = uniformValues[0];
  var i = 0;
  loop {
    continuing {
      if i > 0 {
        discard;
      }
      i++;
      break if i >= 2;
    }
  }
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  `
      : ''
  }
  return vec2u(1);
}
`;

    // No storage writes occur.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === 0;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number) => {
                return 0;
              },
            },
          ],
        }
      );
    };

    // No fragment outputs occur.
    const fbChecker = (a: Uint32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === kWidth * kHeight;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                return kWidth * kHeight;
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('uniform_read_loop')
  .desc('Test that helpers read a uniform value in a loop')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  discard;
  for (var i = 0u; i < uniformValues[0].x; i++) {
  }
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = pos.xy;
  `
      : ''
  }
  return vec2u(1);
}
`;

    // No storage writes occur.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === 0;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number) => {
                return 0;
              },
            },
          ],
        }
      );
    };

    // No fragment outputs occur.
    const fbChecker = (a: Uint32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          return value === kWidth * kHeight;
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                return kWidth * kHeight;
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });

g.test('derivatives')
  .desc('Test that derivatives are correct in the presence of discard')
  .params(u => u.combine('useStorageBuffers', [false, true]))
  .fn(t => {
    const { useStorageBuffers } = t.params;
    const code = `
${kSharedCode}

@fragment
fn fsMain(@builtin(position) pos : vec4f) -> @location(0) vec2u {
  let ipos = vec2i(pos.xy);
  let lsb = ipos & vec2(0x1);
  let left_sel = select(2, 4, lsb.y == 1);
  let right_sel = select(1, 3, lsb.y == 1);
  let uidx = select(left_sel, right_sel, lsb.x == 1);
  if ((lsb.x | lsb.y) & 0x1) == 0 {
    discard;
  }

  let v = uniformValues[uidx].x;
  let dx = dpdx(f32(v));
  let dy = dpdy(f32(v));
  ${
    useStorageBuffers
      ? `
  let idx = atomicAdd(&atomicIndex, 1);
  output[idx] = vec2(dx, dy);
  return vec2u(idx);
    `
      : `
  return bitcast<vec2u>(vec2f(dx, dy));
    `
  }
}
`;

    // One pixel per quad is discarded. The derivatives values are always the same +/- 3.
    const dataChecker = (a: Float32Array) => {
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          if (idx < (3 * (2 * kWidth * kHeight)) / 4) {
            return value === -3 || value === 3;
          } else {
            return value === 0;
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'data exp ==',
              getValueForCell: (idx: number) => {
                if (idx < (3 * (2 * kWidth * kHeight)) / 4) {
                  return '+/- 3';
                } else {
                  return 0;
                }
              },
            },
          ],
        }
      );
    };

    // 3/4 of the fragments are written.
    const fbChecker = (a: Uint32Array) => {
      const discarded = (x: number, y: number) => ((x | y) & 0x1) === 0;
      const asF32 = new Float32Array(1);
      const asU32 = new Uint32Array(asF32.buffer);
      return checkElementsPassPredicate(
        a,
        (idx: number, value: number | bigint) => {
          const fragId = (idx / 2) | 0;
          const x = fragId % kWidth;
          const y = (fragId / kWidth) | 0;
          if (discarded(x, y)) {
            return value === kWidth * kHeight;
          } else {
            if (useStorageBuffers) {
              return value < (3 * (kWidth * kHeight)) / 4;
            } else {
              asU32[0] = value as number;
              const v = asF32[0];
              return v === -3 || v === 3;
            }
          }
        },
        {
          predicatePrinter: [
            {
              leftHeader: 'fb exp ==',
              getValueForCell: (idx: number) => {
                const x = idx % kWidth;
                const y = Math.floor(idx / kWidth);
                if (((x | y) & 0x1) === 0) {
                  return kWidth * kHeight;
                } else {
                  return useStorageBuffers ? 'any' : '+/- 3';
                }
              },
            },
          ],
        }
      );
    };

    drawFullScreen(t, code, useStorageBuffers, dataChecker, fbChecker);
  });