| 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<void>; |
| 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<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'); |
| } |
| }); |