Non-Normative SPIR-V Mappings
This appendix includes:
-
a comparison of feature differences with SPIR-V versus without, for both Vulkan and OpenGL
-
a discussion of how GLSL features logically map to SPIR-V features.
Feature Comparisons
The following features are removed for both OpenGL and Vulkan:
-
subroutines
-
shared and packed block layouts
-
the already deprecated texturing functions (e.g.,
texture2D()
) -
the already deprecated noise functions (e.g.,
noise1()
) -
compatibility-profile features
-
gl_DepthRangeParameters and gl_NumSamples
Vulkan removed the following features, which are still present for OpenGL:
-
Default uniforms, for non-opaque types: The UniformConstant storage class can be used on individual variables at global scope. (That is, uniforms don’t have to be in a block, unless they are built-in members that are in block in GLSL version 4.5 or above.)
-
GLSL atomic-counter bindings have the offset layout qualifier → SPIR-V AtomicCounter storage class using the Offset decoration
-
GLSL origin_lower_left → SPIR-V OriginLowerLeft
-
special rules for locations for input doubles in the vertex shader
-
gl_VertexID and gl_InstanceID (more detail follows)
The following features are added for both OpenGL and Vulkan:
-
specialization constants
-
offset can organize members in a different order than declaration order
-
offset and align layout qualifiers for uniform/buffer blocks for versions that did not support them
Vulkan Only: The following features are added:
-
push-constant buffers
-
shader combining of separate textures and samplers (SPIR-V OpTypeSampler)
-
descriptor sets (DescriptorSet must be 0, if present)
-
gl_VertexIndex and gl_InstanceIndex
-
subpass-input targets and input attachments (input_attachment_index)
The following features are changed in both OpenGL and Vulkan:
-
gl_FragColor will no longer indicate an implicit broadcast
Vulkan Only: The following features are changed:
-
precision qualifiers (mediump and lowp) will be respected for all versions, not dropped for desktop versions (default precision for desktop versions is highp for all types)
-
arrays of uniforms and buffer blocks take only one binding number for the entire object, not one per array element
-
the default origin is origin_upper_left instead of origin_lower_left
Vulkan does not allow multi-dimensional arrays of resources like UBOs and SSBOs in its SPIR-V environment spec. SPIR-V supports it and OpenGL already allows this for GLSL shaders. SPIR-V for OpenGL also allows it.
Mapping from GLSL to SPIR-V
Specialization Constants
SPIR-V specialization constants, which can be set later by the client API,
can be declared using layout(constant_id=…)
. For example, to make a
specialization constant with a default value of 12:
layout(constant_id = 17) const int arraySize = 12;
Above, 17
is the ID by which the API or other tools can later refer to
this specific specialization constant. The API or an intermediate tool can
then change its value to another constant integer before it is fully
lowered to executable code. If it is never changed before final lowering,
it will retain the value of 12.
Specialization constants have const semantics, except they don’t fold.
Hence, an array can be declared with arraySize
from above:
vec4 data[arraySize]; // legal, even though arraySize might change
Specialization constants can be in expressions:
vec4 data2[arraySize + 2];
This will make data2
be sized by 2 more than whatever constant value
arraySize
has when it is time to lower the shader to executable code.
An expression formed with specialization constants also behaves in the shader like a specialization constant, not a like a constant.
arraySize + 2 // a specialization constant (with no constant_id)
Such expressions can be used in the same places as a constant.
The constant_id can only be applied to a scalar integer, a scalar floating-point or a scalar Boolean.
Only basic operators and constructors can be applied to a specialization constant and still result in a specialization constant:
layout(constant_id = 17) const int arraySize = 12; sin(float(arraySize)); // result is not a specialization constant
While SPIR-V specialization constants are only for scalars, a vector can be made by operations on scalars:
layout(constant_id = 18) const int scX = 1; layout(constant_id = 19) const int scZ = 1; const ivec3 scVec = ivec3(scX, 1, scZ); // partially specialized vector
A built-in variable can have a constant_id attached to it:
layout(constant_id = 18) gl_MaxImageUnits;
This makes it behave as a specialization constant. It is not a full redeclaration; all other characteristics are left intact from the original built-in declaration.
The built-in vector gl_WorkGroupSize can be specialized using special
layout local_size_{xyz}_id
applied to the in qualifier. For example:
layout(local_size_x_id = 18, local_size_z_id = 19) in;
This leaves gl_WorkGroupSize.y as a non-specialization constant, with gl_WorkGroupSize being a partially specialized vector. Its x and z components can be later specialized using the ID’s 18 and 19.
Vulkan Only: Push Constants
Push constants reside in a uniform block declared using the new layout-qualifier-id push_constant applied to a uniform-block declaration. The API writes a set of constants to a push-constant buffer, and the shader reads them from a push_constant block:
layout(push_constant) uniform BlockName { int member1; float member2; ... } InstanceName; // optional instance name ... = InstanceName.member2; // read a push constant
The memory accounting used for the push_constant uniform block is different than for other uniform blocks: There is a separate small pool of memory it must fit within. By default, a push_constant buffer follows the std430 packing rules.
Vulkan Only: Descriptor Sets
Each shader resource in a descriptor set is assigned a tuple of (set number, binding number, array element) that defines its location within a descriptor set layout. In GLSL, the set number and binding number are assigned via the set and binding layout qualifiers respectively, and the array element is implicitly assigned consecutively starting with index equal to zero for the first element of an array (and array element is zero for non-array variables):
// Assign set number = M, binding number = N, array element = 0 layout (set=M, binding=N) uniform sampler2D variableName;
// Assign set number = M, binding number = N for all array elements, // and array element = i for the ith member of an array of size I. layout (set=M, binding=N) uniform sampler2D variableNameArray[I];
For example, two combined texture/sampler objects can be declared in two different descriptor sets as follows
layout(set = 0, binding = 0) uniform sampler2D ts3; layout(set = 1, binding = 0) uniform sampler2D ts4;
See the API documentation for more detail on the operation model of descriptor sets.
Vulkan Only: Samplers, Images, Textures, and Buffers
Storage Images
Storage images are declared in GLSL shader source using uniform image variables of the appropriate dimensionality as well as a format layout qualifier (if necessary):
layout (set=m, binding=n, r32f) uniform image2D myStorageImage;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "myStorageImage" OpDecorate %9 DescriptorSet m OpDecorate %9 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 2D 0 0 0 2 R32f %8 = OpTypePointer UniformConstant %7 %9 = OpVariable %8 UniformConstant ...
Samplers
SPIR-V samplers are declared in GLSL shader source using uniform sampler and samplerShadow types:
layout (set=m, binding=n) uniform sampler mySampler;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %8 "mySampler" OpDecorate %8 DescriptorSet m OpDecorate %8 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeSampler %7 = OpTypePointer UniformConstant %6 %8 = OpVariable %7 UniformConstant ...
Textures (Sampled Images)
Textures are declared in GLSL shader source using uniform texture variables of the appropriate dimensionality:
layout (set=m, binding=n) uniform texture2D mySampledImage;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "mySampledImage" OpDecorate %9 DescriptorSet m OpDecorate %9 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 2D 0 0 0 1 Unknown %8 = OpTypePointer UniformConstant %7 %9 = OpVariable %8 UniformConstant ...
Combined Texture and Samplers
Combined textures and samplers are declared in GLSL shader source using uniform texture-combined sampler variables of the appropriate dimensionality:
layout (set=m, binding=n) uniform sampler2D myCombinedImageSampler;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %10 "myCombinedImageSampler" OpDecorate %10 DescriptorSet m OpDecorate %10 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 2D 0 0 0 1 Unknown %8 = OpTypeSampledImage %7 %9 = OpTypePointer UniformConstant %8 %10 = OpVariable %9 UniformConstant ...
Note that a combined image sampler descriptor can be referred to as just an image or sampler in the shader as per the above sections.
Combining Separate Samplers and Textures
A sampler, declared with the keyword sampler, contains just filtering information, containing neither a texture nor an image:
uniform sampler s; // a handle to filtering information
A texture, declared with keywords like texture2D, contains just image information, not filtering information:
uniform texture2D t; // a handle to a texture (an image in SPIR-V)
Constructors can then be used to combine a sampler and a texture at the point of making a texture lookup call:
texture(sampler2D(t, s), ...);
Note, layout()
information is omitted above for clarity of this feature.
Texture Buffers (Uniform Texel Buffers)
Texture buffers are declared in GLSL shader source using uniform textureBuffer variables:
layout (set=m, binding=n) uniform textureBuffer myUniformTexelBuffer;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "myUniformTexelBuffer" OpDecorate %9 DescriptorSet m OpDecorate %9 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 Buffer 0 0 0 1 Unknown %8 = OpTypePointer UniformConstant %7 %9 = OpVariable %8 UniformConstant ...
Image Buffers (Storage Texel Buffers)
Image buffers are declared in GLSL shader source using uniform imageBuffer variables:
layout (set=m, binding=n, r32f) uniform imageBuffer myStorageTexelBuffer;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "myStorageTexelBuffer" OpDecorate %9 DescriptorSet m OpDecorate %9 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 Buffer 0 0 0 2 R32f %8 = OpTypePointer UniformConstant %7 %9 = OpVariable %8 UniformConstant ...
Storage Buffers
Storage buffers are declared in GLSL shader source using buffer storage qualifier and block syntax:
layout (set=m, binding=n) buffer myStorageBuffer { vec4 myElement[]; };
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "myStorageBuffer" OpMemberName %9 0 "myElement" OpName %11 "" OpDecorate %8 ArrayStride 16 OpMemberDecorate %9 0 Offset 0 OpDecorate %9 BufferBlock OpDecorate %11 DescriptorSet m OpDecorate %11 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeVector %6 4 %8 = OpTypeRuntimeArray %7 %9 = OpTypeStruct %8 %10 = OpTypePointer Uniform %9 %11 = OpVariable %10 Uniform ...
Uniform Buffers
Uniform buffers are declared in GLSL shader source using the uniform storage qualifier and block syntax:
layout (set=m, binding=n) uniform myUniformBuffer { vec4 myElement[32]; };
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %11 "myUniformBuffer" OpMemberName %11 0 "myElement" OpName %13 "" OpDecorate %10 ArrayStride 16 OpMemberDecorate %11 0 Offset 0 OpDecorate %11 Block OpDecorate %13 DescriptorSet m OpDecorate %13 Binding n %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeVector %6 4 %8 = OpTypeInt 32 0 %9 = OpConstant %8 32 %10 = OpTypeArray %7 %9 %11 = OpTypeStruct %10 %12 = OpTypePointer Uniform %11 %13 = OpVariable %12 Uniform ...
Subpass Inputs
Within a rendering pass, a subpass can write results to an output target that can then be read by the next subpass as an input subpass. The "Subpass Input" feature regards the ability to read an output target.
Subpass inputs are read through a new set of types, available only to fragment shaders:
subpassInput subpassInputMS isubpassInput isubpassInputMS usubpassInput usubpassInputMS
Unlike sampler and image objects, subpass inputs are implicitly addressed by the fragment’s (x, y, layer) coordinate.
Input attachments are decorated with their input attachment index in addition to descriptor set and binding numbers.
layout (input_attachment_index=i, set=m, binding=n) uniform subpassInput myInputAttachment;
Which maps to the following SPIR-V:
... %1 = OpExtInstImport "GLSL.std.450" ... OpName %9 "myInputAttachment" OpDecorate %9 DescriptorSet m OpDecorate %9 Binding n OpDecorate %9 InputAttachmentIndex i %2 = OpTypeVoid %3 = OpTypeFunction %2 %6 = OpTypeFloat 32 %7 = OpTypeImage %6 SubpassData 0 0 0 2 Unknown %8 = OpTypePointer UniformConstant %7 %9 = OpVariable %8 UniformConstant ...
An input_attachment_index of i selects the ith entry in the input pass list. (See API specification for more information.)
These objects support reading the subpass input through the following functions:
gvec4 subpassLoad(gsubpassInput subpass); gvec4 subpassLoad(gsubpassInputMS subpass, int sample);
Mapping Variables
gl_FragColor
The fragment-stage built-in gl_FragColor, which implies a broadcast to all outputs, is not present in SPIR-V. Shaders where writing to gl_FragColor is allowed can still write to it, but it only means to write to an output:
-
of the same type as gl_FragColor
-
decorated with location 0
-
not decorated as a built-in variable.
There is no implicit broadcast.
Vulkan gl_VertexIndex and gl_InstanceIndex
Adds two new built-in variables, gl_VertexIndex and gl_InstanceIndex to replace the existing built-in variables gl_VertexID and gl_InstanceID.
In the situations where the indexing is relative to some base offset, these built-in variables are defined, for Vulkan, to take on values as follows:
gl_VertexIndex base, base+1, base+2, ... gl_InstanceIndex base, base+1, base+2, ...
Where it depends on the situation what the base actually is.
Storage Classes:
uniform sampler2D...; -> UniformConstant uniform blockN { ... } ...; -> Uniform, with Block decoration in / out variable -> Input/Output, possibly with block (below) in / out block... -> Input/Output, with Block decoration buffer blockN { ... } ...; -> Uniform, with BufferBlock decoration shared -> Workgroup <normal global> -> Private
Vulkan Only: buffer blockN { ... } ...; -> StorageBuffer, when requested OpenGL Only: uniform variable (non-block) -> UniformConstant OpenGL Only: ... uniform atomic_uint ... -> AtomicCounter
Input/Output
Mapping of input/output blocks or variables is the same for all versions of GLSL or ESSL. To the extent variables or members are available in a version, its location is as follows:
These are mapped to SPIR-V individual variables, with similarly spelled built-in decorations (except as noted):
Any stage:
in gl_VertexIndex (Vulkan only) in gl_VertexID (OpenGL only) in gl_InstanceIndex (Vulkan only) in gl_InstanceID (OpenGL only) in gl_InvocationID in gl_PatchVerticesIn (PatchVertices) in gl_PrimitiveIDIn (PrimitiveID) in/out gl_PrimitiveID (in/out based only on storage qualifier) in gl_TessCoord
in/out gl_Layer in/out gl_ViewportIndex
patch in/out gl_TessLevelOuter (uses Patch decoration) patch in/out gl_TessLevelInner (uses Patch decoration)
Compute stage only:
in gl_NumWorkGroups in gl_WorkGroupSize in gl_WorkGroupID in gl_LocalInvocationID in gl_GlobalInvocationID in gl_LocalInvocationIndex
Fragment stage only:
in gl_FragCoord in gl_FrontFacing in gl_ClipDistance in gl_CullDistance in gl_PointCoord in gl_SampleID in gl_SamplePosition in gl_HelperInvocation out gl_FragDepth in gl_SampleMaskIn (SampleMask) out gl_SampleMask (in/out based only on storage qualifier)
These are mapped to SPIR-V blocks, as implied by the pseudo code, with the members decorated with similarly spelled built-in decorations:
Non-fragment stage:
in/out gl_PerVertex { // some subset of these members will be used gl_Position gl_PointSize gl_ClipDistance gl_CullDistance } // name of block is for debug only
There is at most one input and one output block per stage in SPIR-V. The subset and order of members will match between stages sharing an interface.
Vulkan Only: Mapping of Precision Qualifiers
lowp -> RelaxedPrecision, on storage variable and operation mediump -> RelaxedPrecision, on storage variable and operation highp -> 32-bit, same as int or float
portability tool/mode -> OpQuantizeToF16
Mapping of Images
imageLoad() -> OpImageRead imageStore() -> OpImageWrite texelFetch() -> OpImageFetch subpassInput -> OpTypeImage with Dim of SubpassData (Vulkan only) subpassLoad() -> OpImageRead (Vulkan only)
imageAtomicXXX(params, data) -> %ptr = OpImageTexelPointer params OpAtomicXXX %ptr, data
XXXQueryXXX(combined) -> %image = OpImage combined OpXXXQueryXXX %image
Mapping of Layouts
std140/std430 -> explicit Offset, ArrayStride, and MatrixStride Decoration on struct members shared/packed -> not allowed <default> -> not shared, but std140 or std430 xfb_offset -> Offset Decoration on the object or struct member xfb_buffer -> XfbBuffer Decoration on the object xfb_stride -> XfbStride Decoration on the object any xfb_* -> the Xfb Execution Mode is set captured XFB -> has both XfbBuffer and Offset non-captured -> lacking XfbBuffer or Offset
max_vertices -> OutputVertices
Mapping of barriers
barrier() (compute) -> OpControlBarrier(/*Execution*/Workgroup, /*Memory*/Workgroup, /*Semantics*/AcquireRelease | WorkgroupMemory)
barrier() (tess control) -> OpControlBarrier(/*Execution*/Workgroup, /*Memory*/Invocation, /*Semantics*/None)
memoryBarrier() -> OpMemoryBarrier(/*Memory*/Device, /*Semantics*/AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)
memoryBarrierBuffer() -> OpMemoryBarrier(/*Memory*/Device, /*Semantics*/AcquireRelease | UniformMemory)
memoryBarrierShared() -> OpMemoryBarrier(/*Memory*/Device, /*Semantics*/AcquireRelease | WorkgroupMemory)
memoryBarrierImage() -> OpMemoryBarrier(/*Memory*/Device, /*Semantics*/AcquireRelease | ImageMemory)
groupMemoryBarrier() -> OpMemoryBarrier(/*Memory*/Workgroup, /*Semantics*/AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)
Mapping of atomics
all atomic builtin functions -> Semantics = None(Relaxed)
atomicExchange() -> OpAtomicExchange imageAtomicExchange() -> OpAtomicExchange atomicCompSwap() -> OpAtomicCompareExchange imageAtomicCompSwap() -> OpAtomicCompareExchange N/A -> OpAtomicCompareExchangeWeak