2 Structs
dxtr edited this page 2026-10-14 03:35:08 +02:00

Structs

A struct groups named, typed slots into one value, as defstruct does in Lisp. Structs work in shader functions and stages, as uniforms, and in storage buffers.

Defining a struct

(define-struct name [docstring] (slot type) ...)
(define-struct light
  "A point light."
  (position :vec3)
  (color :vec3)
  (radius :float))

This makes light a shader type, and defines, as defstruct would:

Name What it is
(make-light :position p :color c :radius r) The constructor. It takes every slot by keyword.
(light-position l), (light-color l), (light-radius l) One accessor per slot. Each is a place, so setf, incf and the rest assign it.

The constructor and accessors are interned in the current package, as defstruct's are. They exist in shader code only; define-struct defines no Lisp functions.

A slot's type is any value type: a scalar, vector, matrix, complex number, ratio, another struct, or an array of these. The last slot may also be an array without a length; see Structs in storage buffers.

These are errors, reported when the struct is defined:

  • a slot of an opaque type, such as :sampler2d: pass textures to functions as parameters instead;
  • a struct that holds itself, directly or through other structs;
  • the same slot name twice.

Using structs in shader code

(define-function lit ((l light) (p :vec3))
  "The light L gives the point P."
  (/ (light-color l) (+ 1 (distance (light-position l) p))))

(define-function dimmed ((l light))
  "L at half its brightness."
  (setf (light-color l) (* (light-color l) 0.5))
  l)

(define-function white-light-at ((p :vec3))
  (make-light :position p :color (vec3 1) :radius 2))

Structs are values, as vectors are: binding one, passing one to a function, or returning one copies it. dimmed above changes its own copy and returns it; the caller's light is unchanged unless it assigns the result.

Constructing. The constructor needs a value for every slot. Leaving one out is an error:

In CP: The slot COLOR of LIGHT has no value.  Give every slot a value.
  in: (MAKE-LIGHT :POSITION (VEC3 0))

Reading and assigning slots. Besides the accessors, slot-value and with-slots work as in Lisp:

(slot-value l 'radius)                 ; as (light-radius l), and a place

(with-slots (radius (c color)) l       ; SLOT, or (VARIABLE SLOT)
  (* c radius))

In with-slots, the struct form is evaluated once, and assigning a slot's name assigns that slot of the variable's struct.

Comparing. (= a b) compares two structs whole, slot by slot.

Nesting. Structs may hold structs and arrays of structs, and accessors and aref chain as places:

(define-struct scene
  (sun light)
  (lights (:array light 4))
  (count :int))

(define-function brighten-first ((s scene))
  (incf (light-radius (aref (scene-lights s) 0)) 1)
  s)

Structs as uniforms

A struct, or an array of them, can be a uniform. It lives in its group's uniform block, laid out by std140:

(define-shader lights-fragment :fragment ((p :vec3)
                                          &uniform (lights (:array light 4)))
  (let ((sum (vec3 0)))
    (dotimes (i (length lights))
      (incf sum (lit (aref lights i) p)))
    (vec4 sum 1)))

The GLSL defines the struct once and uses it in the block:

struct light {
    vec3 position;
    vec3 color;
    float radius;
};

layout(set = 1, binding = 0, std140) uniform MaterialUniforms {
    light[4] lights;
} material_uniforms;

vec3 lit(light l, vec3 p_1) {
    return (l.color / (1.0 + distance(l.position, p_1)));
}

A uniform block holds numbers only, so a struct with a :bool slot can be used in shader code but not as a uniform. Use an :int or :uint slot for a flag.

Structs cannot be passed between stages: a stage's inputs and outputs are scalars and vectors. Pass the slots you need as separate values.

Structs in storage buffers

A storage buffer may hold a struct, or an array of structs, laid out by std430:

(define-struct particle
  (position :vec4)
  (velocity :vec4))

(define-shader advance (:compute :workgroup-size (64))
    (&uniform (particles (:array particle) :storage :read-write)
              (dt :float :group :frame))
  (let ((i (x (global-invocation-id))))
    (when (< i (uint (length particles)))
      (let ((p (aref particles i)))
        (incf (particle-position p) (* dt (particle-velocity p)))
        (setf (aref particles i) p)))))

(aref particles i) copies the element out; (setf (aref particles i) p) writes it back. To change one slot in place, assign it directly: (incf (particle-position (aref particles i)) ...).

A struct may end in an array without a length, (:array type), whose length is known as the shader runs. Such a struct can only be held by a storage buffer: it cannot be a uniform in a block, a variable, or a function parameter.

(define-struct bucket
  (count :uint)
  (items (:array :float)))

(define-shader fill (:compute :workgroup-size (64))
    (&uniform (b bucket :storage :read-write))
  (let ((i (x (global-invocation-id))))
    (when (< i (uint (length (bucket-items b))))
      (setf (aref (bucket-items b) i) (float i)))))

Memory layout

struct-layout gives a struct's slots with their byte offsets, and its size, by :std140 (the default, for uniform blocks) or :std430 (for storage buffers):

(struct-layout 'light)
;; => ((POSITION :VEC3 0) (COLOR :VEC3 16) (RADIUS :FLOAT 28)), 32

(struct-layout 'scene)
;; => ((SUN LIGHT 0) (LIGHTS (:ARRAY LIGHT 4) 32) (COUNT :INT 160)), 176

The size is also the stride of an array of the struct. layout-size, layout-alignment and array-stride take struct types too. A :vec3 is aligned to 16 bytes, so a :float after one fills its last 4 bytes, as radius does here. See Types.

Packing structs from Lisp

pack-value and pack-uniforms take a struct as a plist of its slots, as its constructor takes them:

(let ((buffer (make-array 32 :element-type '(unsigned-byte 8) :initial-element 0)))
  (pack-value 'light '(:position (1 2 3) :color (4 5 6) :radius 7) buffer))

(pack-uniforms program :material
               :values (list :lights (list '(:position #(0 2 0) :color #(1 1 1) :radius 5)
                                           '(:position #(4 2 0) :color #(1 0 0) :radius 3)
                                           ...)))

Every slot needs a value; a missing one is an error that names it:

The value of the struct LIGHT has no slot :COLOR: (:POSITION (1 2 3)).

See Uniforms and Resources.

Redefining and removing

  • Redefining a struct with define-struct replaces it. Programs compiled with the old definition become stale; recompile-program compiles them against the new one. See Programs.
  • (remove-definition 'light) forgets the struct, its constructor and its accessors.
  • A shader function cannot take the name of a struct's constructor or accessor.

Documentation

shader-documentation describes a struct, its constructor and its accessors:

(shader-documentation 'light)        ; => "A point light."
(shader-documentation 'make-light)
;; => "(make-light :position value :color value :radius value): a LIGHT, from a value for every slot."
(shader-documentation 'light-color)
;; => "(light-color struct): the COLOR slot of the LIGHT STRUCT, a :VEC3.  A place."