Table of Contents
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.