I have done some work on spirv-tools since the posts about the human friendly SPIR-V representation and Python API, so it makes sense to do a status update now.
Most of the development has been to actually make the code work. And I think it does now. For example, one of my tests is to disassemble all the shaders from the Mesa shader-db (containing shaders from some old games), assemble the result, and verifying that the binaries are identical.
Most of the development has been to actually make the code work. And I think it does now. For example, one of my tests is to disassemble all the shaders from the Mesa shader-db (containing shaders from some old games), assemble the result, and verifying that the binaries are identical.
API
IR improvements
The major user-visible change in the API is that the IR has been modified so that
I'm mostly working on the optimizations just to verify that the API makes sense, and some of the passes (constprop and instcombine) are essentially placeholders right now, but I will finish up the code when the final SPIR-V specification is released.
- An ID is represented by an ID object. The
ir.Id
class contains a reference to the instruction defining the ID, so there is no need to usemodule.id_to_inst[id]
each time you want to get the instruction (which you usually want each time you have an ID). The instruction is now accessed asid.inst
. - A literal number is represented as an integer.
- A literal string is represented as a string.
- An enumerated value is represented as a string of the enumeration name. I choose this representation in order to make it easy to see what the code does. For example
if inst.operands[0] == 'FPFastMathMode':
checks if a decoration is specifying the floating-point fast math flags. - A mask is represented as a list of strings of the enumeration names, and the empty list is used when no value is set in the mask. Checking if a bit is set is done as
if 'NotNaN' in inst.operands[1]:
Optimizations
I have also added optimization passes corresponding to the LLVM instcombine, constprop, die (Dead Instruction Elimination), and simplifycfg passes. And a mem2reg pass will be available soon.I'm mostly working on the optimizations just to verify that the API makes sense, and some of the passes (constprop and instcombine) are essentially placeholders right now, but I will finish up the code when the final SPIR-V specification is released.
Plans
My short-term plan for the API:- Make this a real Python package, installable with
pip
. And make it work for Python 3.x too. - Add a mem2reg pass (including infrastructure for simple data flow analysis).
- Implement a better API for handling constants, types, and global instructions.
- Clean up the API. There are some things that need to be renamed and tweaked a little bit (for example some functions having "uses" in their name treat decorations as a usage, and some does not).
- Document the API. Add examples/tutorials.
Assembler / disassembler
The biggest user-visible change in the assembler/disassembler is that global symbols now use normal ID tokens (such as
The assembler/disassembler are mostly done, but two things needs to be implemented:
%main
) instead of prefixing the name with @
(such as @main
). The original implementation used @
in order to simplify parsing of a more convenient syntax for declaring global variables
@gl_FragColor = Output <4 x f32> BuiltIn(FragColor)but decorations are appended to normal instructions, so this is not much more convenient than using an
OpVariable
instruction
%gl_FragColor = OpVariable %44 BuiltIn(FragColor) OutputThe only real difference is that the type must be specified as a pointer type for
OpVariable
, so it is not pretty-printed (The reason is that the pointer type contains a storage class, and I have not found a good way to include it in a pretty-printed type. The storage class is an operand to OpVariable
too, so this could be written as
%gl_FragColor = OpVariable *<4 x f32> BuiltIn(FragColor) Outputif the assembler is updated to infer the storage class from the instruction. But I'm not sure if that is a good idea or not...).
The assembler/disassembler are mostly done, but two things needs to be implemented:
- Floating-point literals
- Correct name handling
Floating-point literals
The assembler is supposed to allow%52 = OpFSub <4 x f32> %49, (0.5, 0.5, 0.5, 0.5)instead of
%50 = OpConstant f32 0x3f000000 %51 = OpConstantComposite <4 x f32> %50, %50, %50, %50 %52 = OpFSub <4 x f32> %49, %51but the current implementation does only handle integer/Boolean literals.
Name handling
The assembler accepts named IDs (such as%main
) and the disassembler is using the names from OpName
decorations to create named IDs. But there are some problems with the implementation:- Name mangling need to be implemented in order to handle polymorphism (the disassembler currently use the numerical value for IDs if it finds several identical names in the binary, and the assembler returns errors for re-declared names).
- ID names declared within a function should be local to the function.
- How should the tools handle multiple names for the same ID? For example, if the name in
OpEntryPoint
differs from the name in anOpName
debug instruction for the function. Or if one instruction is decorated with multipleOpName
names.
Minor tweaks
SPIR-V spreads out some information over the file (e.g. decorations are placed in the beginning of the file, far from the instruction they refer to), and the goal of the textual representation is to collect it in a way that makes it is easy to see all relevant details affecting each instruction, as well as supressing irrelevant details. But it is a bit unclear how to make this as useful as possible...
Some examples of things to consider:
Some examples of things to consider:
- Is it better to be consistent or compact? Decorations are written after the instruction name, and a
FPFastMathMode
decoration is currently written as%52 = OpFSub <4 x f32> FPFastMathMode(NotNaN | NotInf) %49, %50
The values are unique, so it could be written more compact, without theFPFastMathMode
keyword%52 = OpFSub <4 x f32> NotNaN | NotInf %49, %50
But there may exist better ways of improving this... - Pointers are heavily used in Kernels, so it makes sense to pretty-print them. But how should the storage class be handled?
- Should structures be pretty printed?
- Are
OpLoopMerge
andOpSelectionMerge
instructions necessary, or should the assembler insert them automatically when needed?
Plans
My short-term plan for the assembler/disassembler:- Implement floating-point literals
- Document the assembler syntax