Skip to content

(query-set …)

Declares a pool of :count GPU queries of one :type. A render pass writes into the set, either by bracketing draws in an (occlusion-query …) or by attaching (timestamp-writes …) to the pass, and a (resolve-query-set …) queue action copies the results into a buffer, 8 bytes per query.

(query-set :name occ :type occlusion :count 6)
(query-set :name ts :type timestamp :count 2)
Key Type Required Description
:name symbol Yes Unique query-set name
:type symbol Yes occlusion or timestamp
:count number Yes Number of queries in the set (WebGPU’s ceiling is 4096)

:count, like every numeric slot, takes a literal, a bare (define …) constant, or a bounded expression over those constants.

Value Measures
occlusion Samples that passed depth/stencil within an (occlusion-query …) bracket
timestamp A GPU timestamp in nanoseconds (64-bit)

Set :occlusion-query-set on the (render-pass …), then bracket the draws you want counted. (occlusion-query …) is not a sibling of the draws; the draws are its positional children:

(query-set :name occ :type occlusion :count 6)
(buffer :name results :size 48 :usage [query-resolve copy-src])
(render-pass :name draw
(color-attachment :view context-current-texture
:clear-value [0 0 0 1] :load-op clear :store-op store)
:pipeline pipe
:occlusion-query-set occ
(occlusion-query :query-index 0 (draw :vertex-count 36 :first-instance 0))
(occlusion-query :query-index 1 (draw :vertex-count 36 :first-instance 1)))
(queue :name readback
(resolve-query-set :query-set occ :first-query 0 :query-count 6
:destination results :destination-offset 0))
Key / Sub-form Type Default Description
:query-index number 0 Index within the pass’s :occlusion-query-set
(draw …) / (draw-indexed …) sub-form - The bracketed draw calls

Those two are the only heads a bracket accepts; anything else is a not_head_member. A bracket takes its place among the pass’s other commands, in the order everything is written.

The pass must state :occlusion-query-set once it holds a bracket. The key is optional only because a pass may hold none, so the compiler checks the pairing and reports it on the pass:

render-pass 'draw' brackets draws in an (occlusion-query …) but states no
:occlusion-query-set

Timestamps attach to the pass rather than bracketing draws. One query index is written at pass start, another at pass end:

(query-set :name ts :type timestamp :count 2)
(render-pass :name draw
(color-attachment :view context-current-texture
:clear-value [0 0 0 1] :load-op clear :store-op store)
(timestamp-writes :query-set ts :beginning-of-pass-write-index 0 :end-of-pass-write-index 1)
:pipeline pipe
(draw :vertex-count 3))
Key Type Required Default Description
:query-set reference Yes - The (query-set :type timestamp) written into
:beginning-of-pass-write-index number No 0 Query index written at pass start (the IDL leaves it absent, meaning not written; pngine bakes 0)
:end-of-pass-write-index number No 1 Query index written at pass end (pngine bakes 1)

A pass carries at most one (timestamp-writes …); a second is a positional_too_many.

Timestamp queries need the timestamp-query device feature. The browser runtime requests it at device creation wherever the adapter reports it, along with the other optional features a payload might use.

Query results live on the GPU. A (resolve-query-set …) queue action copies them into a buffer that carries query-resolve usage; to read them on the CPU, copy that buffer into a map-read buffer with (copy-buffer-to-buffer …):

(buffer :name resolveBuf :size 48 :usage [query-resolve copy-src])
(buffer :name readbackBuf :size 48 :usage [map-read copy-dst])
(queue :name readback
(resolve-query-set :query-set occ :first-query 0 :query-count 6
:destination resolveBuf :destination-offset 0)
(copy-buffer-to-buffer :source resolveBuf :source-offset 0
:destination readbackBuf :destination-offset 0 :size 48))

WebGPU requires :destination-offset to be a multiple of 256 on resolve-query-set, and multiples of 4 on copy-buffer-to-buffer.

Rule Error
A name is unique across every form kind duplicate_cross_ref_target
:type and :count are required missing_required_key
:type must be occlusion or timestamp not_member
A referenced query set must exist not_cross_ref
An (occlusion-query …) child other than a draw not_head_member
A second (timestamp-writes …) in one pass positional_too_many
resolve-query-set’s :destination-offset is a multiple of 256 number_not_multiple

Two more are the compiler’s, reading values across forms, so they carry a located message and no code: a bracket without the pass’s :occlusion-query-set (quoted above), and a (copy-buffer-to-buffer …) endpoint whose (buffer …) lacks the usage the copy needs:

buffer 'readbackBuf' is the :destination of a copy but its :usage has no copy-dst

The query-resolve usage a resolve-query-set destination needs is WebGPU’s rule, not one the compiler checks: get it wrong and the error arrives from the device at run time.

const occ = device.createQuerySet({ type: 'occlusion', count: 6 });
const pass = encoder.beginRenderPass({ colorAttachments, occlusionQuerySet: occ });
pass.beginOcclusionQuery(0);
pass.draw(36, 1, 0, 0);
pass.endOcclusionQuery();
pass.end();
encoder.resolveQuerySet(occ, 0, 6, resolveBuf, 0);

The whole query family is browser-only: the native --frame renderer treats occlusion and timestamp queries as no-ops, so a payload that depends on query results renders identically without them.

  • (render-pass …) - :occlusion-query-set, (timestamp-writes …), (occlusion-query …)
  • (queue …) - (resolve-query-set …) and (copy-buffer-to-buffer …)
  • (buffer …) - query-resolve / map-read destinations