Signatur
Beschreibung
resolveQuerySet() ist der letzte Schritt, wenn man mit WebGPU-Abfragen arbeitet. Während eines Render- oder Compute-Passes schreibt die GPU Messwerte in ein GPUQuerySet: Zeitstempel ("timestamp") oder Verdeckungswerte ("occlusion"). Diese Werte liegen in einem undurchsichtigen, GPU-internen Format. Mit resolveQuerySet() wandelt der Command-Encoder sie in einen normalen Puffer um, den man anschließend auslesen kann.
Jedes Ergebnis belegt 8 Byte (vorzeichenloser 64-Bit-Integer). Man liest die Werte deshalb am besten mit einem BigUint64Array aus. Zeitstempel sind in Nanosekunden angegeben. Bei Verdeckungsabfragen ist ein Wert ungleich 0 das Zeichen dafür, dass mindestens ein Sample den Tiefen- und Stenciltest bestanden hat.
Der Zielpuffer braucht das Usage-Flag GPUBufferUsage.QUERY_RESOLVE. Ein solcher Puffer darf aber nicht gleichzeitig MAP_READ haben. Das übliche Vorgehen sieht so aus:
- Die Ergebnisse in einen Puffer mit
QUERY_RESOLVE | COPY_SRCauflösen. - Mit
copyBufferToBuffer()in einen Staging-Puffer mitCOPY_DST | MAP_READkopieren. - Den Staging-Puffer mit
mapAsync()auf der CPU lesen.
Die Methode führt nichts sofort aus. Sie hängt nur einen Befehl an den Encoder an. Ausgeführt wird er erst, wenn der mit finish() erzeugte Command-Buffer über device.queue.submit() abgeschickt wird.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $querySet Pflicht | GPUQuerySet | Das Query-Set, dessen Ergebnisse aufgelöst werden. | |
| $firstQuery Pflicht | number | Index der ersten Abfrage im Set, die aufgelöst wird. | |
| $queryCount Pflicht | number | Anzahl der aufzulösenden Abfragen. firstQuery + queryCount darf nicht größer sein als querySet.count. |
|
| $destination Pflicht | GPUBuffer | Zielpuffer. Er muss mit GPUBufferUsage.QUERY_RESOLVE erstellt worden sein. |
|
| $destinationOffset Pflicht | number | Byte-Offset im Zielpuffer. Er muss ein Vielfaches von 256 sein, und destinationOffset + 8 * queryCount darf destination.size nicht überschreiten. |
Rückgabewert
Beispiele
GPU-Laufzeit eines Compute-Passes mit Zeitstempeln messen
const adapter = await navigator.gpu.requestAdapter();
if (!adapter.features.has('timestamp-query')) {
throw new Error('timestamp-query wird nicht unterstützt');
}
const device = await adapter.requestDevice({ requiredFeatures: ['timestamp-query'] });
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
// Puffer für das Auflösen (nicht mappbar)
const resolveBuffer = device.createBuffer({
size: 2 * 8,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
// Staging-Puffer zum Lesen auf der CPU
const readBuffer = device.createBuffer({
size: 2 * 8,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const module = device.createShaderModule({
code: '@compute @workgroup_size(64) fn main() {}',
});
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module, entryPoint: 'main' },
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass({
timestampWrites: {
querySet,
beginningOfPassWriteIndex: 0,
endOfPassWriteIndex: 1,
},
});
pass.setPipeline(pipeline);
pass.dispatchWorkgroups(1024);
pass.end();
// Abfragen 0 und 1 an Offset 0 auflösen
encoder.resolveQuerySet(querySet, 0, 2, resolveBuffer, 0);
encoder.copyBufferToBuffer(resolveBuffer, 0, readBuffer, 0, 16);
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const [start, end] = new BigUint64Array(readBuffer.getMappedRange());
console.log(`GPU-Zeit: ${Number(end - start) / 1e6} ms`);
readBuffer.unmap();
Verdeckungsabfrage (Occlusion Query) auswerten
// Annahme: device, renderPassDescriptor (mit Tiefenpuffer), pipeline und
// vertexBuffer sind bereits eingerichtet.
const OBJECTS = 4;
const occlusionSet = device.createQuerySet({ type: 'occlusion', count: OBJECTS });
const resolveBuffer = device.createBuffer({
size: OBJECTS * 8,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
const readBuffer = device.createBuffer({
size: OBJECTS * 8,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
...renderPassDescriptor,
occlusionQuerySet: occlusionSet,
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
for (let i = 0; i < OBJECTS; i++) {
pass.beginOcclusionQuery(i);
pass.draw(36, 1, i * 36); // Bounding-Box von Objekt i
pass.endOcclusionQuery();
}
pass.end();
encoder.resolveQuerySet(occlusionSet, 0, OBJECTS, resolveBuffer, 0);
encoder.copyBufferToBuffer(resolveBuffer, 0, readBuffer, 0, OBJECTS * 8);
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const results = new BigUint64Array(readBuffer.getMappedRange());
const visible = [...results].map((v) => v !== 0n);
console.log('Sichtbar:', visible);
readBuffer.unmap();
Teilbereich an einen ausgerichteten Offset auflösen
// Abfragen 4 bis 7 aus einem Set mit 8 Abfragen an Byte-Offset 256 schreiben
const querySet = device.createQuerySet({ type: 'occlusion', count: 8 });
const buffer = device.createBuffer({
size: 256 + 4 * 8, // Offset + 4 Ergebnisse à 8 Byte
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
const encoder = device.createCommandEncoder();
// ... Render-Pass mit Occlusion-Queries ...
encoder.resolveQuerySet(querySet, 4, 4, buffer, 256); // gültig: 256 % 256 === 0
// encoder.resolveQuerySet(querySet, 4, 4, buffer, 8); // Validierungsfehler!
device.queue.submit([encoder.finish()]);