COMP5822M – Exercise 1.3 Vulkan Window
1 Project Updates 1
2 GLFWSetup 2
3 Swapchain Setup 6
Copyright By PowCoder代写 加微信 powcoder
4 Handling User Input 13
5 MainLoop 13
Exercise 3 takes the renderer from Exercise 2 and updates it to render into a window. For this purpose, the exercise utilizes GLFW, a cross-platform library that abstracts platform-specific window-system interaction into a platform independent interface.
GLFW implements the platform specific window creation. It uses the Vulkan Window System Integration (WSI) extensions to create a VkSurfaceKHR. Using the VkSurfaceKHR, our application can select a device that can render to the surface/window, create a swapchain with images, and set up the graphics pipeline to render to the swapchain images.
Much of the setup from Exercise 2 remains the same. One of the major changes relates to the target images: there are now have multiple different images that will be used in turn. Each frame, we are provided with one specific target image and expected to render to this. The image format is determined by the swapchain.
To provide a general and extensible solution, Exercise 3 will record rendering commands each frame (even though the commands do not change just yet). To avoid asynchronous execution hazards, this requires the use of multiple command buffers and extended synchronization.
1 Project Updates
Grab the exercise code and inspect it. You will be working with the code in the exercise3 directory and with some of the code in labutils project. Exercise 3 builds on Exercise 2. You will want to transfer some of your solutions from Exercise 2 to Exercise 3:
• Transfer your implementations from labutils/vkutils.cpp from Exercise 2. Do not overwrite the vkutils.hpp, as it contains a few new prototypes. Note that some of the function declarations have changed, so if you copy over your old vkutils.cpp, make sure to update the function definitions with the additional parameters. The additional parameters will be unused for the moment; the exercise has instructions on how they are used later.
• Transfer your shaders (exercise2/shaders/triangle.{vert,frag}) to Exericse 3.
• Lookatthelocalfunctionsdefinedinexercise3/main.cpp.Donottransferanycodeyet–theexercise
will instruct you when to do this.
COMP5822M – Exercise 1.3 2
Exercise 3 will change some of the functions defined in vkutils.{hpp,cpp}. The updates are such that they remain source compatible with Exercise 2. You can copy your solution for Exercise 2 into the exercise2 subdirectory of the Exercise 3 project. It will be built alongside Exercise 3’s main project in exercise3.
Third Party Software As indicated in the introductory text, Exercise 3 uses GLFW. You can find it in the third party directory, along with the third party software from previous exercises. Exercise 3 builds GLFW from source.
Labutils You will find a few new files in labutils:
vulkan window.{hpp,cpp} Defines the VulkanWindow class that extends the VulkanContext with addi-
tions necessary to render to a GLFW window. Section 2 will introduce the VulkanWindow in detail.
context helpers.{hxx,cpp} To be able to reuse some of the functions related to Vulkan initialization that were previously (Exercise 2) defined locally in vulkan context.cpp, the relevant functions have been broken out into the context helpers.{hxx,cpp} header-source pair.
The GLFW build has been tested under Windows/Visual Studio and Linux/make & GCC. There are some MacOS-specific build instructions (see “x-glfw” project in third party/premake5.lua), but these are untested.
The functions defined in context helpers.{hxx,cpp} are intended to be used only to help im- plement the functionality in vulkan {context,window}.cpp in labutils. As such, I consider these to be internal to the labutils project, and do not intended them to be used outside of it. I indicate this in two ways: all the functions defined in the internal headers are defined in a nested detail namespace, and I give headers that only define internal functions a .hxx extensions instead of the more common .hpp one. The former (detail namespace) is fairly common practice, whereas the latter (.hxx extension) is more of a personal convention (although I have seen it used in other projects as well).
2 GLFW Setup
(GLFW Initialization — Vulkan Instance — GLFW Window — Device Selection — Vulkan Device)
Study the vulkan window.hpp header. It declares the VulkanWindow class, which inherits from Exercise 2’s VulkanContext class. VulkanWindow extends the VulkanContext with a few additional members:
GLFWwindow*: GLFW window handle.
VkSurfaceKHR: surface handle, that is a platform-independent abstraction from native platform win- dow or surface objects. The term surface is frequently used in this context to refer to a block of pixels that we can draw to (such as the block of pixels that define a window’s contents).
A second queue – the presentQueue and its associated queue family index (presentFamilyIndex).
VkSwapchainKHR and the associated VkImages and their image views (VkImageView).
Information about the swapchain, specifically its format (VkFormat) and size in pixels (VkExtent2D)
The header further declares two functions. The first one, make_vulkan_window, is analogous to Exercise 2’s make_vulkan_context function, and is used to create a default window and the associated Vulkan resources. The second one, recreate_swapchain, is used to the re-create the swapchain. A swapchain may become outdated and no longer usable, e.g., when the underlying window changes size or shape. In this case, Vulkan requires that the swapchain to be re-created, taking the changed properties into account.
Swapchain creation and re-creation is dealt with in Sections 3 and 5. First, we will focus on setting up a window with GLFW, and implement the necessary changes to instance and device creation.
GLFW Initialization The GLFW library requires special initialization to take place, performed with the glfwInit function. GLFW allows some options to be set ahead of initializations (via glfwInitHint), but
Exercise 3 does not require this.
As we intend to use Vulkan with GLFW, we should check that GLFW supports Vulkan. GLFW tries to use the Vulkan loader independently from the Volk library (which GLFW does not know about), so the check is necessary to ensure that GLFW has been able to locate and use the system’s Vulkan loader.
COMP5822M – Exercise 1.3 3
In make_vulkan_window (labutils/vulkan window.cpp) locate the //TODO: initialize GLFW line and add the following code:
v// Initialize GLFW and make sure this GLFW supports Vulkan. 1 v// Note: this assumes that we will not create multiple windows that 2 v// exist concurrently. If multiple windows are to be used, the 3 v// glfwInit() and the glfwTerminate() (see destructor) calls should be 4 v// moved elsewhere. 5 vif( GLFW_TRUE != glfwInit() ) 6 v{ 7 v vchar const* errMsg = nullptr; 8 v vglfwGetError( &errMsg ); 9
10 v vthrow lut::Error( “GLFW initialization failed: %s”, errMsg ); 11 v} 12 v 13 vif( !glfwVulkanSupported() ) 14 v{ 15 v vthrow lut::Error( “GLFW: Vulkan not supported.” ); 16 v} 17
This can take place before or after the initialization of Volk (via volkInitialize); choose whichever you prefer.
Vulkan Instance Creation Rendering to a window surface requires platform-specific instance extensions. These extensions link the platform-specific window handle types to a platform-independent VkSurfaceKHR handle. While GLFW deals with the exact setup that this requires, we need to make sure that the necessary extensions are enabled in the Vulkan instance. To facilitate this, GLFW provides a list of extensions it will require, via the glfwGetRequiredInstanceExtensions function.
The following code will check that the required extensions are available and request them to be enabled during instance creation.
After calling glfwInit, we should call glfwTerminate when we are done with GLFW. Right now, the destructor of VulkanWindow does this when it destroys the GLFW window. Technically, this is not entirely correct, as there are some conditions in which GLFW does not get unloaded properly, and it also prevents us from using more than one VulkanWindow concurrently. None of the exercises will do so, and this keeps the code a bit simpler.
v// GLFW may require a number of instance extensions 1 v// GLFW returns a bunch of pointers-to-strings; however, GLFW manages 2 v// these internally, so we must not free them ourselves. GLFW 3 v// guarantees that the strings remain valid until GLFW terminates. 4 vstd::uint32_t reqExtCount = 0; 5 vchar const** requiredExt = glfwGetRequiredInstanceExtensions( &reqExtCount ); 6
7 vfor( std::uint32_t i = 0; i < reqExtCount; ++i ) 8
v vif( !supportedExtensions.count( requiredExt[i] ) ) 10 v v{ 11 v v vthrow lut::Error( "GLFW/Vulkan: required instance extension %s not supported",▽12
◃ requiredExt[i] );
14 v venabledExensions.emplace_back( requiredExt[i] ); 15
You would expect to see one or more of the following extensions to be required, depending on the platform that you are running on:
• VK KHR surface
• VK KHR xcb surface (for Linux/X11 with the XCB X11 interface)
• VK KHR win32 surface (for Windows with the standard Windows API)
• VK EXT metal surface (for MacOS, for surfaces created for Apple’s Metal framework) • VK MVK macos surface (older extension, now replaced by the above extension)
COMP5822M – Exercise 1.3 4
Aside from requesting the additional instance extensions, the Vulkan instance object, along with the optional debug messenger object, is created as before. Once we have the instance, we can proceed with the window creation.
GLFW Window Creation GLFW windows are created with the glfwCreateWindow function. GLFW was originally designed with OpenGL in mind, so by default GLFW will perform all the necessary OpenGL setup, including the creation of an OpenGL rendering context. For Vulkan, no OpenGL context is desired. We indicate this to GLFW via the GLFW_CLIENT_API window hint.
Once we have created the window, we need to get the VkSurfaceKHR handle corresponding to the window. GLFW handles this for us with the glfwCreateWindowSurface function. If you are interested in the under- lying platform-specific mechanism, check the documentation of the platform extensions above. These mainly provide Vulkan functions to take the platform’s native window handle (HWND, xcb_window_t, ...) and create a generic VkSurfaceKHR object from it.
Putting the above into code:
Technically, the window creation can take place before creating the Vulkan instance. However, my thinking is to delay the window creation as long as possible. Creating a window is noticeable to a user, especially if the window is created in a visible state. Delaying the window creation reduces the number of code paths where we create a window and then immediately tear it down due to an error.
v// Create GLFW Window and the Vulkan surface 1 vglfwWindowHint( GLFW_CLIENT_API, GLFW_NO_API ); 2 3 vret.window = glfwCreateWindow( 1280, 720, "Exercise 3", nullptr, nullptr ); 4 vif( !ret.window ) 5 v{ 6 v vchar const* errMsg = nullptr; 7 v vglfwGetError( &errMsg ); 8 9
v vthrow lut::Error( "Unable to create GLFW window\n" 10 v v v"Last error = %s", errMsg 11 v v); 12 v} 13
14 vif( auto const res = glfwCreateWindowSurface( ret.instance, ret.window, nullptr, ▽15
◃ &ret.surface ); VK_SUCCESS != res )
v{ 16 v vthrow lut::Error( "Unable to create VkSurfaceKHR\n" 17 v v v"glfwCreateWindowSurface() returned %s", lut::to_string(res).c_str() 18 v v); 19 v} 20
Vulkan Device Selection The underlying logic for selecting devices remains unchanged from Exercise 1. We will however need to take a few additional factors into account. The device must support the neces- sary device extensions, specifically VK KHR swapchain. Further, it must support rendering to the provided VkSurfaceKHR with at least one queue family. At this point, we can also check that there is at least one queue family that supports graphics commands.
To facilitate the latter two checks, Exercise 3 introduces the find_queue_family function, which general- izes the old find_graphics_queue_family function. It takes two additional parameters. The first one, of type VkQueueFlags, lets the caller specify which queue flags must be set. The second argument, of type VkSurfaceKHR, indicates that the queue family must also support presenting to the specified surface. (How- ever, if the second argument is set to VK_NULL_HANDLE, we ignore it.).
The second check requires the vkGetPhysicalDeviceSurfaceSupportKHR function that is also part of the VK KHR surface extension.
It is not guaranteed that any graphics queue family can present with the given surface – presenting might be handled by a different queue family. There does not seem to be any device, especially when looking at desktop computers, where the graphics cannot also do the presentation. Neverthe- less, this is not guaranteed to be the case by the Vulkan specification, so if we are aiming to write a portable and correct Vulkan program, we should not assume such.
COMP5822M – Exercise 1.3 5
Implement find_queue_famaily:
vstd::uint32_t numQueues = 0; 1 vvkGetPhysicalDeviceQueueFamilyProperties( aPhysicalDev, &numQueues, nullptr ); 2 3 vstd::vector
vfor( std::uint32_t i = 0; i < numQueues; ++i ) 7
v vauto const& family = families[i]; 9
10 v vif( aQueueFlags == (aQueueFlags & family.queueFlags) ) 11 v v{ 12 v v vif( VK_NULL_HANDLE == aSurface ) 13 v v v vreturn i; 14 15 v v vVkBool32 supported = VK_FALSE; 16 v v vauto const res = vkGetPhysicalDeviceSurfaceSupportKHR( aPhysicalDev, i, ▽ 17
◃ aSurface, &supported );
18 v v vif( VK_SUCCESS == res && supported ) 19 v v v vreturn i; 20 v v} 21
We can now implement the score_device function (which has already been updated to additionally take a VkSurfaceKHR parameter):
v// Check that the device supports the VK KHR swapchain extension 1 vauto const exts = lut::detail::get_device_extensions( aPhysicalDev ); 2 3 vif( !exts.count( VK_KHR_SWAPCHAIN_EXTENSION_NAME ) ) 4
v vstd::fprintf( stderr, "Info: Discarding device ’%s’: extension %s missing\n", ▽ 6
◃ props.deviceName, VK_KHR_SWAPCHAIN_EXTENSION_NAME );
v vreturn -1.f; 7
9 v// Ensure there is a queue family that can present to the given surface 10 vif( !find_queue_family( aPhysicalDev, 0, aSurface ) ) 11
v vstd::fprintf( stderr, "Info: Discarding device ’%s’: can’t present to surface\n",▽13
◃ props.deviceName );
v vreturn -1.f; 14
16 v// Also ensure there is a queue family that supports graphics commands 17 vif( !find_queue_family( aPhysicalDev, VK_QUEUE_GRAPHICS_BIT ) ) 18
v vstd::fprintf( stderr, "Info: Discarding device ’%s’: no graphics queue family\n",▽20
◃ props.deviceName );
v vreturn -1.f; 21
The score_device function has additionally been updated to print the reason for discarding de- vices that do not fulfill certain conditions. This should be relatively useful if, for some reason, the provided logic discards all possible Vulkan devices.
Vulkan Device Creation With an appropriate physical device selected, we can create the logical device in- stance. Unlike earlier exercises, we need enable some device extensions – specifically, the VK KHR swapchain extension mentioned earlier. Additionally, we might need to create two different queues – one for graphics commands and one for presentation. However, if possible, we will prefer a single queue that supports both operations.
COMP5822M – Exercise 1.3 6
The create_device function has already been updated to take two additional arguments: first, a list of queue families from which to instantiate a queue; second, a list of device extensions to enable. Updating the logic of the function with this should be fairly straight forward at this point, so Exercise 3 provides the complete create_device definition for you. Inspect it briefly.
In make_vulkan_window, request the VK KHR swapchain extension to be enabled: venabledDevExensions.emplace_back( VK_KHR_SWAPCHAIN_EXTENSION_NAME ); 1
Below, implement the logic to select the queues that we wish to instantiate:
vif( auto const index = find_queue_family( ret.physicalDevice, VK_QUEUE_GRAPHICS_BIT,▽1 ◃ ret.surface ) )
v{ 2 v vret.graphicsFamilyIndex = *index; 3 4 v vqueueFamilyIndices.emplace_back( *index ); 5 v} 6 velse 7 v{ 8 v vauto graphics = find_queue_family( ret.physicalDevice, VK_QUEUE_GRAPHICS_BIT ); 9
v vauto present = find_queue_family( ret.physicalDevice, 0, ret.surface ); 10 11 v vassert( graphics && present ); 12 13 v vret.graphicsFamilyIndex = *graphics; 14 v vret.presentFamilyIndex = *present; 15 16 v vqueueFamilyIndices.emplace_back( *graphics ); 17 v vqueueFamilyIndices.emplace_back( *present ); 18 v} 19
As before, we need to retrieve the VkQueue handles from the created device with vkGetDeviceQueue. If two separate queues were required, the second queue handle must be retrieved as well. The code for this is already in place in make_vulkan_window.
The final step in make_vulkan_window relates to swapchain creation. This is described in the following section. 3 Swapchain Setup
When creating the swapchain, we are faced with a number of options. One of the more important options is the image format. We cannot choose the image format freely (i.e., not even from a subset of formats like in Excercise 2), but must rather inquire about supported formats and choose from the provided list. The image format affects creation of several of the objects down the line, such as the render pass (which is an argument to both the framebuffer and the pipeline).
Looking at the final few lines of make_vulkan_window, you will find three function calls before the return statement. The first one is to create_swapchain, with which we will start.
Swapchain Creation To create the swapchain object, we need to decide on a few parameters. The image format was already mentioned. A similar approach is required for the presentation mode, as defined by VkPresentModeKHR. Both the format and the presentation mode require us to enumerate possible values,
and then select one from the returned options.
The vulkan window.cpp source declares two local helper functions for this: get_surface_formats and
get_present_modes. These return a list/set of the formats (
(VkPresentModeKHR), respectively. The functions to enumerate the available choices are:
• vkGetPhysicalDeviceSurfaceFormatsKHR and
• vkGetPhysicalDeviceSurfacePresentModesKHR.
Enumerating these properties follows the pattern already seen in Exercise 1: we call each function twice. The first call (last argument set to nullptr) fills in the number of formats/present modes into the provided std▽ ◃ ::uint32_t. We use the count to allocate a buffer to hold the returned options. The buffer is then passed to the second call, which fills the results into the buffer.
VkSurfaceFormatKHR) and presentation modes
COMP5822M – Exercise 1.3 7
Implement the two functions get_surface_formats and get_present_modes. Note that the second function returns a std::unordered_set, so the entries of the filled-in buffer have to be inserted into a set before return- ing it. If necessary, refer back to (for example) the implementation of get_instance_layers in Exercise 1 for additional guidance.
Looking at create_swapchain, you will see the two above methods called at the beginning of the function. With the information about the available formats and modes in hand, we now have to pick appropriate ones from the possible options. In both cases, the appropriate choice will depend on the application. The method presented here is by no means the only correct one.
The VkSurfaceFormatKHR structure has two members: the VkFormat that we are already familiar with from earlier, a
程序代写 CS代考 加微信: powcoder QQ: 1823890830 Email: powcoder@163.com