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.createBufferTracked({ 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.createBufferTracked({ 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.createBufferTracked({ 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.createBufferTracked({ 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(t => { const { size, range, mappable } = t.params; const [, rangeSize] = reifyMapRange(size, range); const buffer = t.createBufferTracked({ 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.createBufferTracked({ 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('usageType', ['invalid', 'read', 'write']) .combine('afterUnmap', [false, true]) .combine('afterDestroy', [false, true]) ) .fn(t => { const { usageType, afterUnmap, afterDestroy } = t.params; const usage = usageType === 'read' ? GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ : usageType === 'write' ? GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE : 0; const validationError = usage === 0; const size = 8; const range = [0, 8]; let buffer: GPUBuffer; t.expectValidationError(() => { buffer = t.createBufferTracked({ mappedAtCreation: true, size, usage, }); }, 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('usageType', ['invalid', 'read', 'write']) .combine('mapModeType', ['READ', 'WRITE'] as const) .combine('beforeUnmap', [false, true]) .combine('beforeDestroy', [false, true]) .combine('afterUnmap', [false, true]) .combine('afterDestroy', [false, true]) ) .fn(async t => { const { usageType, mapModeType, beforeUnmap, beforeDestroy, afterUnmap, afterDestroy } = t.params; const size = 8; const range = [0, 8]; const usage = usageType === 'read' ? GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ : usageType === 'write' ? GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE : 0; const bufferCreationValidationError = usage === 0; const mapMode = GPUMapMode[mapModeType]; let buffer: GPUBuffer; t.expectValidationError(() => { buffer = t.createBufferTracked({ mappedAtCreation: false, size, usage, }); }, bufferCreationValidationError); t.expect(buffer!.mapState === 'unmapped'); { const mapAsyncValidationError = bufferCreationValidationError || (mapMode === GPUMapMode.READ && !(usage & GPUBufferUsage.MAP_READ)) || (mapMode === GPUMapMode.WRITE && !(usage & GPUBufferUsage.MAP_WRITE)); let promise: Promise; t.expectValidationError(() => { promise = buffer!.mapAsync(mapMode); }, 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; 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'); } });