diff options
Diffstat (limited to '')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts new file mode 100644 index 0000000000..c0cdb3acfa --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/buffers/map.spec.ts @@ -0,0 +1,499 @@ +export const description = ` +Test the operation of buffer mapping, specifically the data contents written via +map-write/mappedAtCreation, and the contents of buffers returned by getMappedRange on +buffers which are mapped-read/mapped-write/mappedAtCreation. + +range: used for getMappedRange +mapRegion: used for mapAsync + +mapRegionBoundModes is used to get mapRegion from range: + - default-expand: expand mapRegion to buffer bound by setting offset/size to undefined + - explicit-expand: expand mapRegion to buffer bound by explicitly calculating offset/size + - minimal: make mapRegion to be the same as range which is the minimal range to make getMappedRange input valid +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert, memcpy } from '../../../../common/util/util.js'; +import { checkElementsEqual } from '../../../util/check_contents.js'; + +import { MappingTest } from './mapping_test.js'; + +export const g = makeTestGroup(MappingTest); + +const kSubcases = [ + { size: 0, range: [] }, + { size: 0, range: [undefined] }, + { size: 0, range: [undefined, undefined] }, + { size: 0, range: [0] }, + { size: 0, range: [0, undefined] }, + { size: 0, range: [0, 0] }, + { size: 12, range: [] }, + { size: 12, range: [undefined] }, + { size: 12, range: [undefined, undefined] }, + { size: 12, range: [0] }, + { size: 12, range: [0, undefined] }, + { size: 12, range: [0, 12] }, + { size: 12, range: [0, 0] }, + { size: 12, range: [8] }, + { size: 12, range: [8, undefined] }, + { size: 12, range: [8, 4] }, + { size: 28, range: [8, 8] }, + { size: 28, range: [8, 12] }, + { size: 512 * 1024, range: [] }, +] as const; + +function reifyMapRange(bufferSize: number, range: readonly [number?, number?]): [number, number] { + const offset = range[0] ?? 0; + return [offset, range[1] ?? bufferSize - offset]; +} + +const mapRegionBoundModes = ['default-expand', 'explicit-expand', 'minimal'] as const; +type MapRegionBoundMode = typeof mapRegionBoundModes[number]; + +function getRegionForMap( + bufferSize: number, + range: [number, number], + { + mapAsyncRegionLeft, + mapAsyncRegionRight, + }: { + mapAsyncRegionLeft: MapRegionBoundMode; + mapAsyncRegionRight: MapRegionBoundMode; + } +) { + const regionLeft = mapAsyncRegionLeft === 'minimal' ? range[0] : 0; + const regionRight = mapAsyncRegionRight === 'minimal' ? range[0] + range[1] : bufferSize; + return [ + mapAsyncRegionLeft === 'default-expand' ? undefined : regionLeft, + mapAsyncRegionRight === 'default-expand' ? undefined : regionRight - regionLeft, + ] as const; +} + +g.test('mapAsync,write') + .desc( + `Use map-write to write to various ranges of variously-sized buffers, then expectContents +(which does copyBufferToBuffer + map-read) to ensure the contents were written.` + ) + .params(u => + u + .combine('mapAsyncRegionLeft', mapRegionBoundModes) + .combine('mapAsyncRegionRight', mapRegionBoundModes) + .beginSubcases() + .combineWithParams(kSubcases) + ) + .fn(async t => { + const { size, range } = t.params; + const [rangeOffset, rangeSize] = reifyMapRange(size, range); + + const buffer = t.device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + }); + + const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params); + await buffer.mapAsync(GPUMapMode.WRITE, ...mapRegion); + const arrayBuffer = buffer.getMappedRange(...range); + t.checkMapWrite(buffer, rangeOffset, arrayBuffer, rangeSize); + }); + +g.test('mapAsync,write,unchanged_ranges_preserved') + .desc( + `Use mappedAtCreation or mapAsync to write to various ranges of variously-sized buffers, then +use mapAsync to map a different range and zero it out. Finally use expectGPUBufferValuesEqual +(which does copyBufferToBuffer + map-read) to verify that contents originally written outside the +second mapped range were not altered.` + ) + .params(u => + u + .beginSubcases() + .combine('mappedAtCreation', [false, true]) + .combineWithParams([ + { size: 12, range1: [], range2: [8] }, + { size: 12, range1: [], range2: [0, 8] }, + { size: 12, range1: [0, 8], range2: [8] }, + { size: 12, range1: [8], range2: [0, 8] }, + { size: 28, range1: [], range2: [8, 8] }, + { size: 28, range1: [8, 16], range2: [16, 8] }, + { size: 32, range1: [16, 12], range2: [8, 16] }, + { size: 32, range1: [8, 8], range2: [24, 4] }, + ] as const) + ) + .fn(async t => { + const { size, range1, range2, mappedAtCreation } = t.params; + const [rangeOffset1, rangeSize1] = reifyMapRange(size, range1); + const [rangeOffset2, rangeSize2] = reifyMapRange(size, range2); + + const buffer = t.device.createBuffer({ + mappedAtCreation, + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + }); + + // If the buffer is not mappedAtCreation map it now. + if (!mappedAtCreation) { + await buffer.mapAsync(GPUMapMode.WRITE); + } + + // Set the initial contents of the buffer. + const init = buffer.getMappedRange(...range1); + + assert(init.byteLength === rangeSize1); + const expectedBuffer = new ArrayBuffer(size); + const expected = new Uint32Array( + expectedBuffer, + rangeOffset1, + rangeSize1 / Uint32Array.BYTES_PER_ELEMENT + ); + const data = new Uint32Array(init); + for (let i = 0; i < data.length; ++i) { + data[i] = expected[i] = i + 1; + } + buffer.unmap(); + + // Write to a second range of the buffer + await buffer.mapAsync(GPUMapMode.WRITE, ...range2); + const init2 = buffer.getMappedRange(...range2); + + assert(init2.byteLength === rangeSize2); + const expected2 = new Uint32Array( + expectedBuffer, + rangeOffset2, + rangeSize2 / Uint32Array.BYTES_PER_ELEMENT + ); + const data2 = new Uint32Array(init2); + for (let i = 0; i < data2.length; ++i) { + data2[i] = expected2[i] = 0; + } + buffer.unmap(); + + // Verify that the range of the buffer which was not overwritten was preserved. + t.expectGPUBufferValuesEqual(buffer, expected, rangeOffset1); + }); + +g.test('mapAsync,read') + .desc( + `Use mappedAtCreation to initialize various ranges of variously-sized buffers, then +map-read and check the read-back result.` + ) + .params(u => + u + .combine('mapAsyncRegionLeft', mapRegionBoundModes) + .combine('mapAsyncRegionRight', mapRegionBoundModes) + .beginSubcases() + .combineWithParams(kSubcases) + ) + .fn(async t => { + const { size, range } = t.params; + const [rangeOffset, rangeSize] = reifyMapRange(size, range); + + const buffer = t.device.createBuffer({ + mappedAtCreation: true, + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const init = buffer.getMappedRange(...range); + + assert(init.byteLength === rangeSize); + const expected = new Uint32Array(new ArrayBuffer(rangeSize)); + const data = new Uint32Array(init); + for (let i = 0; i < data.length; ++i) { + data[i] = expected[i] = i + 1; + } + buffer.unmap(); + + const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params); + await buffer.mapAsync(GPUMapMode.READ, ...mapRegion); + const actual = new Uint8Array(buffer.getMappedRange(...range)); + t.expectOK(checkElementsEqual(actual, new Uint8Array(expected.buffer))); + }); + +g.test('mapAsync,read,typedArrayAccess') + .desc(`Use various TypedArray types to read back from a mapped buffer`) + .params(u => + u + .combine('mapAsyncRegionLeft', mapRegionBoundModes) + .combine('mapAsyncRegionRight', mapRegionBoundModes) + .beginSubcases() + .combineWithParams([ + { size: 80, range: [] }, + { size: 160, range: [] }, + { size: 160, range: [0, 80] }, + { size: 160, range: [80] }, + { size: 160, range: [40, 120] }, + { size: 160, range: [40] }, + ] as const) + ) + .fn(async t => { + const { size, range } = t.params; + const [rangeOffset, rangeSize] = reifyMapRange(size, range); + + // Fill an array buffer with a variety of values of different types. + const expectedArrayBuffer = new ArrayBuffer(80); + const uint8Expected = new Uint8Array(expectedArrayBuffer, 0, 2); + uint8Expected[0] = 1; + uint8Expected[1] = 255; + + const int8Expected = new Int8Array(expectedArrayBuffer, 2, 2); + int8Expected[0] = -1; + int8Expected[1] = 127; + + const uint16Expected = new Uint16Array(expectedArrayBuffer, 4, 2); + uint16Expected[0] = 1; + uint16Expected[1] = 65535; + + const int16Expected = new Int16Array(expectedArrayBuffer, 8, 2); + int16Expected[0] = -1; + int16Expected[1] = 32767; + + const uint32Expected = new Uint32Array(expectedArrayBuffer, 12, 2); + uint32Expected[0] = 1; + uint32Expected[1] = 4294967295; + + const int32Expected = new Int32Array(expectedArrayBuffer, 20, 2); + int32Expected[2] = -1; + int32Expected[3] = 2147483647; + + const float32Expected = new Float32Array(expectedArrayBuffer, 28, 3); + float32Expected[0] = 1; + float32Expected[1] = -1; + float32Expected[2] = 12345.6789; + + const float64Expected = new Float64Array(expectedArrayBuffer, 40, 5); + float64Expected[0] = 1; + float64Expected[1] = -1; + float64Expected[2] = 12345.6789; + float64Expected[3] = Number.MAX_VALUE; + float64Expected[4] = Number.MIN_VALUE; + + const buffer = t.device.createBuffer({ + mappedAtCreation: true, + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const init = buffer.getMappedRange(...range); + + // Copy the expected values into the mapped range. + assert(init.byteLength === rangeSize); + memcpy({ src: expectedArrayBuffer }, { dst: init }); + buffer.unmap(); + + const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params); + await buffer.mapAsync(GPUMapMode.READ, ...mapRegion); + const mappedArrayBuffer = buffer.getMappedRange(...range); + t.expectOK(checkElementsEqual(new Uint8Array(mappedArrayBuffer, 0, 2), uint8Expected)); + t.expectOK(checkElementsEqual(new Int8Array(mappedArrayBuffer, 2, 2), int8Expected)); + t.expectOK(checkElementsEqual(new Uint16Array(mappedArrayBuffer, 4, 2), uint16Expected)); + t.expectOK(checkElementsEqual(new Int16Array(mappedArrayBuffer, 8, 2), int16Expected)); + t.expectOK(checkElementsEqual(new Uint32Array(mappedArrayBuffer, 12, 2), uint32Expected)); + t.expectOK(checkElementsEqual(new Int32Array(mappedArrayBuffer, 20, 2), int32Expected)); + t.expectOK(checkElementsEqual(new Float32Array(mappedArrayBuffer, 28, 3), float32Expected)); + t.expectOK(checkElementsEqual(new Float64Array(mappedArrayBuffer, 40, 5), float64Expected)); + }); + +g.test('mappedAtCreation') + .desc( + `Use mappedAtCreation to write to various ranges of variously-sized buffers created either +with or without the MAP_WRITE usage (since this could affect the mappedAtCreation upload path), +then expectContents (which does copyBufferToBuffer + map-read) to ensure the contents were written.` + ) + .params(u => + u // + .combine('mappable', [false, true]) + .beginSubcases() + .combineWithParams(kSubcases) + ) + .fn(async t => { + const { size, range, mappable } = t.params; + const [, rangeSize] = reifyMapRange(size, range); + + const buffer = t.device.createBuffer({ + mappedAtCreation: true, + size, + usage: GPUBufferUsage.COPY_SRC | (mappable ? GPUBufferUsage.MAP_WRITE : 0), + }); + const arrayBuffer = buffer.getMappedRange(...range); + t.checkMapWrite(buffer, range[0] ?? 0, arrayBuffer, rangeSize); + }); + +g.test('remapped_for_write') + .desc( + `Use mappedAtCreation or mapAsync to write to various ranges of variously-sized buffers created +with the MAP_WRITE usage, then mapAsync again and ensure that the previously written values are +still present in the mapped buffer.` + ) + .params(u => + u // + .combine('mapAsyncRegionLeft', mapRegionBoundModes) + .combine('mapAsyncRegionRight', mapRegionBoundModes) + .beginSubcases() + .combine('mappedAtCreation', [false, true]) + .combineWithParams(kSubcases) + ) + .fn(async t => { + const { size, range, mappedAtCreation } = t.params; + const [rangeOffset, rangeSize] = reifyMapRange(size, range); + + const buffer = t.device.createBuffer({ + mappedAtCreation, + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + }); + + // If the buffer is not mappedAtCreation map it now. + if (!mappedAtCreation) { + await buffer.mapAsync(GPUMapMode.WRITE); + } + + // Set the initial contents of the buffer. + const init = buffer.getMappedRange(...range); + + assert(init.byteLength === rangeSize); + const expected = new Uint32Array(new ArrayBuffer(rangeSize)); + const data = new Uint32Array(init); + for (let i = 0; i < data.length; ++i) { + data[i] = expected[i] = i + 1; + } + buffer.unmap(); + + // Check that upon remapping the for WRITE the values in the buffer are + // still the same. + const mapRegion = getRegionForMap(size, [rangeOffset, rangeSize], t.params); + await buffer.mapAsync(GPUMapMode.WRITE, ...mapRegion); + const actual = new Uint8Array(buffer.getMappedRange(...range)); + t.expectOK(checkElementsEqual(actual, new Uint8Array(expected.buffer))); + }); + +g.test('mappedAtCreation,mapState') + .desc('Test that exposed map state of buffer created with mappedAtCreation has expected values.') + .params(u => + u + .combine('validationError', [false, true]) + .combine('afterUnmap', [false, true]) + .combine('afterDestroy', [false, true]) + ) + .fn(async t => { + const { validationError, afterUnmap, afterDestroy } = t.params; + const size = 8; + const range = [0, 8]; + + let buffer: GPUBuffer; + t.expectValidationError(() => { + buffer = t.device.createBuffer({ + mappedAtCreation: true, + size, + usage: validationError ? 0 : GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + }); + }, validationError); + + // mapState must be "mapped" regardless of validation error + t.expect(buffer!.mapState === 'mapped'); + + // getMappedRange must not change the map state + buffer!.getMappedRange(...range); + t.expect(buffer!.mapState === 'mapped'); + + if (afterUnmap) { + buffer!.unmap(); + t.expect(buffer!.mapState === 'unmapped'); + } + + if (afterDestroy) { + buffer!.destroy(); + t.expect(buffer!.mapState === 'unmapped'); + } + }); + +g.test('mapAsync,mapState') + .desc('Test that exposed map state of buffer mapped with mapAsync has expected values.') + .params(u => + u + .combine('bufferCreationValidationError', [false, true]) + .combine('mapAsyncValidationError', [false, true]) + .combine('beforeUnmap', [false, true]) + .combine('beforeDestroy', [false, true]) + .combine('afterUnmap', [false, true]) + .combine('afterDestroy', [false, true]) + ) + .fn(async t => { + const { + bufferCreationValidationError, + mapAsyncValidationError, + beforeUnmap, + beforeDestroy, + afterUnmap, + afterDestroy, + } = t.params; + const size = 8; + const range = [0, 8]; + + let buffer: GPUBuffer; + t.expectValidationError(() => { + buffer = t.device.createBuffer({ + mappedAtCreation: false, + size, + usage: bufferCreationValidationError + ? 0 + : GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, + }); + }, bufferCreationValidationError); + + t.expect(buffer!.mapState === 'unmapped'); + + { + let promise: Promise<void>; + t.expectValidationError(() => { + promise = buffer!.mapAsync(mapAsyncValidationError ? 0 : GPUMapMode.WRITE); + }, bufferCreationValidationError || mapAsyncValidationError); + t.expect(buffer!.mapState === 'pending'); + + try { + if (beforeUnmap) { + buffer!.unmap(); + t.expect(buffer!.mapState === 'unmapped'); + } + if (beforeDestroy) { + buffer!.destroy(); + t.expect(buffer!.mapState === 'unmapped'); + } + + await promise!; + t.expect(buffer!.mapState === 'mapped'); + + // getMappedRange must not change the map state + buffer!.getMappedRange(...range); + t.expect(buffer!.mapState === 'mapped'); + } catch { + // unmapped before resolve, destroyed before resolve, or mapAsync validation error + // will end up with rejection and 'unmapped' + t.expect(buffer!.mapState === 'unmapped'); + } + } + + // If buffer is already mapped test mapAsync on already mapped buffer + if (buffer!.mapState === 'mapped') { + // mapAsync on already mapped buffer must be rejected with a validation error + // and the map state must keep 'mapped' + let promise: Promise<void>; + t.expectValidationError(() => { + promise = buffer!.mapAsync(GPUMapMode.WRITE); + }, true); + t.expect(buffer!.mapState === 'mapped'); + + try { + await promise!; + t.fail('mapAsync on already mapped buffer must not succeed.'); + } catch { + t.expect(buffer!.mapState === 'mapped'); + } + } + + if (afterUnmap) { + buffer!.unmap(); + t.expect(buffer!.mapState === 'unmapped'); + } + + if (afterDestroy) { + buffer!.destroy(); + t.expect(buffer!.mapState === 'unmapped'); + } + }); |