Archived: clsl replaced it on 2026-10-15. Shaders written in Common Lisp: typed shader functions and stages, checked and translated to GLSL, then compiled to SPIR-V for Vulkan.
This repository has been archived on 2026-10-15. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • Common Lisp 100%
Find a file
dxtr 621ec1f074
All checks were successful
CI / test (push) Successful in 3m26s
Say cl-gsl is archived: clsl replaced it on 2026-10-15
Pyssel and cl-slug compile their shaders with clsl, which writes SPIR-V
itself; nothing uses cl-gsl any more, and the repository is kept,
read-only, as a record.
2026-10-15 09:28:59 +02:00
.forgejo/workflows Run CI on SBCL 2.6.9 2026-10-11 16:44:50 +02:00
ffi Add cl-gsl: shaders in Lisp, compiled to GLSL and SPIR-V 2026-10-10 11:58:31 +02:00
gmath Pin cl-gmath at f68ff1e, whose vectors are typed structures 2026-10-14 13:46:06 +02:00
src Make &UNIFORM and &SHARED cl-gsl's symbols 2026-10-11 15:52:16 +02:00
tests Pin cl-gmath at f68ff1e, whose vectors are typed structures 2026-10-14 13:46:06 +02:00
.gitignore Add cl-gsl: shaders in Lisp, compiled to GLSL and SPIR-V 2026-10-10 11:58:31 +02:00
cl-gsl-build.asd Add cl-gsl: shaders in Lisp, compiled to GLSL and SPIR-V 2026-10-10 11:58:31 +02:00
cl-gsl.asd Add doubles, numbers, shader variables and every stage 2026-10-11 14:50:20 +02:00
LICENSE Add cl-gsl: shaders in Lisp, compiled to GLSL and SPIR-V 2026-10-10 11:58:31 +02:00
qlfile Add shader macros, type aliases and cl-gmath's names 2026-10-10 23:45:39 +02:00
qlfile.lock Pin cl-gmath at f68ff1e, whose vectors are typed structures 2026-10-14 13:46:06 +02:00
README.org Say cl-gsl is archived: clsl replaced it on 2026-10-15 2026-10-15 09:28:59 +02:00

cl-gsl

Archived. cl-gsl was Pyssel's shader compiler until 2026-10-15, when clsl replaced it. clsl compiles shader code in Common Lisp to SPIR-V by itself, with no glslang and no C++ toolchain. This repository is kept, read-only, as a record.

Shaders written in Common Lisp. Define shader functions and stages as Lisp, with typed parameters and inferred types everywhere else; cl-gsl checks them, translates them to GLSL and compiles that to SPIR-V for Vulkan.

(asdf:load-system :cl-gsl)

(defpackage #:example
  (:use #:cl #:cl-gsl))
(in-package #:example)

(define-shader triangle-vertex :vertex ((position :vec3) (color :vec3))
  "Place each vertex where its POSITION says, and pass its COLOR on."
  (values (vec4 position 1) color))

(define-function brightness ((color :vec3))
  "Return the perceived brightness of the linear COLOR."
  (dot color (vec3 0.2126 0.7152 0.0722)))

(define-shader triangle-fragment :fragment ((color :vec3))
  "Draw COLOR, dimmed where it is bright."
  (vec4 (mix color (* color 0.5) (brightness color)) 1))

(defvar *program*
  (compile-program :vertex 'triangle-vertex :fragment 'triangle-fragment))

(program-spirv *program* :vertex)    ; => #(119734787 67072 ...), 32-bit words
(program-vertex-inputs *program*)
;; => (#S(VERTEX-INPUT :NAME POSITION :LOCATION 0 :TYPE :VEC3)
;;     #S(VERTEX-INPUT :NAME COLOR :LOCATION 1 :TYPE :VEC3))
(write-string (program-glsl *program* :fragment))

prints the GLSL the fragment shader was compiled from:

#version 450

layout(location = 0) in vec3 color;
layout(location = 0) out vec4 fragment_output_0;

float brightness(vec3 color_1) {
    return dot(color_1, vec3(0.2126, 0.7152, 0.0722));
}

void main() {
    fragment_output_0 = vec4(mix(color, (color * 0.5), brightness(color)), 1.0);
}

cl-gfx takes the SPIR-V as it is:

(gfx:create-render-pipeline
 device
 :vertex (gfx:create-shader-module device (program-spirv *program* :vertex))
 :fragment (gfx:create-shader-module device (program-spirv *program* :fragment))
 :vertex-buffers '((:stride 24 :attributes ((0 :float32x3 0) (1 :float32x3 12))))
 :color-formats (list (gfx:surface-format surface)))

Building

cl-gsl compiles GLSL to SPIR-V in-process with glslang, through its C interface. Distributions ship glslang as static archives only, so building cl-gsl links them into a shared library of its own, which ASDF keeps with the compiled files and rebuilds when glslang is upgraded. That needs:

  • a C++ compiler: c++, or $CXX;
  • glslang's development files: glslang-dev on Debian and Ubuntu. If its archives are somewhere the compiler does not look, set GLSLANG_LIBRARY_DIR to their directory.

This is a stopgap. It works where the archives are position-independent code, as Debian's are, and not yet on Windows.

Shader code

A shader stage is defined with define-shader, a shader function with define-function. Both look like defun with typed parameters. Every other type is inferred. A definition is checked as soon as it is evaluated. If the check fails, the error names the definition and the form, and the previous definition stays.

(define-function lighten ((color :vec3) (amount :float))
  (let ((white (vec3 1)))
    (mix color white (clamp amount 0 1))))

A function may call one defined later. That gives a forward-reference-warning warning, and the call is checked again when a program is compiled.

Stages are functions from their inputs to their outputs.

  • A :vertex shader's parameters are its vertex inputs, at locations 0, 1, … in order unless given :location. It returns the clip-space position, a :vec4, then any values for the fragment shader, with values.
  • A :fragment shader's parameters receive those values, in the same order and with the same types. Integer inputs are flat. Others take :interpolation :flat or :noperspective. It returns one colour per render target, with values if there are several, or calls (discard).
  • A compute shader, (:compute :workgroup-size (x y z)), has no inputs and returns nothing; see Compute shaders.

Clip space is Vulkan's, as in cl-gfx: y points down, so the top of the target is at y -1, and depth runs from 0 to 1.

Uniforms come after &uniform in a stage's parameter list. It is cl-gsl's symbol, written gsl:&uniform through a nickname, as cl-gsl's other names are. Uniforms are the same for every vertex or fragment of a draw, and shader code reads them but cannot assign them:

(define-shader sprite-vertex :vertex ((position :vec2) (uv :vec2)
                                      &uniform (view :mat4 :group :frame))
  (values (* view (vec4 position 0 1)) uv))

(define-shader sprite-fragment :fragment ((uv :vec2)
                                          &uniform (tint :vec4)
                                                   (image :sampler2d :group :draw))
  (* (sample image uv) tint))

Uniforms fall into groups by how often they change, as Godot's uniform sets do, and each group is one bind group, or descriptor set:

Group Bind group For
:frame 0 what the engine sets for a frame or pass, such as the camera or the target's size
:material 1 what a material sets; the default
:draw 2 what changes from draw to draw, such as a sprite's texture

In a group, the uniforms of value types are the members of a uniform block, laid out by the rules of std140, at binding 0; the resources, textures, storage buffers and storage images, follow from binding 1. Both are in the order the stages first declare them, vertex before fragment. (uniform-group-index group) gives a group's bind group. Two stages that declare the same uniform share it, and must declare it alike. A shader function takes textures as parameters; it cannot store one in a variable or return one.

Types

Kind Types
Scalars :bool :int :uint :float :double
Vectors :vec2 to :vec4, :dvec2 to :dvec4, :ivec2 to :ivec4, :uvec2 to :uvec4, :bvec2 to :bvec4
Matrices :mat2 :mat3 :mat4, and :matCxR with C columns and R rows, such as :mat2x3; :dmat2 and the rest of doubles
Numbers :complex and :dcomplex, complex numbers of floats and doubles; :ratio, a ratio of 32-bit integers
Arrays (:array type length), such as (:array :vec4 8); (:array type) in storage buffers
Structs a name defined with define-struct
Textures :sampler1d :sampler2d :sampler3d :sampler-cube, the -array kinds, :sampler2d-ms, :sampler-buffer, each also i- and u-prefixed for ints and uints, such as :usampler2d; and the -shadow kinds, such as :sampler2d-shadow
Storage images :image2d, :uimage3d, and so on, as the textures, without shadows

(shader-documentation 'shader-type) lists them all. An integer literal also fits where a :float or :uint is expected, so (* 2 v) and (max x 0) need no decimal point. A double literal, 1d0, is a :double, and fits where a :float is expected only through float.

Numbers

Complex numbers and ratios work as Lisp's do, with its contagion:

(define-function spin ((z :complex) (turns :float))
  "Turn Z by TURNS whole turns about the origin."
  (* z (cis (* turns 6.2831855))))

(define-function share ((part :int) (whole :int))
  "PART of WHOLE as a ratio, exact until it is made a float."
  (/ (rational part) whole))

#c(1 2) and 2/3 are literals; complex, realpart, imagpart, conjugate, phase, cis, numerator, denominator and rational make and take them apart; +, -, *, /, =, abs, exp, sqrt, log, expt, sin and their kin take them, and < and floor take ratios. A :complex is held as a vec2 and a :ratio as an ivec2, so they pass between stages and live in uniform blocks. Integer / stays GLSL's truncating division, since a shader's types are fixed: make a ratio with rational, or write one. Ratios are reduced, and their parts are 32 bits, so they overflow sooner than Lisp's.

Operators

Operators are Common Lisp's where the meaning carries over, and this package's where it is particular to shaders:

What Operators
Binding and control let let* progn if when unless cond case and or not the
Loops dotimes, the simple loop, return
Values values multiple-value-bind nth-value multiple-value-setq multiple-value-prog1 multiple-value-call
Local definitions flet labels macrolet symbol-macrolet funcall
Assignment setf setq incf decf; places are variables, (x v), (swizzle v :xy), (aref v i), struct slots
Arithmetic + - * / per component, with linear algebra * for matrices; 1+ 1- abs signum min max expt sqrt isqrt exp log fma
Division floor ceiling truncate round and their f kinds, giving the quotient and remainder; mod rem
Comparison < > <= >= = /= with any number of scalars; two vectors compare per component, which any all not reduce
Predicates zerop plusp minusp evenp oddp float-nan-p float-infinity-p
Trigonometry sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh radians degrees
Construction vec3 ivec2 mat4 ..., float int uint bool, vector make-array, a struct's make-
Components x y z w, (swizzle v :zyx), (aref v i), length, a struct's accessors, slot-value with-slots
Geometry dot cross length distance normalize reflect refract face-forward
Interpolation mix clamp step smoothstep fract inverse-sqrt
Matrices transpose determinant inverse outer-product matrix-component-product
Bits logand logior logxor lognot logtest logbitp logeqv lognand lognor logandc1 logandc2 logorc1 logorc2 ash ldb dpb byte logcount integer-length find-lsb find-msb bitfield-reverse add-carry subtract-borrow multiply-extended
Floats float-bits float-from-bits float-sign decode-float scale-float; pack-unorm-4x8 unpack-half-2x16 and the rest of GLSL's packing
Textures sample fetch gather texture-size texture-levels texture-samples texture-query-lod
Storage images image-load image-store image-size image-samples
Derivatives dfdx dfdy fwidth and their -fine and -coarse kinds; interpolate-at-centroid interpolate-at-sample interpolate-at-offset
Stage inputs (vertex-index) (instance-index) in vertex shaders; (fragment-coord) (front-facing-p) (point-coord) (sample-index) (sample-position) (helper-invocation-p) in fragment shaders
Compute (global-invocation-id) and kin, barrier memory-barrier, atomic-incf atomic-decf atomic-min atomic-max atomic-logand atomic-logior atomic-logxor atomic-exchange compare-and-swap
Escape (glsl type "control" argument ...): GLSL written by hand

(shader-documentation 'mix) describes any of them, and any function you define.

Loops and values

Loops are dotimes and the simple loop, and return leaves the innermost one, giving the loop its value, as in Lisp:

(define-function lowest-set-bit ((bits :uint))
  "The index of the lowest bit set in BITS, or -1 if none is."
  (dotimes (i 32 -1)
    (when (logtest bits (ash (uint 1) i))
      (return i))))

(define-function first-square-above ((limit :int))
  (let ((n 0))
    (loop (when (> (* n n) limit) (return n))
          (incf n))))

dotimes evaluates its count once, an :int or :uint, and its variable has the count's type. A loop gives no value if no return gives one; one that does gives a value of one type from every return, and a dotimes from its result form too. loop with keywords, such as (loop for i below n ...), is not shader code: count with dotimes.

Several values work as in Lisp. A function returns them with values, multiple-value-bind receives them, and where one value is wanted the first is used. floor and its kin give the quotient and the remainder:

(define-function polar ((v :vec2))
  (values (length v) (atan (y v) (x v))))

(define-function spiral ((v :vec2))
  (multiple-value-bind (radius angle) (polar v)
    (multiple-value-bind (turns rest) (floor angle 6.2831855)
      (+ radius turns rest))))

In GLSL the values after the first are out parameters.

Structs, arrays and local functions

define-struct defines a struct, as defstruct does, with a constructor taking every slot by keyword and an accessor per slot, which setf assigns:

(define-struct light
  "A point light."
  (position :vec3)
  (color :vec3))

(define-function lit ((l light) (p :vec3))
  (/ (light-color l) (+ 1 (distance (light-position l) p))))

(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)))

Structs and arrays are values, as vectors are: binding or passing one copies it. vector and (make-array n :initial-element x) make arrays, aref reads and writes their elements, and length counts them. struct-layout, layout-size and array-stride say how a uniform block, by :std140, or a storage buffer, by :std430, lays them out.

A struct of numbers, scalars, vectors and structs of them, also passes from one stage to the next:

(define-struct shade
  (color :vec3)
  (material :int))

(define-shader shade-vertex :vertex ((position :vec3))
  (values (vec4 position 1) (make-shade :color (vec3 1 0 0) :material 2)))

(define-shader shade-fragment :fragment ((shade shade))
  (vec4 (* (shade-color shade) (if (= (shade-material shade) 2) 1.0 0.5)) 1))

Each slot is a stage output and input of its own, so each is interpolated by its type: color smoothly, material flat, as integers must be. A vertex shader's inputs, which vertex buffers hold, and a fragment shader's outputs, which render targets take, are not structs.

flet and labels define local functions, which see the variables around them; one that assigns such a variable changes it outside too, as in Lisp. macrolet and symbol-macrolet define local macros. case tests a key against values written in the code.

Textures

sample reads a texture filtered, fetch one texel unfiltered, and gather the four texels filtering would blend. Keywords choose how:

(sample image uv)                        ; by the derivatives, in a fragment shader
(sample image uv :level 2)               ; from mipmap level 2
(sample image uv :bias 1 :offset (ivec2 1 0))
(sample image uv :ddx (dfdx uv) :ddy (dfdy uv))
(sample shadow-map uv :compare depth)    ; a :sampler2d-shadow, giving a :float
(fetch atlas (ivec2 x y) :level 0)       ; integer coordinates
(fetch samples (ivec2 x y) :sample 3)    ; a multisampled texture
(gather image uv :component 3)

A texel of a :sampler... is a :vec4, of an :isampler... an :ivec4 and of a :usampler... a :uvec4. cl-gsl knows which texture kinds GLSL reads each way, and says so when one does not fit, rather than leaving glslang to reject the GLSL.

Shader code is not Lisp everywhere

Shader code differs from Lisp where GPUs do. Recursion is not allowed, a variable keeps the type of its initial value, and floor and its kin give float quotients for floats. Integers are 32 bits and wrap around, and ash by 32 bits or more is undefined. ash by a constant shifts one way; by a variable it chooses the way by the sign as the shader runs. Derivatives are undefined where neighbouring fragments take different branches.

Compute shaders

A compute shader runs in workgroups of the size its stage gives. It has no inputs: (global-invocation-id) tells its invocations apart. It reads and writes storage buffers and images, and shares &shared variables across a workgroup:

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

(define-shader advance (:compute :workgroup-size (64))
    (&uniform (particles (:array particle) :storage :read-write)
              (counter (:array :uint 1) :storage :read-write)
              (picture :image2d :format :rgba8-unorm :access :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)
        (atomic-incf (aref counter 0))
        (image-store picture (ivec2 (int i) 0) (particle-position p))))))

(compile-program :compute 'advance)
  • A storage buffer is a uniform with :storage :read or :storage :read-write. It holds any type a block holds, laid out by std430, and may end in an array without a length, (:array type), whose length is known as the shader runs. A struct may end in one too.
  • A storage image is a uniform of an image type, with its :format, cl-gfx's name such as :rgba8-unorm or :r32-uint, and its :access, :read, :write or :read-write. image-load and image-store read and write it.
  • Atomics change a place in a storage buffer or shared variable at once for every invocation, and give the value it held before: atomic-incf, atomic-max, compare-and-swap and the rest.
  • barrier waits for the workgroup; memory-barrier and its kin order memory writes.

Vertex and fragment shaders may use storage buffers and images too.

Uniforms from Lisp variables

define-variable defines a special variable that shader code reads as a uniform of its group. Every stage and function that reads it shares it, with no parameter for it:

(define-variable *sun* :vec3
  :value (vector 0.0 1.0 0.0) :group :frame
  :documentation "The direction light comes from.")

(define-function lambert ((normal :vec3))
  (max (dot normal *sun*) 0))

(define-shader lit :fragment ((normal :vec3) &uniform (tint :vec4))
  (* tint (lambert normal)))

pack-uniforms writes a program's uniform block of one group, laid out by std140, ready for a uniform buffer: each shader variable from its Lisp variable's value, and the other uniforms from :values. Given the vector to write into, it allocates nothing:

(let ((program (compile-program :fragment 'lit)))
  (pack-uniforms program :frame)                                ; *sun*
  (pack-uniforms program :material :values (list :tint #(1 0.5 0.5 1))))

pack-value writes one value of any shader type by std140 or std430: a vector or sequence for a vector, a matrix's components column by column, a plist of slots for a struct.

Geometry and tessellation

A tessellation control, tessellation evaluation or geometry shader receives every vertex of its patch or primitive: each parameter is an array of the values the stage before passed, one per vertex. The tessellation stages return their vertex's position and values, as a vertex shader does; a geometry shader emits vertices instead:

(define-shader split (:tessellation-control :patch-size 3 :vertices 3) ((color :vec3))
  (set-tessellation-levels (vec4 4 4 4 1) (vec2 4 0))
  (values (input-position (invocation-id)) (aref color (invocation-id))))

(define-shader place (:tessellation-evaluation :patch-size 3 :domain :triangles)
    ((color :vec3))
  (let ((w (tessellation-coord)))
    (values (+ (* (x w) (input-position 0)) (* (y w) (input-position 1))
               (* (z w) (input-position 2)))
            (+ (* (x w) (aref color 0)) (* (y w) (aref color 1)) (* (z w) (aref color 2))))))

(define-shader outline (:geometry :input :triangles :output :line-strip :max-vertices 4)
    ((color :vec3))
  (dotimes (corner 4)
    (emit-vertex (input-position (mod corner 3)) (aref color (mod corner 3))))
  (end-primitive))

(compile-program :vertex 'triangle-vertex :tessellation-control 'split
                 :tessellation-evaluation 'place :geometry 'outline
                 :fragment 'triangle-fragment)

compile-program checks that each stage takes what the one before passes, that the patches agree in size, and that a geometry shader after tessellation takes the primitives tessellation makes. (program-patch-size program) is the size of the patches to draw.

Macros and other names

define-macro defines a shader macro, as defmacro defines a Lisp macro. A form naming it is replaced by what its body returns, and the replacement is checked in its place:

(define-macro squared (x)
  "X times itself, evaluated once."
  (let ((value (gensym)))
    `(let ((,value ,x)) (* ,value ,value))))

define-type-alias makes a symbol another name for a shader type, in parameter lists and the.

Together they let shader code use a Lisp library's names. Loading cl-gsl/gmath does this for cl-gmath:

  • Its types become aliases: gm:vec3 for :vec3, gm:quat for :vec4.
  • Its vector, matrix and quaternion functions become shader macros, with the same arguments, less the result Lisp callers may pass.

Code written with cl-gmath's names is then shader code too:

(define-function lambert ((normal gm:vec3) (light gm:vec3))
  (max (gm:vec3-dot (gm:vec3-normalize normal) light) 0))

Where GLSL differs, shader-documentation says so: GLSL's normalize of a zero vector, and its inverse of a singular matrix, are undefined.

Programs

(compile-program :vertex v :fragment f) checks the stages and every function they call. It checks that each stage's outputs match the next one's inputs, generates GLSL for each stage, and compiles it to SPIR-V for Vulkan 1.3. Either stage may be left out; :tessellation-control, :tessellation-evaluation and :geometry stages go between them; (compile-program :compute c) compiles a compute shader alone. The result is a program:

  • (program-spirv program stage): the SPIR-V, as (simple-array (unsigned-byte 32) (*)).
  • (program-glsl program stage): the GLSL, for reading.
  • (program-vertex-inputs program): a list of vertex-input objects, each with a vertex-input-name, vertex-input-location and vertex-input-type, to build a vertex buffer layout from. An input of doubles is read as 32-bit unsigned words, which every GPU reads from vertex buffers, as not every GPU reads doubles: a uvec4 per location and a uvec2 for an odd double, low word first, from the same bytes the doubles fill. A :dvec3 is fed by a :uint32x4 at its location and a :uint32x2 at the next.
  • (program-workgroup-size program): a compute shader's (x y z).
  • (program-patch-size program): the vertices of each patch tessellation takes, or nil.
  • (program-uniforms program): the members of the groups' uniform blocks, as uniform objects, with a uniform-name, uniform-type, uniform-group and uniform-offset, in bytes into the group's block; (program-uniform-block-size program group), the size of a buffer that holds one.
  • (program-resources program &key kind): the textures, storage buffers and storage images, or those of kind (:texture, :storage-buffer or :image), as resource objects, with a resource-name, resource-type, resource-group, resource-binding, resource-kind, resource-access and resource-format.
  • (program-bindings program): the bindings the uniforms need, as program-binding objects, with a program-binding-group, program-binding-set, program-binding-number, program-binding-kind and program-binding-stages. A kind is :uniform-block, :storage-buffer, :read-only-storage-buffer, or a texture's or image's type, which texture-type-p and image-type-p tell apart and texture-type-shape gives the shape of, such as :2d-array or :buffer: what a GPU layer such as cl-gfx builds its bind group layouts from.
  • (program-stages program): (:vertex :fragment), or the stages it has.
  • (program-shader-name program stage): the name of the shader it was compiled from for stage.
(dolist (binding (program-bindings *program*))
  (format t "~&set ~D, binding ~D: ~S~%" (program-binding-set binding)
          (program-binding-number binding) (program-binding-kind binding)))

Redefinition

A program keeps the definitions it was compiled from. Code that holds programs and draws with them, such as a frame loop, can follow redefinitions made at the REPL:

  • (definitions-generation) grows whenever a shader, shader function, struct or shader variable is defined, redefined or removed. Compare it with the number seen last, and look further only when it has changed. It allocates nothing.
  • (program-stale-p program) is true once something the program was compiled from has been redefined or removed.
  • (recompile-program program) compiles the same stages again from their current definitions and returns a new program. It signals shader-error if they no longer fit together, for example when the vertex shader now passes values the fragment shader does not take.

A definition that fails its check is never stored, so a mistake at the REPL leaves programs, and what is drawn with them, as they were.

GLSL that was compiled before is not compiled again. Recompiling an unchanged program costs only the check and the translation.

Errors

  • shader-error: a mistake in shader code. The message names the definition and the form, and says what would fit, such as the signatures a built-in accepts.
  • undefined-shader-function: a call of something neither built in nor defined. This is a shader-error.
  • glslang-error: glslang rejected the generated GLSL. That is a bug in cl-gsl. The condition carries glslang's log and the GLSL.

All of them are gsl-error conditions.

Threads

Definitions may be made, and programs compiled, from any thread. Compiling serialises its use of glslang. A program never changes, so it may be shared between threads.

Status

Every stage: vertex, tessellation, geometry, fragment and compute; GLSL output, SPIR-V through glslang; programs that follow redefinitions; macros, local functions, several values, structs and arrays; doubles, complex numbers and ratios; every GLSL texture kind and way of reading one; storage buffers and images, atomics and barriers; uniforms from Lisp variables and a std140 packer; cl-gmath's names through cl-gsl/gmath. Next come uniform metadata for the inspector, and a plain-OpenGL dialect of the GLSL output.

Tests

qlot install
qlot exec sbcl --non-interactive --eval '(asdf:test-system :cl-gsl)'