diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/src/RenderPass.cpp b/src/RenderPass.cpp index dbfbcb9..8cbef6d 100644 --- a/src/RenderPass.cpp +++ b/src/RenderPass.cpp @@ -13,6 +13,7 @@ RenderPass::RenderPass(const RenderPassOptions& options, fpr::Device* device) { + //Construct render pass based on passed options vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.setPDepthStencilAttachment(&options.depth_attachment_ref); @@ -34,6 +35,7 @@ RenderPassOptions RenderPass::MakeDefaultRenderPass(vk::Format swapchain_format, vk::Format depth_image_format) { + //Default render pass does write to the depth buffer, only the color attachmnt. vk::AttachmentDescription color_attachment; color_attachment.format = swapchain_format; color_attachment.samples = vk::SampleCountFlagBits::e1; @@ -62,22 +64,22 @@ depth_attachment_ref.attachment = 1; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - - std::vector subpasses(1); - + //Subpass dependencies to synchronise the two renderpasses, to allow for maximum performance and to transition between optimal image layouts. + //As per the Vulkan documentation, warning against RAW and WAW errors. + std::vector subpasses(2); subpasses[0].srcSubpass = VK_SUBPASS_EXTERNAL; subpasses[0].dstSubpass = 0; subpasses[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - // subpasses[1].srcSubpass = 0; - // subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; - // subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - // subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | - // vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; - // subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; - // subpasses[1].dependencyFlags = {}; + subpasses[1].srcSubpass = 0; + subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; + subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; + subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; + subpasses[1].dependencyFlags = {}; std::vector attachment_descs = { color_attachment, depth_attachment }; std::vector attachment_refs = { color_attachment_ref }; @@ -93,6 +95,7 @@ RenderPassOptions RenderPass::MakeDefaultDepthPass(vk::Format depth_image_format) { + //Depth pre pass only writes to depth buffer. vk::AttachmentReference depth_attachment_ref; depth_attachment_ref.attachment = 0; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; @@ -107,7 +110,7 @@ depth_attachment.initialLayout = vk::ImageLayout::eUndefined; depth_attachment.finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - + //Transition into optimal layout for depth sampling. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/src/RenderPass.cpp b/src/RenderPass.cpp index dbfbcb9..8cbef6d 100644 --- a/src/RenderPass.cpp +++ b/src/RenderPass.cpp @@ -13,6 +13,7 @@ RenderPass::RenderPass(const RenderPassOptions& options, fpr::Device* device) { + //Construct render pass based on passed options vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.setPDepthStencilAttachment(&options.depth_attachment_ref); @@ -34,6 +35,7 @@ RenderPassOptions RenderPass::MakeDefaultRenderPass(vk::Format swapchain_format, vk::Format depth_image_format) { + //Default render pass does write to the depth buffer, only the color attachmnt. vk::AttachmentDescription color_attachment; color_attachment.format = swapchain_format; color_attachment.samples = vk::SampleCountFlagBits::e1; @@ -62,22 +64,22 @@ depth_attachment_ref.attachment = 1; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - - std::vector subpasses(1); - + //Subpass dependencies to synchronise the two renderpasses, to allow for maximum performance and to transition between optimal image layouts. + //As per the Vulkan documentation, warning against RAW and WAW errors. + std::vector subpasses(2); subpasses[0].srcSubpass = VK_SUBPASS_EXTERNAL; subpasses[0].dstSubpass = 0; subpasses[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - // subpasses[1].srcSubpass = 0; - // subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; - // subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - // subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | - // vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; - // subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; - // subpasses[1].dependencyFlags = {}; + subpasses[1].srcSubpass = 0; + subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; + subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; + subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; + subpasses[1].dependencyFlags = {}; std::vector attachment_descs = { color_attachment, depth_attachment }; std::vector attachment_refs = { color_attachment_ref }; @@ -93,6 +95,7 @@ RenderPassOptions RenderPass::MakeDefaultDepthPass(vk::Format depth_image_format) { + //Depth pre pass only writes to depth buffer. vk::AttachmentReference depth_attachment_ref; depth_attachment_ref.attachment = 0; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; @@ -107,7 +110,7 @@ depth_attachment.initialLayout = vk::ImageLayout::eUndefined; depth_attachment.finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - + //Transition into optimal layout for depth sampling. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index e4556cb..711b33a 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -1,7 +1,8 @@ #include +#include "Context.h" #include "SwapChain.h" #include "Buffer/DepthBuffer.h" -#include "Context.h" + #include "RenderPass.h" #include "Scene.h" #include "Texture.h" @@ -10,7 +11,11 @@ namespace fpr { -SwapChain::SwapChain(fpr::Device* device, vk::SurfaceKHR& surface, std::optional old_swapchain) +SwapChain::SwapChain( + fpr::Device* device, + vk::SurfaceKHR& surface, + std::optional old_swapchain, + glm::ivec2 tile_size) { if(old_swapchain != std::nullopt) { @@ -20,12 +25,21 @@ CreateSwapchain(device, surface); CreateImageViews(device); m_depth_buffer = std::make_unique(device, this); + CreateRenderPasses(device); CreateFrameBuffers(device); CreateSynchronisation(device); - m_viewport_push_constant.viewport_size_tile_size = { m_extent.width, m_extent.height, DEFAULT_TILE_SIZE }; + + // Construct push constants. Used in shaders. + glm::ivec2 viewport_size = { m_extent.width, m_extent.height }; + m_viewport_push_constant.viewport_size = viewport_size; + m_viewport_push_constant.tile_size = tile_size; + m_viewport_push_constant.horizontal_tile_count = (viewport_size.x + 1) / (tile_size.x - 1); + m_viewport_push_constant.vertical_tile_count = (viewport_size.y + 1) / (tile_size.y - 1); + m_viewport_push_constant.MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + m_viewport_push_constant.MAX_LIGHTS = FPR_MAX_LIGHTS; } vk::UniqueSwapchainKHR& SwapChain::GetUniqueHandle() @@ -48,13 +62,11 @@ m_surface_format = QueryFirstSuitableFormat(surface_formats); m_present_mode = fifo_or_immediate(present_modes); - m_extent = QueryExtenfpromCapabilities(surface_capabilities); + m_extent = QueryExtenfromCapabilities(surface_capabilities); vk::SwapchainCreateInfoKHR swap_chain_create_info{}; - // TODO: Refactor this, instead of commenting make it actually readable - // instead + // Determine how many swapchain images can be handled. Allowing for double/triple buffering. uint32_t image_count = surface_capabilities.minImageCount + 1; - if(surface_capabilities.maxImageCount > 0 && image_count > surface_capabilities.maxImageCount) { if(image_count < surface_capabilities.maxImageCount) @@ -66,11 +78,8 @@ image_count = surface_capabilities.minImageCount; } } - MAX_IMAGES_IN_FLIGHT = image_count; - // end of confusion - swap_chain_create_info.setSurface(surface); swap_chain_create_info.imageFormat = m_surface_format.format; swap_chain_create_info.imageColorSpace = m_surface_format.colorSpace; @@ -88,6 +97,7 @@ uint32_t queue_family_indices[] = { device->GetGraphicsQueueIndex(), device->GetPresentQueueIndex() }; + if(queue_family_indices[0] != queue_family_indices[1]) { swap_chain_create_info.imageSharingMode = vk::SharingMode::eConcurrent; @@ -134,6 +144,7 @@ void SwapChain::SyncPresentation(fpr::Device* device, uint32_t image_index) { + // Sync swapchain presentation so it happens after the final color pass. vk::PresentInfoKHR present_info{}; present_info.setImageIndices(image_index); present_info.setSwapchains(m_swapchain.get()); @@ -142,11 +153,28 @@ assert(present_res == vk::Result::eSuccess); } +void SwapChain::SyncCompute(fpr::Device* device) +{ + // Sync the compute light culling shader. + // Depth prepass -> Compute -> Final shading + vk::SubmitInfo compute_submit_info{}; + vk::PipelineStageFlags stages = { vk::PipelineStageFlagBits::eComputeShader }; + compute_submit_info.setSignalSemaphores(m_compute_semaphores[m_current_frame].get()) + .setCommandBuffers(m_compute_command_buffers[m_current_frame].get()) + .setWaitSemaphores(m_earlyz_semaphores[m_current_frame].get()) + .setWaitDstStageMask(stages); + + [[maybe_unused]] vk::Result submit_res = device->GetComputeQueue().submit(compute_submit_info); + assert(("Failed to sync compute pass.", submit_res == vk::Result::eSuccess)); +} + void SwapChain::SyncColorPass(fpr::Device* device) { - std::array wait_flags = { vk::PipelineStageFlagBits::eEarlyFragmentTests, + // Sync the final shading stage. + // Depth prepass -> Compute -> Final shading + std::array wait_flags = { vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eColorAttachmentOutput }; - std::array wait_semaphores{ m_earlyz_semaphores[m_current_frame].get(), + std::array wait_semaphores{ m_compute_semaphores[m_current_frame].get(), m_image_ready_semaphores[m_current_frame].get() }; vk::SubmitInfo submit_info{}; submit_info.setWaitSemaphores(wait_semaphores); @@ -169,13 +197,20 @@ RenderPass::MakeDefaultRenderPass(m_surface_format.format, depth_format); const fpr::RenderPassOptions& early_z_options = RenderPass::MakeDefaultDepthPass(depth_format); + context.m_render_graph->RenderPasses.Add("Default", std::make_unique(color_pass_options, device)); context.m_render_graph->RenderPasses.Add("EarlyZ", std::make_unique(early_z_options, device)); } +const ViewportConstants& SwapChain::GetViewportConstants() const +{ + return m_viewport_push_constant; +} + void SwapChain::SyncDepthPass(fpr::Device* device) { + // Depth prepass -> Compute -> Final shading vk::SubmitInfo depth_submit_info{}; depth_submit_info.setSignalSemaphores(m_earlyz_semaphores[m_current_frame].get()) .setCommandBuffers(m_depth_command_buffers[m_current_frame].get()); @@ -185,9 +220,11 @@ } - void SwapChain::CreateFrameBuffers(fpr::Device* device) { + // Create swapchain framebuffers. + // One is used for color shading and swapchain image presentation. + // Other is used for the depth buffer. auto& context = fpr::Context::Get(); auto renderpass = context.m_render_graph->RenderPasses.Get("Default"); auto depth_pass = context.m_render_graph->RenderPasses.Get("EarlyZ"); @@ -239,13 +276,13 @@ RecordComputeCommands(); RecordRenderCommands(); - } void SwapChain::CreateCommandBuffers() { - auto& context = fpr::Context::Get(); - // vk::commandbuffer + // Tree command buffers are required. + // Grapphics command buffer, depth pre pass command buffer and compute command buffer. + auto& context = fpr::Context::Get(); vk::CommandBufferAllocateInfo command_buffer_alloc_info{}; command_buffer_alloc_info.commandPool = context.GetGraphicsCmdPool(); command_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; @@ -267,24 +304,43 @@ context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(depth_buffer_alloc); assert(("Failed to create depth command buffers", depth_cmd_alloc_result == vk::Result::eSuccess)); m_depth_command_buffers = std::move(depth_cmd_buffers); + + + vk::CommandBufferAllocateInfo compute_buffer_alloc{}; + compute_buffer_alloc.commandPool = context.GetComputeCmdPool(); + compute_buffer_alloc.level = vk::CommandBufferLevel::ePrimary; + compute_buffer_alloc.commandBufferCount = (uint32_t)m_swapchain_framebuffers.size(); + + auto [compute_cmd_alloc_result, compute_cmd_buffers] = + context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(compute_buffer_alloc); + assert(("Failed to create compute command buffers", compute_cmd_alloc_result == vk::Result::eSuccess)); + m_compute_command_buffers = std::move(compute_cmd_buffers); } void SwapChain::CreateSynchronisation(fpr::Device* device) { + // Create all the synchronisation objets. + // Fences and semaphores are used to ensure that buffers being read from aren't currently being wriitten to and vice + // versa. m_image_ready_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_render_done_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_earlyz_semaphores.resize(MAX_IMAGES_IN_FLIGHT); + m_compute_semaphores.resize(MAX_IMAGES_IN_FLIGHT); vk::SemaphoreCreateInfo semaphore_create_info{}; vk::FenceCreateInfo fence_create_info(vk::FenceCreateFlagBits::eSignaled); - auto create_semaphore = [&](vk::UniqueSemaphore& s) + + // Simple lambda that creates an unique handle to a semaphore. Used to construct all the semaphores used for + // synchronisation. + auto create_semaphore = [&](vk::UniqueSemaphore& s) { auto [semaphore_result, semaphore] = device->GetLogicalDeviceHandle().createSemaphoreUnique(semaphore_create_info); assert(("Semaphore creation failed!", semaphore_result == vk::Result::eSuccess)); s = std::move(semaphore); }; + // Simple lambda that creates an unique handle to a fence auto create_fence = [&](vk::UniqueFence& f) { auto [fence_result, fence] = device->GetLogicalDeviceHandle().createFenceUnique(fence_create_info); @@ -295,6 +351,7 @@ std::for_each(m_image_ready_semaphores.begin(), m_image_ready_semaphores.end(), create_semaphore); std::for_each(m_render_done_semaphores.begin(), m_render_done_semaphores.end(), create_semaphore); std::for_each(m_earlyz_semaphores.begin(), m_earlyz_semaphores.end(), create_semaphore); + std::for_each(m_compute_semaphores.begin(), m_compute_semaphores.end(), create_semaphore); create_fence(m_draw_fence); } @@ -310,7 +367,7 @@ assert(("Timed out waiting for draw fence", fence_result == vk::Result::eSuccess)); assert(("Failed to reset draw fence", fence_reset_result == vk::Result::eSuccess)); - + // Ensure that an image is ready and the prior frame has been submitted. vk::Result acquire_image_result = device->GetLogicalDeviceHandle().acquireNextImageKHR( *m_swapchain, std::numeric_limits::max(), @@ -320,9 +377,12 @@ if(acquire_image_result != vk::Result::eSuccess) return acquire_image_result; - + // Synchronise the different command buffers so they execute in sequence. SyncDepthPass(device); + SyncCompute(device); SyncColorPass(device); + + // Synchronise presentation and present image. SyncPresentation(device, image_index); m_current_frame = (m_current_frame + 1) % MAX_IMAGES_IN_FLIGHT; @@ -332,17 +392,8 @@ { auto [capabilities_result, surface_capabilities] = device->GetPhysicalDeviceHandle().getSurfaceCapabilitiesKHR(surface); - - // todo: assert result - - auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); - - // todo: assert formats - + auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device->GetPhysicalDeviceHandle().getSurfacePresentModesKHR(surface); - - // todo: assert modes - return std::make_tuple(surface_capabilities, formats, present_modes); } @@ -390,11 +441,12 @@ render_pass_info.setClearValues(m_clear_values); m_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport can be inverted in the Y axis. However, it is less overheadd to invert the projection matrix. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; // static_cast(m_extent.height); viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); // -static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -413,13 +465,14 @@ auto cam_desc_set = render_graph->DescriptorSets.Get("Camera" + std::to_string(i)); auto light_desc_set = render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)); - + //Draw each mesh of each model. for(auto& model : scene->GetModels()) { m_command_buffers[i]->bindIndexBuffer(model->GetIndexBuffer().GetBuffer(), 0, vk::IndexType::eUint32); for(auto&& mesh : model->GetMeshes()) { + // Each mesh is stored in the same vertex buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->bindVertexBuffers(0, model->GetVertexBuffer().GetBuffer(), mesh.GetVertexOffset()); std::vector desc_sets = { @@ -448,6 +501,8 @@ vk::PipelineBindPoint::eGraphics, graphics_pipeline->GetLayout(), 3, light_desc_set, nullptr); + + //Each mesh is stored in the same index buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -460,9 +515,10 @@ void SwapChain::RecordDepthCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); - fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); + //Depth prepass. + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); + fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); for(size_t i = 0; i < m_depth_command_buffers.size(); ++i) { vk::CommandBufferBeginInfo cmd_begin_info; @@ -479,11 +535,12 @@ m_depth_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport not inverted, instead projection matrix is. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -496,6 +553,7 @@ auto scene = context.m_loaded_scene; + //Buffers used in the depth prepass shader. std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), render_graph->DescriptorSets.Get("Model" + std::to_string(i)), @@ -523,6 +581,7 @@ render_graph->DescriptorSets.Get("Model" + std::to_string(i)), (uint32_t)(mesh.GetMeshIndex() * context.GetRequiredDynamicAlignment())); + m_depth_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -533,10 +592,10 @@ } } -void SwapChain::RecordComputeCommands() +void SwapChain::RecordComputeCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); fpr::Pipeline* compute_pipeline = render_graph->ComputePipelines.Get("LightCulling"); for(size_t i = 0; i < m_compute_command_buffers.size(); ++i) { @@ -545,25 +604,91 @@ [[maybe_unused]] auto cmd_begin_result = m_compute_command_buffers[i]->begin(cmd_begin_info); assert(cmd_begin_result == vk::Result::eSuccess); - //todo barriers + //Need to retrieve the actual buffers, not descriptors. Light culling compute shaders require barriers to buffers. + fpr::Buffer* culled_buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(m_current_frame)); + fpr::Buffer* light_buffer = render_graph->Buffers.Get("PointLight" + std::to_string(m_current_frame)); - std::array desc_sets = { - render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), - render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)), - render_graph->DescriptorSets.Get("DepthSampler"), - }; + std::vector barriers; + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + + //Pre cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eFragmentShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, + nullptr, + barriers, + nullptr); + + std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), + render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)) }; + + m_compute_command_buffers[i]->bindDescriptorSets( vk::PipelineBindPoint::eCompute, compute_pipeline->GetLayout(), 0, desc_sets, nullptr); - m_compute_command_buffers[i]-> - pushConstants( - compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + m_compute_command_buffers[i]->bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + compute_pipeline->GetLayout(), + 2, + render_graph->DescriptorSets.Get("DepthSampler"), + nullptr); - glm::vec3 group_count = { m_viewport_push_constant.viewport_size_tile_size.z, - m_viewport_push_constant.viewport_size_tile_size.w, + m_compute_command_buffers[i]->pushConstants( + compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + + glm::vec3 group_count = { m_viewport_push_constant.horizontal_tile_count, + m_viewport_push_constant.vertical_tile_count, 1 }; + //Bind compute pipeline and dispatch shader for each tile. + m_compute_command_buffers[i]->bindPipeline(vk::PipelineBindPoint::eCompute, compute_pipeline->GetPipeline()); + m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, (uint32_t)group_count.z); - m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, 1); + + barriers.clear(); + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + //Post cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + nullptr, + barriers, + nullptr); [[maybe_unused]] auto cmd_end_result = m_compute_command_buffers[i]->end(); assert(cmd_end_result == vk::Result::eSuccess); @@ -571,7 +696,7 @@ } -vk::Extent2D SwapChain::QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) +vk::Extent2D SwapChain::QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) { vk::Extent2D result{}; @@ -597,7 +722,7 @@ return result; } -const vk::Format& SwapChain::GetFormat() const FPR_NOEXCEPT +const vk::Format& SwapChain::GetSwapchainImageFormat() const FPR_NOEXCEPT { return m_surface_format.format; } @@ -621,6 +746,4 @@ { return m_depth_buffer.get(); } - - } // namespace fpr \ No newline at end of file diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/src/RenderPass.cpp b/src/RenderPass.cpp index dbfbcb9..8cbef6d 100644 --- a/src/RenderPass.cpp +++ b/src/RenderPass.cpp @@ -13,6 +13,7 @@ RenderPass::RenderPass(const RenderPassOptions& options, fpr::Device* device) { + //Construct render pass based on passed options vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.setPDepthStencilAttachment(&options.depth_attachment_ref); @@ -34,6 +35,7 @@ RenderPassOptions RenderPass::MakeDefaultRenderPass(vk::Format swapchain_format, vk::Format depth_image_format) { + //Default render pass does write to the depth buffer, only the color attachmnt. vk::AttachmentDescription color_attachment; color_attachment.format = swapchain_format; color_attachment.samples = vk::SampleCountFlagBits::e1; @@ -62,22 +64,22 @@ depth_attachment_ref.attachment = 1; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - - std::vector subpasses(1); - + //Subpass dependencies to synchronise the two renderpasses, to allow for maximum performance and to transition between optimal image layouts. + //As per the Vulkan documentation, warning against RAW and WAW errors. + std::vector subpasses(2); subpasses[0].srcSubpass = VK_SUBPASS_EXTERNAL; subpasses[0].dstSubpass = 0; subpasses[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - // subpasses[1].srcSubpass = 0; - // subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; - // subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - // subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | - // vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; - // subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; - // subpasses[1].dependencyFlags = {}; + subpasses[1].srcSubpass = 0; + subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; + subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; + subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; + subpasses[1].dependencyFlags = {}; std::vector attachment_descs = { color_attachment, depth_attachment }; std::vector attachment_refs = { color_attachment_ref }; @@ -93,6 +95,7 @@ RenderPassOptions RenderPass::MakeDefaultDepthPass(vk::Format depth_image_format) { + //Depth pre pass only writes to depth buffer. vk::AttachmentReference depth_attachment_ref; depth_attachment_ref.attachment = 0; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; @@ -107,7 +110,7 @@ depth_attachment.initialLayout = vk::ImageLayout::eUndefined; depth_attachment.finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - + //Transition into optimal layout for depth sampling. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index e4556cb..711b33a 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -1,7 +1,8 @@ #include +#include "Context.h" #include "SwapChain.h" #include "Buffer/DepthBuffer.h" -#include "Context.h" + #include "RenderPass.h" #include "Scene.h" #include "Texture.h" @@ -10,7 +11,11 @@ namespace fpr { -SwapChain::SwapChain(fpr::Device* device, vk::SurfaceKHR& surface, std::optional old_swapchain) +SwapChain::SwapChain( + fpr::Device* device, + vk::SurfaceKHR& surface, + std::optional old_swapchain, + glm::ivec2 tile_size) { if(old_swapchain != std::nullopt) { @@ -20,12 +25,21 @@ CreateSwapchain(device, surface); CreateImageViews(device); m_depth_buffer = std::make_unique(device, this); + CreateRenderPasses(device); CreateFrameBuffers(device); CreateSynchronisation(device); - m_viewport_push_constant.viewport_size_tile_size = { m_extent.width, m_extent.height, DEFAULT_TILE_SIZE }; + + // Construct push constants. Used in shaders. + glm::ivec2 viewport_size = { m_extent.width, m_extent.height }; + m_viewport_push_constant.viewport_size = viewport_size; + m_viewport_push_constant.tile_size = tile_size; + m_viewport_push_constant.horizontal_tile_count = (viewport_size.x + 1) / (tile_size.x - 1); + m_viewport_push_constant.vertical_tile_count = (viewport_size.y + 1) / (tile_size.y - 1); + m_viewport_push_constant.MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + m_viewport_push_constant.MAX_LIGHTS = FPR_MAX_LIGHTS; } vk::UniqueSwapchainKHR& SwapChain::GetUniqueHandle() @@ -48,13 +62,11 @@ m_surface_format = QueryFirstSuitableFormat(surface_formats); m_present_mode = fifo_or_immediate(present_modes); - m_extent = QueryExtenfpromCapabilities(surface_capabilities); + m_extent = QueryExtenfromCapabilities(surface_capabilities); vk::SwapchainCreateInfoKHR swap_chain_create_info{}; - // TODO: Refactor this, instead of commenting make it actually readable - // instead + // Determine how many swapchain images can be handled. Allowing for double/triple buffering. uint32_t image_count = surface_capabilities.minImageCount + 1; - if(surface_capabilities.maxImageCount > 0 && image_count > surface_capabilities.maxImageCount) { if(image_count < surface_capabilities.maxImageCount) @@ -66,11 +78,8 @@ image_count = surface_capabilities.minImageCount; } } - MAX_IMAGES_IN_FLIGHT = image_count; - // end of confusion - swap_chain_create_info.setSurface(surface); swap_chain_create_info.imageFormat = m_surface_format.format; swap_chain_create_info.imageColorSpace = m_surface_format.colorSpace; @@ -88,6 +97,7 @@ uint32_t queue_family_indices[] = { device->GetGraphicsQueueIndex(), device->GetPresentQueueIndex() }; + if(queue_family_indices[0] != queue_family_indices[1]) { swap_chain_create_info.imageSharingMode = vk::SharingMode::eConcurrent; @@ -134,6 +144,7 @@ void SwapChain::SyncPresentation(fpr::Device* device, uint32_t image_index) { + // Sync swapchain presentation so it happens after the final color pass. vk::PresentInfoKHR present_info{}; present_info.setImageIndices(image_index); present_info.setSwapchains(m_swapchain.get()); @@ -142,11 +153,28 @@ assert(present_res == vk::Result::eSuccess); } +void SwapChain::SyncCompute(fpr::Device* device) +{ + // Sync the compute light culling shader. + // Depth prepass -> Compute -> Final shading + vk::SubmitInfo compute_submit_info{}; + vk::PipelineStageFlags stages = { vk::PipelineStageFlagBits::eComputeShader }; + compute_submit_info.setSignalSemaphores(m_compute_semaphores[m_current_frame].get()) + .setCommandBuffers(m_compute_command_buffers[m_current_frame].get()) + .setWaitSemaphores(m_earlyz_semaphores[m_current_frame].get()) + .setWaitDstStageMask(stages); + + [[maybe_unused]] vk::Result submit_res = device->GetComputeQueue().submit(compute_submit_info); + assert(("Failed to sync compute pass.", submit_res == vk::Result::eSuccess)); +} + void SwapChain::SyncColorPass(fpr::Device* device) { - std::array wait_flags = { vk::PipelineStageFlagBits::eEarlyFragmentTests, + // Sync the final shading stage. + // Depth prepass -> Compute -> Final shading + std::array wait_flags = { vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eColorAttachmentOutput }; - std::array wait_semaphores{ m_earlyz_semaphores[m_current_frame].get(), + std::array wait_semaphores{ m_compute_semaphores[m_current_frame].get(), m_image_ready_semaphores[m_current_frame].get() }; vk::SubmitInfo submit_info{}; submit_info.setWaitSemaphores(wait_semaphores); @@ -169,13 +197,20 @@ RenderPass::MakeDefaultRenderPass(m_surface_format.format, depth_format); const fpr::RenderPassOptions& early_z_options = RenderPass::MakeDefaultDepthPass(depth_format); + context.m_render_graph->RenderPasses.Add("Default", std::make_unique(color_pass_options, device)); context.m_render_graph->RenderPasses.Add("EarlyZ", std::make_unique(early_z_options, device)); } +const ViewportConstants& SwapChain::GetViewportConstants() const +{ + return m_viewport_push_constant; +} + void SwapChain::SyncDepthPass(fpr::Device* device) { + // Depth prepass -> Compute -> Final shading vk::SubmitInfo depth_submit_info{}; depth_submit_info.setSignalSemaphores(m_earlyz_semaphores[m_current_frame].get()) .setCommandBuffers(m_depth_command_buffers[m_current_frame].get()); @@ -185,9 +220,11 @@ } - void SwapChain::CreateFrameBuffers(fpr::Device* device) { + // Create swapchain framebuffers. + // One is used for color shading and swapchain image presentation. + // Other is used for the depth buffer. auto& context = fpr::Context::Get(); auto renderpass = context.m_render_graph->RenderPasses.Get("Default"); auto depth_pass = context.m_render_graph->RenderPasses.Get("EarlyZ"); @@ -239,13 +276,13 @@ RecordComputeCommands(); RecordRenderCommands(); - } void SwapChain::CreateCommandBuffers() { - auto& context = fpr::Context::Get(); - // vk::commandbuffer + // Tree command buffers are required. + // Grapphics command buffer, depth pre pass command buffer and compute command buffer. + auto& context = fpr::Context::Get(); vk::CommandBufferAllocateInfo command_buffer_alloc_info{}; command_buffer_alloc_info.commandPool = context.GetGraphicsCmdPool(); command_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; @@ -267,24 +304,43 @@ context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(depth_buffer_alloc); assert(("Failed to create depth command buffers", depth_cmd_alloc_result == vk::Result::eSuccess)); m_depth_command_buffers = std::move(depth_cmd_buffers); + + + vk::CommandBufferAllocateInfo compute_buffer_alloc{}; + compute_buffer_alloc.commandPool = context.GetComputeCmdPool(); + compute_buffer_alloc.level = vk::CommandBufferLevel::ePrimary; + compute_buffer_alloc.commandBufferCount = (uint32_t)m_swapchain_framebuffers.size(); + + auto [compute_cmd_alloc_result, compute_cmd_buffers] = + context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(compute_buffer_alloc); + assert(("Failed to create compute command buffers", compute_cmd_alloc_result == vk::Result::eSuccess)); + m_compute_command_buffers = std::move(compute_cmd_buffers); } void SwapChain::CreateSynchronisation(fpr::Device* device) { + // Create all the synchronisation objets. + // Fences and semaphores are used to ensure that buffers being read from aren't currently being wriitten to and vice + // versa. m_image_ready_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_render_done_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_earlyz_semaphores.resize(MAX_IMAGES_IN_FLIGHT); + m_compute_semaphores.resize(MAX_IMAGES_IN_FLIGHT); vk::SemaphoreCreateInfo semaphore_create_info{}; vk::FenceCreateInfo fence_create_info(vk::FenceCreateFlagBits::eSignaled); - auto create_semaphore = [&](vk::UniqueSemaphore& s) + + // Simple lambda that creates an unique handle to a semaphore. Used to construct all the semaphores used for + // synchronisation. + auto create_semaphore = [&](vk::UniqueSemaphore& s) { auto [semaphore_result, semaphore] = device->GetLogicalDeviceHandle().createSemaphoreUnique(semaphore_create_info); assert(("Semaphore creation failed!", semaphore_result == vk::Result::eSuccess)); s = std::move(semaphore); }; + // Simple lambda that creates an unique handle to a fence auto create_fence = [&](vk::UniqueFence& f) { auto [fence_result, fence] = device->GetLogicalDeviceHandle().createFenceUnique(fence_create_info); @@ -295,6 +351,7 @@ std::for_each(m_image_ready_semaphores.begin(), m_image_ready_semaphores.end(), create_semaphore); std::for_each(m_render_done_semaphores.begin(), m_render_done_semaphores.end(), create_semaphore); std::for_each(m_earlyz_semaphores.begin(), m_earlyz_semaphores.end(), create_semaphore); + std::for_each(m_compute_semaphores.begin(), m_compute_semaphores.end(), create_semaphore); create_fence(m_draw_fence); } @@ -310,7 +367,7 @@ assert(("Timed out waiting for draw fence", fence_result == vk::Result::eSuccess)); assert(("Failed to reset draw fence", fence_reset_result == vk::Result::eSuccess)); - + // Ensure that an image is ready and the prior frame has been submitted. vk::Result acquire_image_result = device->GetLogicalDeviceHandle().acquireNextImageKHR( *m_swapchain, std::numeric_limits::max(), @@ -320,9 +377,12 @@ if(acquire_image_result != vk::Result::eSuccess) return acquire_image_result; - + // Synchronise the different command buffers so they execute in sequence. SyncDepthPass(device); + SyncCompute(device); SyncColorPass(device); + + // Synchronise presentation and present image. SyncPresentation(device, image_index); m_current_frame = (m_current_frame + 1) % MAX_IMAGES_IN_FLIGHT; @@ -332,17 +392,8 @@ { auto [capabilities_result, surface_capabilities] = device->GetPhysicalDeviceHandle().getSurfaceCapabilitiesKHR(surface); - - // todo: assert result - - auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); - - // todo: assert formats - + auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device->GetPhysicalDeviceHandle().getSurfacePresentModesKHR(surface); - - // todo: assert modes - return std::make_tuple(surface_capabilities, formats, present_modes); } @@ -390,11 +441,12 @@ render_pass_info.setClearValues(m_clear_values); m_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport can be inverted in the Y axis. However, it is less overheadd to invert the projection matrix. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; // static_cast(m_extent.height); viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); // -static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -413,13 +465,14 @@ auto cam_desc_set = render_graph->DescriptorSets.Get("Camera" + std::to_string(i)); auto light_desc_set = render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)); - + //Draw each mesh of each model. for(auto& model : scene->GetModels()) { m_command_buffers[i]->bindIndexBuffer(model->GetIndexBuffer().GetBuffer(), 0, vk::IndexType::eUint32); for(auto&& mesh : model->GetMeshes()) { + // Each mesh is stored in the same vertex buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->bindVertexBuffers(0, model->GetVertexBuffer().GetBuffer(), mesh.GetVertexOffset()); std::vector desc_sets = { @@ -448,6 +501,8 @@ vk::PipelineBindPoint::eGraphics, graphics_pipeline->GetLayout(), 3, light_desc_set, nullptr); + + //Each mesh is stored in the same index buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -460,9 +515,10 @@ void SwapChain::RecordDepthCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); - fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); + //Depth prepass. + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); + fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); for(size_t i = 0; i < m_depth_command_buffers.size(); ++i) { vk::CommandBufferBeginInfo cmd_begin_info; @@ -479,11 +535,12 @@ m_depth_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport not inverted, instead projection matrix is. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -496,6 +553,7 @@ auto scene = context.m_loaded_scene; + //Buffers used in the depth prepass shader. std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), render_graph->DescriptorSets.Get("Model" + std::to_string(i)), @@ -523,6 +581,7 @@ render_graph->DescriptorSets.Get("Model" + std::to_string(i)), (uint32_t)(mesh.GetMeshIndex() * context.GetRequiredDynamicAlignment())); + m_depth_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -533,10 +592,10 @@ } } -void SwapChain::RecordComputeCommands() +void SwapChain::RecordComputeCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); fpr::Pipeline* compute_pipeline = render_graph->ComputePipelines.Get("LightCulling"); for(size_t i = 0; i < m_compute_command_buffers.size(); ++i) { @@ -545,25 +604,91 @@ [[maybe_unused]] auto cmd_begin_result = m_compute_command_buffers[i]->begin(cmd_begin_info); assert(cmd_begin_result == vk::Result::eSuccess); - //todo barriers + //Need to retrieve the actual buffers, not descriptors. Light culling compute shaders require barriers to buffers. + fpr::Buffer* culled_buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(m_current_frame)); + fpr::Buffer* light_buffer = render_graph->Buffers.Get("PointLight" + std::to_string(m_current_frame)); - std::array desc_sets = { - render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), - render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)), - render_graph->DescriptorSets.Get("DepthSampler"), - }; + std::vector barriers; + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + + //Pre cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eFragmentShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, + nullptr, + barriers, + nullptr); + + std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), + render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)) }; + + m_compute_command_buffers[i]->bindDescriptorSets( vk::PipelineBindPoint::eCompute, compute_pipeline->GetLayout(), 0, desc_sets, nullptr); - m_compute_command_buffers[i]-> - pushConstants( - compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + m_compute_command_buffers[i]->bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + compute_pipeline->GetLayout(), + 2, + render_graph->DescriptorSets.Get("DepthSampler"), + nullptr); - glm::vec3 group_count = { m_viewport_push_constant.viewport_size_tile_size.z, - m_viewport_push_constant.viewport_size_tile_size.w, + m_compute_command_buffers[i]->pushConstants( + compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + + glm::vec3 group_count = { m_viewport_push_constant.horizontal_tile_count, + m_viewport_push_constant.vertical_tile_count, 1 }; + //Bind compute pipeline and dispatch shader for each tile. + m_compute_command_buffers[i]->bindPipeline(vk::PipelineBindPoint::eCompute, compute_pipeline->GetPipeline()); + m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, (uint32_t)group_count.z); - m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, 1); + + barriers.clear(); + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + //Post cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + nullptr, + barriers, + nullptr); [[maybe_unused]] auto cmd_end_result = m_compute_command_buffers[i]->end(); assert(cmd_end_result == vk::Result::eSuccess); @@ -571,7 +696,7 @@ } -vk::Extent2D SwapChain::QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) +vk::Extent2D SwapChain::QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) { vk::Extent2D result{}; @@ -597,7 +722,7 @@ return result; } -const vk::Format& SwapChain::GetFormat() const FPR_NOEXCEPT +const vk::Format& SwapChain::GetSwapchainImageFormat() const FPR_NOEXCEPT { return m_surface_format.format; } @@ -621,6 +746,4 @@ { return m_depth_buffer.get(); } - - } // namespace fpr \ No newline at end of file diff --git a/src/Texture.cpp b/src/Texture.cpp index fc1d838..64bb030 100644 --- a/src/Texture.cpp +++ b/src/Texture.cpp @@ -4,7 +4,6 @@ #include "Buffer/Buffer.h" #include "Context.h" #include "Device.h" -#include "RNG.h" @@ -41,6 +40,7 @@ std::pair Texture::GetImageDetails(std::string_view file_name, int desired_channels) { + //Load raw image data and image size. int width; int height; int image_size; @@ -62,6 +62,7 @@ void Texture::GenerateMipMaps() { + //Run time mip map generation. auto& context = fpr::Context::Get(); auto& cmd_buffer = context.BeginSingleTimeCommands(); @@ -79,6 +80,7 @@ uint32_t mip_width = m_width; uint32_t mip_height = m_height; + //For each mip level, blit image and reduce resolution in half. for(uint32_t i = 1; i < m_mip_levels; ++i) { mip_img_barrier.subresourceRange.baseMipLevel = i - 1; @@ -134,6 +136,7 @@ mip_height = half_else_one(mip_height); } + //pipeline barrier to transition mip image layout. mip_img_barrier.subresourceRange.baseMipLevel = m_mip_levels - 1; mip_img_barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; mip_img_barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; @@ -156,6 +159,8 @@ auto& context = fpr::Context::Get(); auto device = context.GetDevice(); + //Load raw bytes of image and load it into a staging buffer. Textures are generally unchanged throughout a program + //so they can be transferred to the GPU with a staging buffer and left untouched. auto [size, raw_data] = GetImageDetails(file_name, desired_channels); vk::MemoryPropertyFlags flags = vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent; fpr::Buffer staging_buffer(size, vk::BufferUsageFlagBits::eTransferSrc, flags); @@ -210,7 +215,7 @@ { auto& context = fpr::Context::Get(); auto device = context.GetDevice(); - + //Vulkan allows for image and samppler to be combined. Texture loaded in with Sampler. vk::DescriptorSetLayout sampler_layout = context.m_render_graph->DescriptorSetLayouts.Get("Sampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; @@ -236,6 +241,7 @@ void TransitionImageLayout(vk::Image image, const LayoutTransitionRules& transition_rules, uint32_t mip_levels) { + //Transition the image into the layout specified by the rules passed. auto& context = fpr::Context::Get(); auto& single_use_cmd_buffer = context.BeginSingleTimeCommands(); diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/src/RenderPass.cpp b/src/RenderPass.cpp index dbfbcb9..8cbef6d 100644 --- a/src/RenderPass.cpp +++ b/src/RenderPass.cpp @@ -13,6 +13,7 @@ RenderPass::RenderPass(const RenderPassOptions& options, fpr::Device* device) { + //Construct render pass based on passed options vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.setPDepthStencilAttachment(&options.depth_attachment_ref); @@ -34,6 +35,7 @@ RenderPassOptions RenderPass::MakeDefaultRenderPass(vk::Format swapchain_format, vk::Format depth_image_format) { + //Default render pass does write to the depth buffer, only the color attachmnt. vk::AttachmentDescription color_attachment; color_attachment.format = swapchain_format; color_attachment.samples = vk::SampleCountFlagBits::e1; @@ -62,22 +64,22 @@ depth_attachment_ref.attachment = 1; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - - std::vector subpasses(1); - + //Subpass dependencies to synchronise the two renderpasses, to allow for maximum performance and to transition between optimal image layouts. + //As per the Vulkan documentation, warning against RAW and WAW errors. + std::vector subpasses(2); subpasses[0].srcSubpass = VK_SUBPASS_EXTERNAL; subpasses[0].dstSubpass = 0; subpasses[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - // subpasses[1].srcSubpass = 0; - // subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; - // subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - // subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | - // vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; - // subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; - // subpasses[1].dependencyFlags = {}; + subpasses[1].srcSubpass = 0; + subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; + subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; + subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; + subpasses[1].dependencyFlags = {}; std::vector attachment_descs = { color_attachment, depth_attachment }; std::vector attachment_refs = { color_attachment_ref }; @@ -93,6 +95,7 @@ RenderPassOptions RenderPass::MakeDefaultDepthPass(vk::Format depth_image_format) { + //Depth pre pass only writes to depth buffer. vk::AttachmentReference depth_attachment_ref; depth_attachment_ref.attachment = 0; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; @@ -107,7 +110,7 @@ depth_attachment.initialLayout = vk::ImageLayout::eUndefined; depth_attachment.finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - + //Transition into optimal layout for depth sampling. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index e4556cb..711b33a 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -1,7 +1,8 @@ #include +#include "Context.h" #include "SwapChain.h" #include "Buffer/DepthBuffer.h" -#include "Context.h" + #include "RenderPass.h" #include "Scene.h" #include "Texture.h" @@ -10,7 +11,11 @@ namespace fpr { -SwapChain::SwapChain(fpr::Device* device, vk::SurfaceKHR& surface, std::optional old_swapchain) +SwapChain::SwapChain( + fpr::Device* device, + vk::SurfaceKHR& surface, + std::optional old_swapchain, + glm::ivec2 tile_size) { if(old_swapchain != std::nullopt) { @@ -20,12 +25,21 @@ CreateSwapchain(device, surface); CreateImageViews(device); m_depth_buffer = std::make_unique(device, this); + CreateRenderPasses(device); CreateFrameBuffers(device); CreateSynchronisation(device); - m_viewport_push_constant.viewport_size_tile_size = { m_extent.width, m_extent.height, DEFAULT_TILE_SIZE }; + + // Construct push constants. Used in shaders. + glm::ivec2 viewport_size = { m_extent.width, m_extent.height }; + m_viewport_push_constant.viewport_size = viewport_size; + m_viewport_push_constant.tile_size = tile_size; + m_viewport_push_constant.horizontal_tile_count = (viewport_size.x + 1) / (tile_size.x - 1); + m_viewport_push_constant.vertical_tile_count = (viewport_size.y + 1) / (tile_size.y - 1); + m_viewport_push_constant.MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + m_viewport_push_constant.MAX_LIGHTS = FPR_MAX_LIGHTS; } vk::UniqueSwapchainKHR& SwapChain::GetUniqueHandle() @@ -48,13 +62,11 @@ m_surface_format = QueryFirstSuitableFormat(surface_formats); m_present_mode = fifo_or_immediate(present_modes); - m_extent = QueryExtenfpromCapabilities(surface_capabilities); + m_extent = QueryExtenfromCapabilities(surface_capabilities); vk::SwapchainCreateInfoKHR swap_chain_create_info{}; - // TODO: Refactor this, instead of commenting make it actually readable - // instead + // Determine how many swapchain images can be handled. Allowing for double/triple buffering. uint32_t image_count = surface_capabilities.minImageCount + 1; - if(surface_capabilities.maxImageCount > 0 && image_count > surface_capabilities.maxImageCount) { if(image_count < surface_capabilities.maxImageCount) @@ -66,11 +78,8 @@ image_count = surface_capabilities.minImageCount; } } - MAX_IMAGES_IN_FLIGHT = image_count; - // end of confusion - swap_chain_create_info.setSurface(surface); swap_chain_create_info.imageFormat = m_surface_format.format; swap_chain_create_info.imageColorSpace = m_surface_format.colorSpace; @@ -88,6 +97,7 @@ uint32_t queue_family_indices[] = { device->GetGraphicsQueueIndex(), device->GetPresentQueueIndex() }; + if(queue_family_indices[0] != queue_family_indices[1]) { swap_chain_create_info.imageSharingMode = vk::SharingMode::eConcurrent; @@ -134,6 +144,7 @@ void SwapChain::SyncPresentation(fpr::Device* device, uint32_t image_index) { + // Sync swapchain presentation so it happens after the final color pass. vk::PresentInfoKHR present_info{}; present_info.setImageIndices(image_index); present_info.setSwapchains(m_swapchain.get()); @@ -142,11 +153,28 @@ assert(present_res == vk::Result::eSuccess); } +void SwapChain::SyncCompute(fpr::Device* device) +{ + // Sync the compute light culling shader. + // Depth prepass -> Compute -> Final shading + vk::SubmitInfo compute_submit_info{}; + vk::PipelineStageFlags stages = { vk::PipelineStageFlagBits::eComputeShader }; + compute_submit_info.setSignalSemaphores(m_compute_semaphores[m_current_frame].get()) + .setCommandBuffers(m_compute_command_buffers[m_current_frame].get()) + .setWaitSemaphores(m_earlyz_semaphores[m_current_frame].get()) + .setWaitDstStageMask(stages); + + [[maybe_unused]] vk::Result submit_res = device->GetComputeQueue().submit(compute_submit_info); + assert(("Failed to sync compute pass.", submit_res == vk::Result::eSuccess)); +} + void SwapChain::SyncColorPass(fpr::Device* device) { - std::array wait_flags = { vk::PipelineStageFlagBits::eEarlyFragmentTests, + // Sync the final shading stage. + // Depth prepass -> Compute -> Final shading + std::array wait_flags = { vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eColorAttachmentOutput }; - std::array wait_semaphores{ m_earlyz_semaphores[m_current_frame].get(), + std::array wait_semaphores{ m_compute_semaphores[m_current_frame].get(), m_image_ready_semaphores[m_current_frame].get() }; vk::SubmitInfo submit_info{}; submit_info.setWaitSemaphores(wait_semaphores); @@ -169,13 +197,20 @@ RenderPass::MakeDefaultRenderPass(m_surface_format.format, depth_format); const fpr::RenderPassOptions& early_z_options = RenderPass::MakeDefaultDepthPass(depth_format); + context.m_render_graph->RenderPasses.Add("Default", std::make_unique(color_pass_options, device)); context.m_render_graph->RenderPasses.Add("EarlyZ", std::make_unique(early_z_options, device)); } +const ViewportConstants& SwapChain::GetViewportConstants() const +{ + return m_viewport_push_constant; +} + void SwapChain::SyncDepthPass(fpr::Device* device) { + // Depth prepass -> Compute -> Final shading vk::SubmitInfo depth_submit_info{}; depth_submit_info.setSignalSemaphores(m_earlyz_semaphores[m_current_frame].get()) .setCommandBuffers(m_depth_command_buffers[m_current_frame].get()); @@ -185,9 +220,11 @@ } - void SwapChain::CreateFrameBuffers(fpr::Device* device) { + // Create swapchain framebuffers. + // One is used for color shading and swapchain image presentation. + // Other is used for the depth buffer. auto& context = fpr::Context::Get(); auto renderpass = context.m_render_graph->RenderPasses.Get("Default"); auto depth_pass = context.m_render_graph->RenderPasses.Get("EarlyZ"); @@ -239,13 +276,13 @@ RecordComputeCommands(); RecordRenderCommands(); - } void SwapChain::CreateCommandBuffers() { - auto& context = fpr::Context::Get(); - // vk::commandbuffer + // Tree command buffers are required. + // Grapphics command buffer, depth pre pass command buffer and compute command buffer. + auto& context = fpr::Context::Get(); vk::CommandBufferAllocateInfo command_buffer_alloc_info{}; command_buffer_alloc_info.commandPool = context.GetGraphicsCmdPool(); command_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; @@ -267,24 +304,43 @@ context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(depth_buffer_alloc); assert(("Failed to create depth command buffers", depth_cmd_alloc_result == vk::Result::eSuccess)); m_depth_command_buffers = std::move(depth_cmd_buffers); + + + vk::CommandBufferAllocateInfo compute_buffer_alloc{}; + compute_buffer_alloc.commandPool = context.GetComputeCmdPool(); + compute_buffer_alloc.level = vk::CommandBufferLevel::ePrimary; + compute_buffer_alloc.commandBufferCount = (uint32_t)m_swapchain_framebuffers.size(); + + auto [compute_cmd_alloc_result, compute_cmd_buffers] = + context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(compute_buffer_alloc); + assert(("Failed to create compute command buffers", compute_cmd_alloc_result == vk::Result::eSuccess)); + m_compute_command_buffers = std::move(compute_cmd_buffers); } void SwapChain::CreateSynchronisation(fpr::Device* device) { + // Create all the synchronisation objets. + // Fences and semaphores are used to ensure that buffers being read from aren't currently being wriitten to and vice + // versa. m_image_ready_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_render_done_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_earlyz_semaphores.resize(MAX_IMAGES_IN_FLIGHT); + m_compute_semaphores.resize(MAX_IMAGES_IN_FLIGHT); vk::SemaphoreCreateInfo semaphore_create_info{}; vk::FenceCreateInfo fence_create_info(vk::FenceCreateFlagBits::eSignaled); - auto create_semaphore = [&](vk::UniqueSemaphore& s) + + // Simple lambda that creates an unique handle to a semaphore. Used to construct all the semaphores used for + // synchronisation. + auto create_semaphore = [&](vk::UniqueSemaphore& s) { auto [semaphore_result, semaphore] = device->GetLogicalDeviceHandle().createSemaphoreUnique(semaphore_create_info); assert(("Semaphore creation failed!", semaphore_result == vk::Result::eSuccess)); s = std::move(semaphore); }; + // Simple lambda that creates an unique handle to a fence auto create_fence = [&](vk::UniqueFence& f) { auto [fence_result, fence] = device->GetLogicalDeviceHandle().createFenceUnique(fence_create_info); @@ -295,6 +351,7 @@ std::for_each(m_image_ready_semaphores.begin(), m_image_ready_semaphores.end(), create_semaphore); std::for_each(m_render_done_semaphores.begin(), m_render_done_semaphores.end(), create_semaphore); std::for_each(m_earlyz_semaphores.begin(), m_earlyz_semaphores.end(), create_semaphore); + std::for_each(m_compute_semaphores.begin(), m_compute_semaphores.end(), create_semaphore); create_fence(m_draw_fence); } @@ -310,7 +367,7 @@ assert(("Timed out waiting for draw fence", fence_result == vk::Result::eSuccess)); assert(("Failed to reset draw fence", fence_reset_result == vk::Result::eSuccess)); - + // Ensure that an image is ready and the prior frame has been submitted. vk::Result acquire_image_result = device->GetLogicalDeviceHandle().acquireNextImageKHR( *m_swapchain, std::numeric_limits::max(), @@ -320,9 +377,12 @@ if(acquire_image_result != vk::Result::eSuccess) return acquire_image_result; - + // Synchronise the different command buffers so they execute in sequence. SyncDepthPass(device); + SyncCompute(device); SyncColorPass(device); + + // Synchronise presentation and present image. SyncPresentation(device, image_index); m_current_frame = (m_current_frame + 1) % MAX_IMAGES_IN_FLIGHT; @@ -332,17 +392,8 @@ { auto [capabilities_result, surface_capabilities] = device->GetPhysicalDeviceHandle().getSurfaceCapabilitiesKHR(surface); - - // todo: assert result - - auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); - - // todo: assert formats - + auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device->GetPhysicalDeviceHandle().getSurfacePresentModesKHR(surface); - - // todo: assert modes - return std::make_tuple(surface_capabilities, formats, present_modes); } @@ -390,11 +441,12 @@ render_pass_info.setClearValues(m_clear_values); m_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport can be inverted in the Y axis. However, it is less overheadd to invert the projection matrix. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; // static_cast(m_extent.height); viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); // -static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -413,13 +465,14 @@ auto cam_desc_set = render_graph->DescriptorSets.Get("Camera" + std::to_string(i)); auto light_desc_set = render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)); - + //Draw each mesh of each model. for(auto& model : scene->GetModels()) { m_command_buffers[i]->bindIndexBuffer(model->GetIndexBuffer().GetBuffer(), 0, vk::IndexType::eUint32); for(auto&& mesh : model->GetMeshes()) { + // Each mesh is stored in the same vertex buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->bindVertexBuffers(0, model->GetVertexBuffer().GetBuffer(), mesh.GetVertexOffset()); std::vector desc_sets = { @@ -448,6 +501,8 @@ vk::PipelineBindPoint::eGraphics, graphics_pipeline->GetLayout(), 3, light_desc_set, nullptr); + + //Each mesh is stored in the same index buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -460,9 +515,10 @@ void SwapChain::RecordDepthCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); - fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); + //Depth prepass. + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); + fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); for(size_t i = 0; i < m_depth_command_buffers.size(); ++i) { vk::CommandBufferBeginInfo cmd_begin_info; @@ -479,11 +535,12 @@ m_depth_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport not inverted, instead projection matrix is. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -496,6 +553,7 @@ auto scene = context.m_loaded_scene; + //Buffers used in the depth prepass shader. std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), render_graph->DescriptorSets.Get("Model" + std::to_string(i)), @@ -523,6 +581,7 @@ render_graph->DescriptorSets.Get("Model" + std::to_string(i)), (uint32_t)(mesh.GetMeshIndex() * context.GetRequiredDynamicAlignment())); + m_depth_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -533,10 +592,10 @@ } } -void SwapChain::RecordComputeCommands() +void SwapChain::RecordComputeCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); fpr::Pipeline* compute_pipeline = render_graph->ComputePipelines.Get("LightCulling"); for(size_t i = 0; i < m_compute_command_buffers.size(); ++i) { @@ -545,25 +604,91 @@ [[maybe_unused]] auto cmd_begin_result = m_compute_command_buffers[i]->begin(cmd_begin_info); assert(cmd_begin_result == vk::Result::eSuccess); - //todo barriers + //Need to retrieve the actual buffers, not descriptors. Light culling compute shaders require barriers to buffers. + fpr::Buffer* culled_buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(m_current_frame)); + fpr::Buffer* light_buffer = render_graph->Buffers.Get("PointLight" + std::to_string(m_current_frame)); - std::array desc_sets = { - render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), - render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)), - render_graph->DescriptorSets.Get("DepthSampler"), - }; + std::vector barriers; + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + + //Pre cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eFragmentShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, + nullptr, + barriers, + nullptr); + + std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), + render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)) }; + + m_compute_command_buffers[i]->bindDescriptorSets( vk::PipelineBindPoint::eCompute, compute_pipeline->GetLayout(), 0, desc_sets, nullptr); - m_compute_command_buffers[i]-> - pushConstants( - compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + m_compute_command_buffers[i]->bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + compute_pipeline->GetLayout(), + 2, + render_graph->DescriptorSets.Get("DepthSampler"), + nullptr); - glm::vec3 group_count = { m_viewport_push_constant.viewport_size_tile_size.z, - m_viewport_push_constant.viewport_size_tile_size.w, + m_compute_command_buffers[i]->pushConstants( + compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + + glm::vec3 group_count = { m_viewport_push_constant.horizontal_tile_count, + m_viewport_push_constant.vertical_tile_count, 1 }; + //Bind compute pipeline and dispatch shader for each tile. + m_compute_command_buffers[i]->bindPipeline(vk::PipelineBindPoint::eCompute, compute_pipeline->GetPipeline()); + m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, (uint32_t)group_count.z); - m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, 1); + + barriers.clear(); + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + //Post cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + nullptr, + barriers, + nullptr); [[maybe_unused]] auto cmd_end_result = m_compute_command_buffers[i]->end(); assert(cmd_end_result == vk::Result::eSuccess); @@ -571,7 +696,7 @@ } -vk::Extent2D SwapChain::QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) +vk::Extent2D SwapChain::QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) { vk::Extent2D result{}; @@ -597,7 +722,7 @@ return result; } -const vk::Format& SwapChain::GetFormat() const FPR_NOEXCEPT +const vk::Format& SwapChain::GetSwapchainImageFormat() const FPR_NOEXCEPT { return m_surface_format.format; } @@ -621,6 +746,4 @@ { return m_depth_buffer.get(); } - - } // namespace fpr \ No newline at end of file diff --git a/src/Texture.cpp b/src/Texture.cpp index fc1d838..64bb030 100644 --- a/src/Texture.cpp +++ b/src/Texture.cpp @@ -4,7 +4,6 @@ #include "Buffer/Buffer.h" #include "Context.h" #include "Device.h" -#include "RNG.h" @@ -41,6 +40,7 @@ std::pair Texture::GetImageDetails(std::string_view file_name, int desired_channels) { + //Load raw image data and image size. int width; int height; int image_size; @@ -62,6 +62,7 @@ void Texture::GenerateMipMaps() { + //Run time mip map generation. auto& context = fpr::Context::Get(); auto& cmd_buffer = context.BeginSingleTimeCommands(); @@ -79,6 +80,7 @@ uint32_t mip_width = m_width; uint32_t mip_height = m_height; + //For each mip level, blit image and reduce resolution in half. for(uint32_t i = 1; i < m_mip_levels; ++i) { mip_img_barrier.subresourceRange.baseMipLevel = i - 1; @@ -134,6 +136,7 @@ mip_height = half_else_one(mip_height); } + //pipeline barrier to transition mip image layout. mip_img_barrier.subresourceRange.baseMipLevel = m_mip_levels - 1; mip_img_barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; mip_img_barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; @@ -156,6 +159,8 @@ auto& context = fpr::Context::Get(); auto device = context.GetDevice(); + //Load raw bytes of image and load it into a staging buffer. Textures are generally unchanged throughout a program + //so they can be transferred to the GPU with a staging buffer and left untouched. auto [size, raw_data] = GetImageDetails(file_name, desired_channels); vk::MemoryPropertyFlags flags = vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent; fpr::Buffer staging_buffer(size, vk::BufferUsageFlagBits::eTransferSrc, flags); @@ -210,7 +215,7 @@ { auto& context = fpr::Context::Get(); auto device = context.GetDevice(); - + //Vulkan allows for image and samppler to be combined. Texture loaded in with Sampler. vk::DescriptorSetLayout sampler_layout = context.m_render_graph->DescriptorSetLayouts.Get("Sampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; @@ -236,6 +241,7 @@ void TransitionImageLayout(vk::Image image, const LayoutTransitionRules& transition_rules, uint32_t mip_levels) { + //Transition the image into the layout specified by the rules passed. auto& context = fpr::Context::Get(); auto& single_use_cmd_buffer = context.BeginSingleTimeCommands(); diff --git a/src/VulkanRenderer.cpp b/src/VulkanRenderer.cpp index 0fadb80..324756f 100644 --- a/src/VulkanRenderer.cpp +++ b/src/VulkanRenderer.cpp @@ -6,7 +6,6 @@ #include "Vertex.h" #include "Scene.h" #include "Model.h" -#include "RNG.h" #include "Camera.h" namespace fpr @@ -26,6 +25,7 @@ void VulkanRenderer::SetScene(fpr::Scene* scene) { + //Setting scene requires that commands are re-recorded, as now different models are present. fpr::Context& context = fpr::Context::Get(); context.m_loaded_scene = scene; context.GetSwapChain()->RecordCommands(); @@ -33,34 +33,22 @@ { for(auto& mesh : model->GetMeshes()) { - auto matrix = mesh.GetMatrix(); - //auto matrix = glm::scale(glm::mat4(1), {100,100,100}); + auto matrix = mesh.GetMatrix(); auto index = mesh.GetMeshIndex(); memcpy(&context.PER_MODEL_UBO.model_matrix[index], &matrix, sizeof(glm::mat4)); - //auto debug = PER_MODEL_UBO.model_matrix[index]; - //(debug); + } } - - } - - void VulkanRenderer::Draw(fpr::Camera* camera) { - glfwPollEvents(); - - static double now; static double delta; static double last_time; - auto& context_ref = fpr::Context::Get(); - auto debug = context_ref.PER_MODEL_UBO.model_matrix[0]; - (debug); - - + //Update the model matrix. + //Big performance hit. Could differentiate between static and dynamic models, as most models are static iin most cases and can live in a buffer that doesn't need to be touched. context_ref.m_render_graph->Buffers.Get("Model" + std::to_string(context_ref.GetSwapChain()->GetCurrentFrameIdx())) ->Update(context_ref.PER_MODEL_UBO.model_matrix); @@ -86,6 +74,8 @@ now = glfwGetTime(); delta = now - last_time; last_time = now; + + glfwPollEvents(); } diff --git a/Forward Plus Renderer.vcxproj b/Forward Plus Renderer.vcxproj index db3a3af..2eae8d7 100644 --- a/Forward Plus Renderer.vcxproj +++ b/Forward Plus Renderer.vcxproj @@ -170,6 +170,7 @@ true MultiThreadedDebugDLL + OldStyle Console @@ -228,6 +229,7 @@ + false @@ -279,15 +281,14 @@ - + - diff --git a/Forward Plus Renderer.vcxproj.filters b/Forward Plus Renderer.vcxproj.filters index 4615c5f..7a63d94 100644 --- a/Forward Plus Renderer.vcxproj.filters +++ b/Forward Plus Renderer.vcxproj.filters @@ -45,6 +45,7 @@ Resource Files\Shaders + @@ -139,9 +140,6 @@ Header Files - - Header Files - Header Files @@ -166,9 +164,6 @@ Header Files\Buffers - - Header Files - Header Files @@ -181,5 +176,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 0000000..76fb673 --- /dev/null +++ b/cpp.hint @@ -0,0 +1,4 @@ +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 +#define FPR_NOEXCEPT diff --git a/include/Buffer/Buffer.h b/include/Buffer/Buffer.h index 8bb66d7..8bff9cb 100644 --- a/include/Buffer/Buffer.h +++ b/include/Buffer/Buffer.h @@ -22,8 +22,6 @@ Buffer operator=(const Buffer& other) = delete; - Buffer(const Buffer& other) = delete; - //Buffer(Buffer&& other) FPR_NOEXCEPT; virtual void Transfer(Buffer* other); void Update(const void* new_data); diff --git a/include/Buffer/DepthBuffer.h b/include/Buffer/DepthBuffer.h index b56fb62..28a4919 100644 --- a/include/Buffer/DepthBuffer.h +++ b/include/Buffer/DepthBuffer.h @@ -20,6 +20,8 @@ vk::FormatFeatureFlagBits::eDepthStencilAttachment; vk::Format QueryBestFormat(fpr::Device* device); + + public: DepthBuffer(fpr::Device* device, fpr::SwapChain* swapchain, vk::ImageTiling tiling = vk::ImageTiling::eOptimal); @@ -34,9 +36,12 @@ vk::ImageTiling tiling, vk::FormatFeatureFlags required_flags); - static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { vk::Format::eD32SfloatS8Uint, - vk::Format::eD16Unorm, - vk::Format::eD24UnormS8Uint }; + static constexpr std::array DEPTH_BUFFER_IMAGE_FORMATS = { + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint, + + + }; DepthBuffer(const DepthBuffer& other) = delete; DepthBuffer(const DepthBuffer&& other) = delete; diff --git a/include/Camera.h b/include/Camera.h index 89f93d9..664387b 100644 --- a/include/Camera.h +++ b/include/Camera.h @@ -21,8 +21,8 @@ glm::vec3 position = { 0.0f, 0, 0.0f }, glm::quat rotation = { 1.0f, 0.0f, 0.0f, 0.0f }, float FOV = 60.0f, - float near = 1.5f, - float far = 100000.0f); + float near = 1.0f, + float far = 10000); void Update(); diff --git a/include/ConstrainedMap.h b/include/ConstrainedMap.h index afb91bb..c09f595 100644 --- a/include/ConstrainedMap.h +++ b/include/ConstrainedMap.h @@ -1,17 +1,21 @@ #pragma once #include + +//Concept constraining a type to have a .get() method. template concept unique_handle = requires { { std::declval().get() }; //-> std::same_as())>; }; + template struct unique_handle_type { using type = T; }; +//Requires a type to have a ::element_type alias. template requires unique_handle struct unique_handle_type @@ -19,12 +23,14 @@ using type = T::element_type; }; +//Not every type has a pointer that is functionally equivalent, i.e file handles. template struct unique_handle_type> { using type = std::unique_ptr::pointer; }; +//Requires that std::hash overload is provided for the given type. template concept Hashable = requires(T a) { @@ -36,39 +42,47 @@ template class ConstrainedMap { + //Type aliases using map_type = typename std::unordered_map; using iterator = typename map_type::iterator; using mapped_type = typename map_type::mapped_type; using handle_element_type = typename unique_handle_type::type; + //helper function to extract iterator to mapped value. map_type hash_table; iterator found_value_iter(const std::string& value) { return hash_table.find(value); }; public: + + //Get overload that requires an unique handle handle_element_type Get(const std::string& name) requires(unique_handle) { return found_value_iter(name)->second.get(); }; + //Add overload that requires a unique handle handle_element_type Add(const std::string& name, map_value& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Add overload that requires a unique handle and takes an r-value ref handle_element_type Add(const std::string& name, map_value&& resource) requires(unique_handle) { hash_table[name] = std::move(resource); return hash_table[name].get(); }; + //Get overload that returns a reference to the value and that the held value is not an unique handle. mapped_type& Get(const std::string& name) requires(not(unique_handle)) { return found_value_iter(name)->second; }; + //Add overload that requires the mapped valuee to not be an unique handle. mapped_type& Add(const std::string& name, map_value resource) requires(not(unique_handle)) { hash_table[name] = resource; diff --git a/include/Context.h b/include/Context.h index d5c6689..344ee7a 100644 --- a/include/Context.h +++ b/include/Context.h @@ -11,7 +11,6 @@ #include "InputHandler.h" namespace fpr { - struct Version { uint32_t m_variant; @@ -25,6 +24,7 @@ uint32_t MakeVersion() const FPR_NOEXCEPT; }; +// Context singleton, cannot be derived from, has no public constructor. Init() has to be called before it is used. class Context final { static VulkanWindow* s_window; @@ -34,6 +34,7 @@ static fpr::Version s_engine_version; static fpr::Version s_vulkan_version; + // Vulkan validation layer handling. Massive performance overhead so turned off if not in debug. #ifdef NDEBUG const bool validation_layers_enabled = false; const std::vector m_validation_layers; @@ -46,6 +47,7 @@ vk::UniqueInstance m_instance; std::vector m_extensions; + // Vulkan struct initialiser helper function. [[nodiscard]] vk::ApplicationInfo ApplicationInfo( const char* application_name, const Version& app_version, @@ -68,27 +70,23 @@ vk::UniqueSwapchainKHR m_old_swapchain; ; public: + // Type alias for what a PipelineCreation function should consume. // Type, name, settings, parent(optional); using PipelineCreationData = std::vector>>; private: - std::unique_ptr m_device; vk::UniqueSurfaceKHR m_surface; vk::UniqueCommandPool m_command_pool; vk::UniqueCommandPool m_compute_comand_pool; std::unique_ptr m_swapchain; std::function m_pipeline_creation_fn; - vk::UniqueDescriptorPool m_descriptor_pool; - - vk::UniqueCommandBuffer m_single_time_cmd_buffer; - VulkanWindow* m_window; - uint32_t m_dynamic_alignment = sizeof(glm::mat4); + // Default pipeline creation callback for "out-of-the-box" renderer usage. static std::function DEFAULT_PIPELINE_CREATION_CALLBACK; Context( @@ -103,6 +101,8 @@ void CreateSingleTimeCommandBuffer(); + // Myers' singleton pattern, requires a public "Get" function with a private constructor and public "Init()" function. + // Ensures no memory. static Context& GetImpl( VulkanWindow* Window = nullptr, const std::string& app_name = s_app_name, @@ -112,8 +112,6 @@ fpr::Version vulkan_version = s_vulkan_version); - - void CreateModelDescriptorSet(); void CreateCameraDescriptorSet(); void CreateCameraDescriptorSetLayout(); @@ -127,9 +125,9 @@ static constexpr glm::vec3 RIGHT_VECTOR = glm::vec3(1, 0, 0); - void CreateDescriptorSets(); + void CreateDescriptorSets(); - struct PerModelData + struct PerModelData { glm::mat4* model_matrix; } PER_MODEL_UBO; @@ -160,9 +158,8 @@ void CreatePipelines(std::function fn = DEFAULT_PIPELINE_CREATION_CALLBACK); void CreateSceneBuffers(); - void LoadScene(const char* file_name); - vk::CommandPool& GetGraphicsCmdPool() FPR_NOEXCEPT; + vk::CommandPool& GetComputeCmdPool() FPR_NOEXCEPT; vk::DescriptorPool& GetDescriptorPool(); vk::SurfaceKHR& GetSurface() FPR_NOEXCEPT; [[nodiscard]] Device* GetDevice() const FPR_NOEXCEPT; diff --git a/include/Device.h b/include/Device.h index 849e307..5c396b7 100644 --- a/include/Device.h +++ b/include/Device.h @@ -7,7 +7,7 @@ class Device { const std::vector required_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - + // List of wanted formats for swapchain image presentation. const vk::Format wanted_formats[2] = { vk::Format::eR8G8B8A8Srgb, vk::Format::eB8G8R8A8Srgb }; vk::UniqueDevice m_logical_device; @@ -27,9 +27,7 @@ bool IsRequiredExtensionsSupported(vk::PhysicalDevice device) const; vk::SurfaceFormatKHR QueryBestSurfaceFormat(const std::vector& formats); - public: - Device(vk::SurfaceKHR surface, vk::Instance& instance); [[nodiscard]] uint32_t GetGraphicsQueueIndex() const FPR_NOEXCEPT; diff --git a/include/InputHandler.h b/include/InputHandler.h index 054b877..5b37dc4 100644 --- a/include/InputHandler.h +++ b/include/InputHandler.h @@ -186,6 +186,12 @@ static glm::highp_dvec2 new_cursor_pos; static glm::highp_dvec2 dx_dy; + //IMPORTANT: + //An input handler is registered and held in this **STATIC** vector when constructed. This allows for multiple input handlers. + //Therefore, I can bind different keys to different actions based on what they are resposible for. + //For example, a camera input handler may bind the mouse for movoement. + //However, I may also want to use the mouse for camera picking. + //Having seperate input handlers allows for usage of same keys, and the user can pick between different input handlers based on the internal state of their program. static std::vector S_INPUT_HANDLERS; static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); diff --git a/include/Light.h b/include/Light.h index 0f8ee70..a1de4c8 100644 --- a/include/Light.h +++ b/include/Light.h @@ -1,28 +1,21 @@ #pragma once #include + #include "Buffer/Buffer.h" +#include "Context.h" namespace fpr { -struct PointLightUBO -{ - alignas(16) glm::vec3 color; - alignas(4) float luminance; - alignas(16) glm::vec3 position; -}; - class PointLight { static size_t light_count; - glm::vec3 m_position; glm::vec3 m_color; float m_luminance; - PointLightUBO m_ubo; + PointLightData m_ubo; public: size_t light_index; - static constexpr uint32_t MAX_LIGHTS = 1024; - static PointLightUBO s_point_light_data[MAX_LIGHTS]; + static PointLightUBO s_point_light_data; PointLight(glm::vec3 m_position, glm::vec3 color = { 1.0f, 1.0f, 1.0f }, float radius = 50.0f); PointLight() = default; diff --git a/include/Material.h b/include/Material.h deleted file mode 100644 index 61dc43d..0000000 --- a/include/Material.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -#include "Texture.h" -#include "ConstrainedMap.h" -namespace fpr -{ -enum class ETextureType -{ - ETT_Diffuse, - ETT_Unlit, - ETT_AmbientOcclusion, - ETT_BaseColor, - ETT_DiffuseRoughness, - ETT_Displacement, - ETT_EmissionColor, - ETT_Emissive, - ETT_Height, - ETT_Lightmap, - ETT_Metalness, - ETT_Normals, - ETT_NormalCamera, - ETT_Opacity, - ETT_Reflection, - ETT_Shininess, - ETT_Specular, - ETT_Unknown, - ETT_MetallicRoughness -}; -enum class EAlphaMode -{ - EAM_MASK, - EAM_OPAQUE -}; - -class Material -{ - std::string m_name; -public: - ConstrainedMap> Textures; - Material(); -}; - -class gltfPBRMaterial : public fpr::Material -{ - EAlphaMode m_alpha_mode; - float m_base_color_factor; -}; - -} // namespace fpr diff --git a/include/Model.h b/include/Model.h index 9f1be76..8d6b798 100644 --- a/include/Model.h +++ b/include/Model.h @@ -4,6 +4,7 @@ namespace fpr { + //Helper struct to pass on primitive data to construct meshes and buffers. struct ModelPrimitives { std::vector vertices; diff --git a/include/Pipeline.h b/include/Pipeline.h index 22c3675..2addc4d 100644 --- a/include/Pipeline.h +++ b/include/Pipeline.h @@ -5,6 +5,8 @@ #include "Vertex.h" namespace fpr { + //Pipelines have many different possible options. + //All of these are contained in this struct. This struct can be passed into a Pipeline constructor instead of all of the members individually. struct PipelineOptions { std::vector> modules; @@ -20,8 +22,8 @@ vk::PipelineCreateFlags flags; vk::RenderPass render_pass; uint32_t subpass = 0; - vk::Pipeline base_pipeline_handle = nullptr; - int32_t base_pipeline_index = -1; + vk::Pipeline base_pipeline_handle = nullptr; //most pipelines don't have a parent + int32_t base_pipeline_index = -1; // thus they don't have a pipeline index. std::vector push_constant_ranges; }; diff --git a/include/RNG.h b/include/RNG.h deleted file mode 100644 index 68f0eff..0000000 --- a/include/RNG.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include -#include -#include -#include -template -concept distribution_type = requires -{ - T::param_type::distribution_type; -} -|| requires -{ - T::distribution_type; -}; - -template -class RNG -{ - using result_type = typename Distribution::result_type; - mutable std::mt19937 mt; - std::random_device random_device; - Distribution m_distribution; - std::seed_seq m_seed; -public: - RNG(result_type min = 0, result_type max = 1): m_distribution(min, max) - { - mt.seed(random_device()); - } - - [[nodiscard]] constexpr result_type Get() const - { - return m_distribution(mt); - } - - [[nodiscard]] constexpr std::vector Get(size_t count) const - { - std::vector result; - result.reserve(count); - for(size_t i = 0; i < count; ++i) { result.push_back(m_distribution(mt)); } - return result; - } - - void Seed(std::seed_seq seed) - { - mt.seed(seed); - } - - [[nodiscard]] constexpr auto& GetSeed() const FPR_NOEXCEPT - { - return m_seed; - } -}; - diff --git a/include/RenderGraph.h b/include/RenderGraph.h index f14a166..897666f 100644 --- a/include/RenderGraph.h +++ b/include/RenderGraph.h @@ -7,23 +7,24 @@ #include "Scene.h" namespace fpr { +// Central reource management. class RenderGraph { template using LabelledResource = ConstrainedMap; public: RenderGraph() = default; - LabelledResource> Cameras; - LabelledResource> RenderPasses; - LabelledResource> Scenes; - LabelledResource> GraphicsPipelines; - LabelledResource> ComputePipelines; - LabelledResource DescriptorSetLayouts; - LabelledResource> DiffuseTextures; - LabelledResource> NormalMaps; - LabelledResource> SpecularTextures; - LabelledResource Samplers; - LabelledResource> Buffers; - LabelledResource DescriptorSets; + LabelledResource> Cameras; + LabelledResource> RenderPasses; + LabelledResource> Scenes; + LabelledResource> GraphicsPipelines; + LabelledResource> ComputePipelines; + LabelledResource DescriptorSetLayouts; + LabelledResource> DiffuseTextures; + LabelledResource> NormalMaps; + LabelledResource> SpecularTextures; + LabelledResource Samplers; + LabelledResource> Buffers; + LabelledResource DescriptorSets; }; } // namespace fpr \ No newline at end of file diff --git a/include/Scene.cpp b/include/Scene.cpp index f1dc8da..013fe78 100644 --- a/include/Scene.cpp +++ b/include/Scene.cpp @@ -6,6 +6,7 @@ { Scene::Scene(const char* file_name) { + //Parse scene YAML file and load in meshes. std::vector yaml_bytes = ReadFile(file_name); std::string yaml(yaml_bytes.begin(), yaml_bytes.end()); ryml::Tree tree = ryml::parse(ryml::to_csubstr(yaml)); @@ -13,7 +14,6 @@ ryml::NodeRef model_data = tree["Models"]; for(auto child : tree["Models"].children()) { - std::string file; child["File"] >> file; @@ -64,6 +64,7 @@ void fpr::Scene::AddLights(std::vector& new_lights) { m_lights.insert(m_lights.end(), new_lights.begin(), new_lights.end()); + } const std::vector>& Scene::GetModels() const FPR_NOEXCEPT diff --git a/include/Scene.h b/include/Scene.h index 99938e1..1a1f52a 100644 --- a/include/Scene.h +++ b/include/Scene.h @@ -9,19 +9,17 @@ class Scene { std::vector> m_models; - std::vector m_lights; + std::vector m_lights; public: static constexpr const char* SCENE_FILE_EXT = ".scene"; Scene(const char* file_name); [[nodiscard]] const std::vector>& GetModels() const FPR_NOEXCEPT; - [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; + [[nodiscard]] const std::vector& GetLights() const FPR_NOEXCEPT; void AddModel(std::shared_ptr& new_model); void AddModels(std::vector>& new_models); void AddLight(PointLight new_light); void AddLights(std::vector& new_lights); - - }; } // namespace fpr \ No newline at end of file diff --git a/include/ShaderObjects.h b/include/ShaderObjects.h new file mode 100644 index 0000000..8f2e9ec --- /dev/null +++ b/include/ShaderObjects.h @@ -0,0 +1,45 @@ +#pragma once +#include +#define FPR_MAX_LIGHTS 8192 +#define FPR_MAX_LIGHTS_PER_TILE 1024 +namespace fpr +{ +static constexpr unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +struct ViewportConstants +{ + alignas(16) glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; + alignas(8) glm::ivec2 viewport_size = {}; + alignas(8) glm::ivec2 tile_size; + alignas(4) unsigned int horizontal_tile_count; + alignas(4) unsigned int vertical_tile_count; + alignas(4) unsigned int MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + alignas(4) unsigned int MAX_LIGHTS = FPR_MAX_LIGHTS; +}; + +struct LightCullingData +{ + unsigned int visible_light_indices[FPR_MAX_LIGHTS_PER_TILE]; + unsigned int total_visible_lights; +}; + + +struct PointLightData +{ + alignas(16) glm::vec3 color; + alignas(4) float luminance; + alignas(16) glm::vec3 position; +}; + +struct PointLightUBO +{ + PointLightData lights[FPR_MAX_LIGHTS]; + alignas(4) unsigned int light_count; +}; + +struct UBOPerFrame +{ + alignas(16) glm::mat4 projection_matrix; + alignas(16) glm::mat4 view_matrix; + alignas(16) glm::vec3 camera_position; +}; +} // namespace fpr \ No newline at end of file diff --git a/include/SwapChain.h b/include/SwapChain.h index e324011..0fe5dfe 100644 --- a/include/SwapChain.h +++ b/include/SwapChain.h @@ -3,18 +3,20 @@ #include "Buffer/DepthBuffer.h" #include "VulkanWindow.h" + namespace fpr { class RenderPass; -struct ViewportConstants -{ - glm::ivec4 viewport_size_tile_size = {}; - glm::vec4 ambient_light = { 1.0, 0.8, 0.3, 0.05f }; -}; + + class SwapChain { ViewportConstants m_viewport_push_constant; - static constexpr glm::vec2 DEFAULT_TILE_SIZE = { 16, 16 }; + static constexpr glm::vec2 DEFAULT_TILE_SIZE = { + 32, + 32, + }; // Tile are used use to subdivide the view frustum for light culling. + using SwapchainDetails = std::tuple, std::vector>; @@ -28,12 +30,15 @@ std::vector m_swapchain_framebuffers; std::vector m_image_ready_semaphores; std::vector m_render_done_semaphores; - vk::UniqueFence m_draw_fence; - std::vector m_earlyz_semaphores; - uint32_t MAX_IMAGES_IN_FLIGHT = 2; - uint32_t m_current_frame = 0; - vk::Extent2D m_extent; - bool is_recreation = false; + std::vector m_compute_semaphores; + + vk::UniqueFence m_draw_fence; + std::vector m_earlyz_semaphores; + uint32_t MAX_IMAGES_IN_FLIGHT = 2; //Want at least double buffering if possible. + uint32_t m_current_frame = 0; + vk::Extent2D m_extent; + + bool is_recreation = false; //Resizing requires swapchain recreation. Most of the time, not needed std::vector m_depth_framebuffers; std::vector m_depth_command_buffers; @@ -53,30 +58,32 @@ SwapchainDetails QuerySwapChainDetails(fpr::Device* device, vk::SurfaceKHR& surface); vk::SurfaceFormatKHR QueryFirstSuitableFormat(const std::vector& formats); - vk::Extent2D QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); + vk::Extent2D QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities); void CreateSynchronisation(fpr::Device* device); void SyncDepthPass(fpr::Device* device); void SyncColorPass(fpr::Device* device); void SyncPresentation(fpr::Device* device, uint32_t inmage_index); + void SyncCompute(fpr::Device* device); + void CreateSwapchain(fpr::Device* device, vk::SurfaceKHR& surface); void CreateImageViews(fpr::Device* device); void CreateFrameBuffers(fpr::Device* device); void CreateRenderPasses(fpr::Device* device); - public: - void RecordCommands(); - void CreateCommandBuffers(); - vk::Result SubmitFrame(); - void RecordRenderCommands(); - void RecordDepthCommands(); + const ViewportConstants& GetViewportConstants() const FPR_NOEXCEPT; + void RecordCommands(); + void CreateCommandBuffers(); + vk::Result SubmitFrame(); + void RecordRenderCommands(); + void RecordDepthCommands(); void RecordComputeCommands(); const std::vector& GetImages(); fpr::DepthBuffer* GetDepthBuffer() FPR_NOEXCEPT; const vk::Extent2D& GetCurrentExtent(); - const vk::Format& GetFormat() const FPR_NOEXCEPT; + const vk::Format& GetSwapchainImageFormat() const FPR_NOEXCEPT; uint32_t GetCurrentFrameIdx() const; uint32_t GetMaxFrameIdx() const; @@ -84,7 +91,8 @@ SwapChain( fpr::Device* device, vk::SurfaceKHR& surface, - std::optional old_swapchain = std::nullopt); + std::optional old_swapchain = std::nullopt, + glm::ivec2 tile_size = DEFAULT_TILE_SIZE); SwapChain(const SwapChain& other) = delete; SwapChain(const SwapChain&& other) = delete; diff --git a/include/Texture.h b/include/Texture.h index 61ae214..40a26da 100644 --- a/include/Texture.h +++ b/include/Texture.h @@ -4,6 +4,7 @@ #include namespace fpr { + //Laout transition is relatively complicated. Easier to reason about when the rules can be packed into a struct. struct LayoutTransitionRules { vk::ImageLayout old_layout; @@ -18,6 +19,7 @@ class Texture { + //The layout any texture must be in before run-time mip-map generation. static constexpr const LayoutTransitionRules PRE_MIP_LAYOUT = { vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::ImageAspectFlagBits::eColor, vk::AccessFlagBits::eNoneKHR, vk::AccessFlagBits::eTransferWrite, vk::PipelineStageFlagBits::eTopOfPipe, diff --git a/include/Vertex.h b/include/Vertex.h index d6c91a4..632bdf3 100644 --- a/include/Vertex.h +++ b/include/Vertex.h @@ -3,6 +3,7 @@ #include "Vertex.h" namespace fpr { + //Used in pipeline creation, needed to allow shaders to access vertex data appropriately. struct VertexInfo { std::vector input_bindings; diff --git a/include/VulkanRenderer.h b/include/VulkanRenderer.h index 14ff181..d64e0eb 100644 --- a/include/VulkanRenderer.h +++ b/include/VulkanRenderer.h @@ -1,25 +1,14 @@ #pragma once #include - #include "IO.h" #include "VulkanWindow.h" - - namespace fpr { -struct UBOPerFrame -{ - alignas(16) glm::mat4 projection_matrix; - alignas(16) glm::mat4 view_matrix; - alignas(16) glm::vec3 camera_position; -}; - class Camera; class Scene; class VulkanRenderer { - UBOPerFrame m_per_frame_UBO; public: void Draw(fpr::Camera* camera); diff --git a/include/stdafx.h b/include/stdafx.h index e462faa..82cae5c 100644 --- a/include/stdafx.h +++ b/include/stdafx.h @@ -1,9 +1,10 @@ #pragma once #define GLFW_INCLUDE_VULKAN #define VULKAN_HPP_NO_EXCEPTIONS +//Precompiled headers. Used to speed up compile times. #define GLM_FORCE_ZERO_TO_ONE +#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define FPR_NOEXCEPT - #include #include #include @@ -13,10 +14,8 @@ #include #include #include - #include #include - #include #include #include @@ -28,4 +27,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include "ShaderObjects.h" \ No newline at end of file diff --git a/shaders/FragmentShader.frag b/shaders/FragmentShader.frag index 4c939b9..a19337d 100644 --- a/shaders/FragmentShader.frag +++ b/shaders/FragmentShader.frag @@ -1,11 +1,10 @@ #version 450 -const int light_count = 1024; - +const int light_count = 8192; struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; @@ -16,6 +15,7 @@ layout(location = 4) in vec3 in_tangent; layout(location = 5) in vec3 in_bitangent; +layout(early_fragment_tests) in; layout(binding = 0) uniform CameraUBO { mat4 projection; @@ -25,43 +25,70 @@ layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 ambient_light; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint max_lights_per_tile; + uint max_lights; + } viewport_constants; layout(set = 2, binding = 0) uniform sampler2D texture_sampler; +struct LightCullingData +{ + uint visible_light_indices[1024]; + uint total_visible_lights; +}; + layout(set = 3, binding = 0) buffer PointLightSSBO { PointLight lights[light_count]; } point_light_SSBO; +layout(set = 3, binding = 1) buffer readonly VisibleLights +{ + LightCullingData visible_lights[]; +}; layout(location = 0) out vec4 out_color; void main() { - vec4 diffuse_map = texture(texture_sampler, in_uv); + //Get the tile based on current fragment. + ivec2 tile = ivec2(gl_FragCoord.xy / viewport_constants.tile_size); + uint tile_index = tile.y * viewport_constants.horizontal_tile_count + tile.x; + + //Base color and base illuminance from texture and ambient light. + vec4 diffuse_map = texture(texture_sampler, in_uv); vec3 base_illuminance = diffuse_map.rgb * (viewport_constants.ambient_light.rgb * viewport_constants.ambient_light.a); vec3 illuminance = base_illuminance; - for(int i = 0; i < light_count; ++i) + + //Iterate through the visible lights in the current tile. + for(int i = 0; i < visible_lights[tile_index].total_visible_lights; ++i) { - PointLight light = point_light_SSBO.lights[i]; + //Extract the light based on the compute shader culled storage buffer. + PointLight light = point_light_SSBO.lights[visible_lights[tile_index].visible_light_indices[i]]; vec3 light_direction = normalize(light.position - in_position); - - float lambertian = max(dot(light_direction, in_normal), 0.0); - if(lambertian > 0.0f) + + //If the light points away, fragment not affected. + float lambertian = max(dot(light_direction, in_normal), 0.0); + if(lambertian > 0.0f) + { + //if the distance to light is smaller than light radius, fragment not affected. + float light_distance = distance(light.position, in_position); + if (light_distance < light.luminance) { - float light_distance = distance(light.position, in_position); - if (light_distance < light.luminance) - { - vec3 camera_direction = normalize(camera.position - in_position); - vec3 H = normalize(light_direction + camera_direction); - float theta = max(dot(H, in_normal), 0.0); - float specular = pow(theta, 256.0f); - float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); - illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); - } + //Fake specular + vec3 camera_direction = normalize(camera.position - in_position); + vec3 H = normalize(light_direction + camera_direction); + float theta = max(dot(H, in_normal), 0.0); + float specular = pow(theta, 256.0f); + float attenuation = clamp(1.0 - light_distance * light_distance / (light.luminance * light.luminance), 0.0, 1.0); + illuminance += light.color * attenuation * (lambertian * diffuse_map.rgb + specular); } } - out_color = vec4(illuminance, 1.0f); + } + out_color = vec4(illuminance, 1.0f); } \ No newline at end of file diff --git a/shaders/FragmentShader.spv b/shaders/FragmentShader.spv index ee7173f..cf8eeaf 100644 --- a/shaders/FragmentShader.spv +++ b/shaders/FragmentShader.spv Binary files differ diff --git a/shaders/LightComputeShader.comp b/shaders/LightComputeShader.comp index b886448..1059685 100644 --- a/shaders/LightComputeShader.comp +++ b/shaders/LightComputeShader.comp @@ -1,68 +1,234 @@ #version 450 -struct PointLight +//CONSTANTS +const int MAX_LIGHTS = 8192; +#define MAX_LIGHTS_IN_TILE 1024 +const vec2 ndc_begin = vec2(-1.0, -1.0); + +//STRUCTS +struct Frustum { + vec4 planes[6]; + vec3 vertices[8]; +}; + +struct LightsInTile +{ + uint indices[MAX_LIGHTS_IN_TILE]; + uint total; +}; + +struct PointLight { vec3 color; - float luminance; + float luminance; vec3 position; }; -layout(binding = 0) uniform CameraUBO -{ - mat4 projection; - mat4 view; - vec3 position; -} camera; +//SHARED VARIABLES +shared Frustum tile_frustum; +shared uint current_light_count; +shared float tile_depth_low; +shared float tile_depth_high; +//SHADER BUFERS layout(push_constant) uniform ViewportConstants { - vec4 viewport_size_tile_size; - vec4 ambient_light; + vec4 discarded1; + ivec2 viewport_size; + ivec2 tile_size; + uint horizontal_tile_count; + uint vertical_tile_count; + uint discarded2; + uint discarded3; + } viewport_constants; -vec2 viewport_size = vec2(viewport_constants.x, viewport_constants.y); -vec2 tile_size = vec2(viewport_constants.z, viewport_constants.w); +layout(std140, set = 0, binding = 0) uniform CameraUBO +{ + mat4 projection; + mat4 view; + vec3 position; +}camera; + +layout(std140, set = 1, binding =0) buffer PointLights +{ + PointLight lights[MAX_LIGHTS]; +}; + +layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO +{ + LightsInTile lights_in_tile[]; +}; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; -layout(set = 3, binding = 0) buffer PointLightSSBO -{ - PointLight lights[1024]; -}; -layout(set = 3, binding = 1) buffer writeonly LightCullingData -{ - uint visible_light_indices[]; - uint total_visible_lights; -}; +//NON-CONST VARIABLES +ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); -struct Frustum -{ - vec4 planes[6]; - vec3 vertices[8]; -}; -struct NormalisedDeviceCoordinates +//SHADER FUNCTIONS +Frustum CreateTileFrustum(ivec2 tile) { - const vec2 top_left; - const vec2 top_right; - const vec2 bottom_left; - const vec2 bottom_right; -} ndc; + //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. + mat4 inverted_pv = inverse(camera.projection * camera.view); -Frustum Create() -{ - //TODO: precalculate in CPU side - mat4 projection_view = camera.projection* camera.view; - mat4 inverse_pv = inverse(projection_view); + //Size of tiled scaled down to normalised device coordinates. + vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; - vec2 tile_vertices[4]; - tile_vertices + //Get the vertices for the given tile, used to construct the planes of the viewing frustum. + vec2 tile_vertices[4]; + tile_vertices[0] = ndc_begin + tile * tile_size_NDC; + tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); + tile_vertices[2] = tile_vertices[0] + tile_size_NDC; + tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); + + Frustum frustum; + + vec4 plane = vec4(1); + for (int i = 0; i < 4; i++) + { + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); + frustum.vertices[i] = plane.xyz / plane.w; + plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); + frustum.vertices[i + 4] = plane.xyz / plane.w; + } + + vec3 normal_to_plane = vec3(1); + //0-3 surrounding planes + for (int i = 0; i < 4; i++) + { + normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); + } + + //last two are near/far plane + normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); + + normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); + normal_to_plane = normalize(normal_to_plane); + frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); + + return frustum; } +bool LightToFrustumCollision(PointLight light, Frustum frustum) +{ + bool sphere_to_plane = true; + for (int i = 0; i < 6; i++) + { + if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) + { + sphere_to_plane = false; + break; + } + } + //Sphere to plane test failed, early return to avoid further checks. + if (!sphere_to_plane) return false; + //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. + vec3 min_bounds = light.position - vec3(light.luminance); + vec3 max_bounds = light.position + vec3(light.luminance); + //X CHECKS + int current_vertex = 0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Y CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].y > max_bounds.y)++current_vertex; + if(current_vertex == 8) return false; + } + + current_vertex=0; + for(int i=0; i<8;++i) + { + if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; + if(current_vertex == 8) return false; + } + + //Z CHECKS + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z > max_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + current_vertex=0; + for(int i=0;i<8;++i) + { + if(frustum.vertices[i].z < min_bounds.z)++current_vertex; + if(current_vertex == 8) return false; + } + //Sphere to plane test failed, box test failed, light is in tile. + return true; +} void main() { + //Extract the current tile based on the tile coordinates. + ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); + uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; + + if (gl_LocalInvocationIndex == 0) + { + //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. + tile_depth_low = 1.0f; + tile_depth_high = 0.0f; + + for (int y = 0; y < viewport_constants.tile_size.y; y++) + { + for (int x = 0; x < viewport_constants.tile_size.x; x++) + { + vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; + float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; + tile_depth_low = min(tile_depth_low, z_buffer_sample); + tile_depth_high = max(tile_depth_high, z_buffer_sample); + } + } + + //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ + if (tile_depth_low >= tile_depth_high) + { + tile_depth_low = tile_depth_high; + } + + tile_frustum = CreateTileFrustum(tile_coords); + current_light_count = 0; + } + + barrier(); + + for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) + { + if (LightToFrustumCollision(lights[i], tile_frustum)) + { + //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. + uint current_light_index = atomicAdd(current_light_count, 1); + if (current_light_index >= MAX_LIGHTS_IN_TILE)break; + lights_in_tile[tile_index].indices[current_light_index] = i; + } + } + + barrier(); + + if (gl_LocalInvocationIndex == 0) + { + lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); + } } \ No newline at end of file diff --git a/shaders/LightComputeShader.spv b/shaders/LightComputeShader.spv index 6e93382..ace3ff4 100644 --- a/shaders/LightComputeShader.spv +++ b/shaders/LightComputeShader.spv Binary files differ diff --git a/src/Buffer/Buffer.cpp b/src/Buffer/Buffer.cpp index 06badc2..47820cc 100644 --- a/src/Buffer/Buffer.cpp +++ b/src/Buffer/Buffer.cpp @@ -43,6 +43,7 @@ Buffer::~Buffer() { + //Most buffers are constructed and permanently mapped. Therefore, check if they are still mapped upon destructiion. if(mapped_data) Flush(); } @@ -52,13 +53,11 @@ return m_buffer_size; } - vk::Buffer& Buffer::GetBuffer() { return *m_buffer; } - uint32_t FindMemoryTypeIndex( vk::PhysicalDevice physical_device, uint32_t type_field, @@ -130,11 +129,12 @@ range.size = m_buffer_size; [[maybe_unused]] vk::Result flush_result = fpr::Context::Get().GetDevice()->GetLogicalDeviceHandle().flushMappedMemoryRanges(range); - assert(("Failed to flush mnapped memory.",flush_result == vk::Result::eSuccess)); + assert(("Failed to flush mapped memory.",flush_result == vk::Result::eSuccess)); } void* Buffer::GetMapped() { + //Get mapped memory. If not mapped, map it and return pointer to memory. if(!mapped_data) [[unlikely]] { vk::Result map_result; diff --git a/src/Buffer/DepthBuffer.cpp b/src/Buffer/DepthBuffer.cpp index dfb7cd1..e06cf9f 100644 --- a/src/Buffer/DepthBuffer.cpp +++ b/src/Buffer/DepthBuffer.cpp @@ -11,6 +11,7 @@ vk::Format fpr::DepthBuffer::QueryBestFormat(fpr::Device* device) { + //Iterate over wanted formats, ensure that they meet the required properties. for(const auto& format : DEPTH_BUFFER_IMAGE_FORMATS) { vk::FormatProperties format_properties = device->GetPhysicalDeviceHandle().getFormatProperties(format); @@ -29,6 +30,7 @@ return vk::Format::eUndefined; } + const vk::Image& DepthBuffer::GetDepthImage() const FPR_NOEXCEPT { return m_depth_image.get(); @@ -60,8 +62,7 @@ depth_image_create_info.extent = extent; depth_image_create_info.format = m_depth_image_format; - depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | - vk::ImageUsageFlagBits::eTransientAttachment; + depth_image_create_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled; //Sampled in compute buffer depth_image_create_info.imageType = vk::ImageType::e2D; depth_image_create_info.mipLevels = 1; depth_image_create_info.arrayLayers = 1; @@ -75,6 +76,7 @@ m_depth_image_view = CreateImageView( device, *m_depth_image, m_depth_image_format, vk::ImageAspectFlagBits::eDepth, vk::ImageViewType::e2D); + } vk::Format DepthBuffer::QueryBestImageFormat( diff --git a/src/Camera.cpp b/src/Camera.cpp index 7dc92dc..ff0a160 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -11,6 +11,8 @@ float aspect_ratio = static_cast((float)extent.width / (float)extent.height); m_projection_matrix = glm::perspective(glm::radians(FOV), aspect_ratio, near, far); + m_projection_matrix[1][1] *= -1; //INVERTED: Vulkan has inverted Y. + //One solution is to invert viewport. Instead, I invert the projection matrix as this requires less overhead. m_view_matrix = glm::transpose(glm::toMat4(rotation)) * glm::translate(glm::mat4(1.0f), -position); } diff --git a/src/Context.cpp b/src/Context.cpp index 2ca2ed0..e62ebbe 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -43,7 +43,6 @@ std::vector Context::GetRequiredExtensions(const std::vector& validation_layers) FPR_NOEXCEPT { auto [enum_result, available_layers] = vk::enumerateInstanceLayerProperties(); - // TODO: assert enum result if(validation_layers_enabled) { [[maybe_unused]] bool validation_supported = IsValidationLayersValid(validation_layers, available_layers); @@ -64,6 +63,7 @@ const std::vector& validation_layers, const std::vector& available_layers) FPR_NOEXCEPT { + // Iterate over requested validation layers, ensure they are supported. for(auto& validation_layer : validation_layers) { auto layer_name_matches = [&](const auto& layer) { return strcmp(layer.layerName, validation_layer) == 0; }; @@ -76,10 +76,10 @@ bool Context::IsVulkanSupported() const FPR_NOEXCEPT { - auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); - // TODO: assert + auto [enum_result, ext_props] = vk::enumerateInstanceExtensionProperties(); std::vector extensions = ext_props; + // Check each extension and if the current system can support Vulkan. for(const auto extensionToCheck : m_extensions) { bool hasExtensions = false; @@ -128,6 +128,7 @@ void Context::CreatePipelines(std::function fn) { + // Takes a callback that returns configuration for pipeline creation. auto pipeline_configs = std::invoke(std::bind(fn)); for(auto& pipeline_config : pipeline_configs) { @@ -160,6 +161,7 @@ void Context::CreateSingleTimeCommandBuffer() { + // Command buffers used for transitioning image layouts and so on can be 'discarded' once they have been used. vk::CommandBufferAllocateInfo cmd_buffer_alloc_info{}; cmd_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; cmd_buffer_alloc_info.commandPool = GetGraphicsCmdPool(); @@ -168,7 +170,6 @@ auto [cmd_buff_alloc_result, cmd_buff] = m_device->GetLogicalDeviceHandle().allocateCommandBuffersUnique(cmd_buffer_alloc_info); - // todo: assert m_single_time_cmd_buffer = std::move(cmd_buff[0]); } Context& Context::GetImpl( @@ -179,6 +180,7 @@ fpr::Version engine_version, fpr::Version vulkan_version) { + // Myers' singleton design pattern. Public function invokes private constructor. static Context context = fpr::Context(Window, app_name, app_version, engine_name, engine_version, vulkan_version); return context; } @@ -187,6 +189,8 @@ { auto& device = GetDevice()->GetLogicalDeviceHandle(); + + // Allow model data to be accessed in shader. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Model"); @@ -202,7 +206,7 @@ vk::DescriptorBufferInfo uniform_buffer_info; uniform_buffer_info.buffer = m_render_graph->Buffers.Get("Model" + std::to_string(i))->GetBuffer(); uniform_buffer_info.offset = 0; - uniform_buffer_info.range = GetRequiredDynamicAlignment(); + uniform_buffer_info.range = GetRequiredDynamicAlignment(); // DYNAMIC uniform buffer. requires specific alignment. std::vector desc_writes = {}; @@ -219,9 +223,10 @@ } } -void fpr::Context::CreateCameraDescriptorSet() +void fpr::Context::CreateCameraDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + // Allow camera data to be accessed in shader. Simple uniform buffer for each frame in flighht. for(size_t i = 0; i < GetSwapChain()->GetMaxFrameIdx(); ++i) { auto layout = m_render_graph->DescriptorSetLayouts.Get("Camera"); @@ -254,12 +259,12 @@ } } -void fpr::Context::CreateCameraDescriptorSetLayout() +void fpr::Context::CreateCameraDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags camera_stage_flags = vk::ShaderStageFlagBits::eVertex | - vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eCompute; // Camera data accessed in all shaders. vk::DescriptorSetLayoutBinding camera_desc_set_layout_bind; camera_desc_set_layout_bind.binding = 0; @@ -275,12 +280,12 @@ m_render_graph->DescriptorSetLayouts.Add("Camera", std::move(layout)); } -void fpr::Context::CreateModelDescriptorSetLayout() +void fpr::Context::CreateModelDescriptorSetLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); const vk::ShaderStageFlags model_stage_flags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment | - vk::ShaderStageFlagBits::eCompute; + vk::ShaderStageFlagBits::eCompute; // Model data accessed in all shaders. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -298,11 +303,13 @@ m_render_graph->DescriptorSetLayouts.Add("Model", std::move(layout)); } -void fpr::Context::CreateDepthSamplerLayout() +void fpr::Context::CreateDepthSamplerLayout() { auto& device = GetDevice()->GetLogicalDeviceHandle(); - const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute; + const vk::ShaderStageFlags depth_sampler_flags = vk::ShaderStageFlagBits::eCompute | + vk::ShaderStageFlagBits::eFragment; // Depth buffer is sampled in compute for light culling. + // Can be sampled in fragment shader for further optimisations. vk::DescriptorSetLayoutBinding model_desc_set_layout_bind; @@ -320,10 +327,10 @@ m_render_graph->DescriptorSetLayouts.Add("DepthSampler", std::move(layout)); } -void fpr::Context::CreateDepthSamplerDescriptorSet() +void fpr::Context::CreateDepthSamplerDescriptorSet() { - auto& device = GetDevice()->GetLogicalDeviceHandle(); - auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); + auto& device = GetDevice()->GetLogicalDeviceHandle(); + auto layout = m_render_graph->DescriptorSetLayouts.Get("DepthSampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; desc_set_alloc_info.descriptorPool = GetDescriptorPool(); @@ -335,14 +342,15 @@ m_render_graph->DescriptorSets.Add("DepthSampler", descriptor_sets[0]); vk::DescriptorImageInfo depth_sampler_info; - depth_sampler_info.setSampler(m_render_graph->Samplers.Get("Sampler")); - depth_sampler_info.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal); + depth_sampler_info.setSampler(m_render_graph->Samplers.Get( + "Sampler")); // Use already created texture sampler, no need to create a different one. + depth_sampler_info.setImageLayout(vk::ImageLayout::eDepthStencilReadOnlyOptimal); depth_sampler_info.setImageView(GetSwapChain()->GetDepthBuffer()->GetDepthBufferView()); std::vector desc_writes = {}; vk::WriteDescriptorSet depth_sampler_write; - depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("Sampler"); + depth_sampler_write.dstSet = m_render_graph->DescriptorSets.Get("DepthSampler"); depth_sampler_write.dstBinding = 0; depth_sampler_write.dstArrayElement = 0; depth_sampler_write.descriptorCount = 1; @@ -353,7 +361,7 @@ device.updateDescriptorSets(desc_writes, nullptr); } -void fpr::Context::CreateDescriptorSets() +void fpr::Context::CreateDescriptorSets() { CreateModelDescriptorSetLayout(); CreateCameraDescriptorSetLayout(); @@ -368,6 +376,8 @@ void Context::CreateSceneBuffers() { + // Ensure that model data alignment meets the device requirements, + // otherwise buffer access can cause crashes or severere performance loss. vk::DeviceSize min_alignment = m_device->GetPhysicalDeviceHandle().getProperties().limits.minUniformBufferOffsetAlignment; uint32_t dynamic_alignment = (uint32_t)(m_dynamic_alignment + min_alignment - 1) & ~(min_alignment - 1); @@ -377,12 +387,15 @@ for(size_t i = 0; i < m_swapchain->GetMaxFrameIdx(); ++i) { + // Aligned malloc for dynamic uniform buffer. Allows for a massive buffer but needs specially aligned allocation. PER_MODEL_UBO.model_matrix = (glm::mat4*)_aligned_malloc(buffer_size, dynamic_alignment); m_render_graph->Buffers.Add( "Model" + std::to_string(i), std::make_unique( buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible)); + // Model and camera buffer are HostVisible. This is due to the fact that they are updated every frame and thus + // cannot "live on the GPU" alone. m_render_graph->Buffers.Add( "Camera" + std::to_string(i), @@ -391,8 +404,6 @@ } } - - void Context::Init( VulkanWindow* Window, const std::string& app_name, @@ -407,8 +418,10 @@ s_engine_name = engine_name; s_engine_version = engine_version; s_vulkan_version = vulkan_version; - auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + auto& context = GetImpl(Window, app_name, app_version, engine_name, engine_version, vulkan_version); + // The context handles the bulk of the renderer, can be initialised once and referred back to from child objects. + // This allows for central resource management whilst avoiding globals, amongst other benefits.s context.CreateGraphicsCmdPool(); context.CreateComputeCmdPool(); context.CreateSwapChain(); @@ -418,6 +431,8 @@ context.CreateSceneBuffers(); context.CreateDescriptorSets(); fpr::PointLight::CreateDescriptorSet(); + context.CreateDepthSamplerLayout(); + context.CreateDepthSamplerDescriptorSet(); context.CreatePipelines(DEFAULT_PIPELINE_CREATION_CALLBACK); } @@ -466,18 +481,21 @@ void Context::CreateDescriptorPools() { - // TODO: FIXME + // Camera std::array pool_sizes; pool_sizes[0].type = vk::DescriptorType::eUniformBuffer; pool_sizes[0].descriptorCount = 2; + // Model data. pool_sizes[1].type = vk::DescriptorType::eUniformBufferDynamic; - pool_sizes[1].descriptorCount = 2; + pool_sizes[1].descriptorCount = 2; + // Depth + texture sampler + the textures. pool_sizes[2].type = vk::DescriptorType::eCombinedImageSampler; pool_sizes[2].descriptorCount = MAX_TEXTURES + 1; + // Light culling buffer. PointLight buffer. pool_sizes[3].type = vk::DescriptorType::eStorageBuffer; pool_sizes[3].descriptorCount = 4; @@ -501,16 +519,20 @@ void Context::CreateTextureSampler() { + // Anisotropy x16 assumed to be supported. May need to check for older machines. + // Other options mostly set to default but specified for clarity. vk::SamplerCreateInfo create_info; create_info.setMagFilter(vk::Filter::eLinear); create_info.setMinFilter(vk::Filter::eLinear); - + create_info.setAddressModeU(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeV(vk::SamplerAddressMode::eRepeat); + create_info.setAddressModeW(vk::SamplerAddressMode::eRepeat); create_info.setUnnormalizedCoordinates(false); create_info.setMipmapMode(vk::SamplerMipmapMode::eLinear); - create_info.setMaxLod(VK_LOD_CLAMP_NONE); - create_info.borderColor = vk::BorderColor::eFloatTransparentBlack; + create_info.borderColor = vk::BorderColor::eIntOpaqueBlack; create_info.setAnisotropyEnable(true); create_info.setMinLod(0.0f); + create_info.setMaxLod(VK_LOD_CLAMP_NONE); create_info.setMaxAnisotropy(16.0f); create_info.compareOp = vk::CompareOp::eAlways; @@ -523,7 +545,9 @@ sampler_binding.setBinding(0); sampler_binding.setDescriptorType(vk::DescriptorType::eCombinedImageSampler); sampler_binding.setDescriptorCount(1); - sampler_binding.setStageFlags(vk::ShaderStageFlagBits::eFragment); + sampler_binding.setStageFlags( + vk::ShaderStageFlagBits::eFragment | + vk::ShaderStageFlagBits::eCompute); // Used for model texturees AND depth sampling in compute shader. vk::DescriptorSetLayoutCreateInfo sampler_layout_info; sampler_layout_info.setBindings(sampler_binding); @@ -539,6 +563,11 @@ return *m_command_pool; } +vk::CommandPool& Context::GetComputeCmdPool() +{ + return *m_compute_comand_pool; +} + vk::DescriptorPool& Context::GetDescriptorPool() { return m_descriptor_pool.get(); diff --git a/src/Device.cpp b/src/Device.cpp index d5f21ea..7c5d28a 100644 --- a/src/Device.cpp +++ b/src/Device.cpp @@ -13,6 +13,7 @@ m_presentation_queue_index, m_compute_queue_index }; + //Create queues based on the previously acquired queue indices. for(const auto& queue_family_index : queue_family_indices) { vk::DeviceQueueCreateInfo queue_create_info{}; @@ -46,6 +47,7 @@ bool Device::IsDeviceSuitable(vk::PhysicalDevice device, vk::SurfaceKHR surface) const FPR_NOEXCEPT { + //Check the device against the requested features and properties. vk::PhysicalDeviceProperties device_properties = device.getProperties(); vk::PhysicalDeviceFeatures device_features = device.getFeatures(); @@ -62,12 +64,12 @@ auto [capabilities_result, surface_capabilities] = device.getSurfaceCapabilitiesKHR(surface); auto [formats_result, formats] = device.getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device.getSurfacePresentModesKHR(surface); - // TODO, assert successful execution return !present_modes.empty() && !formats.empty(); } vk::SurfaceFormatKHR Device::QueryBestSurfaceFormat(const std::vector& formats) { + //Check for best supported surface format. const auto wanted_color_space = vk::ColorSpaceKHR::eSrgbNonlinear; for(const auto& available_format : formats) @@ -84,13 +86,12 @@ if(formats.empty()) return vk::SurfaceFormatKHR{}; else - return formats[0]; + return formats[0]; //Assumed to be ordered in terms of preference. } bool Device::IsRequiredExtensionsSupported(vk::PhysicalDevice device) const { auto [enum_result, available_extensions] = device.enumerateDeviceExtensionProperties(); - // todo:: assert bool has_extension = false; for(const auto& device_extension : required_extensions) { @@ -115,8 +116,15 @@ for(const auto& queue_family : queue_family_props) { - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)) + { m_graphics_queue_index = queue_index; + if(queue_family.queueFlags & vk::QueueFlagBits::eCompute) + { + m_compute_queue_index = queue_index; + } + } + auto [support_result, presentation_support] = m_physical_device.getSurfaceSupportKHR(queue_index, surface); @@ -125,8 +133,6 @@ m_presentation_queue_index = queue_index; } - if(queue_family.queueCount > 0 && (queue_family.queueFlags & vk::QueueFlagBits::eCompute)) - m_compute_queue_index = queue_index; ++queue_index; } diff --git a/src/IO.cpp b/src/IO.cpp index d58a35e..186a5f2 100644 --- a/src/IO.cpp +++ b/src/IO.cpp @@ -4,6 +4,7 @@ { std::vector ReadFile(const std::string_view filename) FPR_NOEXCEPT { + //Read file, return raw bytes. std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); assert(file.is_open()); diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp index 097c10f..b2c614a 100644 --- a/src/InputHandler.cpp +++ b/src/InputHandler.cpp @@ -35,6 +35,8 @@ void InputHandler::Update([[maybe_unused]] GLFWwindow* window) { + //Iterate over each input handler. + //Iterate over each mapped key and invoke the function that is bound to that key, based on the 'type' of key press or mouse axis change. for(InputHandler* instance : S_INPUT_HANDLERS) { for(auto& [button, callback_data] : instance->m_tracked_mouse_btns) diff --git a/src/Light.cpp b/src/Light.cpp index ede457b..2afbe24 100644 --- a/src/Light.cpp +++ b/src/Light.cpp @@ -1,25 +1,27 @@ #include -#include "Light.h" #include "Context.h" +#include "Light.h" + + namespace fpr { size_t fpr::PointLight::light_count = 0; -fpr::PointLightUBO fpr::PointLight::s_point_light_data[fpr::PointLight::MAX_LIGHTS]; - +fpr::PointLightUBO fpr::PointLight::s_point_light_data; fpr::PointLight::PointLight(glm::vec3 position, glm::vec3 color, float luminance): - m_color(color), m_position(position), m_luminance(luminance), m_ubo({ color, luminance, m_position }) + m_color(color), m_position(position), m_luminance(luminance), m_ubo({color, luminance, m_position }) { light_index = light_count; - s_point_light_data[light_index] = m_ubo; + s_point_light_data.lights[light_index] = m_ubo; ++light_count; } void fpr::PointLight::Update() { + //Update the whole buffer at once. Should refactor to allow updating of ranges. auto& context = fpr::Context::Get(); auto& render_graph = context.m_render_graph; render_graph->Buffers.Get("PointLight" + std::to_string(context.GetSwapChain()->GetCurrentFrameIdx())) - ->Update(s_point_light_data); + ->Update(&s_point_light_data); } void fpr::PointLight::CreateDescriptorSet() @@ -27,8 +29,9 @@ auto& context = fpr::Context::Get(); auto& device = context.GetDevice()->GetLogicalDeviceHandle(); auto render_graph = context.m_render_graph.get(); + auto& constants = context.GetSwapChain()->GetViewportConstants(); - + //Two storage buffers. One for point lights. One for culled light indices. vk::DescriptorSetLayoutBinding light_buffer_layout_binding; light_buffer_layout_binding.binding = 0; light_buffer_layout_binding.descriptorCount = 1; @@ -67,7 +70,7 @@ render_graph->Buffers.Add( "PointLight" + std::to_string(i), std::make_unique( - sizeof(PointLightUBO) * MAX_LIGHTS, + sizeof PointLightUBO, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); @@ -75,25 +78,25 @@ render_graph->Buffers.Add( "VisibleLights" + std::to_string(i), std::make_unique( - sizeof(int) * MAX_LIGHTS, + sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible)); - + std::vector buffer_infos(2); buffer_infos[0] = { { .buffer = render_graph->Buffers.Get("PointLight" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof PointLightUBO } }; + .range = sizeof PointLightUBO } }; buffer_infos[1] = { { .buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(i))->GetBuffer(), .offset = 0, - .range = MAX_LIGHTS * sizeof(int) } }; + .range = sizeof(LightCullingData) * constants.horizontal_tile_count * constants.vertical_tile_count} }; + + //Update descriptor sets to reflect the usage of the buffers. std::vector write_sets(2); - - write_sets[0].descriptorCount = 1; write_sets[0].descriptorType = vk::DescriptorType::eStorageBuffer; write_sets[0].dstBinding = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe9d85d..bead853 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -23,6 +23,7 @@ { auto& rendergraph = fpr::Context::Get().m_render_graph; bool already_loaded = rendergraph->DiffuseTextures.Exists(texture_name); + //If texture is already loaded, just fetch it from the render graph. if(!already_loaded) [[likely]] { return rendergraph->DiffuseTextures.Add(texture_name, std::make_unique(texture_name)); diff --git a/src/Model.cpp b/src/Model.cpp index ed825a2..6f950f5 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -21,6 +21,7 @@ vk::MemoryPropertyFlagBits::eHostVisible), m_model_matrix(model_matrix) { + //Store the primitives. Index/Vertex buffer per MODEL not per Mesh. Allows for less frequent binding. m_index_buffer.Update(m_primitives.indices.data()); m_vertex_buffer.Update(m_primitives.vertices.data()); for(auto& mesh : m_primitives.meshes) diff --git a/src/ModelLoading.cpp b/src/ModelLoading.cpp index 2cbb9eb..80fd0d7 100644 --- a/src/ModelLoading.cpp +++ b/src/ModelLoading.cpp @@ -13,9 +13,11 @@ uint32_t ModelLoader::mesh_index = 0; ModelPrimitives ModelLoader::LoadModelAssimp(const char* file_name) { + //Load model data and parse it using assimp. std::string_view file_name_sv(file_name); Assimp::Importer importer; const aiScene* scene = importer.ReadFile(file_name, USING_ASSIMP_DEFAULT_FLAGS); + if(!scene) std::cout << "Failed to load scene:\t" << file_name << '\n'; size_t last_slash_pos = file_name_sv.rfind("/") + 1; //+1 to include the "/" @@ -23,6 +25,7 @@ ModelPrimitives data; data.meshes.reserve(scene->mNumMeshes); + //Iterate over the meshes in the model. for(size_t mesh = 0; mesh < scene->mNumMeshes; ++mesh) { std::vector mesh_vertices; @@ -32,15 +35,16 @@ aiMaterial* material = scene->mMaterials[current_mesh->mMaterialIndex]; bool using_normals = current_mesh->HasNormals(); - // TODO + //Potential materials to be used. aiString albedo_path; aiString metal; aiString ao; aiString normal; aiString bump; aiString roughness; - // FIX + + //Output the available materials, mostly for debugging purposes. for(auto& [name, type] : ASSIMP_TEXTURE_TYPES) { aiString mat_name; @@ -48,11 +52,13 @@ printf("%s\t %s\n", name, mat_name.C_Str()); } + //Store diffuse map. material->GetTexture(aiTextureType_DIFFUSE, 0, &albedo_path); std::string full_albedo_path(file_directory); full_albedo_path.append(albedo_path.C_Str()); + //Store primitves, such as UVs, vertices, normals and tangents. for(size_t vert = 0; vert < current_mesh->mNumVertices; ++vert) { fpr::Vertex vertex; @@ -81,6 +87,7 @@ mesh_vertices.push_back(vertex); } + for(size_t face = 0; face < current_mesh->mNumFaces; ++face) { aiFace* current_face = ¤t_mesh->mFaces[face]; diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index da14cc8..73bc44e 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -17,11 +17,13 @@ { PipelineOptions Pipeline::GetDefaultGraphicsPipeline() { + //The default graphics pipeline for the renderer. Has no significant options eenabled, most are default. PipelineOptions options; auto& context = fpr::Context::Get(); auto vertex_shader = fpr::ReadFile(VERTEX_SHADER_PATH); auto fragment_shader = fpr::ReadFile(FRAGMENT_SHADER_PATH); + //This pipeline handles simple vertex transformation and fragment shading. vk::ShaderModule vertex_shader_module = CreateShaderModule(vertex_shader); vk::ShaderModule fragment_shader_module = CreateShaderModule(fragment_shader); @@ -57,6 +59,7 @@ vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; blend_attach_state.alphaBlendOp = vk::BlendOp::eAdd; + //Buffers to use in the shaders specified in the shader modules above. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("Model"), context.m_render_graph->DescriptorSetLayouts.Get("Sampler"), @@ -91,6 +94,7 @@ auto& context = fpr::Context::Get(); auto early_z_shader = fpr::ReadFile(DEPTH_SHADER_PATH); + //This pipeline is a depth-prepass, only vertex shader required, vk::ShaderModule early_z_module = CreateShaderModule(early_z_shader); options.modules = { { early_z_module, vk::ShaderStageFlagBits::eVertex } }; @@ -117,8 +121,10 @@ blend_attach_state.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + //No texture data needed for the depth prepass. std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), - context.m_render_graph->DescriptorSetLayouts.Get("Model") }; + context.m_render_graph->DescriptorSetLayouts.Get("Model") + }; vk::PipelineDepthStencilStateCreateInfo depth_stencil_ceate_info{}; @@ -149,7 +155,6 @@ PipelineOptions options; auto compute_shader = fpr::ReadFile(COMPUTE_SHADER_PATH); - vk::ShaderModule compute_shader_module = CreateShaderModule(compute_shader); vk::PushConstantRange push_constant_range; @@ -163,8 +168,9 @@ std::vector layouts = { context.m_render_graph->DescriptorSetLayouts.Get("Camera"), context.m_render_graph->DescriptorSetLayouts.Get("PointLight"), - context.m_render_graph->DescriptorSetLayouts.Get("Sampler") }; - options.layouts = layouts; + context.m_render_graph->DescriptorSetLayouts.Get("DepthSampler") }; + + options.layouts = layouts; return options; } @@ -204,6 +210,7 @@ m_cache = std::move(cache); + //Based on the given pipeline, construct it based on its options. switch(type) { case EPipelineType::EPT_Graphics: @@ -258,15 +265,17 @@ case EPipelineType::EPT_Compute: { + //Compute pipelines don't concern themselves with a viewport or rasterisation and so on. vk::ComputePipelineCreateInfo create_info{}; create_info.setLayout(*m_layout).setStage(shader_stages[0]); auto [pipeline_result, pipeline] = context.GetDevice()->GetLogicalDeviceHandle().createComputePipelineUnique(*m_cache, create_info); + m_pipeline = std::move(pipeline); break; } } - + //Shader modules are no longer needed, they have been loaded in already. for(auto& shader : shader_stages) context.GetDevice()->GetLogicalDeviceHandle().destroyShaderModule(shader.module); } diff --git a/src/RenderPass.cpp b/src/RenderPass.cpp index dbfbcb9..8cbef6d 100644 --- a/src/RenderPass.cpp +++ b/src/RenderPass.cpp @@ -13,6 +13,7 @@ RenderPass::RenderPass(const RenderPassOptions& options, fpr::Device* device) { + //Construct render pass based on passed options vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.setPDepthStencilAttachment(&options.depth_attachment_ref); @@ -34,6 +35,7 @@ RenderPassOptions RenderPass::MakeDefaultRenderPass(vk::Format swapchain_format, vk::Format depth_image_format) { + //Default render pass does write to the depth buffer, only the color attachmnt. vk::AttachmentDescription color_attachment; color_attachment.format = swapchain_format; color_attachment.samples = vk::SampleCountFlagBits::e1; @@ -62,22 +64,22 @@ depth_attachment_ref.attachment = 1; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - - std::vector subpasses(1); - + //Subpass dependencies to synchronise the two renderpasses, to allow for maximum performance and to transition between optimal image layouts. + //As per the Vulkan documentation, warning against RAW and WAW errors. + std::vector subpasses(2); subpasses[0].srcSubpass = VK_SUBPASS_EXTERNAL; subpasses[0].dstSubpass = 0; subpasses[0].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; subpasses[0].dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - // subpasses[1].srcSubpass = 0; - // subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; - // subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - // subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | - // vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; - // subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; - // subpasses[1].dependencyFlags = {}; + subpasses[1].srcSubpass = 0; + subpasses[1].dstSubpass = VK_SUBPASS_EXTERNAL; + subpasses[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + subpasses[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite; subpasses[1].dstStageMask = vk::PipelineStageFlagBits::eTopOfPipe; + subpasses[1].dstAccessMask = vk::AccessFlagBits::eMemoryRead; + subpasses[1].dependencyFlags = {}; std::vector attachment_descs = { color_attachment, depth_attachment }; std::vector attachment_refs = { color_attachment_ref }; @@ -93,6 +95,7 @@ RenderPassOptions RenderPass::MakeDefaultDepthPass(vk::Format depth_image_format) { + //Depth pre pass only writes to depth buffer. vk::AttachmentReference depth_attachment_ref; depth_attachment_ref.attachment = 0; depth_attachment_ref.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; @@ -107,7 +110,7 @@ depth_attachment.initialLayout = vk::ImageLayout::eUndefined; depth_attachment.finalLayout = vk::ImageLayout::eDepthStencilReadOnlyOptimal; - + //Transition into optimal layout for depth sampling. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index e4556cb..711b33a 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -1,7 +1,8 @@ #include +#include "Context.h" #include "SwapChain.h" #include "Buffer/DepthBuffer.h" -#include "Context.h" + #include "RenderPass.h" #include "Scene.h" #include "Texture.h" @@ -10,7 +11,11 @@ namespace fpr { -SwapChain::SwapChain(fpr::Device* device, vk::SurfaceKHR& surface, std::optional old_swapchain) +SwapChain::SwapChain( + fpr::Device* device, + vk::SurfaceKHR& surface, + std::optional old_swapchain, + glm::ivec2 tile_size) { if(old_swapchain != std::nullopt) { @@ -20,12 +25,21 @@ CreateSwapchain(device, surface); CreateImageViews(device); m_depth_buffer = std::make_unique(device, this); + CreateRenderPasses(device); CreateFrameBuffers(device); CreateSynchronisation(device); - m_viewport_push_constant.viewport_size_tile_size = { m_extent.width, m_extent.height, DEFAULT_TILE_SIZE }; + + // Construct push constants. Used in shaders. + glm::ivec2 viewport_size = { m_extent.width, m_extent.height }; + m_viewport_push_constant.viewport_size = viewport_size; + m_viewport_push_constant.tile_size = tile_size; + m_viewport_push_constant.horizontal_tile_count = (viewport_size.x + 1) / (tile_size.x - 1); + m_viewport_push_constant.vertical_tile_count = (viewport_size.y + 1) / (tile_size.y - 1); + m_viewport_push_constant.MAX_LIGHTS_PER_TILE = FPR_MAX_LIGHTS_PER_TILE; + m_viewport_push_constant.MAX_LIGHTS = FPR_MAX_LIGHTS; } vk::UniqueSwapchainKHR& SwapChain::GetUniqueHandle() @@ -48,13 +62,11 @@ m_surface_format = QueryFirstSuitableFormat(surface_formats); m_present_mode = fifo_or_immediate(present_modes); - m_extent = QueryExtenfpromCapabilities(surface_capabilities); + m_extent = QueryExtenfromCapabilities(surface_capabilities); vk::SwapchainCreateInfoKHR swap_chain_create_info{}; - // TODO: Refactor this, instead of commenting make it actually readable - // instead + // Determine how many swapchain images can be handled. Allowing for double/triple buffering. uint32_t image_count = surface_capabilities.minImageCount + 1; - if(surface_capabilities.maxImageCount > 0 && image_count > surface_capabilities.maxImageCount) { if(image_count < surface_capabilities.maxImageCount) @@ -66,11 +78,8 @@ image_count = surface_capabilities.minImageCount; } } - MAX_IMAGES_IN_FLIGHT = image_count; - // end of confusion - swap_chain_create_info.setSurface(surface); swap_chain_create_info.imageFormat = m_surface_format.format; swap_chain_create_info.imageColorSpace = m_surface_format.colorSpace; @@ -88,6 +97,7 @@ uint32_t queue_family_indices[] = { device->GetGraphicsQueueIndex(), device->GetPresentQueueIndex() }; + if(queue_family_indices[0] != queue_family_indices[1]) { swap_chain_create_info.imageSharingMode = vk::SharingMode::eConcurrent; @@ -134,6 +144,7 @@ void SwapChain::SyncPresentation(fpr::Device* device, uint32_t image_index) { + // Sync swapchain presentation so it happens after the final color pass. vk::PresentInfoKHR present_info{}; present_info.setImageIndices(image_index); present_info.setSwapchains(m_swapchain.get()); @@ -142,11 +153,28 @@ assert(present_res == vk::Result::eSuccess); } +void SwapChain::SyncCompute(fpr::Device* device) +{ + // Sync the compute light culling shader. + // Depth prepass -> Compute -> Final shading + vk::SubmitInfo compute_submit_info{}; + vk::PipelineStageFlags stages = { vk::PipelineStageFlagBits::eComputeShader }; + compute_submit_info.setSignalSemaphores(m_compute_semaphores[m_current_frame].get()) + .setCommandBuffers(m_compute_command_buffers[m_current_frame].get()) + .setWaitSemaphores(m_earlyz_semaphores[m_current_frame].get()) + .setWaitDstStageMask(stages); + + [[maybe_unused]] vk::Result submit_res = device->GetComputeQueue().submit(compute_submit_info); + assert(("Failed to sync compute pass.", submit_res == vk::Result::eSuccess)); +} + void SwapChain::SyncColorPass(fpr::Device* device) { - std::array wait_flags = { vk::PipelineStageFlagBits::eEarlyFragmentTests, + // Sync the final shading stage. + // Depth prepass -> Compute -> Final shading + std::array wait_flags = { vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eColorAttachmentOutput }; - std::array wait_semaphores{ m_earlyz_semaphores[m_current_frame].get(), + std::array wait_semaphores{ m_compute_semaphores[m_current_frame].get(), m_image_ready_semaphores[m_current_frame].get() }; vk::SubmitInfo submit_info{}; submit_info.setWaitSemaphores(wait_semaphores); @@ -169,13 +197,20 @@ RenderPass::MakeDefaultRenderPass(m_surface_format.format, depth_format); const fpr::RenderPassOptions& early_z_options = RenderPass::MakeDefaultDepthPass(depth_format); + context.m_render_graph->RenderPasses.Add("Default", std::make_unique(color_pass_options, device)); context.m_render_graph->RenderPasses.Add("EarlyZ", std::make_unique(early_z_options, device)); } +const ViewportConstants& SwapChain::GetViewportConstants() const +{ + return m_viewport_push_constant; +} + void SwapChain::SyncDepthPass(fpr::Device* device) { + // Depth prepass -> Compute -> Final shading vk::SubmitInfo depth_submit_info{}; depth_submit_info.setSignalSemaphores(m_earlyz_semaphores[m_current_frame].get()) .setCommandBuffers(m_depth_command_buffers[m_current_frame].get()); @@ -185,9 +220,11 @@ } - void SwapChain::CreateFrameBuffers(fpr::Device* device) { + // Create swapchain framebuffers. + // One is used for color shading and swapchain image presentation. + // Other is used for the depth buffer. auto& context = fpr::Context::Get(); auto renderpass = context.m_render_graph->RenderPasses.Get("Default"); auto depth_pass = context.m_render_graph->RenderPasses.Get("EarlyZ"); @@ -239,13 +276,13 @@ RecordComputeCommands(); RecordRenderCommands(); - } void SwapChain::CreateCommandBuffers() { - auto& context = fpr::Context::Get(); - // vk::commandbuffer + // Tree command buffers are required. + // Grapphics command buffer, depth pre pass command buffer and compute command buffer. + auto& context = fpr::Context::Get(); vk::CommandBufferAllocateInfo command_buffer_alloc_info{}; command_buffer_alloc_info.commandPool = context.GetGraphicsCmdPool(); command_buffer_alloc_info.level = vk::CommandBufferLevel::ePrimary; @@ -267,24 +304,43 @@ context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(depth_buffer_alloc); assert(("Failed to create depth command buffers", depth_cmd_alloc_result == vk::Result::eSuccess)); m_depth_command_buffers = std::move(depth_cmd_buffers); + + + vk::CommandBufferAllocateInfo compute_buffer_alloc{}; + compute_buffer_alloc.commandPool = context.GetComputeCmdPool(); + compute_buffer_alloc.level = vk::CommandBufferLevel::ePrimary; + compute_buffer_alloc.commandBufferCount = (uint32_t)m_swapchain_framebuffers.size(); + + auto [compute_cmd_alloc_result, compute_cmd_buffers] = + context.GetDevice()->GetLogicalDeviceHandle().allocateCommandBuffersUnique(compute_buffer_alloc); + assert(("Failed to create compute command buffers", compute_cmd_alloc_result == vk::Result::eSuccess)); + m_compute_command_buffers = std::move(compute_cmd_buffers); } void SwapChain::CreateSynchronisation(fpr::Device* device) { + // Create all the synchronisation objets. + // Fences and semaphores are used to ensure that buffers being read from aren't currently being wriitten to and vice + // versa. m_image_ready_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_render_done_semaphores.resize(MAX_IMAGES_IN_FLIGHT); m_earlyz_semaphores.resize(MAX_IMAGES_IN_FLIGHT); + m_compute_semaphores.resize(MAX_IMAGES_IN_FLIGHT); vk::SemaphoreCreateInfo semaphore_create_info{}; vk::FenceCreateInfo fence_create_info(vk::FenceCreateFlagBits::eSignaled); - auto create_semaphore = [&](vk::UniqueSemaphore& s) + + // Simple lambda that creates an unique handle to a semaphore. Used to construct all the semaphores used for + // synchronisation. + auto create_semaphore = [&](vk::UniqueSemaphore& s) { auto [semaphore_result, semaphore] = device->GetLogicalDeviceHandle().createSemaphoreUnique(semaphore_create_info); assert(("Semaphore creation failed!", semaphore_result == vk::Result::eSuccess)); s = std::move(semaphore); }; + // Simple lambda that creates an unique handle to a fence auto create_fence = [&](vk::UniqueFence& f) { auto [fence_result, fence] = device->GetLogicalDeviceHandle().createFenceUnique(fence_create_info); @@ -295,6 +351,7 @@ std::for_each(m_image_ready_semaphores.begin(), m_image_ready_semaphores.end(), create_semaphore); std::for_each(m_render_done_semaphores.begin(), m_render_done_semaphores.end(), create_semaphore); std::for_each(m_earlyz_semaphores.begin(), m_earlyz_semaphores.end(), create_semaphore); + std::for_each(m_compute_semaphores.begin(), m_compute_semaphores.end(), create_semaphore); create_fence(m_draw_fence); } @@ -310,7 +367,7 @@ assert(("Timed out waiting for draw fence", fence_result == vk::Result::eSuccess)); assert(("Failed to reset draw fence", fence_reset_result == vk::Result::eSuccess)); - + // Ensure that an image is ready and the prior frame has been submitted. vk::Result acquire_image_result = device->GetLogicalDeviceHandle().acquireNextImageKHR( *m_swapchain, std::numeric_limits::max(), @@ -320,9 +377,12 @@ if(acquire_image_result != vk::Result::eSuccess) return acquire_image_result; - + // Synchronise the different command buffers so they execute in sequence. SyncDepthPass(device); + SyncCompute(device); SyncColorPass(device); + + // Synchronise presentation and present image. SyncPresentation(device, image_index); m_current_frame = (m_current_frame + 1) % MAX_IMAGES_IN_FLIGHT; @@ -332,17 +392,8 @@ { auto [capabilities_result, surface_capabilities] = device->GetPhysicalDeviceHandle().getSurfaceCapabilitiesKHR(surface); - - // todo: assert result - - auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); - - // todo: assert formats - + auto [formats_result, formats] = device->GetPhysicalDeviceHandle().getSurfaceFormatsKHR(surface); auto [modes_result, present_modes] = device->GetPhysicalDeviceHandle().getSurfacePresentModesKHR(surface); - - // todo: assert modes - return std::make_tuple(surface_capabilities, formats, present_modes); } @@ -390,11 +441,12 @@ render_pass_info.setClearValues(m_clear_values); m_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport can be inverted in the Y axis. However, it is less overheadd to invert the projection matrix. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; // static_cast(m_extent.height); viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); // -static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -413,13 +465,14 @@ auto cam_desc_set = render_graph->DescriptorSets.Get("Camera" + std::to_string(i)); auto light_desc_set = render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)); - + //Draw each mesh of each model. for(auto& model : scene->GetModels()) { m_command_buffers[i]->bindIndexBuffer(model->GetIndexBuffer().GetBuffer(), 0, vk::IndexType::eUint32); for(auto&& mesh : model->GetMeshes()) { + // Each mesh is stored in the same vertex buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->bindVertexBuffers(0, model->GetVertexBuffer().GetBuffer(), mesh.GetVertexOffset()); std::vector desc_sets = { @@ -448,6 +501,8 @@ vk::PipelineBindPoint::eGraphics, graphics_pipeline->GetLayout(), 3, light_desc_set, nullptr); + + //Each mesh is stored in the same index buffer as other meshes that make up a modele, thus an offset is needed. m_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -460,9 +515,10 @@ void SwapChain::RecordDepthCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); - fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); + //Depth prepass. + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); + fpr::Pipeline* depth_pipeline = render_graph->GraphicsPipelines.Get("Depth"); for(size_t i = 0; i < m_depth_command_buffers.size(); ++i) { vk::CommandBufferBeginInfo cmd_begin_info; @@ -479,11 +535,12 @@ m_depth_command_buffers[i]->beginRenderPass(render_pass_info, vk::SubpassContents::eInline); + //Viewport not inverted, instead projection matrix is. vk::Viewport viewport; viewport.x = 0.0f; - viewport.y = static_cast(m_extent.height); + viewport.y = 0.0f; viewport.width = static_cast(m_extent.width); - viewport.height = -static_cast(m_extent.height); + viewport.height = static_cast(m_extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; @@ -496,6 +553,7 @@ auto scene = context.m_loaded_scene; + //Buffers used in the depth prepass shader. std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), render_graph->DescriptorSets.Get("Model" + std::to_string(i)), @@ -523,6 +581,7 @@ render_graph->DescriptorSets.Get("Model" + std::to_string(i)), (uint32_t)(mesh.GetMeshIndex() * context.GetRequiredDynamicAlignment())); + m_depth_command_buffers[i]->drawIndexed(mesh.GetIndexCount(), 1, (uint32_t)mesh.GetIndexOffset(), 0, 0); } } @@ -533,10 +592,10 @@ } } -void SwapChain::RecordComputeCommands() +void SwapChain::RecordComputeCommands() { - auto& context = fpr::Context::Get(); - fpr::RenderGraph* render_graph = context.m_render_graph.get(); + auto& context = fpr::Context::Get(); + fpr::RenderGraph* render_graph = context.m_render_graph.get(); fpr::Pipeline* compute_pipeline = render_graph->ComputePipelines.Get("LightCulling"); for(size_t i = 0; i < m_compute_command_buffers.size(); ++i) { @@ -545,25 +604,91 @@ [[maybe_unused]] auto cmd_begin_result = m_compute_command_buffers[i]->begin(cmd_begin_info); assert(cmd_begin_result == vk::Result::eSuccess); - //todo barriers + //Need to retrieve the actual buffers, not descriptors. Light culling compute shaders require barriers to buffers. + fpr::Buffer* culled_buffer = render_graph->Buffers.Get("VisibleLights" + std::to_string(m_current_frame)); + fpr::Buffer* light_buffer = render_graph->Buffers.Get("PointLight" + std::to_string(m_current_frame)); - std::array desc_sets = { - render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), - render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)), - render_graph->DescriptorSets.Get("DepthSampler"), - }; + std::vector barriers; + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderRead, + vk::AccessFlagBits::eShaderWrite, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + + //Pre cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eFragmentShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, + nullptr, + barriers, + nullptr); + + std::array desc_sets = { render_graph->DescriptorSets.Get("Camera" + std::to_string(i)), + render_graph->DescriptorSets.Get("PointLight" + std::to_string(i)) }; + + m_compute_command_buffers[i]->bindDescriptorSets( vk::PipelineBindPoint::eCompute, compute_pipeline->GetLayout(), 0, desc_sets, nullptr); - m_compute_command_buffers[i]-> - pushConstants( - compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + m_compute_command_buffers[i]->bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + compute_pipeline->GetLayout(), + 2, + render_graph->DescriptorSets.Get("DepthSampler"), + nullptr); - glm::vec3 group_count = { m_viewport_push_constant.viewport_size_tile_size.z, - m_viewport_push_constant.viewport_size_tile_size.w, + m_compute_command_buffers[i]->pushConstants( + compute_pipeline->GetLayout(), vk::ShaderStageFlagBits::eCompute, 0, m_viewport_push_constant); + + glm::vec3 group_count = { m_viewport_push_constant.horizontal_tile_count, + m_viewport_push_constant.vertical_tile_count, 1 }; + //Bind compute pipeline and dispatch shader for each tile. + m_compute_command_buffers[i]->bindPipeline(vk::PipelineBindPoint::eCompute, compute_pipeline->GetPipeline()); + m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, (uint32_t)group_count.z); - m_compute_command_buffers[i]->dispatch((uint32_t)group_count.x, (uint32_t)group_count.y, 1); + + barriers.clear(); + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + culled_buffer->GetBuffer(), + 0, + culled_buffer->GetSize()); + + + barriers.emplace_back( + vk::AccessFlagBits::eShaderWrite, + vk::AccessFlagBits::eShaderRead, + 0, + 0, + light_buffer->GetBuffer(), + 0, + light_buffer->GetSize()); + //Post cull barrier. + m_compute_command_buffers[i]->pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eFragmentShader, + {}, + nullptr, + barriers, + nullptr); [[maybe_unused]] auto cmd_end_result = m_compute_command_buffers[i]->end(); assert(cmd_end_result == vk::Result::eSuccess); @@ -571,7 +696,7 @@ } -vk::Extent2D SwapChain::QueryExtenfpromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) +vk::Extent2D SwapChain::QueryExtenfromCapabilities(vk::SurfaceCapabilitiesKHR capabilities) { vk::Extent2D result{}; @@ -597,7 +722,7 @@ return result; } -const vk::Format& SwapChain::GetFormat() const FPR_NOEXCEPT +const vk::Format& SwapChain::GetSwapchainImageFormat() const FPR_NOEXCEPT { return m_surface_format.format; } @@ -621,6 +746,4 @@ { return m_depth_buffer.get(); } - - } // namespace fpr \ No newline at end of file diff --git a/src/Texture.cpp b/src/Texture.cpp index fc1d838..64bb030 100644 --- a/src/Texture.cpp +++ b/src/Texture.cpp @@ -4,7 +4,6 @@ #include "Buffer/Buffer.h" #include "Context.h" #include "Device.h" -#include "RNG.h" @@ -41,6 +40,7 @@ std::pair Texture::GetImageDetails(std::string_view file_name, int desired_channels) { + //Load raw image data and image size. int width; int height; int image_size; @@ -62,6 +62,7 @@ void Texture::GenerateMipMaps() { + //Run time mip map generation. auto& context = fpr::Context::Get(); auto& cmd_buffer = context.BeginSingleTimeCommands(); @@ -79,6 +80,7 @@ uint32_t mip_width = m_width; uint32_t mip_height = m_height; + //For each mip level, blit image and reduce resolution in half. for(uint32_t i = 1; i < m_mip_levels; ++i) { mip_img_barrier.subresourceRange.baseMipLevel = i - 1; @@ -134,6 +136,7 @@ mip_height = half_else_one(mip_height); } + //pipeline barrier to transition mip image layout. mip_img_barrier.subresourceRange.baseMipLevel = m_mip_levels - 1; mip_img_barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; mip_img_barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; @@ -156,6 +159,8 @@ auto& context = fpr::Context::Get(); auto device = context.GetDevice(); + //Load raw bytes of image and load it into a staging buffer. Textures are generally unchanged throughout a program + //so they can be transferred to the GPU with a staging buffer and left untouched. auto [size, raw_data] = GetImageDetails(file_name, desired_channels); vk::MemoryPropertyFlags flags = vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent; fpr::Buffer staging_buffer(size, vk::BufferUsageFlagBits::eTransferSrc, flags); @@ -210,7 +215,7 @@ { auto& context = fpr::Context::Get(); auto device = context.GetDevice(); - + //Vulkan allows for image and samppler to be combined. Texture loaded in with Sampler. vk::DescriptorSetLayout sampler_layout = context.m_render_graph->DescriptorSetLayouts.Get("Sampler"); vk::DescriptorSetAllocateInfo desc_set_alloc_info; @@ -236,6 +241,7 @@ void TransitionImageLayout(vk::Image image, const LayoutTransitionRules& transition_rules, uint32_t mip_levels) { + //Transition the image into the layout specified by the rules passed. auto& context = fpr::Context::Get(); auto& single_use_cmd_buffer = context.BeginSingleTimeCommands(); diff --git a/src/VulkanRenderer.cpp b/src/VulkanRenderer.cpp index 0fadb80..324756f 100644 --- a/src/VulkanRenderer.cpp +++ b/src/VulkanRenderer.cpp @@ -6,7 +6,6 @@ #include "Vertex.h" #include "Scene.h" #include "Model.h" -#include "RNG.h" #include "Camera.h" namespace fpr @@ -26,6 +25,7 @@ void VulkanRenderer::SetScene(fpr::Scene* scene) { + //Setting scene requires that commands are re-recorded, as now different models are present. fpr::Context& context = fpr::Context::Get(); context.m_loaded_scene = scene; context.GetSwapChain()->RecordCommands(); @@ -33,34 +33,22 @@ { for(auto& mesh : model->GetMeshes()) { - auto matrix = mesh.GetMatrix(); - //auto matrix = glm::scale(glm::mat4(1), {100,100,100}); + auto matrix = mesh.GetMatrix(); auto index = mesh.GetMeshIndex(); memcpy(&context.PER_MODEL_UBO.model_matrix[index], &matrix, sizeof(glm::mat4)); - //auto debug = PER_MODEL_UBO.model_matrix[index]; - //(debug); + } } - - } - - void VulkanRenderer::Draw(fpr::Camera* camera) { - glfwPollEvents(); - - static double now; static double delta; static double last_time; - auto& context_ref = fpr::Context::Get(); - auto debug = context_ref.PER_MODEL_UBO.model_matrix[0]; - (debug); - - + //Update the model matrix. + //Big performance hit. Could differentiate between static and dynamic models, as most models are static iin most cases and can live in a buffer that doesn't need to be touched. context_ref.m_render_graph->Buffers.Get("Model" + std::to_string(context_ref.GetSwapChain()->GetCurrentFrameIdx())) ->Update(context_ref.PER_MODEL_UBO.model_matrix); @@ -86,6 +74,8 @@ now = glfwGetTime(); delta = now - last_time; last_time = now; + + glfwPollEvents(); } diff --git a/src/VulkanWindow.cpp b/src/VulkanWindow.cpp index 54d3e27..9f3eb07 100644 --- a/src/VulkanWindow.cpp +++ b/src/VulkanWindow.cpp @@ -12,10 +12,7 @@ { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - m_window = glfwCreateWindow(width, height, title, nullptr, nullptr); - glfwMaximizeWindow(m_window); - glfwSetWindowUserPointer(m_window, this); glfwSetFramebufferSizeCallback(m_window, VulkanWindow::ResizeCallback); }