![]() |
VPP
0.7
A high-level modern C++ API for Vulkan
|
VPP completely removes the need of using external languages (like GLSL or GLSLang) to write shaders. Just write them in C++. Also it does not need any special support from the compiler. It suffices to have a C++14 compliant compiler.
You write shaders just as regular methods of your vpp::PipelineConfig (or vpp::ComputePipelineConfig) derived subclass. Rendering resources (vertices, textures, buffers, etc.) are accessed just as class fields. You can call other methods from these methods. You can create reusable libraries of shader code. You can use templates, classes, virtual functions, object-oriented code, generic code, anything you want – or almost, as there are some limitations.
To understand what these are, you need to know how the mechanism work. It is actually quite simple.
In order to run code on GPU under Vulkan, you have to supply it in SPIR-V format. SPIR-V is a standard defining a kind of virtual machine, just like Java VM. But this code is not being run by an interpreter (although it could, e.g. for debugging purposes), but rather translated inside a GPU driver to the machine code of the GPU. So this is basically a JIT compilation going on. All these details are hidden, you just need to generate a SPIR-V module and send it to Vulkan.
How to generate a SPIR-V module? This is just some data block in specific format. So VPP can do it without any problems. But how to generate such code from the C++ code, that is an integral part of the process we are running and which generates the SPIR-V? Do we need source code, or special compiler introspection features (like Java)? The answer is: no!
The trick is that we have a C++ regular method which does one thing when being called: it translates ITSELF into SPIR-V. That's all.
This process is correct, because C++ executes code just in the same order as it is written, according to strict rules. There are some ambiguities, e.g. subexpression evaluation order is undefined. But exactly the same situation we have in GLSL/GLSLang. So we can have e.g. an overloaded addition operator, which does just this:
OpFAdd
instruction to SPIR-V output stream.As long as dependencies are satisfied (this process guarantees that), every expression will be correctly translated to SPIR-V.
But what about control constructs, like if
, for
, switch
, etc? There is more complicated work to be done. First of all, we can't use regular C++ keywords. They are not overloadable in C++ so we can't force them to do other things for us than those they were made to. Therefore, VPP introduces its own counterparts starting with capital letters: vpp:If(), vpp::For(), vpp::Switch(), etc.
Now, thanks to some clever planning and bookkeeping, VPP can generate proper SPIR-V control structures from these.
Combining extensive operator overloading with some special functions, VPP can achieve the goal: have a method in C++ being translated into SPIR-V as it is being run.
What are the drawbacks and limitations of the process? There are some, but in practice they are not that severe. for example:
For block control constructs, you must use opening and closing keyword (like in old languages like Algol or Modula-2), for example:
As you can see, the code looks slightly different than regular C++, but not really that different.
static const
variables (e.g. the value of PI), there will be no problem. They will be just regular C++ constants.Int
is immutable and slightly more fancy VInt
is mutable.There are as well some good things, that might not be apparent initially:
if
or switch
to select blocks of code, based on C++ expression which is constant during execution of GPU code. This is easy method to make parameterized shader variants, e.g. to use different lighting equations depending on a parameter. Also regular for
construct might be useful to create unfolded loops with predefined iteration counts.||
) will behave just as expected in SPIR-V.GLCompute
shader model (probably all Vulkan-supporting GPUs).A simple example of coding shaders in VPP:
In general, VPP achieves the level of abstraction and conciseness of the code better than OpenGL, while maintaining high performance of core Vulkan, benefitting from type-safety of C++, and enjoying extreme simplicity and accessibility to new developers.