The motivation for SPV_KHR_untyped_pointers is based on the recognition that explicit pointee types carried no real semantic value. Having an int* ptr really can just be a void* ptr as it can always be cast (with OpBitcast). The core idea of untyped pointers is that until you make the memory access (OpLoad, OpStore, etc) the type is not needed. This matches the mental model of using things like C++ templates or unions.
This idea can also be found in the LLVM community, which has moved to Opaque Pointers starting in LLVM 15.
More information can be found in https://github.com/KhronosGroup/Vulkan-Docs/blob/main/proposals/VK_KHR_shader_untyped_pointers.adoc
This extension adds various Untyped instructions, such as:
- OpUntypedVariableKHR`
- OpUntypedAccessChainKHR`
- OpTypeUntypedPointerKHR`
OpTypeUntypedPointerKHRmay seem strange, but it follows theOpType*convention and is just unfortunate the "type" and "untyped" are both in the name
To better understand this extension, first focus on OpTypeUntypedPointerKHR as that is the "core" change. By using this new pointer type, you will find the reason the other "untyped" instructions were added to go along with it.
Let's use this very basic example of load and storing to a storage buffer (view online):
layout(set = 0, binding = 0) buffer SSBO {
uint a;
};
void main() {
uint b = a;
a = 0;
}This will generate code such as
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%ptr = OpTypePointer StorageBuffer %uint
%SSBO = OpTypeStruct %uint
%SSBO_ptr = OpTypePointer StorageBuffer %SSBO
%variable = OpVariable %SSBO_ptr StorageBuffer
%ac = OpAccessChain %ptr %variable %uint_0
%load = OpLoad %uint %ac
OpStore %ac %uint_0Notice that OpLoad and OpStore both have two operands, and both can figure out the type is %uint. This is the core motivation of this extension, to remove the need for having to declare the %uint type here in the Pointer operand.
With untyped pointer, we still use the access chain to get the StorageClass, but we can now remove the redundancy by first turning the pointer into an untyped pointer.
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
- %ptr = OpTypePointer StorageBuffer %uint
+ %ptr = OpTypeUntypedPointerKHR StorageBuffer
%SSBO = OpTypeStruct %uint
%SSBO_ptr = OpTypePointer StorageBuffer %SSBO
%variable = OpVariable %SSBO_ptr StorageBuffer
%ac = OpAccessChain %ptr %variable %uint_0
%load = OpLoad %uint %ac
OpStore %ac %uint_0But this is still invalid! We now need a matching untyped access chain as well where we now have a new Base Type operand.
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%ptr = OpTypeUntypedPointerKHR StorageBuffer
%SSBO = OpTypeStruct %uint
%SSBO_ptr = OpTypePointer StorageBuffer %SSBO
%variable = OpVariable %SSBO_ptr StorageBuffer
- %ac = OpAccessChain %ptr %variable %uint_0
+ %ac = OpUntypedAccessChainKHR %ptr %SSBO %variable %uint_0
%load = OpLoad %uint %ac
OpStore %ac %uint_0From here, we can now go an extra step and turn the OpVariable into an untyped variable, as it isn't required to be tied to a pointer anymore.
This is analogous to taking an ssbo* ptr and making it a void* ptr.
Note, the following is an optional step. To facilitate transitions, untyped pointer opcodes generally support the input pointers being typed or untyped and generate an untyped result.
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%ptr = OpTypeUntypedPointerKHR StorageBuffer
%SSBO = OpTypeStruct %uint
-%SSBO_ptr = OpTypePointer StorageBuffer %SSBO
-%variable = OpVariable %SSBO_ptr StorageBuffer
+%variable = OpUntypedVariableKHR %ptr StorageBuffer %SSBO
%ac = OpUntypedAccessChainKHR %ptr %SSBO %variable %uint_0
%load = OpLoad %uint %ac
OpStore %ac %uint_0And with that, we are done and have now converted the original SPIR-V to use untyped pointers and still have the same semantic meaning.
Final SPIR-V https://godbolt.org/z/1n9bffq4d
Since the above example has all the indexes with the constant value of zero, we can actually go another step further. The %uint_0 was only necessary prior with typed pointers to satisfy type rules. With untyped pointers, we can directly point to the variable
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%ptr = OpTypeUntypedPointerKHR StorageBuffer
%SSBO = OpTypeStruct %uint
%variable = OpUntypedVariableKHR %ptr StorageBuffer %SSBO
- %ac = OpUntypedAccessChainKHR %ptr %SSBO %variable %uint_0
-%load = OpLoad %uint %ac
- OpStore %ac %uint_0
+%load = OpLoad %uint %variable
+ OpStore %variable %uint_0Adjusted final SPIR-V https://godbolt.org/z/47nqsG7Wc
This applies to any amount of trailing zero indices, even if there are non-zero preceding indices.
For something like:
struct S {
int x;
int a[4][4][4];
};
S s;The following accesses are all at the same offset and, thus, equivalent access chains:
s.a[0]
s.a[0][0]
s.a[0][0][0]
What people parsing SPIR-V need to care about is where, by accident, they are getting the type from the OpTypePointer.
For OpLoad, use the Result Type operand instead of the Pointer operand. This applies to all memory loads (OpCooperativeMatrixLoadKHR, OpAtomicIAdd, etc)
For OpStore, use the type of the value being stored (e.g. type of Object operand) instead of the Pointer operand. This applies to all memory stores (OpCooperativeMatrixStoreKHR, OpAtomicStore, etc)
For OpCopyMemory, either the Source or Target operand will contain a OpTypePointer still.
For OpCopyMemorySized, both the Source and Target could be an untyped pointer, in this case, it interprets the data as an array of 8-bit integers.
For OpVariable, there is an optional Data Type in the OpUntypedVariableKHR to check. If you are using Shader (Vulkan) then this is required to be there.
If you are trying to get the OpTypePointer from the OpAccessChain, the fix will depend on what your goal is. The OpUntypedAccessChainKHR has a Base Type that instructs how the indexes are interpreted. If the OpAccessChain is only used from a memory instruction (OpLoad, OpStore, etc) and as listed above, the type can be found from there already.