COMP5822M – Exercise 1.2 Vulkan Triangle
1 Project Updates 2
2 Labutils 2 2.1VulkanContext….. 2
2.2 Exception type: Error . 3
Copyright By PowCoder代写 加微信 powcoder
2.3 Object wrappers . . . . . 3
2.4 Vulkan utilities . . . . . 4
3 Setup 4 3.1 RenderPass ……. 4
3.2 Graphics Pipeline . . . . 7 3.3 Resources……… 13 3.4 Command objects . . . . 17
4 Command execution 18 4.1 Recording …….. 18 4.2 Submission…….. 20
5 Output 21
A GLSL compiler: glslc 24
In this exercise, you will set up a minimal graphics pipeline and use it to render a very simple scene – a single triangle. The resulting program runs windowless and could thus easily be used on a machine without a display (e.g., headless servers). Unlike OpenGL, Vulkan explicitly supports this kind of operation.
Aside from setting up a simple graphics pipeline (VkPipeline & VkPipelineLayout), you will need set up a number of objects:
• a VkRenderPass with a single subpass that outputs into a single color attachment
• a VKImage and the associated VkImageView that serves as the color attachment into which we render • a VkFramebuffer that references the color attachment image
• a VkBuffer for downloading the rendered image contents
• a VkCommandPool such that we can allocate a VkCommandBuffer to record Vulkan commands into
• a VkFence which is used to have the host code (our C++ program) wait until rendering has finished
To set up the graphics pipeline, you will need to provide the vertex and fragment shader for the pipeline. Exercise 2 therefore takes a brief look at shaders, compiling shaders to SPIR-V code, and at loading SPIR-V shader code at runtime.
With all objects ready, you will record the necessary commands into a command buffer and schedule it for execution. In order to be able to inspect the results, Exercise 2 will write the results to a PNG image on disk.o
The exercise will introduce a few helper classes. However, these are intentionally kept quite simple, with the goal to remain as close to the “standard” Vulkan API as possible. However, the helper classes reduce code complexity quite significantly, particularly in regard to cleanup and error handling.
COMP5822M – Exercise 1.2 2
We want to avoid to build generic abstractions around Vulkan anyway, as this tends to be less useful (but still a lot of work). Instead, when building abstractions, focus on your use case and your data. Build the abstractions around these instead.
1 Project Updates
Download and unpack the exercise sources. Inspect the project. Exercise 2 will require you to work with the code in exercise2 and with the utility functions in labutils. In addition to the C++ sources, the exercise2 directory contains a shaders subdirectory. Shaders (extensions .vert and .frag) in that direc- tory will be automatically compiled to SPIR-V code when the program is built (see the exercise2-shaders subproject defined in the top-level premake5.lua.
Compiled SPIR-V code is placed in the assets/exercise2/shaders directory, with an added .spv exten- sion. For example, when the GLSL source exercise2/shaders/triangle.vert is compiled, it produces the SPIR-V output assets/exercise2/shaders/triangle.vert.spv.
Exercise 2 introduces a few new third party components:
shaderc The exercises include a copy of the glslc compiler for Windows and Linux (x86 64 only). The glslc compiler is shipped with the Vulkan SDK, so you can find it there for other platforms. See Ap- pendix A for more information.
stb Copies of select single header libraries by . In particular, Exercise 2 includes stb image.h and stb image write.h. We use the latter to write the output image. The former is for loading images and will be used in later exercises.
You will also notice a new util containing a glslc.lua file. The Premake instructions to compile GLSL shaders with the included glslc compiler are listed in this file. If you are on a platform different from Windows or Linux, you will need to update this file with a few lines for your platform – see Appendix A. Further, the setup has only been tested with the gmake2 and vs20xx Premake actions, and as such, there is no guarantee that they will work with other build systems!
Try building the project. The provided shaders (triangle.{vert,frag}) do not do anything at the mo- ment, but compiling them will still create the corresponding SPIR-V files (triangle.{vert,frag}.spv) in assets/exercise2/shaders. Verify that this is indeed the case.
2 Labutils
Before we dive into the Vulkan code, we will briefly inspect a few utilities defined in the labutils subpro- ject. The last exercise concluded by indicating that we would want to improve (and automate) Vulkan object cleanup and error handling.
Inspect the labutils subproject/directory. You will find the familiar to string.{hpp,cpp} pair of sources. But there are also a few new files:
• error.{hpp,cpp}
• vkutil.{hpp,cpp}
• vkobject.{hpp,cpp,inl}
• vulkan context.{hpp,cpp}
2.1 Vulkan Context
Study the files vulkan context.{hpp,cpp}. The header declares the VulkanContext class, which holds the Vulkan objects that were introduced in Exercise 1:
• a Vulkan instance (VkInstance)
• the selected physical device (VkPhysicalDevice)
• a logical device instance (VkDevice)
• a graphics queue (VkQueue) and the index of the queue family from which the queue was created • a debug messenger handle (VkDebugUtilsMessengerEXT)
Review Exercise 1 if you need a refresher on these objects. A VulkanContext object owns the Vulkan objects and is therefore responsible for destroying these when the VulkanContext goes out of scope (see destructor of the VulkanContext).
COMP5822M – Exercise 1.2 3
The VulkanContext class is made non-copyable, but is movable. Hence, it is not possible to create copies of a VulkanContext object, but one can transfer ownership of its contents to a different instance. This is imple- mented by deleting the copy constructor and the copy assignment operator, and defining a move constructor and a move assignment operator.
The function make_vulkan_context() performs the necessary setup as described in Exercise 1, and returns a VulkanContext instance with the created objects. Looking at the implementation in vulkan context.cpp, you will find that much of the code matches the code from Exercise 1. The main difference is that errors are signalled by throwing an exception of type labutils::Error instead of a chain of return values.
2.2 Exception type: Error
The exception type labutils::Error is defined by error.{hpp,cpp}. It inherits from the standard excep- tion type std::exception.
With labutils::Error we can construct an error message with std::printf()-like formatting when throw- ing it. This is slightly less convenient with the standard C++ exception types; labutils::Error exists entirely to make producing useful error messages slightly more convenient.
Generating an exception thus looks like (for example)
throw lut::Error( “Unable to create Vulkan instance\n”
“vkCreateInstance() returned %s”, lut::to_string(res).c_str()
Exercise 2 catches exceptions with a function try block in main(). The catch statement simply prints the error
message from the exception and causes the program to exit with an error code.
If an exception is thrown, it will propagate to main(), where it is caught. While propagating up the call stack, any objects with automatic storage will be destroyed (i.e., their destructors are called). By wrapping Vulkan handles into a class with a destructor, we thus ensure that the corresponding Vulkan objects are always destroyed appropriately.
2.3 Object wrappers
The VulkanContext object wraps the Vulkan objects that we have familiarized ourselves with in Exercise 1 in a C++ class, and thereby makes sure that these Vulkan objects are cleaned up properly. However, in Exercise 2 (and in the following exercises), additional Vulkan objects are introduced. We therefore need a strategy to deal with these.
We could try a similar approach as with the VulkanContext: identify reasonable groups of Vulkan objects and wrap these into a C++ class. However, we are not yet familiar with these objects, so we are not quite ready to do so (and in fact, identifying such groupings for general use is quite difficult).
Alternatively, we could create an individual C++ class for each Vulkan object. Such a class would just hold a single Vulkan object handle referring to the Vulkan object that the class owns. (Practically, we also need a handle to the parent Vulkan object to destroy the owned object.) This is the approach taken by some of the automatically generated C++ Vulkan API wrappers. However, for the exercises, we wish to stay with the default C API.
Defining a C++ class for each Vulkan object type that we encounter would quickly become somewhat tedious. Fortunately, C++ offers us a “simple” way out: C++ templates.
In vkobject.hpp, you will find the labutils::UniqueHandle<> template. If you are unfamiliar with tem- plates, think of them as recipes to create ordinary C++ classes. The labutils::UniqueHandle<> template takes three parameters: the Vulkan handle type of the object we wish to wrap, the type of the parent Vulkan object, and a reference to the Vulkan function used to destroy the owned object (technically, a reference to a function pointer, since that is what the Volk library defines).
The reasoning behind making the VulkanContext move only is the fact that the object has owner- ship of the resources contained within it. When the object goes out of scope, it is responsible for destroying the resources it is responsible for. We cannot create copies of the Vulkan resources, hence it is not useful to allow the VulkanContext to be copied. (We could create a copy of the Vulkan han- dles that refer to the Vulkan objects, but then we would no longer know which VulkanContext instance owns the resources and thereby is responsible for destroying them.)
The C++ standard library defines a few move only types. See std::unique_ptr for an example and some additional explanations and examples.
COMP5822M – Exercise 1.2 4
To create a class type that wraps a Vulkan render pass (VkRenderPass), we can use
using RenderPass = UniqueHandle< VkRenderPass, VkDevice, vkDestroyRenderPass >;
The class type RenderPass will hold a VkRenderPass object. Render pass objects belong to a logical device, and therefore we specify VkDevice as the parent object type. Finally, a VkRenderPass is destroyed with the vkDestroyRenderPass function, so we specify this as the final template argument. Looking at the definition of the destructor of UniqueHandle
template< typename tHandle, typename tParent, DestroyFn
UniqueHandle
if( VK_NULL_HANDLE != handle )
assert( VK_NULL_HANDLE != mParent );
tDestroyFn( mParent, handle, nullptr );
we can manually do the substitution of the template parameters. We would end up with something equivalent to the following
RenderPass:: ̃RenderPass()
if( VK_NULL_HANDLE != handle )
assert( VK_NULL_HANDLE != mParent );
vkDestroyRenderPass( mParent /* a VkDevice */, handle /* a VkRenderPass */, ▽ ◃ nullptr );
Similar to VulkanContext, the classes created from the UniqueHandle<> template are move only. In the header vkobject.hpp, you can additionally find the declaration for an equivalent RenderPass C++ class if we were to define it without the template.
Exercise 2 will use these light-weight C++ wrappers around most Vulkan objects.
2.4 Vulkan utilities
In Exercise 2 we will write a few functions that will be reused in future exercises. Such functions are to be declared in vkutil.hpp and defined in vkutil.cpp. However, initially, these files are relatively empty, so there is not much to see in them just yet.
The first step is to set up the Vulkan objects and resources necessary for rendering. The bulk of our work in terms of code will end up here. Setup is split into a few steps. We first create the render pass object. The render pass object is necessary to create the graphics pipeline, which also requires the pipeline layout to be defined. We then create the necessary resources: the image and its image view to which we render, and a buffer which we use to transfer the rendered image to the CPU. With the image view and the render pass objects, we can create the framebuffer. The final step is to prepare for command execute. This requires a command buffer, which is allocated from a command pool. We must wait for rendering to finish before accessing the resulting image data. This is done with a fence, which provides the facilities necessary to have the C++ host program wait for GPU processing to finish.
3.1 Render Pass
(Render Pass Attachments — Subpass Definition — Subpass Dependencies — Render Pass Creation)
All rendering in Vulkan takes place in a render pass. The render pass declares the output from the rendering in terms of individual attachments and their formats. A render pass is split into one or more subpasses, each potentially writing data to a subset of the attachments associated with the render pass. The render pass further defines a load and a store operation for each attachment. The load operation specifies via the
VkAttachmentLoadOp enumeration how the contents of an attachment are treated when the render pass
COMP5822M – Exercise 1.2 5
starts. The store operation specifies via the VkAttachmentStoreOp how contents of an attachment are treated at the end of the render pass. With multiple subpasses, some amount of data can be passed between the sub- passes via input attachments, and subsequently, it is necessary to declare dependencies between subpasses upfront. The same mechanism, i.e., subpass dependencies, may be used to synchronize rendering with exter- nal operations that happen before and after the render pass.
In Exercise 2, the render pass is fairly simple. We have a single attachment: the color image to which we render our results. All rendering takes place in a single subpass, which further simplifies the setup.
Since this is the only rendering that will take place in Exercise 2, we must ensure that the target image is initialized properly. In this case, we want to clear the image to a specific background color. We can do so by specifying the VK_ATTACHMENT_LOAD_OP_CLEAR load operation. Furthermore, we do want to store the rendered results in the target image, and therefore specify the VK_ATTACHMENT_STORE_OP_STORE store operation – this ensures that the rendered color values are stored into the target color attachment at the end of the render pass.
Vulkan image resources have a layout. Inside of a render pass, the image layout can change multiple times, as the image is potentially used in different ways. Therefore the render pass requires declaring multiple image layouts for each attachment that the render pass uses:
• an initial layout, i.e., the layout in which the image of a certain attachment is expected to be in when the render pass starts
• a layout for each subpass. Vulkan will automatically transition the image to the specified layout when the corresponding subpass starts.
• a final layout. Vulkan will transition the image to the specified layout when the render pass ends.
When we first create an image, it is in the VK_IMAGE_LAYOUT_UNDEFINED layout. We do not do anything with the image before rendering, so the image will be in this layout when the render pass starts. Hence we specify it as the initial layout for the render pass. When rendering to a color attachment, the image is required to be in the VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL layout. We specify this as the attachment’s layout in our only subpass. In order to transfer (copy) data out of the image to a host visible buffer, the image must be in the VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL layout. We specify this as the final layout of the render pass, such that our color image is automatically transitioned to this layout when the render pass ends.
Vulkan has very little implicit synchronization. In fact, we need to instruct Vulkan that the copy operation transferring the rendered image’s contents to the host visible buffer cannot commence until after rendering in the render pass has finished. This is done with a subpass dependency that declares the destination subpass to be external (VK_SUBPASS_EXTERNAL). The constant VK_SUBPASS_EXTERNAL refers to operations that happen outside to the render pass.
Render Pass Attachments The render pass object ( VkRenderPass) is created in the create_render_pass function in main.cpp. First we must declare the properties of the attachments that the render pass uses. These are described with the VkAttachmentDescription structure. Each attachment used by the render pass requires a separate instance of this structure; however, in this case we require only one attachment, and therefore only a single copy of the structure:
Even with dynamic rendering (introduced into core Vulkan with version 1.3), rendering still takes place in a “render pass”. The dynamic rendering functionality simply enables bypassing the cre- ation of the VkRenderPass and VkFramebuffer objects when the full flexibility of these is not re- quired (e.g., single subpass only). Instead, the corresponding information is specified when the “render pass” is started – this is done with vkCmdBeginRendering instead of the traditional vkCmdBeginRenderPass. Many of the same parameters still need to be specified, though.
v// Note: the stencilLoadOp & stencilStoreOp members are left initialized 1 v// to 0 (=DONT CARE). The image format (R8G8B8A8 SRGB) of the color 2 v// attachment does not have a stencil component, so these are ignored 3 v// either way. 4 vVkAttachmentDescription attachments[1]{}; 5
vattachments[0].format
vattachments[0].samples
vattachments[0].loadOp
vattachments[0].storeOp
vattachments[0].initialLayout
vattachments[0].finalLayout
= cfg::kImageFormat; // VK FORMAT R8G8B8A8 SRGB 6 = VK_SAMPLE_COUNT_1_BIT; // no multisampling 7 = VK_ATTACHMENT_LOAD_OP_CLEAR; 8 = VK_ATTACHMENT_STORE_OP_STORE; 9 = VK_IMAGE_LAYOUT_UNDEFINED; 10 = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; 11
COMP5822M – Exercise 1.2 6
Subpass Definition Next, we declare our single subpass using the VkSubpassDescription structure. This subpass uses the attachment above as a color attachment. This is expressed by referencing the attachment via a VkAttachmentReference structure; here we also specify that the attachment image should be transitioned to the COLOR_ATTACHMENT_OPTIMAL layout for this subpass.
vVkAttachmentReference subpassAttachments[1]{}; 1 vsubpassAttachments[0].attachment = 0; // the zero refers to attachments[0] declared earlier. 2 vsubpassAttachments[0].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; 3
4 vVkSubpassDescription subpasses[1]{}; 5 vsubpasses[0].pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; 6 vsubpasses[0].colorAttachmentCount = 1; 7 vsubpasses[0].pColorAttachments = subpassAttachments; 8 9
v// This subpass only uses a single color attachment, and does not use any 10 v// other attachmen types. We can therefore leave many of the members at 11 v// zero/nullptr. If this subpass used a depth attachment (=depth buffer), 12 v// we would specify this via the pDepthStencilAttachment member. 13
v// See the documentation for VkSubpassDescription for other attachment 15 v// types and the use/meaning of those. 16
Supass Dependencies Finally, we use a VkSubpassDependency to introduce a dependency/barrier be- tween the rendering in Subpass 0 and the image copy that takes place afterwards (=VK_SUBPASS_EXTERNAL). This ensures that all rendering completes before the copy takes place.
Render Pass Creation With the declarations in place, we can create our render pass. The create info structure VkRenderPassCreateInfo essentially just references the structures just defined:
vVkSubpassDependency dep
程序代写 CS代考 加微信: powcoder QQ: 1823890830 Email: powcoder@163.com