export const description = `createTexture validation tests.`;

import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert, hasFeature, makeValueTestVariant } from '../../../common/util/util.js';
import {
  kTextureDimensions,
  kTextureUsages,
  isValidTextureUsageCombination,
  kValidCombinationsOfOneOrTwoTextureUsages,
  kAllTextureUsages,
  kSomeBogusTextureUsage,
} from '../../capability_info.js';
import { GPUConst } from '../../constants.js';
import {
  kAllTextureFormats,
  kCompressedTextureFormats,
  kUncompressedTextureFormats,
  kRegularTextureFormats,
  kFeaturesForFormats,
  filterFormatsByFeature,
  textureFormatAndDimensionPossiblyCompatible,
  getBlockInfoForTextureFormat,
  isTextureFormatMultisampled,
  isTextureFormatColorRenderable,
  isTextureFormatPossiblyUsableAsColorRenderAttachment,
  isTextureFormatPossiblyStorageReadable,
  isColorTextureFormat,
  textureFormatsAreViewCompatible,
  textureDimensionAndFormatCompatibleForDevice,
  getMaxValidTextureSizeForFormatAndDimension,
  isTextureFormatUsableWithStorageAccessMode,
} from '../../format_info.js';
import { maxMipLevelCount } from '../../util/texture/base.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

g.test('zero_size_and_usage')
  .desc(
    `Test texture creation with zero or nonzero size of
    width, height, depthOrArrayLayers and mipLevelCount, usage for every dimension, and
    representative formats.
  `
  )
  .params(u =>
    u
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', [
        'rgba8unorm',
        'rgb10a2unorm',
        'bc1-rgba-unorm',
        'depth24plus-stencil8',
      ] as const)
      .beginSubcases()
      .combine('zeroArgument', [
        'none',
        'width',
        'height',
        'depthOrArrayLayers',
        'mipLevelCount',
        'usage',
      ] as const)
      // Filter out incompatible dimension type and format combinations.
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
  )
  .fn(t => {
    const { dimension, zeroArgument, format } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);

    const info = getBlockInfoForTextureFormat(format);

    const size = [info.blockWidth, info.blockHeight, 1];
    let mipLevelCount = 1;
    let usage = GPUTextureUsage.TEXTURE_BINDING;

    switch (zeroArgument) {
      case 'width':
        size[0] = 0;
        break;
      case 'height':
        size[1] = 0;
        break;
      case 'depthOrArrayLayers':
        size[2] = 0;
        break;
      case 'mipLevelCount':
        mipLevelCount = 0;
        break;
      case 'usage':
        usage = 0;
        break;
      default:
        break;
    }

    const descriptor = {
      size,
      mipLevelCount,
      dimension,
      format,
      usage,
    };

    const success = zeroArgument === 'none';

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('dimension_type_and_format_compatibility')
  .desc(
    `Test every dimension type on every format. Note that compressed formats and depth/stencil formats are not valid
    for 1D dimension types while it depends on the format for 3D types.`
  )
  .params(u =>
    u //
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', kAllTextureFormats)
  )
  .fn(t => {
    const { dimension, format } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const info = getBlockInfoForTextureFormat(format);

    const descriptor: GPUTextureDescriptor = {
      size: [info.blockWidth, info.blockHeight, 1],
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    t.expectValidationError(
      () => {
        t.createTextureTracked(descriptor);
      },
      !textureDimensionAndFormatCompatibleForDevice(t.device.features, dimension, format)
    );
  });

g.test('mipLevelCount,format')
  .desc(
    `Test texture creation with no mipmap chain, partial mipmap chain, full mipmap chain, out-of-bounds mipmap chain
    for every format with different texture dimension types.`
  )
  .params(u =>
    u
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', kAllTextureFormats)
      .beginSubcases()
      .combine('mipLevelCount', [1, 2, 3, 6, 7])
      // Filter out incompatible dimension type and format combinations.
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
      .combine('largestDimension', [0, 1, 2])
      .unless(({ dimension, largestDimension }) => dimension === '1d' && largestDimension > 0)
  )
  .fn(t => {
    const { dimension, format, mipLevelCount, largestDimension } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
    const info = getBlockInfoForTextureFormat(format);

    // Compute dimensions such that the dimensions are in range [17, 32] and aligned with the
    // format block size so that there will be exactly 6 mip levels.
    const kTargetMipLevelCount = 5;
    const kTargetLargeSize = (1 << kTargetMipLevelCount) - 1;
    const largeSize = [
      Math.floor(kTargetLargeSize / info.blockWidth) * info.blockWidth,
      Math.floor(kTargetLargeSize / info.blockHeight) * info.blockHeight,
      kTargetLargeSize,
    ];
    assert(17 <= largeSize[0] && largeSize[0] <= 32);
    assert(17 <= largeSize[1] && largeSize[1] <= 32);

    // Note that compressed formats are not valid for 1D. They have already been filtered out for 1D
    // in this test. So there is no dilemma about size.width equals 1 vs
    // size.width % info.blockHeight equals 0 for 1D compressed formats.
    const size = [info.blockWidth, info.blockHeight, 1];
    size[largestDimension] = largeSize[largestDimension];

    const descriptor = {
      size,
      mipLevelCount,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const success = mipLevelCount <= maxMipLevelCount(descriptor);

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('mipLevelCount,bound_check')
  .desc(
    `Test mip level count bound check upon different texture size and different texture dimension types.
    The cases below test: 1) there must be no mip levels after a 1 level (1D texture), or 1x1 level (2D texture), or 1x1x1 level (3D texture), 2) array layers are not mip-mapped, 3) power-of-two, non-power-of-two, and non-square sizes.`
  )
  .params(u =>
    u //
      .combine('format', ['rgba8unorm', 'bc1-rgba-unorm'] as const)
      .beginSubcases()
      .combineWithParams([
        { size: [32, 32] }, // Mip level sizes: 32x32, 16x16, 8x8, 4x4, 2x2, 1x1
        { size: [31, 32] }, // Mip level sizes: 31x32, 15x16, 7x8, 3x4, 1x2, 1x1
        { size: [28, 32] }, // Mip level sizes: 28x32, 14x16, 7x8, 3x4, 1x2, 1x1
        { size: [32, 31] }, // Mip level sizes: 32x31, 16x15, 8x7, 4x3, 2x1, 1x1
        { size: [32, 28] }, // Mip level sizes: 32x28, 16x14, 8x7, 4x3, 2x1, 1x1
        { size: [31, 31] }, // Mip level sizes: 31x31, 15x15, 7x7, 3x3, 1x1
        { size: [32], dimension: '1d' as const }, // Mip level sizes: 32, 16, 8, 4, 2, 1
        { size: [31], dimension: '1d' as const }, // Mip level sizes: 31, 15, 7, 3, 1
        { size: [32, 32, 32], dimension: '3d' as const }, // Mip level sizes: 32x32x32, 16x16x16, 8x8x8, 4x4x4, 2x2x2, 1x1x1
        { size: [32, 31, 31], dimension: '3d' as const }, // Mip level sizes: 32x31x31, 16x15x15, 8x7x7, 4x3x3, 2x1x1, 1x1x1
        { size: [31, 32, 31], dimension: '3d' as const }, // Mip level sizes: 31x32x31, 15x16x15, 7x8x7, 3x4x3, 1x2x1, 1x1x1
        { size: [31, 31, 32], dimension: '3d' as const }, // Mip level sizes: 31x31x32, 15x15x16, 7x7x8, 3x3x4, 1x1x2, 1x1x1
        { size: [31, 31, 31], dimension: '3d' as const }, // Mip level sizes: 31x31x31, 15x15x15, 7x7x7, 3x3x3, 1x1x1
        { size: [32, 8] }, // Mip levels: 32x8, 16x4, 8x2, 4x1, 2x1, 1x1
        { size: [32, 32, 64] }, // Mip levels: 32x32x64, 16x16x64, 8x8x64, 4x4x64, 2x2x64, 1x1x64
        { size: [32, 32, 64], dimension: '3d' as const }, // Mip levels: 32x32x64, 16x16x32, 8x8x16, 4x4x8, 2x2x4, 1x1x2, 1x1x1
      ])
      .unless(
        ({ format, size, dimension }) =>
          format === 'bc1-rgba-unorm' &&
          (dimension === '1d' ||
            dimension === '3d' ||
            size[0] % getBlockInfoForTextureFormat(format).blockWidth !== 0 ||
            size[1] % getBlockInfoForTextureFormat(format).blockHeight !== 0)
      )
  )
  .fn(t => {
    const { format, size, dimension } = t.params;
    t.skipIfTextureFormatNotSupported(format);

    const descriptor = {
      size,
      mipLevelCount: 0,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const mipLevelCount = maxMipLevelCount(descriptor);
    descriptor.mipLevelCount = mipLevelCount;
    t.createTextureTracked(descriptor);

    descriptor.mipLevelCount = mipLevelCount + 1;
    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    });
  });

g.test('mipLevelCount,bound_check,bigger_than_integer_bit_width')
  .desc(`Test mip level count bound check when mipLevelCount is bigger than integer bit width`)
  .fn(t => {
    const descriptor = {
      size: [32, 32],
      mipLevelCount: 100,
      format: 'rgba8unorm' as const,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    });
  });

g.test('sampleCount,various_sampleCount_with_all_formats')
  .desc(
    `Test texture creation with various (valid or invalid) sample count and all formats. Note that 1D and 3D textures can't support multisample.`
  )
  .params(u =>
    u
      .combine('dimension', [undefined, '2d'] as const)
      .combine('format', kAllTextureFormats)
      .beginSubcases()
      .combine('sampleCount', [0, 1, 2, 4, 8, 16, 32, 256])
  )
  .fn(t => {
    const { dimension, sampleCount, format } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const info = getBlockInfoForTextureFormat(format);

    const usage =
      sampleCount > 1
        ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
        : GPUTextureUsage.TEXTURE_BINDING;
    const descriptor = {
      size: [32 * info.blockWidth, 32 * info.blockHeight, 1],
      sampleCount,
      dimension,
      format,
      usage,
    };

    const success =
      sampleCount === 1 ||
      (sampleCount === 4 && isTextureFormatMultisampled(t.device.features, format));

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('sampleCount,valid_sampleCount_with_other_parameter_varies')
  .desc(
    `Test texture creation with valid sample count when dimensions, arrayLayerCount, mipLevelCount,
     format, and usage varies. Texture can be single sample (sampleCount is 1) or multi-sample
     (sampleCount is 4). Multisample texture requires that
     1) its dimension is 2d or undefined,
     2) its format supports multisample,
     3) its mipLevelCount and arrayLayerCount are 1,
     4) its usage doesn't include STORAGE_BINDING,
     5) its usage includes RENDER_ATTACHMENT.`
  )
  .params(u =>
    u
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', kAllTextureFormats)
      .beginSubcases()
      .combine('sampleCount', [1, 4])
      .combine('arrayLayerCount', [1, 2])
      .unless(
        ({ dimension, arrayLayerCount }) =>
          arrayLayerCount === 2 && dimension !== '2d' && dimension !== undefined
      )
      .combine('mipLevelCount', [1, 2])
      .combine('usage', kValidCombinationsOfOneOrTwoTextureUsages)
      // Filter out incompatible dimension type and format combinations.
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
      .unless(({ usage, format, arrayLayerCount, mipLevelCount, dimension }) => {
        return (
          ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
            (!isTextureFormatPossiblyUsableAsColorRenderAttachment(format) ||
              dimension !== '2d')) ||
          ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 &&
            !isTextureFormatPossiblyStorageReadable(format)) ||
          (mipLevelCount !== 1 && dimension === '1d') ||
          ((usage & GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) !== 0 &&
            (usage !==
              (GPUConst.TextureUsage.RENDER_ATTACHMENT |
                GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) ||
              mipLevelCount !== 1 ||
              arrayLayerCount !== 1))
        );
      })
  )
  .fn(t => {
    const { dimension, sampleCount, format, mipLevelCount, arrayLayerCount, usage } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
    if ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0) {
      t.skipIfTextureFormatNotUsableAsRenderAttachment(format);
    }
    // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
    if ((usage & GPUConst.TextureUsage.TRANSIENT_ATTACHMENT) !== 0) {
      t.skipIfTransientAttachmentNotSupported();
    }
    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);

    const size =
      dimension === '1d'
        ? [32 * blockWidth, 1 * blockHeight, 1]
        : dimension === '2d' || dimension === undefined
        ? [32 * blockWidth, 32 * blockHeight, arrayLayerCount]
        : [32 * blockWidth, 32 * blockHeight, 32];
    const descriptor = {
      size,
      mipLevelCount,
      sampleCount,
      dimension,
      format,
      usage,
    };

    const satisfyWithStorageUsageRequirement =
      (usage & GPUConst.TextureUsage.STORAGE_BINDING) === 0 ||
      isTextureFormatUsableWithStorageAccessMode(t.device.features, format, 'write-only');

    const success =
      (sampleCount === 1 && satisfyWithStorageUsageRequirement) ||
      (sampleCount === 4 &&
        isTextureFormatMultisampled(t.device.features, format) &&
        (dimension === '2d' || dimension === undefined) &&
        mipLevelCount === 1 &&
        arrayLayerCount === 1 &&
        (usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
        (usage & GPUConst.TextureUsage.STORAGE_BINDING) === 0);

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('sample_count,1d_2d_array_3d')
  .desc(`Test that you can not create 1d, 2d_array, and 3d multisampled textures`)
  .params(u =>
    u.combineWithParams([
      { dimension: '2d', size: [4, 4, 1], shouldError: false },
      { dimension: '1d', size: [4, 1, 1], shouldError: true },
      { dimension: '2d', size: [4, 4, 4], shouldError: true },
      { dimension: '2d', size: [4, 4, 6], shouldError: true },
      { dimension: '3d', size: [4, 4, 4], shouldError: true },
    ] as const)
  )
  .fn(t => {
    const { dimension, size, shouldError } = t.params;

    t.expectValidationError(() => {
      t.createTextureTracked({
        size,
        dimension,
        sampleCount: 4,
        format: 'rgba8unorm',
        usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
      });
    }, shouldError);
  });

g.test('texture_size,default_value_and_smallest_size,uncompressed_format')
  .desc(
    `Test default values for height and depthOrArrayLayers for every dimension type and every uncompressed format.
    It also tests smallest size (lower bound) for every dimension type and every uncompressed format, while other texture_size tests are testing the upper bound.`
  )
  .params(u =>
    u
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', kUncompressedTextureFormats)
      .beginSubcases()
      .combine('size', [[1], [1, 1], [1, 1, 1]])
      // Filter out incompatible dimension type and format combinations.
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
  )
  .fn(t => {
    const { dimension, format, size } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    t.createTextureTracked(descriptor);
  });

g.test('texture_size,default_value_and_smallest_size,compressed_format')
  .desc(
    `Test default values for height and depthOrArrayLayers for every dimension type and every compressed format.
    It also tests smallest size (lower bound) for every dimension type and every compressed format, while other texture_size tests are testing the upper bound.`
  )
  .params(u =>
    u
      // Compressed formats are invalid for 1D.
      .combine('dimension', [undefined, '2d', '3d'] as const)
      .combine('format', kCompressedTextureFormats)
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
      .beginSubcases()
      .expandWithParams(p => {
        const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(p.format);
        return [
          { size: [1], _success: false },
          { size: [blockWidth], _success: false },
          { size: [1, 1], _success: false },
          { size: [blockWidth, blockHeight], _success: true },
          { size: [1, 1, 1], _success: false },
          { size: [blockWidth, blockHeight, 1], _success: true },
        ];
      })
  )
  .fn(t => {
    const { dimension, format, size, _success } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    // With 'texture-compression-unaligned', mip level 0 is no longer required to be a multiple of
    // the texel block size, so every (small, in-range) size in this test becomes valid.
    const supportsUnaligned = hasFeature(
      t.device.features,
      'texture-compression-unaligned' as GPUFeatureName
    );
    const success = supportsUnaligned || _success;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_size,1d_texture')
  .desc(`Test texture size requirement for 1D texture`)
  .params(u =>
    u //
      // Compressed and depth-stencil textures are invalid for 1D.
      .combine('format', kRegularTextureFormats)
      .beginSubcases()
      .combine('widthVariant', [
        { mult: 1, add: -1 },
        { mult: 1, add: 0 },
        { mult: 1, add: 1 },
      ])
      .combine('height', [1, 2])
      .combine('depthOrArrayLayers', [1, 2])
  )
  .fn(t => {
    const { format, widthVariant, height, depthOrArrayLayers } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const width = t.makeLimitVariant('maxTextureDimension1D', widthVariant);

    const descriptor: GPUTextureDescriptor = {
      size: [width, height, depthOrArrayLayers],
      dimension: '1d' as const,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const success =
      width <= t.device.limits.maxTextureDimension1D && height === 1 && depthOrArrayLayers === 1;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_size,2d_texture,uncompressed_format')
  .desc(`Test texture size requirement for 2D texture with uncompressed format.`)
  .params(u =>
    u
      .combine('dimension', [undefined, '2d'] as const)
      .combine('format', kUncompressedTextureFormats)
      .combine(
        'sizeVariant',
        /* prettier-ignore */ [
          // Test the bound of width
          [{ mult: 1, add: -1 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          [{ mult: 1, add:  0 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          [{ mult: 1, add:  1 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          // Test the bound of height
          [{ mult: 0, add:  1 }, { mult: 1, add: -1 }, { mult: 0, add:  1 }],
          [{ mult: 0, add:  1 }, { mult: 1, add:  0 }, { mult: 0, add:  1 }],
          [{ mult: 0, add:  1 }, { mult: 1, add:  1 }, { mult: 0, add:  1 }],
          // Test the bound of array layers
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add: -1 }],
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add:  0 }],
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add:  1 }],
        ]
      )
  )
  .fn(t => {
    const { dimension, format, sizeVariant } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const size = [
      t.device.limits.maxTextureDimension2D,
      t.device.limits.maxTextureDimension2D,
      t.device.limits.maxTextureArrayLayers,
    ].map((limit, ndx) => makeValueTestVariant(limit, sizeVariant[ndx]));

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const success =
      size[0] <= t.device.limits.maxTextureDimension2D &&
      size[1] <= t.device.limits.maxTextureDimension2D &&
      size[2] <= t.device.limits.maxTextureArrayLayers;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_size,2d_texture,compressed_format')
  .desc(`Test texture size requirement for 2D texture with compressed format.`)
  .params(u =>
    u
      .combine('dimension', [undefined, '2d'] as const)
      .combine('format', kCompressedTextureFormats)
      .beginSubcases()
      .expand('sizeVariant', p => {
        const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(p.format);
        return [
          // Test the bound of width
          [
            { mult: 1, add: -1 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: -blockWidth },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: -blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: 0 },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: 1 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          // Test the bound of height
          [
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: -blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: -blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: +blockWidth },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: +blockHeight },
            { mult: 0, add: 1 },
          ],
          // Test the bound of array layers
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: +1 },
          ],
        ];
      })
  )
  .fn(t => {
    const { dimension, format, sizeVariant } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    const info = getBlockInfoForTextureFormat(format);
    const size = getMaxValidTextureSizeForFormatAndDimension(t.device, format, '2d').map(
      (limit, ndx) => makeValueTestVariant(limit, sizeVariant[ndx])
    );

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    // With 'texture-compression-unaligned', mip level 0 of a compressed texture is no longer
    // required to be a multiple of the texel block size, so unaligned widths/heights are valid.
    const supportsUnaligned = hasFeature(
      t.device.features,
      'texture-compression-unaligned' as GPUFeatureName
    );
    const success =
      (supportsUnaligned ||
        (size[0] % info.blockWidth === 0 && size[1] % info.blockHeight === 0)) &&
      size[0] <= t.device.limits.maxTextureDimension2D &&
      size[1] <= t.device.limits.maxTextureDimension2D &&
      size[2] <= t.device.limits.maxTextureArrayLayers;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_size,3d_texture,uncompressed_format')
  .desc(
    `Test texture size requirement for 3D texture with uncompressed format. Note that depth/stencil formats are invalid for 3D textures, so we only test regular formats.`
  )
  .params(u =>
    u //
      .combine('format', kRegularTextureFormats)
      .beginSubcases()
      .combine(
        'sizeVariant',
        /* prettier-ignore */ [
          // Test the bound of width
          [{ mult: 1, add: -1 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          [{ mult: 1, add:  0 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          [{ mult: 1, add: +1 }, { mult: 0, add:  1 }, { mult: 0, add:  1 }],
          // Test the bound of height
          [{ mult: 0, add:  1 }, { mult: 1, add: -1 }, { mult: 0, add:  1 }],
          [{ mult: 0, add:  1 }, { mult: 1, add:  0 }, { mult: 0, add:  1 }],
          [{ mult: 0, add:  1 }, { mult: 1, add: +1 }, { mult: 0, add:  1 }],
          // Test the bound of depth
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add: -1 }],
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add:  0 }],
          [{ mult: 0, add:  1 }, { mult: 0, add:  1 }, { mult: 1, add: +1 }],
        ]
      )
  )
  .fn(t => {
    const { format, sizeVariant } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, '3d');
    const maxTextureDimension3D = t.device.limits.maxTextureDimension3D;
    const size = sizeVariant.map(variant => t.makeLimitVariant('maxTextureDimension3D', variant));

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension: '3d' as const,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    const success =
      size[0] <= maxTextureDimension3D &&
      size[1] <= maxTextureDimension3D &&
      size[2] <= maxTextureDimension3D;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_size,3d_texture,compressed_format')
  .desc(`Test texture size requirement for 3D texture with compressed format.`)
  .params(u =>
    u //
      .combine('format', kCompressedTextureFormats)
      .beginSubcases()
      .expand('sizeVariant', p => {
        const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(p.format);
        return [
          // Test the bound of width
          [
            { mult: 1, add: -1 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: -blockWidth },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: -blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: 0 },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: +1 },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: +blockWidth },
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 1, add: +blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 0, add: 1 },
          ],
          // Test the bound of height
          [
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: -blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: -blockHeight },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: 0 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 1, add: +blockWidth },
            { mult: 0, add: 1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 1, add: +blockHeight },
            { mult: 0, add: 1 },
          ],
          // Test the bound of depth
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: -1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: 0 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: 1 },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: 1 },
            { mult: 0, add: blockHeight },
            { mult: 1, add: +1 },
          ],
          [
            { mult: 0, add: blockWidth },
            { mult: 0, add: blockHeight },
            { mult: 1, add: +1 },
          ],
        ];
      })
  )
  .fn(t => {
    const { format, sizeVariant } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, '3d');
    const maxTextureDimension3D = t.device.limits.maxTextureDimension3D;
    const info = getBlockInfoForTextureFormat(format);
    const size = getMaxValidTextureSizeForFormatAndDimension(t.device, format, '3d').map(
      (limit, ndx) => makeValueTestVariant(limit, sizeVariant[ndx])
    );

    const descriptor: GPUTextureDescriptor = {
      size,
      dimension: '3d' as const,
      format,
      usage: GPUTextureUsage.TEXTURE_BINDING,
    };

    // With 'texture-compression-unaligned', mip level 0 of a compressed texture is no longer
    // required to be a multiple of the texel block size, so unaligned widths/heights are valid.
    const supportsUnaligned = hasFeature(
      t.device.features,
      'texture-compression-unaligned' as GPUFeatureName
    );
    const success =
      (supportsUnaligned ||
        (size[0] % info.blockWidth === 0 && size[1] % info.blockHeight === 0)) &&
      size[0] <= maxTextureDimension3D &&
      size[1] <= maxTextureDimension3D &&
      size[2] <= maxTextureDimension3D &&
      textureDimensionAndFormatCompatibleForDevice(t.device.features, '3d', format);

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('texture_usage')
  .desc(
    `Test texture usage (single usage or combined usages) for every texture format and every dimension type`
  )
  .params(u =>
    u
      .combine('dimension', [undefined, ...kTextureDimensions])
      .combine('format', kAllTextureFormats)
      .beginSubcases()
      // If usage0 and usage1 are the same, then the usage being test is a single usage. Otherwise, it is a combined usage.
      .combine('usage0', kTextureUsages)
      .combine('usage1', kTextureUsages)
      // Filter out incompatible dimension type and format combinations.
      .filter(({ dimension, format }) =>
        textureFormatAndDimensionPossiblyCompatible(dimension, format)
      )
      .unless(({ usage0, usage1 }) => {
        return !isValidTextureUsageCombination(usage0 | usage1);
      })
  )
  .fn(t => {
    const { dimension, format, usage0, usage1 } = t.params;
    t.skipIfTextureFormatNotSupported(format);
    t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
    const info = getBlockInfoForTextureFormat(format);

    const size = [info.blockWidth, info.blockHeight, 1];
    const usage = usage0 | usage1;
    const descriptor = {
      size,
      dimension,
      format,
      usage,
    };

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

    let success = true;
    const appliedDimension = dimension ?? '2d';
    // Note that we unconditionally test copy usages for all formats and
    // expect failure if copying from or to is not supported.
    if (usage & GPUTextureUsage.STORAGE_BINDING) {
      if (!isTextureFormatUsableWithStorageAccessMode(t.device.features, format, 'write-only'))
        success = false;
    }
    if (usage & GPUTextureUsage.RENDER_ATTACHMENT) {
      if (appliedDimension === '1d') success = false;
      if (
        isColorTextureFormat(format) &&
        !isTextureFormatColorRenderable(t.device.features, format)
      )
        success = false;
    }
    if (usage & GPUTextureUsage.TRANSIENT_ATTACHMENT) {
      if (appliedDimension !== '2d') success = false;
    }

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('depthOrArrayLayers_and_mipLevelCount_for_transient_attachments')
  .desc(`Test depthOrArrayLayers and mipLevelCount must be 1 for transient attachments`)
  .params(u =>
    u
      .combine('format', ['rgba8unorm', 'depth24plus'] as const)
      .beginSubcases()
      .combine('depthOrArrayLayers', [1, 2])
      .combine('mipLevelCount', [1, 2])
  )
  .fn(t => {
    // MAINTENANCE_TODO(#4509): Remove this after all implementations have TRANSIENT_ATTACHMENT.
    t.skipIfTransientAttachmentNotSupported();

    const { format, depthOrArrayLayers, mipLevelCount } = t.params;

    const info = getBlockInfoForTextureFormat(format);
    const size = [info.blockWidth, info.blockHeight, depthOrArrayLayers];
    const descriptor = {
      size,
      mipLevelCount,
      format,
      usage: GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.TRANSIENT_ATTACHMENT,
    };

    let success = true;
    if (depthOrArrayLayers !== 1) success = false;
    if (mipLevelCount !== 1) success = false;

    t.expectValidationError(() => {
      t.createTextureTracked(descriptor);
    }, !success);
  });

g.test('usage')
  .desc('Test combinations of zero to two usage flags are validated to be valid.')
  .params(u =>
    u
      .combine('usage1', [0, ...kTextureUsages, kSomeBogusTextureUsage])
      .combine('usage2', [0, ...kTextureUsages, kSomeBogusTextureUsage])
      .filter(p => p.usage1 <= p.usage2)
  )
  .fn(t => {
    const { usage1, usage2 } = t.params;
    const usage = usage1 | usage2;

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

    const isValid =
      usage !== 0 &&
      (usage & ~kAllTextureUsages) === 0 &&
      ((usage & GPUTextureUsage.TRANSIENT_ATTACHMENT) === 0 ||
        usage === (GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT));

    t.expectGPUError(
      'validation',
      () => {
        t.createTextureTracked({ format: 'rgba8unorm', size: [1, 1], usage });
      },
      !isValid
    );
  });

g.test('new_usages')
  .desc(`Valid usages not present in GPUTextureUsage shouldn't be accepted by createTexture().`)
  .params(u =>
    u
      .combine('usage', [
        ...kTextureUsages,
        GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.TRANSIENT_ATTACHMENT,
      ])
      .filter(p => isValidTextureUsageCombination(p.usage))
  )
  .fn(t => {
    const { usage } = t.params;

    let exposedUsages = 0;
    for (const v of Object.values(GPUTextureUsage)) {
      exposedUsages |= v;
    }

    const success = (usage & exposedUsages) === usage;

    t.expectGPUError(
      'validation',
      () => {
        t.createTextureTracked({ format: 'rgba8unorm', size: [1, 1], usage });
      },
      !success
    );
  });

g.test('viewFormats')
  .desc(
    `Test creating a texture with viewFormats list for all {texture format}x{view format}. Only compatible view formats should be valid.`
  )
  .params(u =>
    u
      .combine('formatFeature', kFeaturesForFormats)
      .combine('viewFormatFeature', kFeaturesForFormats)
      .beginSubcases()
      .expand('format', ({ formatFeature }) =>
        filterFormatsByFeature(formatFeature, kAllTextureFormats)
      )
      .expand('viewFormat', ({ viewFormatFeature }) =>
        filterFormatsByFeature(viewFormatFeature, kAllTextureFormats)
      )
  )
  .fn(t => {
    const { format, viewFormat } = t.params;
    t.skipIfTextureFormatNotSupported(format, viewFormat);

    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);
    const compatible = textureFormatsAreViewCompatible(t.device.features, format, viewFormat);

    // Test the viewFormat in the list.
    t.expectValidationError(() => {
      t.createTextureTracked({
        format,
        size: [blockWidth, blockHeight],
        usage: GPUTextureUsage.TEXTURE_BINDING,
        viewFormats: [viewFormat],
      });
    }, !compatible);

    // Test the viewFormat and the texture format in the list.
    t.expectValidationError(() => {
      t.createTextureTracked({
        format,
        size: [blockWidth, blockHeight],
        usage: GPUTextureUsage.TEXTURE_BINDING,
        viewFormats: [viewFormat, format],
      });
    }, !compatible);

    // Test the viewFormat multiple times in the list.
    t.expectValidationError(() => {
      t.createTextureTracked({
        format,
        size: [blockWidth, blockHeight],
        usage: GPUTextureUsage.TEXTURE_BINDING,
        viewFormats: [viewFormat, viewFormat],
      });
    }, !compatible);
  });

g.test('transient_viewFormats')
  .desc(`Test that viewFormats is not allowed with TRANSIENT_ATTACHMENT textures.`)
  .params(u =>
    u
      // Just test rgba8unorm formats as this check doesn't care about what the format is.
      .combineWithParams([
        { format: 'rgba8unorm', _otherFormat: 'rgba8unorm-srgb' },
        { format: 'rgba8unorm-srgb', _otherFormat: 'rgba8unorm' },
      ] as const)
      .beginSubcases()
      .expandWithParams(({ format, _otherFormat }) => [
        // Control cases
        { useTransient: true, viewFormat: undefined },
        { useTransient: false, viewFormat: format },
        { useTransient: false, viewFormat: _otherFormat },
        // Invalid cases
        { useTransient: true, viewFormat: format },
        { useTransient: true, viewFormat: _otherFormat },
      ])
  )
  .fn(t => {
    const { format, viewFormat, useTransient } = t.params;
    if (viewFormat && !textureFormatsAreViewCompatible(t.device.features, format, viewFormat)) {
      t.skip(`"${format}" and "${viewFormat}" are not view-compatible`);
    }

    const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);

    const invalid = useTransient && viewFormat !== undefined;
    let tex: GPUTexture;
    t.expectValidationError(() => {
      tex = t.createTextureTracked({
        format,
        size: [blockWidth, blockHeight],
        usage:
          GPUConst.TextureUsage.RENDER_ATTACHMENT |
          (useTransient ? GPUConst.TextureUsage.TRANSIENT_ATTACHMENT : 0),
        // Doesn't matter what formats we request, TRANSIENT_ATTACHMENT doesn't allow it.
        // So we use [format] since that's otherwise ALWAYS valid regardless of format.
        viewFormats: viewFormat ? [viewFormat] : undefined,
      });
    }, invalid);

    if (invalid) {
      // When we reach here the texture should be invalid, so creating a view should be invalid.
      // But try it anyway just to see what happens - if the browser had a bug above, this could
      // issue a bad command to the backend API.
      t.expectValidationError(() => {
        tex.createView({ format: viewFormat });
      }, true);
    }
  });
