export const description = `
Atomically stores the value v in the atomic object pointed to atomic_ptr and returns the original value stored in the atomic object.
`;
import { makeTestGroup } from '../../../../../../../common/framework/test_group.js';
import { keysOf } from '../../../../../../../common/util/data_tables.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../../../gpu_test.js';
import { checkElementsEqual } from '../../../../../../util/check_contents.js';
import { dispatchSizes, workgroupSizes, typedArrayCtor, kMapId } from './harness.js';
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
g.test('exchange_storage_basic')
.specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
.desc(
`
AS is storage or workgroup
T is i32 or u32
fn atomicExchange(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T
`
)
.params(u =>
u
.combine('workgroupSize', workgroupSizes)
.combine('dispatchSize', dispatchSizes)
.combine('mapId', keysOf(kMapId))
.combine('scalarType', ['u32', 'i32'] as const)
)
.fn(t => {
const numInvocations = t.params.workgroupSize * t.params.dispatchSize;
const bufferNumElements = numInvocations;
const scalarType = t.params.scalarType;
const mapId = kMapId[t.params.mapId];
const extra = mapId.wgsl(numInvocations, t.params.scalarType);
const wgsl =
`
@group(0) @binding(0)
var<storage, read_write> input : array<atomic<${scalarType}>>;
@group(0) @binding(1)
var<storage, read_write> output : array<${scalarType}>;
@compute @workgroup_size(${t.params.workgroupSize})
fn main(
@builtin(global_invocation_id) global_invocation_id : vec3<u32>,
) {
let id = ${scalarType}(global_invocation_id[0]);
output[id] = atomicExchange(&input[id], map_id(id * 2));
}
` + extra;
const pipeline = t.device.createComputePipeline({
layout: 'auto',
compute: {
module: t.device.createShaderModule({ code: wgsl }),
entryPoint: 'main',
},
});
const arrayType = typedArrayCtor(scalarType);
const inputBuffer = t.createBufferTracked({
size: bufferNumElements * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
const data = new arrayType(inputBuffer.getMappedRange());
data.forEach((_, i) => (data[i] = i));
inputBuffer.unmap();
const outputBuffer = t.createBufferTracked({
size: bufferNumElements * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: inputBuffer } },
{ binding: 1, resource: { buffer: outputBuffer } },
],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(t.params.dispatchSize);
pass.end();
t.queue.submit([encoder.finish()]);
const outputExpected = new (typedArrayCtor(t.params.scalarType))(bufferNumElements);
outputExpected.forEach((_, i) => (outputExpected[i] = i));
t.expectGPUBufferValuesEqual(outputBuffer, outputExpected);
const inputExpected = new (typedArrayCtor(t.params.scalarType))(bufferNumElements);
inputExpected.forEach((_, i) => (inputExpected[i] = mapId.f(i * 2, numInvocations)));
t.expectGPUBufferValuesEqual(inputBuffer, inputExpected);
});
g.test('exchange_workgroup_basic')
.specURL('https://www.w3.org/TR/WGSL/#atomic-load')
.desc(
`
AS is storage or workgroup
T is i32 or u32
fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T
`
)
.params(u =>
u
.combine('workgroupSize', workgroupSizes)
.combine('dispatchSize', dispatchSizes)
.combine('mapId', keysOf(kMapId))
.combine('scalarType', ['u32', 'i32'] as const)
)
.fn(t => {
const numInvocations = t.params.workgroupSize;
const wgNumElements = numInvocations;
const scalarType = t.params.scalarType;
const dispatchSize = t.params.dispatchSize;
const mapId = kMapId[t.params.mapId];
const extra = mapId.wgsl(numInvocations, t.params.scalarType);
const wgsl =
`
var<workgroup> wg: array<atomic<${scalarType}>, ${wgNumElements}>;
// Result of each workgroup is written to output[workgroup_id.x]
@group(0) @binding(0)
var<storage, read_write> output: array<${scalarType}, ${wgNumElements * dispatchSize}>;
@group(0) @binding(1)
var<storage, read_write> wg_copy: array<${scalarType}, ${wgNumElements * dispatchSize}>;
@compute @workgroup_size(${t.params.workgroupSize})
fn main(
@builtin(local_invocation_index) local_invocation_index: u32,
@builtin(workgroup_id) workgroup_id : vec3<u32>
) {
let id = ${scalarType}(local_invocation_index);
let global_id = ${scalarType}(workgroup_id.x * ${wgNumElements} + local_invocation_index);
// Initialize wg[id] with this invocations global id
atomicStore(&wg[id], global_id);
workgroupBarrier();
// Test atomicExchange, storing old value into output
output[global_id] = atomicExchange(&wg[id], map_id(global_id * 2));
// Copy new value into wg_copy
wg_copy[global_id] = atomicLoad(&wg[id]);
}
` + extra;
const pipeline = t.device.createComputePipeline({
layout: 'auto',
compute: {
module: t.device.createShaderModule({ code: wgsl }),
entryPoint: 'main',
},
});
const arrayType = typedArrayCtor(scalarType);
const outputBuffer = t.createBufferTracked({
size: wgNumElements * dispatchSize * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const wgCopyBuffer = t.createBufferTracked({
size: wgNumElements * dispatchSize * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: outputBuffer } },
{ binding: 1, resource: { buffer: wgCopyBuffer } },
],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(dispatchSize);
pass.end();
t.queue.submit([encoder.finish()]);
const outputExpected = new (typedArrayCtor(t.params.scalarType))(wgNumElements * dispatchSize);
outputExpected.forEach((_, i) => (outputExpected[i] = i));
t.expectGPUBufferValuesEqual(outputBuffer, outputExpected);
const wgCopyBufferExpected = new (typedArrayCtor(t.params.scalarType))(
wgNumElements * dispatchSize
);
wgCopyBufferExpected.forEach(
(_, i) => (wgCopyBufferExpected[i] = mapId.f(i * 2, numInvocations))
);
t.expectGPUBufferValuesEqual(wgCopyBuffer, wgCopyBufferExpected);
});
g.test('exchange_storage_advanced')
.specURL('https://www.w3.org/TR/WGSL/#atomic-rmw')
.desc(
`
AS is storage or workgroup
T is i32 or u32
fn atomicExchange(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T) -> T
`
)
.params(u =>
u
.combine('workgroupSize', workgroupSizes)
.combine('dispatchSize', dispatchSizes)
.combine('mapId', keysOf(kMapId))
.combine('scalarType', ['u32', 'i32'] as const)
)
.fn(async t => {
const numInvocations = t.params.workgroupSize * t.params.dispatchSize;
const bufferNumElements = numInvocations;
const scalarType = t.params.scalarType;
const mapId = kMapId[t.params.mapId];
const extra = mapId.wgsl(numInvocations, t.params.scalarType);
const wgsl =
`
@group(0) @binding(0)
var<storage, read_write> input : atomic<${scalarType}>;
@group(0) @binding(1)
var<storage, read_write> output : array<${scalarType}>;
@compute @workgroup_size(${t.params.workgroupSize})
fn main(
@builtin(global_invocation_id) global_invocation_id : vec3<u32>,
) {
let id = ${scalarType}(global_invocation_id[0]);
// All invocations exchange with same single memory address, and we store
// the old value at the current invocation's location in the output buffer.
output[id] = atomicExchange(&input, map_id(id));
}
` + extra;
const pipeline = t.device.createComputePipeline({
layout: 'auto',
compute: {
module: t.device.createShaderModule({ code: wgsl }),
entryPoint: 'main',
},
});
const arrayType = typedArrayCtor(scalarType);
const inputBuffer = t.createBufferTracked({
size: 1 * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const outputBuffer = t.createBufferTracked({
size: bufferNumElements * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: inputBuffer } },
{ binding: 1, resource: { buffer: outputBuffer } },
],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(t.params.dispatchSize);
pass.end();
t.queue.submit([encoder.finish()]);
const inputBufferResult = await t.readGPUBufferRangeTyped(inputBuffer, {
type: arrayType,
typedLength: inputBuffer.size / arrayType.BYTES_PER_ELEMENT,
});
const outputBufferResult = await t.readGPUBufferRangeTyped(outputBuffer, {
type: arrayType,
typedLength: outputBuffer.size / arrayType.BYTES_PER_ELEMENT,
});
const values = new arrayType([...inputBufferResult.data, ...outputBufferResult.data]);
const expected = new arrayType(values.length);
expected.forEach((_, i) => {
if (i === 0) {
expected[0] = 0;
} else {
expected[i] = mapId.f(i - 1, numInvocations);
}
});
values.sort();
expected.sort();
t.expectOK(checkElementsEqual(values, expected));
});
g.test('exchange_workgroup_advanced')
.specURL('https://www.w3.org/TR/WGSL/#atomic-load')
.desc(
`
AS is storage or workgroup
T is i32 or u32
fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T
`
)
.params(u =>
u
.combine('workgroupSize', workgroupSizes)
.combine('dispatchSize', dispatchSizes)
.combine('mapId', keysOf(kMapId))
.combine('scalarType', ['u32', 'i32'] as const)
)
.fn(async t => {
const numInvocations = t.params.workgroupSize;
const scalarType = t.params.scalarType;
const dispatchSize = t.params.dispatchSize;
const mapId = kMapId[t.params.mapId];
const extra = mapId.wgsl(numInvocations, t.params.scalarType);
const wgsl =
`
var<workgroup> wg: atomic<${scalarType}>;
// Will contain the atomicExchange result for each invocation at global index
@group(0) @binding(0)
var<storage, read_write> output: array<${scalarType}, ${numInvocations * dispatchSize}>;
// Will contain the final value in wg in wg_copy for this dispatch
@group(0) @binding(1)
var<storage, read_write> wg_copy: array<${scalarType}, ${dispatchSize}>;
@compute @workgroup_size(${t.params.workgroupSize})
fn main(
@builtin(local_invocation_index) local_invocation_index: u32,
@builtin(workgroup_id) workgroup_id : vec3<u32>
) {
let id = ${scalarType}(local_invocation_index);
let global_id = ${scalarType}(workgroup_id.x * ${numInvocations} + local_invocation_index);
// All invocations exchange with same single memory address, and we store
// the old value at the current invocation's location in the output buffer.
output[global_id] = atomicExchange(&wg, map_id(id));
// Once all invocations have completed, the first one copies the final exchanged value
// to wg_copy for this dispatch (workgroup_id.x)
workgroupBarrier();
if (local_invocation_index == 0u) {
wg_copy[workgroup_id.x] = atomicLoad(&wg);
}
}
` + extra;
const pipeline = t.device.createComputePipeline({
layout: 'auto',
compute: {
module: t.device.createShaderModule({ code: wgsl }),
entryPoint: 'main',
},
});
const arrayType = typedArrayCtor(scalarType);
const outputBuffer = t.createBufferTracked({
size: numInvocations * dispatchSize * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const wgCopyBuffer = t.createBufferTracked({
size: dispatchSize * arrayType.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: outputBuffer } },
{ binding: 1, resource: { buffer: wgCopyBuffer } },
],
});
const encoder = t.device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(dispatchSize);
pass.end();
t.queue.submit([encoder.finish()]);
const outputBufferResult = await t.readGPUBufferRangeTyped(outputBuffer, {
type: arrayType,
typedLength: outputBuffer.size / arrayType.BYTES_PER_ELEMENT,
});
const wgCopyBufferResult = await t.readGPUBufferRangeTyped(wgCopyBuffer, {
type: arrayType,
typedLength: wgCopyBuffer.size / arrayType.BYTES_PER_ELEMENT,
});
const expected = new arrayType(numInvocations + 1);
expected.forEach((_, i) => {
if (i === 0) {
expected[0] = 0;
} else {
expected[i] = mapId.f(i - 1, numInvocations);
}
});
expected.sort();
for (let d = 0; d < dispatchSize; ++d) {
const dispatchOffset = d * numInvocations;
const values = new arrayType([
wgCopyBufferResult.data[d],
...outputBufferResult.data.subarray(dispatchOffset, dispatchOffset + numInvocations),
]);
values.sort();
t.expectOK(checkElementsEqual(values, expected));
}
});