Uniform Control Flow
Some operations are computed by several invocations together, and are undefined where some of them run the operation and others do not. clsl checks each, when a shader is defined, by WGSL's uniformity analysis, and refuses one that may run where control flow is not uniform, with a message that says what it depends on and what to write instead.
What needs it
- Derivatives. A fragment shader runs on quads of two by two fragments together, and a derivative is the difference between the values of the fragments next to each other in the quad, so each fragment needs the others to compute it at the same time.
samplein a fragment shader, which picks its level of detail from the derivatives of its coordinates, unless it is given:level, or:ddxand:ddy; andtexture-query-lod. Elsewheresamplereads level 0, and needs nothing.barrier, in a compute shader, which waits for every invocation of the workgroup, and needs every one of them to reach it.- The tessellation levels, which are the whole patch's: every invocation of the patch sets them alike, to the same levels.
Derivatives
These take floats, and give floats of the same type, component by component, in fragment shaders only.
| Function | Gives |
|---|---|
(dfdx value) |
how much value changes from this fragment to its neighbour on the right, as finely or as coarsely as the GPU chooses |
(dfdy value) |
how much value changes from this fragment to its neighbour below |
(fwidth value) |
(+ (abs (dfdx value)) (abs (dfdy value))): how much value changes around the fragment, as for the size of a pixel in value's units |
(dfdx-fine value), (dfdy-fine value), (fwidth-fine value) |
the same, from this fragment's own row, or column, of its quad |
(dfdx-coarse value), (dfdy-coarse value), (fwidth-coarse value) |
the same, which the four fragments of a quad may share: from its top row, or its left column |
(fwidth uv) of texture coordinates uv is the size of a pixel in them, for
antialiasing:
(define-shader circle :fragment ((uv :vec2))
"An antialiased disc of radius 0.5 around the centre of UV's square."
(let* ((distance (norm (- uv 0.5)))
(edge (fwidth distance))
(coverage (- 1 (smoothstep (- 0.5 edge) (+ 0.5 edge) distance))))
(vec4 coverage coverage coverage 1)))
The rule
An operation that needs uniform control flow is refused
- in a branch of an
if,when,unlessorcondwhose test may differ between invocations, or in an operand ofandororafter such a test, if the operand does more than compute a value, as when it calls a shader function, assigns a variable or holds a loop; - in a loop that some invocations may leave sooner than others: in a
dotimeswhose count may differ, or after(when test (return))whose test may; - in a shader function called where control flow may not be uniform, or one that decides its control flow by an argument that may differ.
After a branch or a loop, control flow is as uniform as it was before it.
What may differ between invocations is the shader's inputs,
(fragment-coord), (front-facing-p), (global-invocation-id) and the like,
storage buffers with :storage :read-write, storage images whose :access is
not :read, shared variables, what atomics give, derivatives and what
sample reads, variables assigned where control flow may not be uniform, and
whatever is computed from them. What is the same for every invocation is
numbers written in the code, uniforms, storage buffers with :storage :read,
(workgroup-id) and (workgroup-count), what fetch, image-load of a
storage image of :access :read, and the queries read of a texture or image
at a place the same for every invocation, and what is computed from them
alone.
Derivatives in branches
Compute a derivative where every fragment does, and choose between the results after:
;; Refused: only the fragments where (x uv) is above 0.5 compute (fwidth uv).
(define-shader edge-fade :fragment ((uv :vec2))
(if (> (x uv) 0.5)
(vec4 (fwidth uv) 0 1)
(vec4 0)))
;; Made: every fragment computes it, and the test chooses.
(define-shader edge-fade :fragment ((uv :vec2))
(let ((width (fwidth uv)))
(if (> (x uv) 0.5)
(vec4 width 0 1)
(vec4 0))))
The first is refused with:
In EDGE-FADE: (FWIDTH UV) computes a derivative, which the fragments of a quad
compute together, so it needs uniform control flow; but it is in a branch of
(IF (> (X UV) 0.5) ...), whose test depends on UV, an input, which may differ
from fragment to fragment. Compute the derivative where every fragment of the
quad does, before the branch; or, if they all do in a way clsl cannot see,
(declare (derivative-uniformity :warning)) or :off where the derivative is
computed makes this a warning, or silent.
in: (FWIDTH UV)
A loop whose count is an input is refused the same way, and one whose count is a uniform, the same for every fragment, is made:
(define-shader uniform-count :fragment ((uv :vec2) &uniform (n :int))
"N, a uniform, is the same for every fragment, so every fragment loops alike."
(let ((total (vec2 0)))
(dotimes (i n)
(incf total (fwidth uv)))
(vec4 total 0 1)))
A shader function that computes a derivative is checked as part of the shaders that call it:
(define-function shade ((uv :vec2))
(vec4 (fwidth uv) 0 1))
(define-shader caller :fragment ((uv :vec2))
(if (> (x uv) 0.5) (shade uv) (vec4 0)))
In CALLER: (SHADE UV) calls SHADE, in which (FWIDTH UV) computes a derivative,
which the fragments of a quad compute together, so it needs uniform control
flow; but it is in a branch of (IF (> (X UV) 0.5) ...), whose test depends on
UV, an input, which may differ from fragment to fragment. Compute the
derivative where every fragment of the quad does, before the branch; or, if
they all do in a way clsl cannot see, (declare (derivative-uniformity
:warning)) or :off where the derivative is computed makes this a warning, or
silent.
in: (SHADE UV)
Texture reads in branches
A sample in a branch is refused with the same advice, and that of giving it
the level of detail:
(define-shader masked :fragment ((uv :vec2) &uniform (mask :sampler2d) (picture :sampler2d))
(if (> (x (sample mask uv)) 0.5)
(sample picture uv)
(vec4 0)))
In MASKED: (SAMPLE PICTURE UV) reads a texture at the level of detail the
derivatives of its coordinates pick, which the fragments of a quad compute
together, so it needs uniform control flow; but it is in a branch of (IF (> (X
(SAMPLE MASK UV)) 0.5) ...), whose test depends on (SAMPLE MASK UV), a texture
read at a level of detail derivatives pick, which may differ from quad to quad.
Read the texture where every fragment of the quad does, before the branch, or
give SAMPLE the level of detail, as :LEVEL, or as :DDX and :DDY computed where
every fragment does; or, if they all do in a way clsl cannot see, (declare
(derivative-uniformity :warning)) or :off where the texture is read makes this a
warning, or silent.
in: (SAMPLE PICTURE UV)
Either of these is made:
(define-shader masked :fragment ((uv :vec2) &uniform (mask :sampler2d) (picture :sampler2d))
(let ((color (sample picture uv)))
(if (> (x (sample mask uv)) 0.5)
color
(vec4 0))))
(define-shader masked-explicit :fragment ((uv :vec2) &uniform (mask :sampler2d) (picture :sampler2d))
(let ((dx (dfdx uv))
(dy (dfdy uv)))
(if (> (x (sample mask uv)) 0.5)
(sample picture uv :ddx dx :ddy dy)
(vec4 0))))
Barriers
A barrier that some invocations of a workgroup may not reach is refused as
an error, which no declaration relaxes: move it where every invocation of the
workgroup reaches it. One under a test of a uniform, or of (workgroup-id),
which is the same for every invocation of a workgroup, is made. See Shared
Memory and Atomics.
Tessellation levels
A tessellation control shader's levels are the whole patch's, which every
invocation of the patch sets: GLSL defines them only where each invocation's
last levels are the same. So the analysis runs over the invocations of one
patch there, and refuses, as an error that no declaration relaxes, a
set-tessellation-levels that some invocations of the patch may skip, and
levels that may differ between them. Over a patch, what is the same in every
invocation is what is the same for the draw, and the patch's inputs at an
index that is the same in each, and (primitive-id); (invocation-id)
differs, and so does an input at the index it gives:
;; Refused: each invocation computes its own levels.
(define-shader refine (:tessellation-control :patch-size 3 :vertices 3)
((detail (:array :float 3)))
(let ((level (aref detail (invocation-id))))
(set-tessellation-levels (vec4 level) (vec2 level)))
(input-position (invocation-id)))
;; Made: every invocation computes them from the same inputs.
(define-shader refine (:tessellation-control :patch-size 3 :vertices 3)
((detail (:array :float 3)))
(let ((level (max (aref detail 0) (aref detail 1) (aref detail 2))))
(set-tessellation-levels (vec4 level) (vec2 level)))
(input-position (invocation-id)))
The first is refused with:
In REFINE: (SET-TESSELLATION-LEVELS (VEC4 LEVEL) (VEC2 LEVEL)) sets the
tessellation levels of the whole patch, which every invocation of the patch
sets alike, to the same levels; but the levels it sets depend on
(INVOCATION-ID), which differs from invocation to invocation. Compute them
from what is the same in every invocation of the patch: the uniforms,
(primitive-id), and the inputs at an index that is the same in each.
in: (SET-TESSELLATION-LEVELS (VEC4 LEVEL) (VEC2 LEVEL))
When clsl cannot see it
The analysis is WGSL's, which takes as uniform only what is the same for a
whole draw, and so sees no input as the same for every fragment of a quad,
even a :flat one, which the fragments of a primitive share. Where the
fragments of a quad agree in a way it cannot see, (declare (derivative-uniformity :warning)) or (declare (derivative-uniformity :off))
where the derivative or the texture read is computed, at the start of a body
that takes declarations, such as a locally around it, makes the finding a
uniformity-warning that compile-program signals, or nothing:
(define-shader per-primitive :fragment ((uv :vec2) (layer :uint) &uniform (pictures :sampler2d-array))
"PICTURES' LAYER, which the vertices of each primitive agree on."
(if (> layer 0)
(locally (declare (derivative-uniformity :warning))
(sample pictures (vec3 uv (float layer))))
(vec4 0)))
(handler-bind ((uniformity-warning
(lambda (warning)
(print (list (uniformity-warning-definition warning)
(uniformity-warning-form warning)))
(muffle-warning warning))))
(program-stages (compile-program '(:fragment per-primitive) :target :vulkan)))
;; prints (PER-PRIMITIVE (SAMPLE PICTURES (VEC3 UV (FLOAT LAYER))))
;; => (:FRAGMENT)
The warning's report is the message the error would have had, and
uniformity-warning-message says why control flow may not be uniform there.
The GPU's results are undefined where it is not uniform after all.
The analysis runs when a shader is defined, and when a program is defined or
compiled; an error refuses the definition, and a shader function is checked
as part of the shaders that call it. In the IR, uniformity-failures runs it
over a module: see The IR.