Table of Contents
Numbers and Precision
On scalars every operator means what it means in Common Lisp, as far as a GPU's numbers allow: integers of 32 bits, floats of 32, doubles where the device has them, complex numbers of either, and float pairs for positions far from the origin. This page says what each computes, and what clsl defines where SPIR-V leaves a result undefined.
Integers
:int and :uint are integers of 32 bits, signed and unsigned, and wrap:
(+ i 1) of an :int that is 2147483647 is -2147483648. An integer written
in the code that does not fit in 32 bits is refused, its arithmetic done
exactly first, as Lisp does it:
(define-function too-big ()
(+ 1 4294967296))
In TOO-BIG: 4294967297 does not fit in 32 bits, as clsl's integers do.
in: (+ 1 4294967296)
/ does not divide integers, which in Lisp would make a ratio: truncate,
floor, ceiling and round do, and give two values, the quotient and the
remainder, as Lisp's do. The bit functions, ldb, ash, logand and the
others, are Lisp's on 32 bits: see Language Functions.
Floats
A :float is a single-float, an IEEE float of 32 bits. A float function keeps
its type, and gives what the GPU gives outside its domain, usually a NaN or an
infinity: (sqrt -1.0) is a NaN, where Lisp's is a complex number. Vulkan
grants each function its precision, which differs from GPU to GPU within it.
Vulkan lets a driver assume that no float is a NaN or an infinity, unless a
shader declares SignedZeroInfNanPreserve. A module whose results depend on
them declares it for the floats it tests: a float converted to an integer with
its guard, decode-float's significand and exponent, float-nan-p,
float-infinity-p and scale-float. The program then needs a device
property, which program-features names:
(define-shader nan-check :fragment ((x :float))
(if (float-nan-p x) (vec4 1 0 0 1) (vec4 0 1 0 1)))
(program-features (compile-program '(:fragment nan-check) :target :vulkan))
;; => (:SIGNED-ZERO-INF-NAN-PRESERVE-FLOAT32)
Nearly every Vulkan 1.4 device has it.
Defined results
At the default safety every operation has a defined result, where SPIR-V leaves some undefined:
| Case | Result |
|---|---|
An integer divided by zero, or the most negative :int by -1, in truncate, floor, ceiling, round, mod and rem |
the quotient is the dividend, and the remainder 0 |
+, - and * of integers overflowing |
they wrap |
| An index out of range, into a vector, a matrix or an array | a read gives zero, and a write does nothing |
| A float converted to an integer it does not fit | the nearest integer that fits |
| A NaN converted to an integer | 0 |
isqrt of a negative :int |
0 |
decode-float of an infinity or a NaN |
its magnitude, the exponent 0, and its sign |
ash by 32 bits or more, and a byte of ldb and its kin past bit 31 |
as in Lisp: an :int's sign bits, or zeros |
A size or position of a byte, or an index of logbitp, that is negative, computed by the code |
the :uint of its bits, past bit 31 |
A texel, a level, a layer or a sample outside a texture, in fetch |
zero |
A level outside a texture, in texture-size |
a size of 0 |
| A texel or a sample outside a storage image | image-load gives zero, and image-store writes nothing |
| An element out of range, in an atomic | nothing changes, and it gives 0 |
An index out of range, in input-position |
the position zero |
| Tessellation levels a control shader does not set | 0, so the tessellator discards the patch |
A vertex emitted past a geometry shader's :max-vertices |
it is not emitted |
A guard is a comparison and a select, or, around a store, a comparison and a branch, and none is made where the operands are constants. A constant integer division by zero, a constant index out of range, or a negative size, position or index written in the code, is refused when the definition is evaluated:
(define-function constant-zero ((a :int))
(floor a 0))
In CONSTANT-ZERO: FLOOR divides an integer by zero, whose quotient Lisp does not
define: clsl gives the dividend, as the quotient of a zero computed as the
shader runs.
in: (FLOOR A 0)
(declare (optimize (safety 0))) leaves out the guards, where a measurement
shows that they cost too much, and those results are then the GPU's. It
leaves out the need for SignedZeroInfNanPreserve with them:
(define-shader whole-part :fragment ((x :float))
(vec4 (float (truncate x)) 0 0 1))
(define-shader whole-part-fast :fragment ((x :float))
(declare (optimize (safety 0)))
(vec4 (float (truncate x)) 0 0 1))
(program-features (compile-program '(:fragment whole-part) :target :vulkan))
;; => (:SIGNED-ZERO-INF-NAN-PRESERVE-FLOAT32)
(program-features (compile-program '(:fragment whole-part-fast) :target :vulkan))
;; => NIL
Floating-point functions outside their domains, and a float divided by zero, give what the GPU gives, at either safety.
Doubles
A :double is a double-float, and :dvec2 to :dvec4 and :dmat2 to
:dmat4x3 hold them. GPUs compute doubles' arithmetic, square roots,
rounding, min and its kin, geometry and matrices, but trigonometry,
exponentials and logarithms of :float values only, so these refuse doubles:
(define-function sine ((d :double))
(sin d))
In SINE: SIN takes a :FLOAT, or a vector of them, or a :COMPLEX, not :DOUBLE.
GPUs compute it of :FLOATs only: (float x 1.0) makes the doubles of X the
:FLOATs nearest them.
in: (SIN D)
Floats and doubles do not mix: (+ x d) of a :float and a :double is
refused, with a message that says (float x 1d0) makes the float a double,
and (+ (float x 1d0) d) adds them as doubles.
A program of doubles needs the device's :float64 feature, which
program-features names, and cl-gfx refuses a shader module of doubles on a
device without it. A vertex buffer gives a vertex shader its doubles as their
32-bit words, which every GPU reads, two words to a double, the low first, as
a double-float is stored:
(define-shader precise-vertex :vertex ((position :dvec3) (scale :double))
"Scale POSITION, in doubles, and place it."
(vec4 (float (* position scale) 1.0) 1))
(let ((program (compile-program '(:vertex precise-vertex) :target :vulkan)))
(values (program-features program)
(program-vertex-inputs program)
(program-vertex-buffers program)))
;; => (:FLOAT64)
;; (#S(VERTEX-INPUT :NAME POSITION :LOCATION 0 :TYPE :DVEC3 :FORMATS (:UINT32X4 :UINT32X2))
;; #S(VERTEX-INPUT :NAME SCALE :LOCATION 2 :TYPE :DOUBLE :FORMATS (:UINT32X2)))
;; ((:STRIDE 32 :ATTRIBUTES ((0 :UINT32X4 0) (1 :UINT32X2 16) (2 :UINT32X2 24)) :STEP :VERTEX))
So the bytes of a vector of double-floats are what the attributes read. Three
or four doubles take two locations, and doubles pass between stages :flat,
as GPUs interpolate none.
Vulkan grants doubles' arithmetic only the precision of single-floats, though GPUs that have doubles compute them as IEEE does. Doubles are slow: GPUs made for games compute them 32 to 64 times slower than floats, or in software, and many of Intel's have none. Keep them to code that needs IEEE doubles, rather than code that runs for each vertex or pixel of a frame; for positions far from the origin, float pairs are faster.
Complex numbers
A :complex holds two :float parts, as Lisp's (complex single-float)
does, and a :dcomplex two doubles. #c(1.0 2.0) written in the code is a
:complex, and (complex x y) makes one; a real meets a complex number of
its kind as Lisp's contagion has it.
(define-function rotate ((z :complex) (angle :float))
"Z turned by ANGLE radians about the origin."
(* z (cis angle)))
A complex number's arithmetic, abs, signum, sqrt, exp, log, expt
and its trigonometric and hyperbolic functions and their inverses mean what
they mean in Lisp, with Lisp's branch cuts: they are W. Kahan's formulas, as
SBCL computes them, written in clsl's own language, and a module includes each
that it calls once. On a cut, the side the sign of a zero part picks is the
GPU's. A :dcomplex has the arithmetic, abs, signum and sqrt, which
need no trigonometry:
(define-function double-exp ((z :dcomplex))
(exp z))
In DOUBLE-EXP: EXP takes a :FLOAT, or a vector of them, or a :COMPLEX, not
:DCOMPLEX. GPUs compute it of :FLOATs only: (float x 1.0) makes the doubles of
X the :FLOATs nearest them.
in: (EXP Z)
A complex number is laid out as its two parts, a :vec2 or a :dvec2, in
uniform blocks, storage buffers and vertex buffers, and passes between stages
interpolated as its parts are; a fragment shader's :complex goes to two
channels of a render target.
Float pairs
A :dfloat is a float pair: a :float, its high part, and the :float
nearest what the high part misses, its low part. Their sum holds about 14
significant digits, with a float's range, and pairs compute at the speed of
floats on every GPU, where doubles are slow or missing: a position 10,000 km
from the origin, which a :float holds to a metre, a pair holds to well under
a micrometre. Unreal's DoubleFloat library and Godot's large worlds keep
positions so. :dfvec2 to :dfvec4 are vectors of pairs, which are no
:dvec values: those hold doubles.
(define-shader far-vertex :vertex ((position :dfvec3)
&uniform (eye :dfvec3 :group :frame)
(view-projection :mat4 :group :frame))
"Place POSITION, far from the origin, as seen from EYE."
;; Relative to the eye, the position is small, and a float holds it.
(* view-projection (vec4 (float (- position eye)) 1)))
(let ((program (compile-program '(:vertex far-vertex) :target :vulkan)))
(values (program-features program)
(program-vertex-inputs program)
(program-vertex-buffers program)))
;; => (:ROUNDING-MODE-RTE-FLOAT32)
;; (#S(VERTEX-INPUT :NAME POSITION :LOCATION 0 :TYPE :DFVEC3 :FORMATS (:FLOAT32X3 :FLOAT32X3)))
;; ((:STRIDE 24 :ATTRIBUTES ((0 :FLOAT32X3 0) (1 :FLOAT32X3 12)) :STEP :VERTEX))
A vertex buffer gives a pair as two float attributes, the high parts at the
input's location and the low parts at the next. In Lisp, a pair is the
double-float it holds: pack-value and pack-uniforms write the pair nearest
a real, and dfloat-parts gives its two floats, as a vertex buffer holds
them:
(dfloat-parts 6371000.123456789d0) ; => 6371000.0, 0.12345679
(dfloat-parts 0.1d0) ; => 0.1, -1.4901161e-9
(dfloat-parts 1.5) ; => 1.5, 0.0
| Function | Takes and gives |
|---|---|
(dfloat number) |
number as a pair, or a vector of them: a :float, an :int or a :uint exactly, a :double as the pair nearest it, which needs the device's doubles, and a pair as it is. A number written in the code becomes the pair nearest it |
(dfvec2 ...) to (dfvec4 ...) |
pairs and vectors of them, as the other constructors take theirs |
(float pair [prototype]) |
the :float nearest the pair, its high part, or a vector of them; with 1d0, the :double nearest it |
(+ ...), (- ...), (* ...), (/ ...) |
pairs of one type, a pair among vectors of them; (- p) negates, and (/ p) is the reciprocal |
(sqrt pair), (abs pair) |
the square root, or the magnitude, component by component |
(min pair ...), (max pair ...) |
as of numbers |
=, /=, <, >, <=, >=, zerop, plusp, minusp |
scalar pairs, as of reals; vectors of them compare through every and its kin |
truncate, floor, ceiling, round, their f kinds, mod and rem |
as of floats: the quotient an :int, a pair for the f kinds, and the remainder a pair |
Floats and pairs do not mix:
(define-function mixed-pair ((p :dfloat) (x :float))
(+ p x))
In MIXED-PAIR: + takes numbers of one type, :INT, :UINT, :FLOAT, :DOUBLE or
:DFLOAT, scalars or vectors, or a vector and a scalar of its components;
matrices of one type; or complex numbers, or a complex number and a real of its
parts' kind, not :DFLOAT, :FLOAT. Float pairs and other floats do not mix:
(dfloat x) makes the :FLOATs or :DOUBLEs of X float pairs, and (float x) the
pairs of X the :FLOATs nearest them.
in: (+ P X)
and (+ p (dfloat x)) adds them as pairs. Other functions refuse pairs.
Each operation is within a few units of the last of the 48 bits a pair holds.
They are the formulas of the QD library of Hida, Li and Bailey, for pairs of
doubles, made for floats: sums by Knuth's two-sum; products by Dekker's, which
needs no fused multiply-add; quotients in three steps, each correcting those
before, as Vulkan grants a division of floats only 2.5 units in the last
place; and square roots by two of Newton's steps from the GPU's. A sum is
about 20 operations of floats, and a product about 25. They are clsl's own
shader functions, and a module includes each that it calls once. Their
operations are decorated NoContraction, which forbids a driver to fuse
them, as a product and a sum into a fused multiply-add, or to reorder or fold
them, as any of these would lose the low part.
Pairs need the GPU to round +, - and * of floats to the nearest float,
which Vulkan promises only to a shader that declares RoundingModeRTE: a
module that uses pairs declares it for floats, and needs the device property
:rounding-mode-rte-float32, as nearly every Vulkan 1.4 device has. A pair
keeps its precision for magnitudes from about 10^-28 to a float's largest:
below, the errors of its parts fall among the floats below 2^-126, which a
GPU may flush to zero. Beyond the range, or divided by zero, a pair gives a
NaN or an infinity, as floats do.
In a uniform block a pair is laid out as a :vec2 of its high and low parts,
and a vector of pairs as an array of them, which std140 strides by 16 bytes:
a :dfvec3 takes 48 bytes there, and 24 in a storage buffer, by std430.
Pairs pass between stages :flat, as a GPU would interpolate a pair as two
floats.