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

export const description = `
Tests for GPUCanvasContext.configure.

TODO:
- Test colorSpace
- Test toneMapping
`;

import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert } from '../../../common/util/util.js';
import {
  isValidTextureUsageCombination,
  kCanvasTextureFormats,
  kSomeBogusTextureUsage,
  kTextureUsages,
  kValidCombinationsOfOneOrTwoTextureUsages,
  usageIsTypeErrorForConfigure,
} from '../../capability_info.js';
import { GPUConst } from '../../constants.js';
import {
  kAllTextureFormats,
  kFeaturesForFormats,
  filterFormatsByFeature,
  textureFormatsAreViewCompatible,
} from '../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../gpu_test.js';
import { kAllCanvasTypes, createCanvas } from '../../util/create_elements.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

g.test('defaults')
  .desc(
    `
    Ensure that the defaults for GPUCanvasConfiguration are correct.
    `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
  )
  .fn(t => {
    const { canvasType } = t.params;
    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    ctx.configure({
      device: t.device,
      format: 'rgba8unorm',
    });

    const configuration = ctx.getConfiguration();
    assert(configuration !== null);
    t.expect(configuration.device === t.device);
    t.expect(configuration.format === 'rgba8unorm');
    t.expect(configuration.usage === GPUTextureUsage.RENDER_ATTACHMENT);
    t.expect(configuration.viewFormats.length === 0);
    t.expect(configuration.colorSpace === 'srgb');
    t.expect(configuration.toneMapping?.mode === 'standard');
    t.expect(configuration.alphaMode === 'opaque');

    const currentTexture = ctx.getCurrentTexture();
    t.expect(currentTexture.format === 'rgba8unorm');
    t.expect(currentTexture.usage === GPUTextureUsage.RENDER_ATTACHMENT);
    t.expect(currentTexture.dimension === '2d');
    t.expect(currentTexture.width === canvas.width);
    t.expect(currentTexture.height === canvas.height);
    t.expect(currentTexture.depthOrArrayLayers === 1);
    t.expect(currentTexture.mipLevelCount === 1);
    t.expect(currentTexture.sampleCount === 1);
  });

g.test('device')
  .desc(
    `
    Ensure that configure reacts appropriately to various device states.
    `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
  )
  .fn(t => {
    const { canvasType } = t.params;
    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    // getConfiguration returns null before configure.
    t.expect(ctx.getConfiguration() === null);

    // Calling configure without a device should throw a TypeError.
    t.shouldThrow('TypeError', () => {
      ctx.configure({
        format: 'rgba8unorm',
      } as GPUCanvasConfiguration);
    });

    // Device is not configured, so getCurrentTexture will throw an InvalidStateError.
    t.shouldThrow('InvalidStateError', () => {
      ctx.getCurrentTexture();
    });

    // Should throw on the second call to getCurrentTexture also.
    t.shouldThrow('InvalidStateError', () => {
      ctx.getCurrentTexture();
    });

    // Calling configure with a device should succeed.
    ctx.configure({
      device: t.device,
      format: 'rgba8unorm',
      alphaMode: 'opaque',
    });

    // getConfiguration will succeed after configure.
    const configuration = ctx.getConfiguration();
    assert(configuration !== null);
    t.expect(configuration.device === t.device);
    t.expect(configuration.format === 'rgba8unorm');
    t.expect(configuration.usage === GPUTextureUsage.RENDER_ATTACHMENT);
    t.expect(configuration.viewFormats.length === 0);
    t.expect(configuration.colorSpace === 'srgb');
    t.expect(configuration.toneMapping?.mode === 'standard');
    t.expect(configuration.alphaMode === 'opaque');

    // getCurrentTexture will succeed with a valid device.
    ctx.getCurrentTexture();

    // Unconfiguring should cause the device to be cleared.
    ctx.unconfigure();
    t.shouldThrow('InvalidStateError', () => {
      ctx.getCurrentTexture();
    });

    // getConfiguration returns null after unconfigure.
    t.expect(ctx.getConfiguration() === null);

    // Should be able to successfully configure again after unconfiguring.
    ctx.configure({
      device: t.device,
      format: 'rgba8unorm',
      alphaMode: 'premultiplied',
    });
    ctx.getCurrentTexture();

    // getConfiguration will succeed after configure.
    const newConfiguration = ctx.getConfiguration();
    assert(newConfiguration !== null);
    t.expect(newConfiguration.device === t.device);
    t.expect(newConfiguration.format === 'rgba8unorm');
    t.expect(newConfiguration.usage === GPUTextureUsage.RENDER_ATTACHMENT);
    t.expect(newConfiguration.viewFormats.length === 0);
    t.expect(newConfiguration.colorSpace === 'srgb');
    t.expect(newConfiguration.toneMapping?.mode === 'standard');
    t.expect(newConfiguration.alphaMode === 'premultiplied');
  });

g.test('format')
  .desc(
    `
    Ensure that only valid texture formats are allowed when calling configure.
    `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .combine('format', kAllTextureFormats)
  )
  .fn(t => {
    const { canvasType, format } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    // Would prefer to use kCanvasTextureFormats.includes(format), but that's giving TS errors.
    let validFormat = false;
    for (const canvasFormat of kCanvasTextureFormats) {
      if (format === canvasFormat) {
        validFormat = true;
        break;
      }
    }

    if (validFormat) {
      ctx.configure({
        device: t.device,
        format,
      });
      const configuration = ctx.getConfiguration();
      t.expect(configuration!.format === format);
    } else {
      t.shouldThrow('TypeError', () => {
        ctx.configure({
          device: t.device,
          format,
        });
      });
    }
  });

g.test('usage')
  .desc(
    `
    Ensure that getCurrentTexture returns a texture with the configured usages.
    `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .beginSubcases()
      .combine(
        'usage',
        kValidCombinationsOfOneOrTwoTextureUsages
          // TypeError cases are tested in the next test.
          .filter(usage => !usageIsTypeErrorForConfigure(usage))
      )
  )
  .fn(t => {
    const { canvasType, usage } = t.params;

    // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
    if ((usage & GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) !== 0) {
      t.skipIfTransientAttachmentNotSupported();
    }

    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    ctx.configure({
      device: t.device,
      format: 'rgba8unorm',
      usage,
    });

    const configuration = ctx.getConfiguration();
    t.expect(configuration!.usage === usage);

    const currentTexture = ctx.getCurrentTexture();
    t.expect(currentTexture instanceof GPUTexture);
    t.expect(currentTexture.usage === usage);

    // Try to use the texture with the given usage

    if (usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) {
      const encoder = t.device.createCommandEncoder();
      const pass = encoder.beginRenderPass({
        colorAttachments: [
          {
            view: currentTexture.createView(),
            clearValue: [1.0, 0.0, 0.0, 1.0],
            loadOp: 'clear',
            storeOp: 'store',
          },
        ],
      });
      pass.end();
      t.device.queue.submit([encoder.finish()]);
    }

    if (usage & GPUConst.TextureUsage.TEXTURE_BINDING) {
      const bgl = t.device.createBindGroupLayout({
        entries: [
          {
            binding: 0,
            visibility: GPUShaderStage.FRAGMENT,
            texture: {},
          },
        ],
      });

      t.device.createBindGroup({
        layout: bgl,
        entries: [
          {
            binding: 0,
            resource: currentTexture.createView(),
          },
        ],
      });
    }

    const canUseStorageTextureInFragmentShader =
      !t.isCompatibility || t.device.limits.maxStorageTexturesInFragmentStage! > 0;

    if (
      (usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 &&
      canUseStorageTextureInFragmentShader
    ) {
      const bgl = t.device.createBindGroupLayout({
        entries: [
          {
            binding: 0,
            visibility: GPUShaderStage.FRAGMENT,
            storageTexture: { access: 'write-only', format: currentTexture.format },
          },
        ],
      });

      t.device.createBindGroup({
        layout: bgl,
        entries: [
          {
            binding: 0,
            resource: currentTexture.createView(),
          },
        ],
      });
    }

    if (usage & GPUConst.TextureUsage.COPY_DST) {
      const rgbaData = new Uint8Array([255, 0, 0, 255]);

      t.device.queue.writeTexture({ texture: currentTexture }, rgbaData, {}, [1, 1, 1]);
    }

    if (usage & GPUConst.TextureUsage.COPY_SRC) {
      const size = [currentTexture.width, currentTexture.height, 1];
      const dstTexture = t.createTextureTracked({
        format: currentTexture.format,
        usage: GPUTextureUsage.COPY_DST,
        size,
      });

      const encoder = t.device.createCommandEncoder();
      encoder.copyTextureToTexture({ texture: currentTexture }, { texture: dstTexture }, size);
      t.device.queue.submit([encoder.finish()]);
    }
  });

g.test('invalid_usage')
  .desc(`Test what happens when an invalid usage is passed to configure().`)
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      // Test all formats because sometimes they're handled differently (require internal blits).
      .combine('format', kCanvasTextureFormats)
      .combine('usage', [
        0,
        GPUConst.TextureUsage.COPY_DST,
        // Special case for bgra8unorm vs other formats
        GPUConst.TextureUsage.STORAGE_BINDING,
        // Special case(s) for canvases vs regular textures
        ...kTextureUsages.filter(usageIsTypeErrorForConfigure),
        GPUConst.TextureUsage.TRANSIENT_ATTACHMENT | GPUConst.TextureUsage.RENDER_ATTACHMENT,
        // Various usages containing a bogus usage
        kSomeBogusTextureUsage,
        GPUConst.TextureUsage.COPY_DST | kSomeBogusTextureUsage,
        GPUConst.TextureUsage.STORAGE_BINDING | kSomeBogusTextureUsage,
        GPUConst.TextureUsage.TRANSIENT_ATTACHMENT | kSomeBogusTextureUsage,
      ])
  )
  .fn(t => {
    const { canvasType, format, usage } = t.params;

    // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
    if ((usage & GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) !== 0) {
      t.skipIfTransientAttachmentNotSupported();
    }

    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    if (usageIsTypeErrorForConfigure(usage)) {
      t.shouldThrow('TypeError', () => {
        ctx.configure({ device: t.device, format, usage });
      });

      const configuration = ctx.getConfiguration();
      assert(configuration === null);

      t.shouldThrow('InvalidStateError', () => {
        ctx.getCurrentTexture();
      });
    } else {
      // Otherwise just the normal createTexture() usage validation applies.
      const invalidUsage = !isValidTextureUsageCombination(usage);
      t.expectValidationError(() => {
        ctx.configure({ device: t.device, format, usage });
      }, invalidUsage);

      // The canvas still gets configured, and getCurrentTexture() can still be called.
      const configuration = ctx.getConfiguration();
      assert(configuration !== null);
      t.expect(configuration.usage === usage);

      let currentTexture: GPUTexture | undefined;
      t.expectValidationError(() => {
        currentTexture = ctx.getCurrentTexture();
      }, invalidUsage);
      assert(currentTexture !== undefined && currentTexture instanceof GPUTexture);
      t.expect(currentTexture.usage === usage);
    }
  });

g.test('alpha_mode')
  .desc(
    `
    Ensure that all valid alphaMode values are allowed when calling configure.
    `
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .beginSubcases()
      .combine('alphaMode', ['opaque', 'premultiplied'] as const)
  )
  .fn(t => {
    const { canvasType, alphaMode } = t.params;
    const canvas = createCanvas(t, canvasType, 2, 2);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    ctx.configure({
      device: t.device,
      format: 'rgba8unorm',
      alphaMode,
    });

    const configuration = ctx.getConfiguration();
    t.expect(configuration!.alphaMode === alphaMode);

    const currentTexture = ctx.getCurrentTexture();
    t.expect(currentTexture instanceof GPUTexture);
  });

g.test('size_zero_before_configure')
  .desc(`Ensure a validation error is raised in configure() if the size of the canvas is zero.`)
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .combine('zeroDimension', ['width', 'height'] as const)
  )
  .fn(t => {
    const { canvasType, zeroDimension } = t.params;
    const canvas = createCanvas(t, canvasType, 1, 1);
    canvas[zeroDimension] = 0;
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    // Validation error, the canvas size is 0 which doesn't make a valid GPUTextureDescriptor.
    t.expectValidationError(() => {
      ctx.configure({
        device: t.device,
        format: 'bgra8unorm',
        usage: GPUTextureUsage.RENDER_ATTACHMENT,
      });
    });

    canvas[zeroDimension] = 1;

    // The size being incorrect doesn't make for an invalid configuration. Now that it is fixed
    // getting textures from the canvas should work.
    const currentTexture = ctx.getCurrentTexture();

    // Try rendering to it even!
    const encoder = t.device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: currentTexture.createView(),
          clearValue: [1.0, 0.0, 0.0, 1.0],
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    });
    pass.end();
    t.device.queue.submit([encoder.finish()]);
  });

g.test('size_zero_after_configure')
  .desc(
    `Ensure a validation error is raised after configure() if the size of the canvas becomes zero.`
  )
  .params(u =>
    u //
      .combine('canvasType', kAllCanvasTypes)
      .combine('zeroDimension', ['width', 'height'] as const)
  )
  .fn(t => {
    const { canvasType, zeroDimension } = t.params;
    const canvas = createCanvas(t, canvasType, 1, 1);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    ctx.configure({
      device: t.device,
      format: 'bgra8unorm',
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    canvas[zeroDimension] = 0;

    // The size is incorrect, we should be getting an error texture and a validation error.
    let currentTexture: GPUTexture;
    t.expectValidationError(() => {
      currentTexture = ctx.getCurrentTexture();
    });

    t.expect(currentTexture![zeroDimension] === 0);

    // Using the texture should produce a validation error.
    t.expectValidationError(() => {
      currentTexture.createView();
    });
  });

g.test('viewFormats')
  .desc(
    `Test the validation that viewFormats are compatible with the format (for all canvas format / view formats)`
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('format', kCanvasTextureFormats)
      .combine('viewFormatFeature', kFeaturesForFormats)
      .beginSubcases()
      .expand('viewFormat', ({ viewFormatFeature }) =>
        filterFormatsByFeature(viewFormatFeature, kAllTextureFormats)
      )
  )
  .fn(t => {
    const { canvasType, format, viewFormat } = t.params;

    t.skipIfTextureFormatNotSupported(viewFormat);

    const canvas = createCanvas(t, canvasType, 1, 1);
    const ctx = canvas.getContext('webgpu');
    assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas');

    const compatible = textureFormatsAreViewCompatible(t.device.features, format, viewFormat);

    // Test configure() produces an error if the formats aren't compatible.
    t.expectValidationError(() => {
      ctx.configure({
        device: t.device,
        format,
        viewFormats: [viewFormat],
      });
    }, !compatible);

    const viewFormats = ctx.getConfiguration()!.viewFormats;
    t.expect(viewFormats[0] === viewFormat);

    // Likewise for getCurrentTexture().
    let currentTexture: GPUTexture;
    t.expectValidationError(() => {
      currentTexture = ctx.getCurrentTexture();
    }, !compatible);

    // The returned texture is an error texture.
    t.expectValidationError(() => {
      currentTexture.createView();
    }, !compatible);
  });