Vulkan Renderer
Project Description:
This was a project developed to help me understand how to integrate the Vulkan API into my personal C++ engine. In turn, it helped me understand advanced concepts like job systems, memory management and synchronization. My end product was a model viewer with a vulkan wrapper that is similar to our previous OpenGL renderer.
Development Specifications:
- Engine: Personal Engine (C++)
- Development Time: 2 Months
- Rendering API: Vulkan / GLSL
Summary of Work Done
- Created an OpenGL style wrapper for Vulkan.
- Integrated “shaderc”, a shader compiler developed by Google.
- Integrated “spirv-cross”, a shader reflection engine by Khronos Group.
- Current supported features:
- Mesh drawing with vertex and index buffers.
- Materials for specification of textures, uniform buffers, shader.
- GLSL shaders that are compiled run-time.
- Automatic shader reflection to identify uniform slots and create descriptor sets from that.
What I Started With
Initially, to first understand what goes into setting up Vulkan and getting that first colored-triangle on the screen, I made these immediate functions that would first get a triangle on the screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------------------------- class VkRenderer { public: //----------------------------------------------------------------------------------------------- // Constructors/Destructors VkRenderer( const char* appName ); ~VkRenderer(); //----------------------------------------------------------------------------------------------- // Accessors/Mutators //----------------------------------------------------------------------------------------------- // Vulkan Initialization Operations private: bool CheckValidationLayerSupport(); bool CheckExtensionsSupport(); void SetupDebugCallback(); void InitializeVulkanInstance( const char* appName ); bool CheckDeviceExtensionsSupport( VkPhysicalDevice device ); QueueFamilyIndices GetQueueFamilyIndices( VkPhysicalDevice device ); SwapChainDetails GetSwapChainDetails( VkPhysicalDevice device ); bool IsDeviceSuitable( VkPhysicalDevice device ); void CreateSurface(); void PickPhysicalDevice(); void CreateLogicalDevice(); VkSurfaceFormatKHR PickSurfaceFormat( const std::vector<VkSurfaceFormatKHR>& formats ); VkPresentModeKHR PickPresentMode( const std::vector<VkPresentModeKHR>& presentModes ); VkExtent2D PickSwapExtent( const VkSurfaceCapabilitiesKHR& capabilities ); void CreateSwapChain(); void CreateImageViews(); VkShaderModule CreateShaderModule( void* byteCode, size_t size ); void CreateRenderPass(); void CreateGraphicsPipeline(); void CreateFrameBuffers(); void CreateCommandPool(); void CreateCommandBuffers(); void CreateSemaphores(); //----------------------------------------------------------------------------------------------- // Methods public: void PostStartup(); void BeginFrame(); void DrawFrame(); void EndFrame(); //----------------------------------------------------------------------------------------------- // Static methods static VkRenderer* CreateInstance( const char* appName ); static void DestroyInstance(); static VkRenderer* GetInstance(); // Debug callback static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objType, uint64_t obj, size_t location, int32_t code, const char* layerPrefix, const char* msg, void* userData); //----------------------------------------------------------------------------------------------- // Members private: VkDebugReportCallbackEXT m_debugCallback; VkInstance m_vkInstance; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkDevice m_logicalDevice = VK_NULL_HANDLE; VkQueue m_graphicsQueue; VkSurfaceKHR m_surface; VkQueue m_presentQueue; VkSwapchainKHR m_swapChain; VkExtent2D m_swapChainExtent; VkFormat m_swapChainImageFormat; std::vector<VkImage> m_swapChainImages; std::vector<VkImageView> m_swapChainImageViews; VkRenderPass m_renderPass; VkPipelineLayout m_pipelineLayout; VkPipeline m_graphicsPipeline; std::vector<VkFramebuffer> m_framebuffers; VkCommandPool m_commandPool; std::vector<VkCommandBuffer> m_commandBuffers; VkSemaphore m_imageAvailableSemaphore; VkSemaphore m_renderFinishedSemaphore; }; //----------------------------------------------------------------------------------------------- // Standalone functions void VkRenderStartup(); void VkShutdown(); |
What It Became
Once I started understanding the basic pipelines and the inner workings, I began to wrap Vulkan and finally ended with the result below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
//----------------------------------------------------------------------------------------------- class VKRenderer { public: //----------------------------------------------------------------------------------------------- // Constructors/Destructors VKRenderer( const char* appName ); ~VKRenderer(); //----------------------------------------------------------------------------------------------- // Accessors/Mutators VkDevice GetLogicalDevice() const { return m_logicalDevice; } VkPhysicalDevice GetPhysicalDevice() const { return m_physicalDevice; } VKTexture* GetDefaultColorTarget() const { return m_defaultColorTarget; } VKTexture* GetDefaultDepthTarget() const { return m_defaultDepthTarget; } //----------------------------------------------------------------------------------------------- // Vulkan Initialization Operations private: bool CheckValidationLayerSupport(); bool CheckExtensionsSupport(); void SetupDebugCallback(); void InitializeVulkanInstance( const char* appName ); bool CheckDeviceExtensionsSupport( VkPhysicalDevice device ); QueueFamilyIndices GetQueueFamilyIndices( VkPhysicalDevice device ); SwapChainDetails GetSwapChainDetails( VkPhysicalDevice device ); bool IsDeviceSuitable( VkPhysicalDevice device ); void CreateSurface(); void PickPhysicalDevice(); void CreateLogicalDevice(); VkSurfaceFormatKHR PickSurfaceFormat( const std::vector<VkSurfaceFormatKHR>& formats ); VkPresentModeKHR PickPresentMode( const std::vector<VkPresentModeKHR>& presentModes ); VkExtent2D PickSwapExtent( const VkSurfaceCapabilitiesKHR& capabilities ); void CreateSwapChain(); void CreateImageViews(); void CreateCommandPool(); void CreateVertexBuffer(); void CreateIndexBuffer(); void CreateSyncStuff(); void CleanupSwapchain(); void RecreateSwapchain(); //----------------------------------------------------------------------------------------------- // Methods public: void PostStartup(); void BeginFrame(); void EndFrame(); //----------------------------------------------------------------------------------------------- // Draw commands void DrawMeshImmediate(const Vertex_3DPCU* vertices, int numVerts, DrawPrimitiveType mode, const Matrix44& modelMatrix); void DrawMesh( const VKMesh& mesh, const Matrix44& modelMatrix = Matrix44::IDENTITY ); //----------------------------------------------------------------------------------------------- // Mesh functions VKMesh* CreateOrGetMesh( const std::string& path ); void InitializeDefaultMeshes(); //----------------------------------------------------------------------------------------------- // Command Buffer ops VkCommandBuffer BeginTemporaryCommandBuffer(); // Begins a command buffer for temp usage and returns the handle void EndTemporaryCommandBuffer( VkCommandBuffer tempBuffer ); //----------------------------------------------------------------------------------------------- // Texture Ops void CreateAndGetImage( VkImage* out_image, VkDeviceMemory* out_devMem, uint32_t width, uint32_t height, VkFormat format, VkImageUsageFlags usage, VkImageTiling tiling, VkMemoryPropertyFlags props ); VkImageView CreateAndGetImageView( VkImage image, VkFormat format, VkImageAspectFlags aspectFlags ); void TransitionImageLayout( VkImage image, VkImageAspectFlags aspectFlags, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStage, VkPipelineStageFlags dstStage, VkAccessFlags srcMask, VkAccessFlags dstMask ); void CopyBufferToImage( VkBuffer buffer, VkImage image, uint32_t width, uint32_t height ); void CopyImages( VkImage dst, VkImageLayout dstLayout, VkImage src, VkImageLayout srcLayout, VkImageCopy copyInfo, VkSemaphore* waitSemaphore = nullptr, uint32_t waitCount = 0, VkSemaphore* signalSemaphores = nullptr, uint32_t signalCount = 0 ); //----------------------------------------------------------------------------------------------- // Texture Helpers VKTexture* GetDefaultTexture() const { return m_defaultTexture; } void SetTexture( VKTexture* texture ); void SetTexture( unsigned int index, VKTexture* texture, VKTexSampler* sampler = nullptr ); void BindTexture2D( const VKTexture* texture ); void BindTexture2D( unsigned int index, const VKTexture* texture ); VKTexture* CreateOrGetTexture(const std::string& path, bool genMipmaps = true); VKTexture* CreateOrGetTexture(const Image& image, bool genMipmaps = true); bool IsTextureLoaded(const std::string& path) const; void SetDefaultTexture(); void CopyTexture2D( VKTexture* dst, VKTexture* src, VkSemaphore* waitSemaphore = nullptr, uint32_t waitCount = 0, VkSemaphore* signalSemaphores = nullptr, uint32_t signalCount = 0 ); VKTexture* CreateRenderTarget(unsigned int width, unsigned int height, eTextureFormat fmt = TEXTURE_FORMAT_RGBA8); VKTexture* CreateDepthStencilTarget( unsigned int width, unsigned int height ); VKTexture* CreateColorTarget( unsigned int width, unsigned int height ); //----------------------------------------------------------------------------------------------- // Shader functions void SetShader( VKShader* shader = nullptr ); VKShaderProgram* CreateOrGetShaderProgram(const std::string& path, const char* defines = nullptr); void UseShaderProgram(const VKShaderProgram* shaderProgram); void SetDefaultShader(); void BindMeshToProgram( const VKMesh* mesh ); void BindRenderState( RenderState state ); void AlphaBlendFunction(BlendFactor sfactor, BlendFactor dfactor ); void ColorBlendFunction(BlendFactor sfactor, BlendFactor dfactor); void SetDepthTestMode(DepthTestOp mode, bool flag); void DisableDepth(); void SetFillMode(FillMode fillMode); void SetCullMode(CullMode cullMode); //----------------------------------------------------------------------------------------------- // Material Functions void BindMaterial( const VKMaterial* material = nullptr ); void SetMaterial( const VKMaterial* material = nullptr ); VKMaterial* CreateOrGetMaterial( const std::string& path ); void SetDefaultMaterial(); void ResetDefaultMaterial(); //----------------------------------------------------------------------------------------------- // Setting uniforms on shaders void BindUBO( int bindPoint, const VKUniformBuffer* ubo ); void SetUniform( const char* name, float value ); void SetUniform( const char* name, int value ); void SetUniform( const char* name, const Rgba& color ); void SetUniform( const char* name, const Matrix44& matrix, bool transpose = false ); void SetUniform( const char* name, const Vector3& value ); //----------------------------------------------------------------------------------------------- // Camera Functions void SetCamera(VKCamera* cam); //----------------------------------------------------------------------------------------------- // Buffer operations uint32_t FindMemoryType( uint32_t requiredTypes, uint32_t requiredProps ) const; void CreateAndGetBuffer( VkBuffer* out_buffer, VkDeviceMemory* out_deviceMem, VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props ); void CopyBuffers( VkBuffer dstBuffer, VkBuffer srcBuffer, VkDeviceSize byteCount ); //----------------------------------------------------------------------------------------------- // Static methods static VKRenderer* CreateInstance( const char* appName ); static void DestroyInstance(); static VKRenderer* GetInstance(); // Debug callback static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objType, uint64_t obj, size_t location, int32_t code, const char* layerPrefix, const char* msg, void* userData); //----------------------------------------------------------------------------------------------- // Vulkan Members private: uint32_t m_currentFrame = 0; VkDebugReportCallbackEXT m_debugCallback; VkInstance m_vkInstance; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkDevice m_logicalDevice = VK_NULL_HANDLE; VkQueue m_graphicsQueue; VkSurfaceKHR m_surface; VkQueue m_presentQueue; VkSwapchainKHR m_swapChain; VkExtent2D m_swapChainExtent; VkFormat m_swapChainImageFormat; std::vector<VkImage> m_swapChainImages; std::vector<VkImageView> m_swapChainImageViews; VkRenderPass m_renderPass; VkPipelineLayout m_pipelineLayout; VkPipeline m_graphicsPipeline; std::vector<VkFramebuffer> m_framebuffers; VkCommandPool m_commandPool; std::vector<VkCommandBuffer> m_commandBuffers; std::vector<VkSemaphore> m_imageAvailableSemaphore; std::vector<VkSemaphore> m_renderFinishedSemaphore; std::vector<VkSemaphore> m_colorTargetAvailableSemaphore; std::vector<VkFence> m_fences; //----------------------------------------------------------------------------------------------- // Data Members std::map<std::string,VKTexture*> m_loadedTextures; std::map<std::string,VKShaderProgram*> m_loadedShaderPrograms; std::map<std::string,VKMesh*> m_loadedMeshes; std::map<std::string,VKMaterial*> m_loadedMaterials; VKVertexBuffer* m_immediateVBO; VKIndexBuffer* m_immediateIBO; VKTexture* m_immediateTexture = nullptr; VKTexture* m_defaultTexture = nullptr; VkDescriptorSetLayout m_descriptorSetLayout; VkDescriptorPool m_descriptorPool; VKCamera* m_defaultCamera = nullptr; VKCamera* m_defaultPerspectiveCamera = nullptr; VKCamera* m_currentCamera = nullptr; VKTexture* m_defaultColorTarget = nullptr; VKTexture* m_defaultDepthTarget = nullptr; VKShader* m_defaultShader = nullptr; VKMaterial* m_defaultMaterial = nullptr; VKMaterial* m_defaultMaterialShared = nullptr; VKMaterial* m_activeMaterial = nullptr; VKPipeline* m_defaultPipeline = nullptr; VKUniformBuffer* m_testBuffer = nullptr; VKUniformBuffer* m_modelBuffer = nullptr; uint32_t m_swapImageIndex = 0; VkBuffer m_ubo; VkDeviceMemory m_uboMemory; VkDescriptorSet m_descriptorSet; }; //----------------------------------------------------------------------------------------------- // Standalone functions void VkRenderStartup(); void VkShutdown(); VkFormat GetVKDataType( VKRenderType type ); VkFormat GetVkFormat( eTextureFormat format ); VkShaderStageFlagBits GetVKShaderStageFlag( ShaderStageSlot stage ); VkPrimitiveTopology GetVKDrawType( DrawPrimitiveType type ); VkPolygonMode GetVKPolygonMode( FillMode mode ); VkCullModeFlags GetVKCullMode( CullMode mode ); VkFrontFace GetVKWindOrder( WindOrder order ); VkCompareOp GetVKDepthOp( DepthTestOp compare ); VkBlendOp GetVKBlendOp( BlendOp op ); VkBlendFactor GetVKBlendFactor( BlendFactor factor ); |
Postmortem
Easier Than Thought
Harder Than Thought and Pain Points
Hard Takeaways
Soft Takeaways
Easier Than Thought
- Setting up the swap chains.
- Setting up pipeline state (w/o wrapping any functionality).
- Getting the first triangle.
Harder Than Thought and Pain Points
- Vulkan offers a lot of control and thus understanding the architecture and how each piece connects with each other was hard.
- Using shader reflection to describe descriptor sets was initially a challenge when I had a tough time understanding how the pipeline layouts were generated.
- Image layouts, stage masks and access masks were fairly new concepts coming out of an OpenGL background. Getting my head around these was a bit tricky at first.
Hard Takeaways
- Job-Based Systems: This was my first time trying to make my code thread-safe and this proved to be a good learn. Going forward I would move towards making my engine a job based system. It was a bit of a challenge to understand how synchronization objects were used.
- Graphics Pipeline: With the low-level nature of Vulkan, I was able to finally understand how the various commands that I’d previously used in OpenGL would have worked internally.
- System architecture: Understanding how each piece interconnected was key to making a good wrapper for Vulkan. This helped me gain a skill point in system design and architecture.
Soft Takeaways
- Vulkan function loader: I made the function loader myself instead of using the one offered by LunarG. This proved to be a good exercise because I was able to learn which functions corresponded to which part, for example, vkCreateImage() is a Vulkan Instance-Level function.