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

export const description = 'Test helpers for texel data produce the expected data in the shader';

import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert } from '../../../common/util/util.js';
import {
  kEncodableTextureFormats,
  EncodableTextureFormat,
  isColorTextureFormat,
  canCopyToAllAspectsOfTextureFormat,
} from '../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../gpu_test.js';
import { gammaCompress, floatAsNormalizedIntegerUnquantized } from '../conversion.js';

import {
  kTexelRepresentationInfo,
  getSingleDataType,
  getComponentReadbackTraits,
  ComponentDataType,
} from './texel_data.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

function isCopyDstColorTextureFormatOfType(
  format: EncodableTextureFormat,
  type: ComponentDataType
) {
  return (
    isColorTextureFormat(format) &&
    canCopyToAllAspectsOfTextureFormat(format) &&
    getSingleDataType(format) === type
  );
}

async function doTest(
  t: GPUTest & {
    params: {
      format: EncodableTextureFormat;
      componentData: {
        R?: number;
        G?: number;
        B?: number;
        A?: number;
      };
    };
  }
) {
  const { format } = t.params;
  t.skipIfTextureFormatNotSupported(format);

  const componentData = t.params.componentData;

  const rep = kTexelRepresentationInfo[format];
  const texelData = rep.pack(componentData);
  const texture = t.createTextureTracked({
    format,
    size: [1, 1, 1],
    usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
  });

  t.device.queue.writeTexture(
    { texture },
    texelData,
    {
      bytesPerRow: texelData.byteLength,
    },
    [1]
  );

  const { ReadbackTypedArray, shaderType } = getComponentReadbackTraits(getSingleDataType(format));

  const shader = `
  @group(0) @binding(0) var tex : texture_2d<${shaderType}>;

  struct Output {
    ${rep.componentOrder.map(C => `result${C} : ${shaderType},`).join('\n')}
  };
  @group(0) @binding(1) var<storage, read_write> output : Output;

  @compute @workgroup_size(1)
  fn main() {
      var texel : vec4<${shaderType}> = textureLoad(tex, vec2<i32>(0, 0), 0);
      ${rep.componentOrder.map(C => `output.result${C} = texel.${C.toLowerCase()};`).join('\n')}
      return;
  }`;

  const pipeline = t.device.createComputePipeline({
    layout: 'auto',
    compute: {
      module: t.device.createShaderModule({
        code: shader,
      }),
      entryPoint: 'main',
    },
  });

  const outputBuffer = t.createBufferTracked({
    size: rep.componentOrder.length * 4,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
  });

  const bindGroup = t.device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      {
        binding: 0,
        resource: texture.createView(),
      },
      {
        binding: 1,
        resource: {
          buffer: outputBuffer,
        },
      },
    ],
  });

  const encoder = t.device.createCommandEncoder();
  const pass = encoder.beginComputePass();
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bindGroup);
  pass.dispatchWorkgroups(1);
  pass.end();
  t.device.queue.submit([encoder.finish()]);

  const idealReadbackData = new ReadbackTypedArray(
    rep.componentOrder.map(c => {
      const value = rep.decode(componentData)[c];
      assert(value !== undefined);
      return value;
    })
  );

  if (format === 'rgba8unorm-srgb' || format === 'bgra8unorm-srgb') {
    // The SRGB -> float conversion is permitted a tolerance of 0.5f ULP on the SRGB side. The
    // procedure for measuring this tolerance is to convert the result back into SRGB space
    // using the ideal float -> SRGB conversion specified below but WITHOUT the rounding to integer,
    // and taking the floating point difference versus the original SRGB value to yield the error.
    // Exact conversion is required: 0.0f and 1.0f (the ends) must be exactly achievable.
    const readBackValue = await t.readGPUBufferRangeTyped(outputBuffer, {
      type: Float32Array,
      typedLength: 4,
    });
    // Gamma correction shouldn't be applied to the alpha channel.
    t.expect(idealReadbackData[3] === readBackValue.data[3]);

    // Do the ideal float -> 8unorm-srgb conversion on the readback float value without the rounding
    // to integer.
    const readBackValueToSRGB = new Array(3);
    for (let i = 0; i < 3; ++i) {
      const gammaCompressed = gammaCompress(readBackValue.data[i]);
      let outputIndex = i;
      if (format === 'bgra8unorm-srgb' && (i === 0 || i === 2)) {
        outputIndex = 2 - i;
      }
      readBackValueToSRGB[outputIndex] = floatAsNormalizedIntegerUnquantized(
        gammaCompressed,
        8,
        false
      );
    }
    readBackValue.cleanup();

    // Take the floating point difference versus the original SRGB value to yield the error.
    const check8UnormSRGB = (inputValue: number, idealValue: number) => {
      const kToleranceULP = 0.5;
      if (idealValue === 0 || idealValue === 255) {
        t.expect(inputValue === idealValue);
      } else {
        t.expect(Math.abs(inputValue - idealValue) <= kToleranceULP);
      }
    };

    assert(
      componentData.R !== undefined &&
        componentData.G !== undefined &&
        componentData.B !== undefined
    );
    check8UnormSRGB(readBackValueToSRGB[0], componentData.R);
    check8UnormSRGB(readBackValueToSRGB[1], componentData.G);
    check8UnormSRGB(readBackValueToSRGB[2], componentData.B);
  } else {
    t.expectGPUBufferValuesEqual(outputBuffer, idealReadbackData);
  }
}

// Make a test parameter by mapping a format and each component to a texel component
// data value.
function makeParam(
  format: EncodableTextureFormat,
  fn: (bitLength: number, index: number) => number
) {
  const rep = kTexelRepresentationInfo[format];
  return {
    R: rep.componentInfo.R ? fn(rep.componentInfo.R.bitLength, 0) : undefined,
    G: rep.componentInfo.G ? fn(rep.componentInfo.G.bitLength, 1) : undefined,
    B: rep.componentInfo.B ? fn(rep.componentInfo.B.bitLength, 2) : undefined,
    A: rep.componentInfo.A ? fn(rep.componentInfo.A.bitLength, 3) : undefined,
  };
}

g.test('unorm_texel_data_in_shader')
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'unorm'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
        return [
          // Test extrema
          makeParam(format, () => 0),
          makeParam(format, bitLength => max(bitLength)),

          // Test a middle value
          makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),

          // Test mixed values
          makeParam(format, (bitLength, i) => {
            const offset = [0.13, 0.63, 0.42, 0.89];
            return Math.floor(offset[i] * max(bitLength));
          }),
        ];
      })
  )
  .fn(doTest);

g.test('snorm_texel_data_in_shader')
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'snorm'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
        return [
          // Test extrema
          makeParam(format, () => 0),
          makeParam(format, bitLength => max(bitLength)),
          makeParam(format, bitLength => -max(bitLength)),
          makeParam(format, bitLength => -max(bitLength) - 1),

          // Test a middle value
          makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),

          // Test mixed values
          makeParam(format, (bitLength, i) => {
            const offset = [0.13, 0.63, 0.42, 0.89];
            const range = 2 * max(bitLength);
            return -max(bitLength) + Math.floor(offset[i] * range);
          }),
        ];
      })
  )
  .fn(doTest);

g.test('uint_texel_data_in_shader')
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'uint'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        const max = (bitLength: number) => Math.pow(2, bitLength) - 1;
        return [
          // Test extrema
          makeParam(format, () => 0),
          makeParam(format, bitLength => max(bitLength)),

          // Test a middle value
          makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),

          // Test mixed values
          makeParam(format, (bitLength, i) => {
            const offset = [0.13, 0.63, 0.42, 0.89];
            return Math.floor(offset[i] * max(bitLength));
          }),
        ];
      })
  )
  .fn(doTest);

g.test('sint_texel_data_in_shader')
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'sint'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        const max = (bitLength: number) => Math.pow(2, bitLength - 1) - 1;
        return [
          // Test extrema
          makeParam(format, () => 0),
          makeParam(format, bitLength => max(bitLength)),
          makeParam(format, bitLength => -max(bitLength) - 1),

          // Test a middle value
          makeParam(format, bitLength => Math.floor(max(bitLength) / 2)),

          // Test mixed values
          makeParam(format, (bitLength, i) => {
            const offset = [0.13, 0.63, 0.42, 0.89];
            const range = 2 * max(bitLength);
            return -max(bitLength) + Math.floor(offset[i] * range);
          }),
        ];
      })
  )
  .fn(doTest);

g.test('float_texel_data_in_shader')
  .desc(
    `
TODO: Test NaN, Infinity, -Infinity [1]`
  )
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'float'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        return [
          // Test extrema
          makeParam(format, () => 0),

          // [1]: Test NaN, Infinity, -Infinity

          // Test some values
          makeParam(format, () => 0.1199951171875),
          makeParam(format, () => 1.4072265625),
          makeParam(format, () => 24928),
          makeParam(format, () => -0.1319580078125),
          makeParam(format, () => -323.25),
          makeParam(format, () => -7440),

          // Test mixed values
          makeParam(format, (bitLength, i) => {
            return [24896, -0.1319580078125, -323.25, -234.375][i];
          }),
        ];
      })
  )
  .fn(doTest);

g.test('ufloat_texel_data_in_shader')
  .desc(
    `
Note: this uses values that are representable by both rg11b10ufloat and rgb9e5ufloat.

TODO: Test NaN, Infinity`
  )
  .params(u =>
    u
      .combine('format', kEncodableTextureFormats)
      .filter(({ format }) => isCopyDstColorTextureFormatOfType(format, 'ufloat'))
      .beginSubcases()
      .expand('componentData', ({ format }) => {
        return [
          // Test extrema
          makeParam(format, () => 0),

          // Test some values
          makeParam(format, () => 128),
          makeParam(format, () => 1984),
          makeParam(format, () => 3968),

          // Test scattered mixed values
          makeParam(format, (bitLength, i) => {
            return [128, 1984, 3968][i];
          }),

          // Test mixed values that are close in magnitude.
          makeParam(format, (bitLength, i) => {
            return [0.05859375, 0.03125, 0.03515625][i];
          }),
        ];
      })
  )
  .fn(doTest);