diff --git a/src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts b/src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts index 0aa1a9b51e2c..ff6e534ed230 100644 --- a/src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts +++ b/src/webgpu/api/operation/command_buffer/queries/timestampQuery.spec.ts @@ -17,6 +17,40 @@ import { AllFeaturesMaxLimitsGPUTest, GPUTest } from '../../../../gpu_test.js'; export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); +function encodeTimestampQueries( + encoder: GPUCommandEncoder, + view: GPUTextureView, + querySet: GPUQuerySet, + stage: 'compute' | 'render', + slot: number +) { + switch (stage) { + case 'compute': { + const pass = encoder.beginComputePass({ + timestampWrites: { + querySet, + beginningOfPassWriteIndex: slot, + endOfPassWriteIndex: slot + 1, + }, + }); + pass.end(); + break; + } + case 'render': { + const pass = encoder.beginRenderPass({ + colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }], + timestampWrites: { + querySet, + beginningOfPassWriteIndex: slot, + endOfPassWriteIndex: slot + 1, + }, + }); + pass.end(); + break; + } + } +} + g.test('many_query_sets') .desc( ` @@ -61,31 +95,7 @@ and prevent pages from running. count: 2, }); - switch (stage) { - case 'compute': { - const pass = encoder.beginComputePass({ - timestampWrites: { - querySet, - beginningOfPassWriteIndex: 0, - endOfPassWriteIndex: 1, - }, - }); - pass.end(); - break; - } - case 'render': { - const pass = encoder.beginRenderPass({ - colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }], - timestampWrites: { - querySet, - beginningOfPassWriteIndex: 0, - endOfPassWriteIndex: 1, - }, - }); - pass.end(); - break; - } - } + encodeTimestampQueries(encoder, view, querySet, stage, 0); } const shouldError = false; // just expect no error @@ -119,34 +129,8 @@ function encoderQueryUsage( count: numSlots, }); - switch (stage) { - case 'compute': { - for (let slot = 0; slot < numSlots; slot += 2) { - const pass = encoder.beginComputePass({ - timestampWrites: { - querySet, - beginningOfPassWriteIndex: slot, - endOfPassWriteIndex: slot + 1, - }, - }); - pass.end(); - } - break; - } - case 'render': { - for (let slot = 0; slot < numSlots; slot += 2) { - const pass = encoder.beginRenderPass({ - colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }], - timestampWrites: { - querySet, - beginningOfPassWriteIndex: slot, - endOfPassWriteIndex: slot + 1, - }, - }); - pass.end(); - } - break; - } + for (let slot = 0; slot < numSlots; slot += 2) { + encodeTimestampQueries(encoder, view, querySet, stage, slot); } return querySet; @@ -217,3 +201,135 @@ to make sure the implementation doesn't mistakenly mark them as used. t.expectGPUBufferValuesEqual(buffer, expected); } }); + +g.test('multi_resolve') + .desc(`Test resolving more than once does not change the results`) + .params(u => u.combine('stage', ['compute', 'render'] as const)) + .fn(async t => { + const { stage } = t.params; + const kNumQueries = 64; + + t.skipIfDeviceDoesNotHaveFeature('timestamp-query'); + + const querySet = t.createQuerySetTracked({ + type: 'timestamp', + count: kNumQueries, + }); + + const view = t + .createTextureTracked({ + size: [1, 1, 1], + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }) + .createView(); + const encoder = t.device.createCommandEncoder(); + + for (let slot = 0; slot < kNumQueries; slot += 2) { + // skip every other pair so we can test resolving un-used slots + const pair = (slot / 2) | 0; + if (pair % 2) { + continue; + } + encodeTimestampQueries(encoder, view, querySet, stage, slot); + } + + const size = kNumQueries * 8; + const resolveBuffer1 = t.createBufferTracked({ + size, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + const resolveBuffer2 = t.createBufferTracked({ + size, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + const resultBuffer1 = t.createBufferTracked({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + encoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer1, 0); + encoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer2, 0); + encoder.copyBufferToBuffer(resolveBuffer1, 0, resultBuffer1, 0, size); + + t.device.queue.submit([encoder.finish()]); + + // Read back the first result. + await resultBuffer1.mapAsync(GPUMapMode.READ); + const expected = new Uint32Array(resultBuffer1.getMappedRange()); + t.expectGPUBufferValuesEqual(resolveBuffer2, expected); + }); + +g.test('unused_slots_are_zero') + .desc(`Test that unused slots are resolved to zero`) + .params(u => u.combine('stage', ['compute', 'render'] as const)) + .fn(t => { + const { stage } = t.params; + const kNumQueries = 64; + + t.skipIfDeviceDoesNotHaveFeature('timestamp-query'); + + const querySet = t.createQuerySetTracked({ + type: 'timestamp', + count: kNumQueries, + }); + + const view = t + .createTextureTracked({ + size: [1, 1, 1], + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }) + .createView(); + const usedEncoder = t.device.createCommandEncoder(); + const unusedEncoder = t.device.createCommandEncoder(); + + for (let slot = 0; slot < kNumQueries; slot += 2) { + const pair = (slot / 2) | 0; + const encoder = pair % 2 ? usedEncoder : unusedEncoder; + encodeTimestampQueries(encoder, view, querySet, stage, slot); + } + + unusedEncoder.finish(); // don't submit this encoder + + const size = kNumQueries * 8; + const resolveBuffer = t.createBufferTracked({ + size, + usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, + }); + + usedEncoder.resolveQuerySet(querySet, 0, kNumQueries, resolveBuffer, 0); + + t.device.queue.submit([usedEncoder.finish()]); + + // Read back the first result. + t.expectGPUBufferValuesPassCheck( + resolveBuffer, + (actualU32: Uint32Array) => { + // MAINTENANCE_TODO: expectGPUBufferValuesPassCheck doesn't work with BigUint64Array. + const actual = new BigUint64Array( + actualU32.buffer, + actualU32.byteOffset, + actualU32.byteLength / 8 + ); + const errors: string[] = []; + for (let slot = 0; slot < kNumQueries; ++slot) { + t.debug(() => `slot ${slot}: ${actual[slot]}`); + const pair = (slot / 2) | 0; + if (pair % 2) { + // used slot, implementation defined so don't check + } else { + // unused slot, expect zero + if (actual[slot] !== 0n) { + errors.push(`slot ${slot} expected 0 but got ${actual[slot]}`); + } + } + } + return errors.length === 0 ? undefined : new Error(errors.join('\n')); + }, + { + type: Uint32Array, + typedLength: kNumQueries * 2, // because it's really BigUint64Array data + } + ); + });