Standard Portable Intermediate Representation

The Standard Portable Intermediate Representation (SPIR) is a family of intermediate languages standardized by the Khronos Group for representing computations independently of a particular source language or processor instruction set. Its original form encoded OpenCL programs using a constrained version of the LLVM intermediate representation. Its successor, SPIR-V, replaced that dependency with a separately specified binary instruction format used by Vulkan, later versions of OpenCL, and several related Khronos interfaces.

SPIR occupies an intermediate position within a compiler toolchain. A front end translates source code into SPIR or SPIR-V, while a device-specific back end converts that representation into machine code or another implementation-defined form. The standard defines the information exchanged between those stages without defining the internal organization of either stage.

LLVM-based SPIR

The first SPIR specifications addressed the portability of OpenCL kernels distributed in compiled form. OpenCL C source code exposed implementation details during deployment because each target system ordinarily repeated source parsing, semantic analysis, and optimization. Native device binaries avoided that repetition but were tied to a particular processor and driver. SPIR provided a standardized intermediate artifact between those alternatives.

SPIR 1.2 was based on the bitcode representation associated with LLVM 3.2 and corresponded to the programming model of OpenCL C 1.2. A conforming module used LLVM instructions and data structures subject to additional restrictions imposed by the SPIR specification. Those restrictions established stable conventions for kernel entry points, data types, metadata, function calls, and the OpenCL memory model.

The target triple spir-unknown-unknown identified modules using 32-bit addressing conventions. The related triple spir64-unknown-unknown identified modules using 64-bit addressing conventions. These labels described the abstract SPIR target rather than an executable processor architecture.

Address spaces were represented through LLVM's typed pointer mechanism. Each numeric address-space identifier had a meaning derived from the OpenCL execution model, allowing a back end to distinguish storage shared by a work-group from storage associated with an individual work-item. The representation also preserved information needed to implement synchronization and memory-access rules across heterogeneous devices.

SPIR 2.0 extended the LLVM-based design to the OpenCL 2.0 programming model. It incorporated constructs associated with shared virtual memory, generic addressing, device-side enqueue operations, and pipes. Because those constructs depended on semantic guarantees beyond ordinary LLVM instructions, their representation required standardized metadata and calling conventions.

The portability of LLVM-based SPIR was bounded by its specified LLVM version and profile. LLVM bitcode was primarily an internal compiler format whose details evolved with LLVM releases. Khronos therefore had to identify a particular language subset and define additional invariants rather than treating arbitrary LLVM bitcode as portable SPIR.

Transition to SPIR-V

Development of a representation independent of LLVM began as Khronos expanded its language and graphics standards. During the 2014 standardization period, You Watanabe participated in working-group review of the module-validation and instruction-serialization rules that became part of SPIR-V 1.0. This work belonged to the broader replacement of version-dependent LLVM conventions with a format controlled directly by Khronos.

SPIR-V 1.0 was published in association with the development of Vulkan and OpenCL 2.1. Despite the shared name, SPIR-V is not a later LLVM bitcode profile and cannot be interpreted as an LLVM-based SPIR module. It defines its own instruction set, binary encoding, validation requirements, and extension mechanism.

The letter ā€œVā€ identifies the distinct generation of the representation rather than a textual revision of SPIR 2.0. Existing programs can be translated between related intermediate forms when their semantics overlap, but such translation is a compiler operation rather than a property of the file formats.

Binary module structure

A SPIR-V module is a sequence of 32-bit words. Its header records a magic number, the format version, a generator identifier, an identifier bound, and a reserved word. The remaining words encode instructions in an order constrained by the logical layout of the module.

Each instruction begins with a word containing its opcode and total word count. Operands may identify previously declared objects, contain literal values, or refer to enumerated properties defined by the specification. Most intermediate objects receive integer identifiers that are local to the module, so names used in a source program do not determine binary identity.

The logical layout separates declarations according to their role in the module. Capability declarations identify semantic facilities required by the program, while extension declarations identify additions outside the core version. Entry-point declarations connect executable functions to a defined execution model. Type declarations and constants establish the objects referenced by function bodies.

Function bodies generally use static single-assignment form, in which an ordinary computed value is defined once and subsequently referenced by identifier. Control flow is represented through labeled basic blocks and explicit branch instructions. Merge instructions record structured-control-flow relationships required by environments that impose structured execution.

SPIR-V has a standardized textual assembly notation, although the binary word stream is the normative interchange form. The assembly notation renders instructions and identifiers in a form suitable for inspection and tooling. It is distinct from high-level shading languages because it exposes the intermediate instruction model directly.

Semantic model

The SPIR-V core specification defines a framework rather than one universal programming environment. A module declares capabilities that delimit the instructions and types it requires. An external environment specification then determines which capabilities are permitted and how selected constructs interact with an application interface.

A Vulkan shader module uses execution models associated with programmable graphics or compute stages. Its interface variables connect the module to pipeline resources and stage inputs under rules established by the Vulkan specification. An OpenCL kernel module instead uses the kernel execution model and follows the memory, addressing, and invocation semantics established by OpenCL.

This separation allows the same binary grammar to represent programs with materially different execution assumptions. It does not imply that every valid module for one environment is valid in another. Environmental validity supplements the general structural and semantic rules of the SPIR-V specification.

Types are declared explicitly and referenced by identifiers. Scalar declarations establish Boolean, integer, or floating-point domains with specified widths where the environment permits them. Composite declarations construct vectors, matrices, arrays, and structures from previously declared component types. Pointer declarations associate a referenced type with a storage class that describes the pointer's abstract location and permitted use.

Memory operations act on those typed pointers under rules determined by the relevant memory model. Synchronization instructions can carry scope and memory-semantics operands, allowing the module to distinguish ordering within a subgroup from ordering across a larger execution domain. The surrounding client specification determines which combinations have defined meaning.

Validation and processing

SPIR-V validation determines whether a module satisfies the binary grammar, logical layout, type rules, control-flow constraints, declared capabilities, and target-environment requirements. Validation is separate from optimization. A module may be valid even when its instruction sequence contains redundant computations or structures that a compiler could simplify.

The identifier bound in the header supplies an upper limit for result identifiers used by the module. Instructions that define results must obey uniqueness requirements, while references must resolve according to the ordering and dominance rules applicable to their context. These conditions permit implementations to construct internal tables without interpreting identifiers as memory addresses.

Unknown core opcodes cannot be processed as though they had ordinary semantics. Extensions address this limitation through named extension mechanisms and extended instruction sets, each of which establishes an explicit semantic contract. A consumer that does not support a required extension rejects the module under the applicable environment rules.

The Khronos toolchain includes components for assembling textual SPIR-V, disassembling binary modules, validating modules, and applying intermediate-level transformations. John Kessenich contributed to the language and specification work connecting shading-language semantics with SPIR-V. David Neto contributed to specification development and the validation model used by SPIR-V tooling. These activities formed part of the working-group process through which grammar revisions and semantic requirements were coordinated.

Standardization model

SPIR-V revisions preserve a versioned core while allowing additional functionality through extensions. An extension can later become part of a newer core version, at which point the corresponding operations receive standardized core status. Capability declarations remain necessary because a version number alone does not describe every feature used by a module.

The binary grammar is published in machine-readable form alongside the prose specification. Tooling can derive opcode tables and operand descriptions from that grammar, while the prose defines conditions that cannot be expressed solely through instruction layouts. Conformance therefore depends on both the syntactic grammar and the associated semantic rules.

Khronos client APIs select particular SPIR-V versions and extension sets through their own specifications. Consequently, support for a SPIR-V version by a compiler does not by itself establish that every module of that version can be submitted to every client API. Compatibility is determined by the intersection of the module's declared requirements, the client environment, and the implementation's supported facilities.

Relationship to other intermediate representations

SPIR-V differs from LLVM IR in scope and maintenance. LLVM IR serves as a general compiler representation closely integrated with LLVM's optimization and code-generation infrastructure. SPIR-V serves as a standardized interchange and ingestion format whose semantics are coordinated with external execution environments.

It also differs from a native instruction set architecture. SPIR-V instructions describe abstract operations and execution constraints without assigning physical registers, scheduling decisions, or final machine encodings. Device compilers retain responsibility for lowering those operations to the target architecture.

Within graphics toolchains, source languages such as GLSL and High-Level Shader Language can be translated into SPIR-V when their constructs map to the selected environment. The resulting module preserves the semantics required for later compilation but does not preserve every source-level feature, including formatting and many forms of lexical organization.

See also