![]() |
VPP
0.8
A high-level modern C++ API for Vulkan
|
Render graph represents a high-level structure of the rendering engine.
The graph is constructed from building blocks connected by dependency arcs. Two main types of these blocks are:
A process node may consume one or more attachments and produce one or more attachments. An attachment may be produced by one process, and then consumed by another one. The latter process is dependent on the former one. In case of many processes, this forms a dependency graph.
Processes on GPU are being executed in parallel. Dependency graph ensures that the data consumed by a process are already prepared by the producing process. Usually this is not being done on whole image level, but rather the image is divided into smaller segments, which may be as small as single pixel. This enables better parallelism.
Additionally, there can also be two special nodes in ther graph: vpp::Preprocess and vpp::Postprocess. The former one is executed before entire graph and allows for preparation steps. The latter one is executed in the end, after entire graph is finished. It allows for finalization steps.
Instead of an vpp::Attachment node, you can use a vpp::Display node which is just a special kind of attachment associated with a vpp::Surface (on-screen display object).
In order to define a render graph, derive your class from the vpp::RenderGraph base class. Inside your class, put fields of type vpp::Process, vpp::Preprocess, vpp::Postprocess, vpp::Attachment and vpp::Display. These classes are explained on their respective documentation pages.
The most important information stored by vpp::Process, vpp::Preprocess and vpp::Postprocess nodes is the command sequence. The sequence is made of commands performing things like:
You can write a complete rendering program by using these commands. This is being done by means of C++ lambda functions. These functions are registered in corresponding vpp::Process, vpp::Preprocess or vpp::Postprocess node by using the <<
operator. An example of simple render graph and command sequence definition is presented below. For brevity, details concerning objects other than render graph are omitted.
There are a couple of important things about this example.
First of all, as it can be seen clearly, render graphs work together with other objects: pipelines, data blocks, buffers. Commands inside registered sequences can (and in fact must) access other objects. Many of them are actually methods of these objects, intended to be called from inside lambda functions of render graphs. Those methods have usually the cmd
prefix in their names. As lambda functions must access external objects, the most convenient way to initialize render graphs is to do it from the enclosing object, in this case MyRenderer
. The lifetime of accessed objects must not be shorter than the render graph itself. The pattern shown in the example assures it.
Defined command sequence need not to be static (like in the example). You can use any kind of algorithm to generate commands, presumably corresponding to mesh organization in your rendering engine. There may be many draw calls, interleaved by vertex and other buffer binds, ane even pipeline changes. Lambda functions give you the full flexibility.
Another question is, what is the vpp::RenderPass object and what it does. The vpp::RenderPass is really a compiled form of render graph. A render graph is abstract representation existing on VPP level only. A render pass is the actual Vulkan object, compiled from the render graph for specified device.
That is, a render pass is constructed from the render graph and device as in the example below.
Some VPP operations require abstract form (vpp::RenderGraph) and some others take compiled form (vpp::RenderPass). One notable example of the latter is registering rendering pipelines for render graph processes. This is being done by means of vpp::RenderPass::addPipeline() method and requires a compiled form of a pipeline (vpp::PipelineLayout) as well.
The following considerations are more advanced and you can skip them on first read (esp. if you are not familiar with core Vulkan).
A question that might arise is where these lambda functions are called from. Directly this is done by the vpp::CommandBufferRecorder class, which takes a vpp::RenderPass object (among other parameters), gets source render graph for it, gather all registered lambdas and record them into supplied Vulkan command buffer.
You can use vpp::CommandBufferRecorder directly, but this is considered advanced usage. A simple way is the vpp::RenderManager class, which just have simple render() method that hides all these details. The method first calls vpp::CommandBufferRecorder for a command buffer, and then sends the buffer to the device for execution.
Lambda functions provide something we call implicit context. As it can be seen in the example, you do not provide any reference to Vulkan command buffer inside the lambdas – although core Vulkan requires supplying that parameter. This is because the commands can detect that they are being called from lambdas and obtain appropriate command buffer reference from calling vpp::CommandBufferRecorder object. This mechanism simplifies syntax and reduces amount of possible errors.
How VPP abstractions used in the example maps to core Vulkan concepts and objects? Here is the translation table:
VkRenderPass
.vkCmdBeginRenderPass
and vkCmdEndRenderPass
.vkCmdBeginRenderPass
.vkCmdEndRenderPass
.