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

export const description = `
Validation tests for GPUBuffer.mapAsync, GPUBuffer.unmap and GPUBuffer.getMappedRange.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { attemptGarbageCollection } from '../../../../common/util/collect_garbage.js';
import { assert, unreachable } from '../../../../common/util/util.js';
import { kBufferUsages } from '../../../capability_info.js';
import { GPUConst } from '../../../constants.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as vtu from '../validation_test_utils.js';

class F extends AllFeaturesMaxLimitsGPUTest {
  async testMapAsyncCall(
    expectation:
      | 'success'
      | { validationError: boolean; earlyRejection: boolean; rejectName: string },
    buffer: GPUBuffer,
    mode: GPUMapModeFlags,
    offset?: number,
    size?: number
  ) {
    if (expectation === 'success') {
      const p = buffer.mapAsync(mode, offset, size);
      await p;
    } else {
      let p: Promise<void>;
      this.expectValidationError(() => {
        p = buffer.mapAsync(mode, offset, size);
      }, expectation.validationError);

      let caught = false;
      let rejectedEarly = false;
      let microtaskBRan = false;
      // If mapAsync rejected early, microtask A will run before B.
      // If not, B will run before A.
      p!.catch(() => {
        // Microtask A
        caught = true;
      });
      queueMicrotask(() => {
        // Microtask B
        rejectedEarly = caught;
        microtaskBRan = true;
      });

      // These handlers should always run after microtasks A and B are both done.
      await p!.then(
        () => {
          unreachable('mapAsync unexpectedly passed');
        },
        ex => {
          const suffix = `\n  Rejection: ${ex}`;

          this.expect(microtaskBRan, 'scheduling problem?: microtaskB has not run yet' + suffix);
          assert(ex instanceof Error, 'mapAsync rejected with non-error' + suffix);
          this.expect(typeof ex.stack === 'string', 'mapAsync rejected without a stack' + suffix);
          this.expect(
            expectation.rejectName === ex.name,
            'mapAsync rejected with wrong exception name' + suffix
          );
          if (expectation.earlyRejection) {
            this.expect(rejectedEarly, 'expected early mapAsync rejection, got deferred' + suffix);
          } else {
            this.expect(!rejectedEarly, 'expected deferred mapAsync rejection, got early' + suffix);
          }
        }
      );
    }
  }

  testGetMappedRangeCall(success: boolean, buffer: GPUBuffer, offset?: number, size?: number) {
    if (success) {
      const data = buffer.getMappedRange(offset, size);
      this.expect(data instanceof ArrayBuffer);
      if (size !== undefined) {
        this.expect(data.byteLength === size);
      }
    } else {
      this.shouldThrow('OperationError', () => {
        buffer.getMappedRange(offset, size);
      });
    }
  }

  createMappableBuffer(type: GPUMapModeFlags, size: number): GPUBuffer {
    switch (type) {
      case GPUMapMode.READ:
        return this.createBufferTracked({
          size,
          usage: GPUBufferUsage.MAP_READ,
        });
      case GPUMapMode.WRITE:
        return this.createBufferTracked({
          size,
          usage: GPUBufferUsage.MAP_WRITE,
        });
      default:
        unreachable();
    }
  }
}

export const g = makeTestGroup(F);

const kMapModeOptions = [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE];
const kOffsetAlignment = 8;
const kSizeAlignment = 4;

g.test('mapAsync,usage')
  .desc(
    `Test the usage validation for mapAsync.

  For each buffer usage:
  For GPUMapMode.READ, GPUMapMode.WRITE, and 0:
    Test that the mapAsync call is valid iff the mapping usage is not 0 and the buffer usage
    the mapMode flag.`
  )
  .paramsSubcasesOnly(u =>
    u //
      .combineWithParams([
        { mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
        { mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
        // Using mapMode 0 is never valid, so there is no validUsage.
        { mapMode: 0, validUsage: null },
      ])
      .combine('usage', kBufferUsages)
  )
  .fn(async t => {
    const { mapMode, validUsage, usage } = t.params;

    const buffer = t.createBufferTracked({
      size: 16,
      usage,
    });

    const successParam =
      usage === validUsage
        ? 'success'
        : {
            validationError: true,
            earlyRejection: false,
            rejectName: 'OperationError',
          };
    await t.testMapAsyncCall(successParam, buffer, mapMode);
  });

g.test('mapAsync,invalidBuffer')
  .desc('Test that mapAsync is an error when called on an invalid buffer.')
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = vtu.getErrorBuffer(t);
    await t.testMapAsyncCall(
      { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
      buffer,
      mapMode
    );
  });

g.test('mapAsync,state,destroyed')
  .desc('Test that mapAsync is an error when called on a destroyed buffer.')
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);

    // Start mapping the buffer, we are going to destroy it before it resolves so it will reject
    // the mapping promise with an AbortError.
    const pending = t.testMapAsyncCall(
      { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
      buffer,
      mapMode
    );

    buffer.destroy();
    await t.testMapAsyncCall(
      { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    await pending;
  });

g.test('mapAsync,state,mappedAtCreation')
  .desc(
    `Test that mapAsync is an error when called on a buffer mapped at creation,
    but succeeds after unmapping it.`
  )
  .paramsSubcasesOnly([
    { mapMode: GPUConst.MapMode.READ, validUsage: GPUConst.BufferUsage.MAP_READ },
    { mapMode: GPUConst.MapMode.WRITE, validUsage: GPUConst.BufferUsage.MAP_WRITE },
  ])
  .fn(async t => {
    const { mapMode, validUsage } = t.params;

    const buffer = t.createBufferTracked({
      size: 16,
      usage: validUsage,
      mappedAtCreation: true,
    });
    await t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    buffer.unmap();
    await t.testMapAsyncCall('success', buffer, mapMode);
  });

g.test('mapAsync,state,mapped')
  .desc(
    `Test that mapAsync is an error when called on a mapped buffer, but succeeds
    after unmapping it.`
  )
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;

    const buffer = t.createMappableBuffer(mapMode, 16);
    await t.testMapAsyncCall('success', buffer, mapMode);
    await t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    buffer.unmap();
    await t.testMapAsyncCall('success', buffer, mapMode);
  });

g.test('mapAsync,state,mappingPending')
  .desc(
    `Test that mapAsync is rejected when called on a buffer that is being mapped,
    but succeeds after the previous mapping request is cancelled.`
  )
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;

    const buffer = t.createMappableBuffer(mapMode, 16);

    // Start mapping the buffer, we are going to unmap it before it resolves so it will reject
    // the mapping promise with an AbortError.
    const pending0 = t.testMapAsyncCall(
      { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
      buffer,
      mapMode
    );

    // Do the test of mapAsync while [[pending_map]] is non-null. It has to be synchronous so
    // that we can unmap the previous mapping in the same stack frame and testing this one doesn't
    // get canceled, but instead is rejected.
    const pending1 = t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    // Unmap the first mapping. It should now be possible to successfully call mapAsync
    // This unmap should cause the first mapAsync rejection.
    buffer.unmap();
    await t.testMapAsyncCall('success', buffer, mapMode);

    await pending0;
    await pending1;
  });

g.test('mapAsync,sizeUnspecifiedOOB')
  .desc(
    `Test that mapAsync with size unspecified rejects if offset > buffer.[[size]],
    with various cases at the limits of the buffer size or with a misaligned offset.
    Also test for an empty buffer.`
  )
  .paramsSubcasesOnly(u =>
    u //
      .combine('mapMode', kMapModeOptions)
      .combineWithParams([
        // 0 size buffer.
        { bufferSize: 0, offset: 0 },
        { bufferSize: 0, offset: 1 },
        { bufferSize: 0, offset: kOffsetAlignment },

        // Test with a buffer that's not empty.
        { bufferSize: 16, offset: 0 },
        { bufferSize: 16, offset: kOffsetAlignment },
        { bufferSize: 16, offset: 16 },
        { bufferSize: 16, offset: 17 },
        { bufferSize: 16, offset: 16 + kOffsetAlignment },
      ])
  )
  .fn(async t => {
    const { mapMode, bufferSize, offset } = t.params;
    const buffer = t.createMappableBuffer(mapMode, bufferSize);

    const successParam =
      offset <= bufferSize
        ? 'success'
        : {
            validationError: true,
            earlyRejection: false,
            rejectName: 'OperationError',
          };
    await t.testMapAsyncCall(successParam, buffer, mapMode, offset);
  });

g.test('mapAsync,offsetAndSizeAlignment')
  .desc("Test that mapAsync fails if the alignment of offset and size isn't correct.")
  .paramsSubcasesOnly(u =>
    u
      .combine('mapMode', kMapModeOptions)
      .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
      .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
  )
  .fn(async t => {
    const { mapMode, offset, size } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);

    const successParam =
      offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0
        ? 'success'
        : {
            validationError: true,
            earlyRejection: false,
            rejectName: 'OperationError',
          };
    await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
  });

g.test('mapAsync,offsetAndSizeOOB')
  .desc('Test that mapAsync fails if offset + size is larger than the buffer size.')
  .paramsSubcasesOnly(u =>
    u //
      .combine('mapMode', kMapModeOptions)
      .combineWithParams([
        // For a 0 size buffer
        { bufferSize: 0, offset: 0, size: 0 },
        { bufferSize: 0, offset: 0, size: 4 },
        { bufferSize: 0, offset: 8, size: 0 },

        // For a small buffer
        { bufferSize: 16, offset: 0, size: 16 },
        { bufferSize: 16, offset: kOffsetAlignment, size: 16 },

        { bufferSize: 16, offset: 16, size: 0 },
        { bufferSize: 16, offset: 16, size: kSizeAlignment },

        { bufferSize: 16, offset: 8, size: 0 },
        { bufferSize: 16, offset: 8, size: 8 },
        { bufferSize: 16, offset: 8, size: 8 + kSizeAlignment },

        // For a larger buffer
        { bufferSize: 1024, offset: 0, size: 1024 },
        { bufferSize: 1024, offset: kOffsetAlignment, size: 1024 },

        { bufferSize: 1024, offset: 1024, size: 0 },
        { bufferSize: 1024, offset: 1024, size: kSizeAlignment },

        { bufferSize: 1024, offset: 512, size: 0 },
        { bufferSize: 1024, offset: 512, size: 512 },
        { bufferSize: 1024, offset: 512, size: 512 + kSizeAlignment },
      ])
  )
  .fn(async t => {
    const { mapMode, bufferSize, size, offset } = t.params;
    const buffer = t.createMappableBuffer(mapMode, bufferSize);

    const successParam =
      offset + size <= bufferSize
        ? 'success'
        : {
            validationError: true,
            earlyRejection: false,
            rejectName: 'OperationError',
          };
    await t.testMapAsyncCall(successParam, buffer, mapMode, offset, size);
  });

g.test('mapAsync,earlyRejection')
  .desc("Test that mapAsync fails immediately if it's pending map.")
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions).combine('offset2', [0, 8]))
  .fn(async t => {
    const { mapMode, offset2 } = t.params;

    const bufferSize = 16;
    const mapSize = 8;
    const offset1 = 0;

    const buffer = t.createMappableBuffer(mapMode, bufferSize);
    const p1 = buffer.mapAsync(mapMode, offset1, mapSize); // succeeds
    await t.testMapAsyncCall(
      {
        validationError: true,
        earlyRejection: true,
        rejectName: 'OperationError',
      },
      buffer,
      mapMode,
      offset2,
      mapSize
    );
    await p1; // ensure the original map still succeeds
  });

g.test('mapAsync,abort_over_invalid_error')
  .desc('Test that unmap abort error should have precedence over validation error')
  .paramsSubcasesOnly(u =>
    u.combine('mapMode', kMapModeOptions).combine('unmapBeforeResolve', [true, false])
  )
  .fn(async t => {
    const { mapMode, unmapBeforeResolve } = t.params;
    const bufferSize = 8;
    const buffer = t.createBufferTracked({
      usage: GPUBufferUsage.STORAGE,
      size: bufferSize,
    });

    if (unmapBeforeResolve) {
      // unmap abort error should have precedence over validation error
      const pending = t.testMapAsyncCall(
        { validationError: true, earlyRejection: false, rejectName: 'AbortError' },
        buffer,
        mapMode
      );
      buffer.unmap();
      await pending;
    } else {
      // invalid map call should cause validation error
      await t.testMapAsyncCall(
        { validationError: true, earlyRejection: false, rejectName: 'OperationError' },
        buffer,
        mapMode
      );
      buffer.unmap();
    }
  });

g.test('getMappedRange,state,mapped')
  .desc('Test that it is valid to call getMappedRange in the mapped state')
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const bufferSize = 16;
    const buffer = t.createMappableBuffer(mapMode, bufferSize);
    await buffer.mapAsync(mapMode);

    const data = buffer.getMappedRange();
    t.expect(data instanceof ArrayBuffer);
    t.expect(data.byteLength === bufferSize);

    // map on already mapped buffer should be rejected
    const pending = t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );
    t.expect(data.byteLength === bufferSize);
    await pending;

    buffer.unmap();

    t.expect(data.byteLength === 0);
  });

g.test('getMappedRange,state,mappedAtCreation')
  .desc(
    `Test that, in the mapped-at-creation state, it is valid to call getMappedRange, for all buffer usages,
    and invalid to call mapAsync, for all map modes.`
  )
  .paramsSubcasesOnly(u =>
    u.combine('bufferUsage', kBufferUsages).combine('mapMode', kMapModeOptions)
  )
  .fn(async t => {
    const { bufferUsage, mapMode } = t.params;
    const bufferSize = 16;
    const buffer = t.createBufferTracked({
      usage: bufferUsage,
      size: bufferSize,
      mappedAtCreation: true,
    });

    const data = buffer.getMappedRange();
    t.expect(data instanceof ArrayBuffer);
    t.expect(data.byteLength === bufferSize);

    // map on already mapped buffer should be rejected
    const pending = t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );
    t.expect(data.byteLength === bufferSize);
    await pending;

    buffer.unmap();

    t.expect(data.byteLength === 0);
  });

g.test('getMappedRange,state,invalid_mappedAtCreation')
  .desc(
    `mappedAtCreation should return a mapped buffer, even if the buffer is invalid.
Like VRAM allocation (see map_oom), validation can be performed asynchronously (in the GPU process)
so the Content process doesn't necessarily know the buffer is invalid.`
  )
  .fn(t => {
    const buffer = t.expectGPUError('validation', () =>
      t.createBufferTracked({
        mappedAtCreation: true,
        size: 16,
        usage: 0xffff_ffff, // Invalid usage
      })
    );

    // Should still be valid.
    buffer.getMappedRange();
  });

g.test('getMappedRange,state,mappedAgain')
  .desc(
    'Test that it is valid to call getMappedRange in the mapped state, even if there is a duplicate mapAsync before'
  )
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);
    await buffer.mapAsync(mapMode);

    // call mapAsync again on already mapped buffer should fail
    await t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    // getMapppedRange should still success
    t.testGetMappedRangeCall(true, buffer);
  });

g.test('getMappedRange,state,unmapped')
  .desc(
    `Test that it is invalid to call getMappedRange in the unmapped state.
Test for various cases of being unmapped: at creation, after a mapAsync call or after being created mapped.`
  )
  .fn(async t => {
    // It is invalid to call getMappedRange when the buffer starts unmapped when created.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      t.testGetMappedRangeCall(false, buffer);
    }

    // It is invalid to call getMappedRange when the buffer is unmapped after mapAsync.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      await buffer.mapAsync(GPUMapMode.READ);
      buffer.unmap();
      t.testGetMappedRangeCall(false, buffer);
    }

    // It is invalid to call getMappedRange when the buffer is unmapped after mappedAtCreation.
    {
      const buffer = t.createBufferTracked({
        usage: GPUBufferUsage.MAP_READ,
        size: 16,
        mappedAtCreation: true,
      });
      buffer.unmap();
      t.testGetMappedRangeCall(false, buffer);
    }
  });

g.test('getMappedRange,subrange,mapped')
  .desc(
    `Test that old getMappedRange returned arraybuffer does not exist after unmap, and newly returned
    arraybuffer after new map has correct subrange`
  )
  .params(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const bufferSize = 16;
    const offset = 8;
    const subrangeSize = bufferSize - offset;
    const buffer = t.createMappableBuffer(mapMode, bufferSize);
    await buffer.mapAsync(mapMode);

    const data0 = buffer.getMappedRange();
    t.expect(data0 instanceof ArrayBuffer);
    t.expect(data0.byteLength === bufferSize);

    buffer.unmap();
    t.expect(data0.byteLength === 0);

    await buffer.mapAsync(mapMode, offset);
    const data1 = buffer.getMappedRange(8);

    t.expect(data0.byteLength === 0);
    t.expect(data1.byteLength === subrangeSize);
  });

g.test('getMappedRange,subrange,mappedAtCreation')
  .desc(
    `Test that old getMappedRange returned arraybuffer does not exist after unmap and newly returned
    arraybuffer after new map has correct subrange`
  )
  .fn(async t => {
    const bufferSize = 16;
    const offset = 8;
    const subrangeSize = bufferSize - offset;
    const buffer = t.createBufferTracked({
      size: bufferSize,
      usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
      mappedAtCreation: true,
    });

    const data0 = buffer.getMappedRange();
    t.expect(data0 instanceof ArrayBuffer);
    t.expect(data0.byteLength === bufferSize);

    buffer.unmap();
    t.expect(data0.byteLength === 0);

    await buffer.mapAsync(GPUMapMode.READ, offset);
    const data1 = buffer.getMappedRange(8);

    t.expect(data0.byteLength === 0);
    t.expect(data1.byteLength === subrangeSize);
  });

g.test('getMappedRange,state,destroyed')
  .desc(
    `Test that it is invalid to call getMappedRange in the destroyed state.
Test for various cases of being destroyed: at creation, after a mapAsync call or after being created mapped.`
  )
  .fn(async t => {
    // It is invalid to call getMappedRange when the buffer is destroyed when unmapped.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      buffer.destroy();
      t.testGetMappedRangeCall(false, buffer);
    }

    // It is invalid to call getMappedRange when the buffer is destroyed when mapped.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      await buffer.mapAsync(GPUMapMode.READ);
      buffer.destroy();
      t.testGetMappedRangeCall(false, buffer);
    }

    // It is invalid to call getMappedRange when the buffer is destroyed when mapped at creation.
    {
      const buffer = t.createBufferTracked({
        usage: GPUBufferUsage.MAP_READ,
        size: 16,
        mappedAtCreation: true,
      });
      buffer.destroy();
      t.testGetMappedRangeCall(false, buffer);
    }
  });

g.test('getMappedRange,state,mappingPending')
  .desc(`Test that it is invalid to call getMappedRange in the mappingPending state.`)
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);

    /* noawait */ const mapping0 = buffer.mapAsync(mapMode);
    // seconding mapping should be rejected
    const mapping1 = t.testMapAsyncCall(
      { validationError: true, earlyRejection: true, rejectName: 'OperationError' },
      buffer,
      mapMode
    );

    // invalid in mappingPending state
    t.testGetMappedRangeCall(false, buffer);

    await mapping0;

    // valid after buffer is mapped
    t.testGetMappedRangeCall(true, buffer);

    await mapping1;
  });

g.test('getMappedRange,offsetAndSizeAlignment,mapped')
  .desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
  .params(u =>
    u
      .combine('mapMode', kMapModeOptions)
      .beginSubcases()
      .combine('mapOffset', [0, kOffsetAlignment])
      .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
      .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
  )
  .fn(async t => {
    const { mapMode, mapOffset, offset, size } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 32);
    await buffer.mapAsync(mapMode, mapOffset);

    const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
    t.testGetMappedRangeCall(success, buffer, offset + mapOffset, size);
  });

g.test('getMappedRange,offsetAndSizeAlignment,mappedAtCreation')
  .desc(`Test that getMappedRange fails if the alignment of offset and size isn't correct.`)
  .paramsSubcasesOnly(u =>
    u
      .combine('offset', [0, kOffsetAlignment, kOffsetAlignment / 2])
      .combine('size', [0, kSizeAlignment, kSizeAlignment / 2])
  )
  .fn(t => {
    const { offset, size } = t.params;
    const buffer = t.createBufferTracked({
      size: 16,
      usage: GPUBufferUsage.COPY_DST,
      mappedAtCreation: true,
    });
    const success = offset % kOffsetAlignment === 0 && size % kSizeAlignment === 0;
    t.testGetMappedRangeCall(success, buffer, offset, size);
  });

g.test('getMappedRange,sizeAndOffsetOOB,mappedAtCreation')
  .desc(
    `Test that getMappedRange size + offset must be less than the buffer size for a
    buffer mapped at creation. (and offset has not constraints on its own)`
  )
  .paramsSubcasesOnly([
    // Tests for a zero-sized buffer, with and without a size defined.
    { bufferSize: 0, offset: undefined, size: undefined },
    { bufferSize: 0, offset: undefined, size: 0 },
    { bufferSize: 0, offset: undefined, size: kSizeAlignment },
    { bufferSize: 0, offset: 0, size: undefined },
    { bufferSize: 0, offset: 0, size: 0 },
    { bufferSize: 0, offset: kOffsetAlignment, size: undefined },
    { bufferSize: 0, offset: kOffsetAlignment, size: 0 },

    // Tests for a non-empty buffer, with an undefined offset.
    { bufferSize: 80, offset: undefined, size: 80 },
    { bufferSize: 80, offset: undefined, size: 80 + kSizeAlignment },

    // Tests for a non-empty buffer, with an undefined size.
    { bufferSize: 80, offset: undefined, size: undefined },
    { bufferSize: 80, offset: 0, size: undefined },
    { bufferSize: 80, offset: kOffsetAlignment, size: undefined },
    { bufferSize: 80, offset: 80, size: undefined },
    { bufferSize: 80, offset: 80 + kOffsetAlignment, size: undefined },

    // Tests for a non-empty buffer with a size defined.
    { bufferSize: 80, offset: 0, size: 80 },
    { bufferSize: 80, offset: 0, size: 80 + kSizeAlignment },
    { bufferSize: 80, offset: kOffsetAlignment, size: 80 },

    { bufferSize: 80, offset: 40, size: 40 },
    { bufferSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
    { bufferSize: 80, offset: 40, size: 40 + kSizeAlignment },
  ])
  .fn(t => {
    const { bufferSize, offset, size } = t.params;
    const buffer = t.createBufferTracked({
      size: bufferSize,
      usage: GPUBufferUsage.COPY_DST,
      mappedAtCreation: true,
    });

    const actualOffset = offset ?? 0;
    const actualSize = size ?? bufferSize - actualOffset;

    const success = actualOffset <= bufferSize && actualOffset + actualSize <= bufferSize;
    t.testGetMappedRangeCall(success, buffer, offset, size);
  });

g.test('getMappedRange,sizeAndOffsetOOB,mapped')
  .desc('Test that getMappedRange size + offset must be less than the mapAsync range.')
  .paramsSubcasesOnly(u =>
    u //
      .combine('mapMode', kMapModeOptions)
      .combineWithParams([
        // Tests for an empty buffer, and implicit mapAsync size.
        { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: undefined },
        { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: undefined, size: 0 },
        {
          bufferSize: 0,
          mapOffset: 0,
          mapSize: undefined,
          offset: undefined,
          size: kSizeAlignment,
        },
        { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: undefined },
        { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: 0, size: 0 },
        {
          bufferSize: 0,
          mapOffset: 0,
          mapSize: undefined,
          offset: kOffsetAlignment,
          size: undefined,
        },
        { bufferSize: 0, mapOffset: 0, mapSize: undefined, offset: kOffsetAlignment, size: 0 },

        // Tests for an empty buffer, and explicit mapAsync size.
        { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: undefined, size: undefined },
        { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: undefined },
        { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: 0, size: 0 },
        { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: undefined },
        { bufferSize: 0, mapOffset: 0, mapSize: 0, offset: kOffsetAlignment, size: 0 },

        // Test for a fully implicit mapAsync call
        { bufferSize: 80, mapOffset: undefined, mapSize: undefined, offset: 0, size: 80 },
        {
          bufferSize: 80,
          mapOffset: undefined,
          mapSize: undefined,
          offset: 0,
          size: 80 + kSizeAlignment,
        },
        {
          bufferSize: 80,
          mapOffset: undefined,
          mapSize: undefined,
          offset: kOffsetAlignment,
          size: 80,
        },

        // Test for a mapAsync call with an implicit size
        { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: 80 - 24 },
        {
          bufferSize: 80,
          mapOffset: 24,
          mapSize: undefined,
          offset: 0,
          size: 80 - 24 + kSizeAlignment,
        },
        {
          bufferSize: 80,
          mapOffset: 24,
          mapSize: undefined,
          offset: kOffsetAlignment,
          size: 80 - 24,
        },

        // Test for a non-empty buffer fully mapped.
        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 },
        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: kOffsetAlignment, size: 80 },
        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 0, size: 80 + kSizeAlignment },

        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 },
        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40 + kOffsetAlignment, size: 40 },
        { bufferSize: 80, mapOffset: 0, mapSize: 80, offset: 40, size: 40 + kSizeAlignment },

        // Test for a buffer partially mapped.
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 },
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 - kOffsetAlignment, size: 40 },
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24 + kOffsetAlignment, size: 40 },
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: 40 + kSizeAlignment },

        // Test for a partially mapped buffer with implicit size and offset for getMappedRange.
        // - Buffer partially mapped in the middle
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: undefined, size: undefined },
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 0, size: undefined },
        { bufferSize: 80, mapOffset: 24, mapSize: 40, offset: 24, size: undefined },
        // - Buffer partially mapped to the end
        { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 24, size: undefined },
        { bufferSize: 80, mapOffset: 24, mapSize: undefined, offset: 80, size: undefined },
        // - Buffer partially mapped from the start
        { bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: undefined },
        { bufferSize: 80, mapOffset: 0, mapSize: 64, offset: undefined, size: 64 },
      ])
  )
  .fn(async t => {
    const { mapMode, bufferSize, mapOffset, mapSize, offset, size } = t.params;
    const buffer = t.createMappableBuffer(mapMode, bufferSize);
    await buffer.mapAsync(mapMode, mapOffset, mapSize);

    const actualMapOffset = mapOffset ?? 0;
    const actualMapSize = mapSize ?? bufferSize - actualMapOffset;

    const actualOffset = offset ?? 0;
    const actualSize = size ?? bufferSize - actualOffset;

    const success =
      actualOffset >= actualMapOffset &&
      actualOffset <= bufferSize &&
      actualOffset + actualSize <= actualMapOffset + actualMapSize;
    t.testGetMappedRangeCall(success, buffer, offset, size);
  });

g.test('getMappedRange,disjointRanges')
  .desc('Test that the ranges asked through getMappedRange must be disjoint.')
  .paramsSubcasesOnly(u =>
    u //
      .combine('remapBetweenCalls', [false, true])
      .combineWithParams([
        // Disjoint ranges with one that's empty.
        { offset1: 8, size1: 0, offset2: 8, size2: 8 },
        { offset1: 16, size1: 0, offset2: 8, size2: 8 },

        { offset1: 8, size1: 8, offset2: 8, size2: 0 },
        { offset1: 8, size1: 8, offset2: 16, size2: 0 },

        // Disjoint ranges with both non-empty.
        { offset1: 0, size1: 8, offset2: 8, size2: 8 },
        { offset1: 16, size1: 8, offset2: 8, size2: 8 },

        { offset1: 8, size1: 8, offset2: 0, size2: 8 },
        { offset1: 8, size1: 8, offset2: 16, size2: 8 },

        // Empty range contained inside another one.
        { offset1: 16, size1: 20, offset2: 24, size2: 0 },
        { offset1: 24, size1: 0, offset2: 16, size2: 20 },

        // Ranges that overlap only partially.
        { offset1: 16, size1: 20, offset2: 8, size2: 20 },
        { offset1: 16, size1: 20, offset2: 32, size2: 20 },

        // Ranges that include one another.
        { offset1: 0, size1: 80, offset2: 16, size2: 20 },
        { offset1: 16, size1: 20, offset2: 0, size2: 80 },
      ])
  )
  .fn(async t => {
    const { offset1, size1, offset2, size2, remapBetweenCalls } = t.params;
    const buffer = t.createBufferTracked({ size: 80, usage: GPUBufferUsage.MAP_READ });
    await buffer.mapAsync(GPUMapMode.READ);

    t.testGetMappedRangeCall(true, buffer, offset1, size1);

    if (remapBetweenCalls) {
      buffer.unmap();
      await buffer.mapAsync(GPUMapMode.READ);
    }

    const range1StartsAfter2 = offset1 >= offset2 + size2;
    const range2StartsAfter1 = offset2 >= offset1 + size1;
    const disjoint = range1StartsAfter2 || range2StartsAfter1;
    const success = disjoint || remapBetweenCalls;

    t.testGetMappedRangeCall(success, buffer, offset2, size2);
  });

g.test('getMappedRange,disjointRanges_many')
  .desc('Test getting a lot of small ranges, and that the disjoint check checks them all.')
  .fn(async t => {
    const kStride = 256;
    const kNumStrides = 256;

    const buffer = t.createBufferTracked({
      size: kStride * kNumStrides,
      usage: GPUBufferUsage.MAP_READ,
    });
    await buffer.mapAsync(GPUMapMode.READ);

    // Get a lot of small mapped ranges.
    for (let stride = 0; stride < kNumStrides; stride++) {
      t.testGetMappedRangeCall(true, buffer, stride * kStride, 8);
    }

    // Check for each range it is invalid to get a range that overlaps it and check that it is valid
    // to get ranges for the rest of the buffer.
    for (let stride = 0; stride < kNumStrides; stride++) {
      t.testGetMappedRangeCall(false, buffer, stride * kStride, kStride);
      t.testGetMappedRangeCall(true, buffer, stride * kStride + 8, kStride - 8);
    }
  });

g.test('unmap,state,unmapped')
  .desc(
    `Test it is valid to call unmap on a buffer that is unmapped (at creation, or after
    mappedAtCreation or mapAsync)`
  )
  .fn(async t => {
    // It is valid to call unmap after creation of an unmapped buffer.
    {
      const buffer = t.createBufferTracked({ size: 16, usage: GPUBufferUsage.MAP_READ });
      buffer.unmap();
    }

    // It is valid to call unmap after unmapping a mapAsynced buffer.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      await buffer.mapAsync(GPUMapMode.READ);
      buffer.unmap();
      buffer.unmap();
    }

    // It is valid to call unmap after unmapping a mappedAtCreation buffer.
    {
      const buffer = t.createBufferTracked({
        usage: GPUBufferUsage.MAP_READ,
        size: 16,
        mappedAtCreation: true,
      });
      buffer.unmap();
      buffer.unmap();
    }
  });

g.test('unmap,state,destroyed')
  .desc(
    `Test it is valid to call unmap on a buffer that is destroyed (at creation, or after
    mappedAtCreation or mapAsync)`
  )
  .fn(async t => {
    // It is valid to call unmap after destruction of an unmapped buffer.
    {
      const buffer = t.createBufferTracked({ size: 16, usage: GPUBufferUsage.MAP_READ });
      buffer.destroy();
      buffer.unmap();
    }

    // It is valid to call unmap after destroying a mapAsynced buffer.
    {
      const buffer = t.createMappableBuffer(GPUMapMode.READ, 16);
      await buffer.mapAsync(GPUMapMode.READ);
      buffer.destroy();
      buffer.unmap();
    }

    // It is valid to call unmap after destroying a mappedAtCreation buffer.
    {
      const buffer = t.createBufferTracked({
        usage: GPUBufferUsage.MAP_READ,
        size: 16,
        mappedAtCreation: true,
      });
      buffer.destroy();
      buffer.unmap();
    }
  });

g.test('unmap,state,mappedAtCreation')
  .desc('Test it is valid to call unmap on a buffer mapped at creation, for various usages')
  .paramsSubcasesOnly(u =>
    u //
      .combine('bufferUsage', kBufferUsages)
  )
  .fn(t => {
    const { bufferUsage } = t.params;
    const buffer = t.createBufferTracked({ size: 16, usage: bufferUsage, mappedAtCreation: true });

    buffer.unmap();
  });

g.test('unmap,state,mapped')
  .desc("Test it is valid to call unmap on a buffer that's mapped")
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);

    await buffer.mapAsync(mapMode);
    buffer.unmap();
  });

g.test('unmap,state,mappingPending')
  .desc("Test it is valid to call unmap on a buffer that's being mapped")
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;
    const buffer = t.createMappableBuffer(mapMode, 16);

    const pending = t.testMapAsyncCall(
      { validationError: false, earlyRejection: false, rejectName: 'AbortError' },
      buffer,
      mapMode
    );
    buffer.unmap();
    await pending;
  });

g.test('gc_behavior,mappedAtCreation')
  .desc(
    "Test that GCing the buffer while mappings are handed out doesn't invalidate them - mappedAtCreation case"
  )
  .fn(async t => {
    let buffer = null;
    buffer = t.createBufferTracked({
      size: 256,
      usage: GPUBufferUsage.COPY_DST,
      mappedAtCreation: true,
    });

    // Write some non-zero data to the buffer.
    const contents = new Uint32Array(buffer.getMappedRange());
    for (let i = 0; i < contents.length; i++) {
      contents[i] = i;
    }

    // Trigger garbage collection that should collect the buffer (or as if it collected it)
    // NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
    // back to the OS. One good option for browsers to check their logic is good is to zero-out the
    // memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
    buffer = null;
    await attemptGarbageCollection();

    // Use the mapping again both for read and write, it should work.
    for (let i = 0; i < contents.length; i++) {
      t.expect(contents[i] === i);
      contents[i] = i + 1;
    }
  });

g.test('gc_behavior,mapAsync')
  .desc(
    "Test that GCing the buffer while mappings are handed out doesn't invalidate them - mapAsync case"
  )
  .paramsSubcasesOnly(u => u.combine('mapMode', kMapModeOptions))
  .fn(async t => {
    const { mapMode } = t.params;

    let buffer = null;
    buffer = t.createMappableBuffer(mapMode, 256);
    await buffer.mapAsync(mapMode);

    // Write some non-zero data to the buffer.
    const contents = new Uint32Array(buffer.getMappedRange());
    for (let i = 0; i < contents.length; i++) {
      contents[i] = i;
    }

    // Trigger garbage collection that should collect the buffer (or as if it collected it)
    // NOTE: This won't fail unless the browser immediately starts reusing the memory, or gives it
    // back to the OS. One good option for browsers to check their logic is good is to zero-out the
    // memory on GPUBuffer (or internal gpu::Buffer-like object) destruction.
    buffer = null;
    await attemptGarbageCollection();

    // Use the mapping again both for read and write, it should work.
    for (let i = 0; i < contents.length; i++) {
      t.expect(contents[i] === i);
      contents[i] = i + 1;
    }
  });