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

export const description = `
Test the result of writing textures through texture views with various options.

Reads value from a shader array, writes the value via various write methods.
Check the texture result with the expected texel view.

All x= every possible view write method: {
  - storage write {fragment, compute}
  - render pass store
  - render pass resolve
}

Format reinterpretation is not tested here. It is in format_reinterpretation.spec.ts.

TODO: Write helper for this if not already available (see resource_init, buffer_sync_test for related code).
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { unreachable } from '../../../../common/util/util.js';
import {
  getTextureFormatType,
  kRegularTextureFormats,
  RegularTextureFormat,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';
import { kFullscreenQuadVertexShaderCode } from '../../../util/shader.js';
import { TexelView } from '../../../util/texture/texel_view.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

const kTextureViewWriteMethods = [
  'storage-write-fragment',
  'storage-write-compute',
  'render-pass-store',
  'render-pass-resolve',
] as const;
type TextureViewWriteMethod = (typeof kTextureViewWriteMethods)[number];

const kTextureViewUsageMethods = ['inherit', 'minimal'] as const;
type TextureViewUsageMethod = (typeof kTextureViewUsageMethods)[number];

// Src color values to read from a shader array.
const kColorsFloat = [
  { R: 1.0, G: 0.0, B: 0.0, A: 0.8 },
  { R: 0.0, G: 1.0, B: 0.0, A: 0.7 },
  { R: 0.0, G: 0.0, B: 0.0, A: 0.6 },
  { R: 0.0, G: 0.0, B: 0.0, A: 0.5 },
  { R: 1.0, G: 1.0, B: 1.0, A: 0.4 },
  { R: 0.7, G: 0.0, B: 0.0, A: 0.3 },
  { R: 0.0, G: 0.8, B: 0.0, A: 0.2 },
  { R: 0.0, G: 0.0, B: 0.9, A: 0.1 },
  { R: 0.1, G: 0.2, B: 0.0, A: 0.3 },
  { R: 0.4, G: 0.3, B: 0.6, A: 0.8 },
];

function floatToIntColor(c: number) {
  return Math.floor(c * 100);
}

const kColorsInt = kColorsFloat.map(c => {
  return {
    R: floatToIntColor(c.R),
    G: floatToIntColor(c.G),
    B: floatToIntColor(c.B),
    A: floatToIntColor(c.A),
  };
});

const kTextureSize = 16;

function writeTextureAndGetExpectedTexelView(
  t: GPUTest,
  method: TextureViewWriteMethod,
  view: GPUTextureView,
  format: RegularTextureFormat,
  sampleCount: number
) {
  const type = getTextureFormatType(format);
  const isFloatType = type === 'float' || type === 'unfilterable-float';
  const kColors = isFloatType ? kColorsFloat : kColorsInt;
  const expectedTexelView = TexelView.fromTexelsAsColors(
    format,
    coords => {
      const pixelPos = coords.y * kTextureSize + coords.x;
      return kColors[pixelPos % kColors.length];
    },
    { clampToFormatRange: true }
  );
  const vecType = isFloatType ? 'vec4f' : type === 'sint' ? 'vec4i' : 'vec4u';
  const kColorArrayShaderString = `array<${vecType}, ${kColors.length}>(
      ${kColors.map(t => `${vecType}(${t.R}, ${t.G}, ${t.B}, ${t.A}) `).join(',')}
    )`;

  switch (method) {
    case 'storage-write-compute':
      {
        const pipeline = t.device.createComputePipeline({
          layout: 'auto',
          compute: {
            module: t.device.createShaderModule({
              code: `
                @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>;
                @compute @workgroup_size(1, 1) fn main(
                  @builtin(global_invocation_id) global_id: vec3<u32>,
                ) {
                  const src = ${kColorArrayShaderString};
                  let coord = vec2u(global_id.xy);
                  let idx = coord.x + coord.y * ${kTextureSize};
                  textureStore(dst, coord, src[idx % ${kColors.length}]);
                }`,
            }),
            entryPoint: 'main',
          },
        });
        const commandEncoder = t.device.createCommandEncoder();
        const pass = commandEncoder.beginComputePass();
        pass.setPipeline(pipeline);
        pass.setBindGroup(
          0,
          t.device.createBindGroup({
            layout: pipeline.getBindGroupLayout(0),
            entries: [
              {
                binding: 0,
                resource: view,
              },
            ],
          })
        );
        pass.dispatchWorkgroups(kTextureSize, kTextureSize);
        pass.end();
        t.device.queue.submit([commandEncoder.finish()]);
      }
      break;

    case 'storage-write-fragment':
      {
        // Create a placeholder color attachment texture,
        // The size of which equals that of format texture we are testing,
        // so that we have the same number of fragments and texels.
        const kPlaceholderTextureFormat = 'rgba8unorm';
        const placeholderTexture = t.createTextureTracked({
          format: kPlaceholderTextureFormat,
          size: [kTextureSize, kTextureSize],
          usage: GPUTextureUsage.RENDER_ATTACHMENT,
        });

        const pipeline = t.device.createRenderPipeline({
          layout: 'auto',
          vertex: {
            module: t.device.createShaderModule({
              code: kFullscreenQuadVertexShaderCode,
            }),
          },
          fragment: {
            module: t.device.createShaderModule({
              code: `
                @group(0) @binding(0) var dst: texture_storage_2d<${format}, write>;
                @fragment fn main(
                  @builtin(position) fragCoord: vec4<f32>,
                ) {
                  const src = ${kColorArrayShaderString};
                  let coord = vec2u(fragCoord.xy);
                  let idx = coord.x + coord.y * ${kTextureSize};
                  textureStore(dst, coord, src[idx % ${kColors.length}]);
                }`,
            }),
            // Set writeMask to 0 as the fragment shader has no output.
            targets: [
              {
                format: kPlaceholderTextureFormat,
                writeMask: 0,
              },
            ],
          },
        });
        const commandEncoder = t.device.createCommandEncoder();
        const pass = commandEncoder.beginRenderPass({
          colorAttachments: [
            {
              view: placeholderTexture.createView(),
              loadOp: 'clear',
              storeOp: 'discard',
            },
          ],
        });
        pass.setPipeline(pipeline);
        pass.setBindGroup(
          0,
          t.device.createBindGroup({
            layout: pipeline.getBindGroupLayout(0),
            entries: [
              {
                binding: 0,
                resource: view,
              },
            ],
          })
        );
        pass.draw(6);
        pass.end();
        t.device.queue.submit([commandEncoder.finish()]);
      }
      break;

    case 'render-pass-store':
    case 'render-pass-resolve':
      {
        // Create a placeholder color attachment texture for the store target when tesing texture is used as resolve target.
        const targetView =
          method === 'render-pass-store'
            ? view
            : t
                .createTextureTracked({
                  format,
                  size: [kTextureSize, kTextureSize],
                  usage: GPUTextureUsage.RENDER_ATTACHMENT,
                  sampleCount: 4,
                })
                .createView();
        const resolveView = method === 'render-pass-store' ? undefined : view;
        const multisampleCount = method === 'render-pass-store' ? sampleCount : 4;

        const pipeline = t.device.createRenderPipeline({
          layout: 'auto',
          vertex: {
            module: t.device.createShaderModule({
              code: kFullscreenQuadVertexShaderCode,
            }),
          },
          fragment: {
            module: t.device.createShaderModule({
              code: `
                @fragment fn main(
                  @builtin(position) fragCoord: vec4<f32>,
                ) -> @location(0) ${vecType} {
                  const src = ${kColorArrayShaderString};
                  let coord = vec2u(fragCoord.xy);
                  let idx = coord.x + coord.y * ${kTextureSize};
                  return src[idx % ${kColors.length}];
                }`,
            }),
            targets: [
              {
                format,
              },
            ],
          },
          multisample: {
            count: multisampleCount,
          },
        });
        const commandEncoder = t.device.createCommandEncoder();
        const pass = commandEncoder.beginRenderPass({
          colorAttachments: [
            {
              view: targetView,
              resolveTarget: resolveView,
              loadOp: 'clear',
              storeOp: 'store',
            },
          ],
        });
        pass.setPipeline(pipeline);
        pass.draw(6);
        pass.end();
        t.device.queue.submit([commandEncoder.finish()]);
      }
      break;
    default:
      unreachable();
  }

  return expectedTexelView;
}

function getTextureViewUsage(
  viewUsageMethod: TextureViewUsageMethod,
  minimalUsageForTest: GPUTextureUsageFlags
) {
  switch (viewUsageMethod) {
    case 'inherit':
      return 0;

    case 'minimal':
      return minimalUsageForTest;

    default:
      unreachable();
  }
}

g.test('format')
  .desc(
    `Views of every allowed format.

Read values from color array in the shader, and write it to the texture view via different write methods.

- x= every texture format
- x= sampleCount {1, 4} if valid
- x= every possible view write method (see above)
- x= inherited or minimal texture view usage

TODO: Test sampleCount > 1 for 'render-pass-store' after extending copySinglePixelTextureToBufferUsingComputePass
      to read multiple pixels from multisampled textures. [1]
TODO: Test rgb10a2uint when TexelRepresentation.numericRange is made per-component. [2]
`
  )
  .params(u =>
    u //
      .combine('method', kTextureViewWriteMethods)
      .combine('format', kRegularTextureFormats)
      .combine('sampleCount', [1, 4])
      .filter(({ format, method, sampleCount }) => {
        // [2]
        if (format === 'rgb10a2uint') {
          return false;
        }

        switch (method) {
          case 'storage-write-compute':
          case 'storage-write-fragment':
            return sampleCount === 1;
          case 'render-pass-resolve':
            return sampleCount === 1;
          case 'render-pass-store':
            // [1]
            if (sampleCount > 1) {
              return false;
            }
            break;
        }

        return true;
      })
      .combine('viewUsageMethod', kTextureViewUsageMethods)
  )
  .fn(t => {
    const { format, method, sampleCount, viewUsageMethod } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    if (sampleCount > 1) {
      t.skipIfTextureFormatNotMultisampled(format);
    }

    switch (method) {
      case 'storage-write-compute':
      case 'storage-write-fragment':
        t.skipIfTextureFormatNotUsableWithStorageAccessMode('write-only', format);
        break;
      case 'render-pass-store':
        t.skipIfTextureFormatNotUsableAsRenderAttachment(format);
        break;
      case 'render-pass-resolve':
        // Requires multisample in `writeTextureAndGetExpectedTexelView`
        t.skipIfTextureFormatNotUsableAsRenderAttachment(format);
        t.skipIfTextureFormatNotResolvable(format);
        break;
    }

    t.skipIf(
      t.isCompatibility &&
        method === 'storage-write-fragment' &&
        !(t.device.limits.maxStorageBuffersInFragmentStage! > 0),
      `maxStorageBuffersInFragmentStage(${t.device.limits.maxStorageBuffersInFragmentStage}) < 1`
    );

    const textureUsageForMethod = method.includes('storage')
      ? GPUTextureUsage.STORAGE_BINDING
      : GPUTextureUsage.RENDER_ATTACHMENT;
    const usage = GPUTextureUsage.COPY_SRC | textureUsageForMethod;

    const texture = t.createTextureTracked({
      format,
      usage,
      size: [kTextureSize, kTextureSize],
      sampleCount,
    });

    const view = texture.createView({
      usage: getTextureViewUsage(viewUsageMethod, textureUsageForMethod),
    });
    const expectedTexelView = writeTextureAndGetExpectedTexelView(
      t,
      method,
      view,
      format,
      sampleCount
    );

    // [1] Use copySinglePixelTextureToBufferUsingComputePass to check multisampled texture.
    ttu.expectTexelViewComparisonIsOkInTexture(t, { texture }, expectedTexelView, [
      kTextureSize,
      kTextureSize,
    ]);
  });

g.test('dimension')
  .desc(
    `Views of every allowed dimension.

- x= a representative subset of formats
- x= {every texture dimension} x {every valid view dimension}
  (per gpuweb#79 no dimension-count reinterpretations, like 2d-array <-> 3d, are possible)
- x= sampleCount {1, 4} if valid
- x= every possible view write method (see above)
`
  )
  .unimplemented();

g.test('aspect')
  .desc(
    `Views of every allowed aspect of depth/stencil textures.

- x= every depth/stencil format
- x= {"all", "stencil-only", "depth-only"} where valid for the format
- x= sampleCount {1, 4} if valid
- x= every possible view write method (see above)
`
  )
  .unimplemented();