1 SPIRV
dxtr edited this page 2026-10-18 19:16:53 +02:00

SPIR-V

clsl/spirv, in the system of the same name, writes, reads and disassembles SPIR-V, the binary form of shaders that Vulkan and OpenGL take. It knows nothing of clsl's IR or language, and may be used on its own; the back end, in the same system, writes its modules with it. The examples on this page use a package of their own:

(asdf:load-system :clsl/spirv)

(defpackage #:spirv-example
  (:use #:cl)
  (:local-nicknames (#:spirv #:clsl/spirv)))
(in-package #:spirv-example)

Instructions

An instruction is a keyword and its operands, in the order of SPIR-V's grammar: (:type-int 3 32 1) is %3 = OpTypeInt 32 1. The keyword is the grammar's name, without Op for an instruction, in lower case, with a hyphen between words: OpFOrdLessThan is :f-ord-less-than, the execution model GLCompute is :gl-compute, and the capability Float64 is :float64. Names that start with a digit, as the dimensions 1D to 3D do, are written :2d, which Lisp reads as :|2D|.

Operand In Lisp
an id an integer that new-id made
a literal number an integer, or a float of the type's width: a single-float for 32 bits, a double-float for 64, and a single-float a half can hold for 16
a string a string
an enumerant a keyword, followed by its parameters: :location 0
a set of bits a keyword, :none for none, or a list of keywords in increasing order, followed by the parameters of each: '(:volatile :aligned) 4
an instruction of an imported set its keyword: :inverse-sqrt of GLSL.std.450
OpSpecConstantOp's operation the instruction's keyword: :i-add

Building a module

make-module makes a module, of SPIR-V 1.6 unless :version says otherwise; new-id makes its ids; emit adds an instruction and returns the id it defines; module-words returns its words, header first, as a vector of 32-bit words that cl-gfx's create-shader-module takes. A compute shader that does nothing:

(let* ((module (spirv:make-module))
       (void (spirv:void-type module))
       (main-type (spirv:function-type module void))
       (main (spirv:new-id module)))
  (spirv:emit module :capability :shader)
  (spirv:emit module :memory-model :logical :glsl450)
  (spirv:emit module :entry-point :gl-compute main "main")
  (spirv:emit module :execution-mode main :local-size 64 1 1)
  (spirv:emit module :name main "main")
  (spirv:emit module :function void main :none main-type)
  (spirv:emit module :label (spirv:new-id module))
  (spirv:emit module :return)
  (spirv:emit module :function-end)
  (spirv:module-words module))
;; => #(119734787 67072 0 5 0 131089 1 196622 0 1 327695 5 3 1852399981 0 393232 3 17 64 1 1 262149
;;      3 1852399981 0 131091 1 196641 2 1 327734 1 3 0 2 131320 4 65789 65592)

spirv-val accepts it. The module puts each instruction where SPIR-V's layout puts it, whatever order they are emitted in: the void type above, emitted first, goes after the names. Three instructions are written by what they refer to, which is emitted before them: an OpConstant needs its type, an OpSwitch its selector's type, and an OpExtInst its import. A function begins with :function and ends with :function-end; the instructions of its blocks go into it in the order they are emitted, and its variables of :function storage go to the start of its first block. A capability or extension emitted twice is declared once.

Types and constants are declared once, and the same id comes back each time one is asked for: void-type, bool-type, int-type, float-type, vector-type, matrix-type, pointer-type, function-type, image-type, sampler-type, sampled-image-type, constant, constant-composite, null-constant and import-instruction-set. SPIR-V allows a type other than an aggregate or a pointer to be declared once only, and emit refuses a second. Aggregates are another matter: one array or struct type may be laid out, with offsets and strides, and another of the same members not, so new-array-type, new-runtime-array-type and new-struct-type declare a new type each time:

(let ((module (spirv:make-module)))
  (list (spirv:int-type module 32 t) (spirv:int-type module 32 t)
        (spirv:new-struct-type module (spirv:int-type module 32 t))
        (spirv:new-struct-type module (spirv:int-type module 32 t))))
;; => (1 1 2 3)

emit refuses operands its instruction's grammar does not take, with an error that says which operand is wrong and what would fit, and an instruction where the layout does not allow it; module-words refuses a module that is not whole: a function not ended, no memory model, or an id that an instruction uses and none defines.

OpTypeInt's operand 3, Signedness, should be an unsigned 32-bit integer, not
:SIGNED.
OpReturn belongs in a block of a function: emit :FUNCTION and a :LABEL first.
A module needs a memory model, such as (emit module :memory-model :logical
:glsl450).

Requirements

A module notes what each instruction and enumerant it holds requires, by the grammar's tables, and by the width of its integer and float types, which the grammar does not list: module-requirements returns them, each a requirement whose requirement-capabilities and requirement-extensions it needs one of, and whose requirement-what names it. The back end chooses the capabilities and extensions to declare from them.

(let ((module (spirv:make-module)))
  (spirv:emit module :capability :shader)
  (spirv:float-type module 64)
  (spirv:emit module :decorate (spirv:new-id module) :built-in :sample-id)
  (map 'list (lambda (requirement)
               (list (spirv:requirement-what requirement)
                     (spirv:requirement-capabilities requirement)
                     (spirv:requirement-extensions requirement)))
       (spirv:module-requirements module)))
;; => (("64-bit floats" (:FLOAT64) NIL) ("the BuiltIn SampleId" (:SAMPLE-RATE-SHADING) NIL))

emit refuses an instruction or enumerant that the module's version of SPIR-V lacks and no extension provides:

OpCopyLogical is in SPIR-V 1.4 and later, and this module is SPIR-V 1.0: make
the module with MAKE-MODULE's :VERSION.

Reading and printing

read-instructions reads a module's words, or a .spv file, back into instructions in the form emit takes, so that what is read can be emitted again. read-capabilities reads only the capabilities a module declares, decoding no other instruction. read-header returns the header's version, generator, bound and schema, and read-words reads a file's words in either byte order.

(asdf:load-system :clsl/vulkan)

(clsl:define-shader tinted :fragment ((color :vec3) clsl:&uniform (tint :vec4))
  (* (clsl:vec4 color 1) tint))

(let ((words (clsl:program-spirv (clsl:compile-program '(:fragment tinted) :target :vulkan)
                                 :fragment)))
  (values (subseq (spirv:read-instructions words) 0 6)
          (spirv:read-capabilities words)
          (multiple-value-list (spirv:read-header words))))
;; => ((:CAPABILITY :SHADER) (:MEMORY-MODEL :LOGICAL :GLSL450)
;;     (:ENTRY-POINT :FRAGMENT 1 "main" 2 6 9) (:EXECUTION-MODE 1 :ORIGIN-UPPER-LEFT)
;;     (:NAME 2 "color") (:NAME 6 "output-0"))
;;    (:SHADER)
;;    ((1 6) 0 24 0)

disassemble-spirv prints a module as SPIR-V assembly, laid out and named as spirv-dis lays out and names it: ids by OpName, built-in variables by GLSL's names, and types and constants by what they are. With :raw-ids t it prints ids as numbers, and spirv-as --preserve-numeric-ids reads the text back into the same words, but for the generator's word in the header. Floats are printed as the shortest decimals that read back as them; infinities, NaNs and 16-bit floats in hexadecimal, as spirv-dis prints them. :stream nil returns the text as a string. Of the module built above:

; SPIR-V
; Version: 1.6
; Generator: 0; 0
; Bound: 5
; Schema: 0
               OpCapability Shader
               OpMemoryModel Logical GLSL450
               OpEntryPoint GLCompute %main "main"
               OpExecutionMode %main LocalSize 64 1 1
               OpName %main "main"
       %void = OpTypeVoid
          %2 = OpTypeFunction %void
       %main = OpFunction %void None %2
          %4 = OpLabel
               OpReturn
               OpFunctionEnd

and with :raw-ids t:

               OpEntryPoint GLCompute %3 "main"
               OpExecutionMode %3 LocalSize 64 1 1
               OpName %3 "main"
          %1 = OpTypeVoid
          %2 = OpTypeFunction %1
          %3 = OpFunction %1 None %2

The grammar's tables

The tables are generated from the SPIR-V grammar that Khronos publishes in SPIRV-Headers: SPIR-V 1.6, revision 7, and GLSL.std.450. They answer what an instruction or enumerant needs:

(list (spirv:instruction-opcode :type-int)
      (spirv:opcode-instruction 21)
      (spirv:instruction-spirv-name :f-ord-less-than)
      (spirv:instruction-version :copy-logical)
      (spirv:instruction-capabilities :demote-to-helper-invocation)
      (spirv:enumerant-capabilities :capability :shader)
      (spirv:enumerant-value :decoration :block)
      (spirv:enumerant-spirv-name :execution-model :gl-compute)
      (spirv:extended-instruction-number "GLSL.std.450" :inverse-sqrt)
      (spirv:extended-instruction-capabilities "GLSL.std.450" :pack-double2x32)
      (multiple-value-list (spirv:grammar-version)))
;; => (21 :TYPE-INT "OpFOrdLessThan" (1 4) (:DEMOTE-TO-HELPER-INVOCATION) (:MATRIX) 2
;;     "GLCompute" 32 (:FLOAT64) ((1 6) 7))
Function Answers
instruction-opcode, opcode-instruction an instruction's opcode, and the instruction of an opcode
instruction-spirv-name the name SPIR-V gives it, such as "OpTypeInt"
instruction-capabilities, instruction-extensions the capabilities of which a module must declare one to use it, unless it declares one of the extensions
instruction-version, instruction-last-version the SPIR-V version from which it is core SPIR-V, and the last in which it is, or nil
enumerant-names the enumerants of an operand kind, such as :built-in, in the grammar's order
enumerant-value, enumerant-spirv-name an enumerant's value, a single bit for a set of bits, and its name in SPIR-V
enumerant-capabilities, enumerant-extensions, enumerant-version, enumerant-last-version as of instructions; for a capability, the capabilities it implies
extended-instruction-number, extended-instruction-capabilities the number of an instruction of an imported set, and what it needs
grammar-version the newest SPIR-V version the tables describe, (1 6), and the grammar's revision

A name the grammar lacks is an error, which suggests the name meant where it can:

SPIR-V has no instruction :FADD.  Did you mean :F-ADD?

Grammar and Generator says how the tables are made.