diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml index 97953adfa..5591a7906 100644 --- a/.github/actions/setup-windows/action.yml +++ b/.github/actions/setup-windows/action.yml @@ -26,9 +26,9 @@ runs: shell: pwsh if: steps.cache-vulkan.outputs.cache-hit != 'true' run: | - curl -LS -o vulkansdk.exe https://sdk.lunarg.com/sdk/download/${{ inputs.vulkan-sdk-version }}/windows/VulkanSDK-${{ inputs.vulkan-sdk-version }}-Installer.exe + curl -LS -o vulkansdk.exe https://sdk.lunarg.com/sdk/download/${{ inputs.vulkan-sdk-version }}/windows/vulkansdk-windows-X64-${{ inputs.vulkan-sdk-version }}.exe if ((Get-FileHash vulkansdk.exe -Algorithm SHA256).Hash.ToLower() -ne "${{ inputs.vulkan-sdk-checksum }}") { Write-Error "Checksum mismatch!"; exit 1 } - 7z x -y vulkansdk.exe -o"${{ github.workspace }}/vulkan_sdk" + ./vulkansdk.exe install --root "${{ github.workspace }}/vulkan_sdk" --accept-licenses --default-answer --confirm-command Remove-Item vulkansdk.exe - name: Set ENV and PATH for Vulkan SDK diff --git a/.github/workflows/gha.yml b/.github/workflows/gha.yml index a6939073e..c719015bb 100644 --- a/.github/workflows/gha.yml +++ b/.github/workflows/gha.yml @@ -14,22 +14,22 @@ jobs: name: Linux uses: ./.github/workflows/linux_builds.yml with: - vulkan-sdk-version: "1.3.283.0" - vulkan-sdk-checksum: "8005e2cf3e89c80cbe1c0d0a259c88248de3257b4fc6fdefb47409edb3e43ecb" + vulkan-sdk-version: "1.4.357.0" + vulkan-sdk-checksum: "0f09bf6a0625e346bf004be70b92907e934a4c76606b323441b2baf3a5a0e66d" windows-build: name: Windows uses: ./.github/workflows/windows_builds.yml with: - vulkan-sdk-version: "1.3.283.0" - vulkan-sdk-checksum: "811fcb9b43d09248520b2f38ae9a3763fc81df950fdab874f23bd762b07a9b12" + vulkan-sdk-version: "1.4.357.0" + vulkan-sdk-checksum: "81f474711e9042f4cd22b31b2f7a8870db2e428b21586fb43dd80150be97310d" documentation: name: Documentation uses: ./.github/workflows/documentation.yml with: - vulkan-sdk-version: "1.3.283.0" - vulkan-sdk-checksum: "8005e2cf3e89c80cbe1c0d0a259c88248de3257b4fc6fdefb47409edb3e43ecb" + vulkan-sdk-version: "1.4.357.0" + vulkan-sdk-checksum: "0f09bf6a0625e346bf004be70b92907e934a4c76606b323441b2baf3a5a0e66d" static-analysis: name: Static Analysis @@ -38,9 +38,9 @@ jobs: - windows-build uses: ./.github/workflows/static_analysis.yml with: - vulkan-sdk-version: "1.3.283.0" - vulkan-sdk-checksum-linux: "8005e2cf3e89c80cbe1c0d0a259c88248de3257b4fc6fdefb47409edb3e43ecb" - vulkan-sdk-checksum-windows: "811fcb9b43d09248520b2f38ae9a3763fc81df950fdab874f23bd762b07a9b12" + vulkan-sdk-version: "1.4.357.0" + vulkan-sdk-checksum-linux: "0f09bf6a0625e346bf004be70b92907e934a4c76606b323441b2baf3a5a0e66d" + vulkan-sdk-checksum-windows: "81f474711e9042f4cd22b31b2f7a8870db2e428b21586fb43dd80150be97310d" release: name: Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 66346b0ec..c3049a19b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.24) +cmake_minimum_required(VERSION 3.25) project(inexor-vulkan-renderer-core-lib CXX) set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake index 572ef043b..502f4b2e9 100644 --- a/cmake/dependencies.cmake +++ b/cmake/dependencies.cmake @@ -27,10 +27,10 @@ FetchContent_Declare(CLI11 SYSTEM # Formatting library FetchContent_Declare(fmt SYSTEM GIT_REPOSITORY https://github.com/fmtlib/fmt.git - GIT_TAG 11.0.1 + GIT_TAG 11.2.0 GIT_SHALLOW ON GIT_PROGRESS ON - FIND_PACKAGE_ARGS 11.0.1 + FIND_PACKAGE_ARGS 11.2.0 ) # Window management for Linux and Microsoft Windows @@ -129,8 +129,8 @@ FetchContent_Declare(volk SYSTEM # The Vulkan API headers FetchContent_Declare(Vulkan SYSTEM GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Headers - GIT_TAG v1.3.283 + GIT_TAG v1.4.357 GIT_SHALLOW ON GIT_PROGRESS ON - FIND_PACKAGE_ARGS 1.3.283 + FIND_PACKAGE_ARGS 1.4.357 ) diff --git a/documentation/source/engine-overview/cpp/design-patterns.rst b/documentation/source/engine-overview/cpp/design-patterns.rst index 5750699c0..77f5353fc 100644 --- a/documentation/source/engine-overview/cpp/design-patterns.rst +++ b/documentation/source/engine-overview/cpp/design-patterns.rst @@ -8,4 +8,3 @@ C++ Design Patterns - Check out `Refactoring Guru `__ to learn more about `software design patterns `__. - `Don't use the singleton pattern `__ as it makes thread safety and refactoring difficult. - Use the `builder pattern `__ for composition of complicated data structures. -- An example of a builder pattern would be the `descriptor builder `__. diff --git a/documentation/source/engine-overview/debugging/cla.rst b/documentation/source/engine-overview/debugging/cla.rst index a2c6fa781..30b9a2f84 100644 --- a/documentation/source/engine-overview/debugging/cla.rst +++ b/documentation/source/engine-overview/debugging/cla.rst @@ -15,6 +15,10 @@ You can start vulkan-renderer with the following command line arguments: Enables `vertical synchronization `__ (limits FPS to monitor refresh rate). +.. option:: --no-cmd-buf-cache + + Disables the secondary command buffer cache and records graphics passes directly into the primary command buffer. + .. option:: --maxfps Limits the max frames per seconds to a specified value. The command line argument will be `clamped `__ in between the values ``1 fps`` and ``2000 fps`` as lower and higher bounds. diff --git a/documentation/source/getting-started.rst b/documentation/source/getting-started.rst index dada81dd1..10bef44de 100644 --- a/documentation/source/getting-started.rst +++ b/documentation/source/getting-started.rst @@ -109,14 +109,14 @@ CMake Build Options - Default Value * - ``INEXOR_BUILD_EXAMPLE`` - Build ``inexor-renderer-example`` - - ``OFF`` + - ``ON`` * - ``INEXOR_BUILD_TESTS`` - Build ``inexor-renderer-tests`` - ``OFF`` * - ``INEXOR_BUILD_BENCHMARKS`` - Build ``inexor-renderer-benchmarks`` - ``OFF`` - * - ``INEXOR_BUILD_DOCUMENTATION`` + * - ``INEXOR_BUILD_DOC`` - Build the documentation with `Sphinx `__ - ``OFF`` * - ``INEXOR_BUILD_DOCUMENTATION_USE_VENV`` @@ -176,7 +176,7 @@ Alternatively, download the source code from the `GitHub repository #include @@ -23,26 +24,19 @@ #include #include +#include #include #include +#include namespace inexor::example_app { +// Using declarations using namespace inexor::vulkan_renderer; void ExampleApp::load_toml_configuration_file(const std::string &file_name) { spdlog::trace("Loading TOML configuration file: {}", file_name); - // @TODO Switch to std::filesystem::exists - std::ifstream toml_file(file_name, std::ios::in); - if (!toml_file) { - // If you are using CLion, go to "Edit Configurations" and select "Working Directory". - throw InexorException("Could not find configuration file: " + file_name + - "! You must set the working directory properly in your IDE"); - } - - toml_file.close(); - // Load the TOML file using tomlplusplus. auto config_file = toml::parse_file(file_name); @@ -52,85 +46,20 @@ void ExampleApp::load_toml_configuration_file(const std::string &file_name) { const std::string_view wnd_mode = config_file["application"]["window"]["mode"].value_or("windowed"); if (wnd_mode == "windowed") { - m_window_mode = Mode::WINDOWED; + m_window_mode = WindowMode::WINDOWED; } else if (wnd_mode == "windowed_fullscreen") { - m_window_mode = Mode::WINDOWED_FULLSCREEN; + m_window_mode = WindowMode::WINDOWED_FULLSCREEN; } else if (wnd_mode == "fullscreen") { - m_window_mode = Mode::FULLSCREEN; + m_window_mode = WindowMode::FULLSCREEN; } else { spdlog::warn("Invalid application window mode: {}", wnd_mode); - m_window_mode = Mode::WINDOWED; + m_window_mode = WindowMode::WINDOWED; } m_window_width = config_file["application"]["window"]["width"].value_or(1280); m_window_height = config_file["application"]["window"]["height"].value_or(720); m_window_title = config_file["application"]["window"]["name"].value_or("Undefined Window Title!"); spdlog::trace("Window: {}, {} x {}", m_window_title, m_window_width, m_window_height); - - spdlog::trace("Textures:"); - const auto texture_files = config_file["textures"]["files"].as_array(); - for (const auto &value : *texture_files) { - const auto texture_file = value.value_or(""); - spdlog::trace(" - {}", texture_file); - m_texture_files.push_back(texture_file); - } - - spdlog::trace("glTF 2.0 models:"); - const auto gltf_models = config_file["glTFmodels"]["files"].as_array(); - for (const auto &value : *gltf_models) { - const std::string gltf_model_file = value.value_or(""); - spdlog::trace(" - {}", gltf_model_file); - m_gltf_model_files.push_back(gltf_model_file); - } - - spdlog::trace("Vertex shaders:"); - const auto vertex_shader_files = config_file["shaders"]["vertex"]["files"].as_array(); - for (const auto &value : *vertex_shader_files) { - const std::string vertex_shader_file = value.value_or(""); - spdlog::trace(" - {}", vertex_shader_file); - m_vertex_shader_files.push_back(vertex_shader_file); - } - - spdlog::trace("Fragment shaders:"); - const auto fragment_shader_files = config_file["shaders"]["fragment"]["files"].as_array(); - for (const auto &value : *fragment_shader_files) { - const std::string fragment_shader_file = value.value_or(""); - spdlog::trace(" - {}", fragment_shader_file); - m_fragment_shader_files.push_back(fragment_shader_file); - } -} - -void ExampleApp::load_shaders() { - spdlog::trace("Loading vertex shaders:"); - - if (m_vertex_shader_files.empty()) { - spdlog::error("No vertex shaders to load!"); - } - - // Loop through the list of vertex shaders and initialise all of them. - for (const auto &vertex_shader_file : m_vertex_shader_files) { - spdlog::trace(" - {}", vertex_shader_file); - - // Insert the new shader into the list of shaders. - m_shaders.emplace_back(*m_device, VK_SHADER_STAGE_VERTEX_BIT, "unnamed vertex shader", vertex_shader_file); - } - - spdlog::trace("Loading fragment shaders:"); - - if (m_fragment_shader_files.empty()) { - spdlog::error("No fragment shaders to load!"); - } - - // Loop through the list of fragment shaders and initialise all of them. - for (const auto &fragment_shader_file : m_fragment_shader_files) { - spdlog::trace(" - {}", fragment_shader_file); - - // Insert the new shader into the list of shaders. - m_shaders.emplace_back(*m_device, VK_SHADER_STAGE_FRAGMENT_BIT, "unnamed fragment shader", - fragment_shader_file); - } - - spdlog::trace("Loading shaders finished"); } VkBool32 ExampleApp::validation_layer_debug_messenger_callback(const VkDebugUtilsMessageSeverityFlagBitsEXT severity, @@ -151,7 +80,7 @@ VkBool32 ExampleApp::validation_layer_debug_messenger_callback(const VkDebugUtil } void ExampleApp::load_octree_geometry(bool initialize) { - spdlog::trace("Creating octree geometry"); + const auto old_vertex_count = m_octree_vertices.size(); // 4: 23 012 | 5: 184352 | 6: 1474162 | 7: 11792978 cubes, DO NOT USE 7! m_worlds.clear(); @@ -175,13 +104,14 @@ void ExampleApp::load_octree_geometry(bool initialize) { } } } + spdlog::trace("Octree vertices generated [new: {}, old: {}]", m_octree_vertices.size(), old_vertex_count); } void ExampleApp::generate_octree_indices() { auto old_vertices = std::move(m_octree_vertices); m_octree_indices.clear(); m_octree_vertices.clear(); - std::unordered_map vertex_map; + std::unordered_map vertex_map; for (auto &vertex : old_vertices) { // TODO: Use std::unordered_map::contains() when we switch to C++ 20. if (vertex_map.count(vertex) == 0) { @@ -191,58 +121,45 @@ void ExampleApp::generate_octree_indices() { } m_octree_indices.push_back(vertex_map.at(vertex)); } - spdlog::trace("Reduced octree by {} vertices (from {} to {})", old_vertices.size() - m_octree_vertices.size(), - old_vertices.size(), m_octree_vertices.size()); - spdlog::trace("Total indices {} ", m_octree_indices.size()); + // @TODO Fix index generation and bring back debug output } void ExampleApp::setup_window_and_input_callbacks() { m_window->set_user_ptr(this); spdlog::trace("Setting up window callback:"); - auto lambda_frame_buffer_resize_callback = [](GLFWwindow *window, int width, int height) { auto *app = static_cast(glfwGetWindowUserPointer(window)); - spdlog::trace("Frame buffer resize callback called. window width: {}, height: {}", width, height); app->m_window_resized = true; }; - m_window->set_resize_callback(lambda_frame_buffer_resize_callback); spdlog::trace(" - keyboard button callback"); - auto lambda_key_callback = [](GLFWwindow *window, int key, int scancode, int action, int mods) { auto *app = static_cast(glfwGetWindowUserPointer(window)); app->m_input->key_callback(window, key, scancode, action, mods); }; - m_window->set_keyboard_button_callback(lambda_key_callback); spdlog::trace(" - cursor position callback"); - auto lambda_cursor_position_callback = [](GLFWwindow *window, double xpos, double ypos) { auto *app = static_cast(glfwGetWindowUserPointer(window)); app->m_input->cursor_position_callback(window, xpos, ypos); }; - m_window->set_cursor_position_callback(lambda_cursor_position_callback); spdlog::trace(" - mouse button callback"); - auto lambda_mouse_button_callback = [](GLFWwindow *window, int button, int action, int mods) { auto *app = static_cast(glfwGetWindowUserPointer(window)); app->m_input->mouse_button_callback(window, button, action, mods); }; - m_window->set_mouse_button_callback(lambda_mouse_button_callback); spdlog::trace(" - mouse wheel scroll callback"); - auto lambda_mouse_scroll_callback = [](GLFWwindow *window, double xoffset, double yoffset) { auto *app = static_cast(glfwGetWindowUserPointer(window)); app->m_input->mouse_scroll_callback(window, xoffset, yoffset); }; - m_window->set_mouse_scroll_callback(lambda_mouse_scroll_callback); } @@ -251,35 +168,37 @@ void ExampleApp::initialize_spdlog() { // messages to be written to the console and the logfile. spdlog::init_thread_pool(8192, 1); + // A copy of the console sink will automatically be saved to a logfile auto console_sink = std::make_shared(); - - // A copy of the console output will automatically be saved to a logfile auto file_sink = std::make_shared(std::string(meta::APP_NAME) + ".log", true); + + // We only use one global logger by default instead of one logger for each code component to keep it simple auto logger = std::make_shared("main", spdlog::sinks_init_list{console_sink, file_sink}, spdlog::thread_pool(), spdlog::async_overflow_policy::block); + spdlog::set_default_logger(logger); logger->flush_on(spdlog::level::trace); logger->set_level(spdlog::level::trace); logger->set_pattern("%Y-%m-%d %T.%f %^%l%$ %5t [%n] %v"); - - // We only use one global logger by default, not one logger for each component of the code. - spdlog::set_default_logger(logger); - - spdlog::trace("Inexor vulkan-renderer, BUILD " + std::string(__DATE__) + ", " + __TIME__); } ExampleApp::ExampleApp(int argc, char **argv) { initialize_spdlog(); + // Print some metadata about the project and build to console using namespace vulkan_renderer::meta; - + spdlog::trace("{}", APP_NAME); spdlog::trace("Application version: {}", APP_VERSION_STR); spdlog::trace("Engine version: {}", ENGINE_VERSION_STR); + spdlog::trace("Build configuration: {}", BUILD_TYPE); + spdlog::trace("Build date: {}, Time: {}", std::string(__DATE__), std::string(__TIME__)); + spdlog::trace("Git SHA: {}", BUILD_GIT); - // Parse command line arguments. + // Parse the command line arguments CLI::App app{"vulkan-renderer"}; argv = app.ensure_utf8(argv); app.add_flag("--vsync", m_vsync_enabled); + app.add_flag("--no-cmd-buf-cache", m_no_cmd_buf_cache); std::optional preferred_gpu; app.add_option("--gpu", preferred_gpu); std::uint32_t max_fps = FPSLimiter::DEFAULT_FPS; @@ -290,8 +209,6 @@ ExampleApp::ExampleApp(int argc, char **argv) { load_toml_configuration_file("assets/configuration/renderer.toml"); - spdlog::trace("Creating Vulkan instance"); - m_window = std::make_unique(m_window_title, m_window_width, m_window_height, true, true, m_window_mode); std::vector instance_layers; @@ -336,41 +253,34 @@ ExampleApp::ExampleApp(int argc, char **argv) { spdlog::error("Instance layer 'VK_LAYER_KHRONOS_validation' is not available on this system!"); } - m_instance = std::make_unique(instance_layers, instance_extensions); + spdlog::trace("Creating Vulkan instance"); + m_instance = std::make_unique(instance_layers, instance_extensions); m_dbg_callback = std::make_unique(*m_instance, validation_layer_debug_messenger_callback); m_input = std::make_unique(); - - m_surface = std::make_unique(m_instance->instance(), m_window->window()); - setup_window_and_input_callbacks(); - spdlog::trace("Creating window surface"); + m_surface = std::make_unique(m_instance->instance(), m_window->window()); if (preferred_gpu) { spdlog::trace("Preferential graphics card index {} specified", *preferred_gpu); } - if (m_vsync_enabled) { - spdlog::trace("V-sync enabled!"); - } else { - spdlog::trace("V-sync disabled!"); - } + spdlog::trace("V-sync {}", m_vsync_enabled ? "enabled" : "disabled"); const auto physical_devices = tools::get_physical_devices(m_instance->instance()); if (preferred_gpu && *preferred_gpu >= physical_devices.size()) { spdlog::critical("GPU index {} is out of range!", *preferred_gpu); - // The most suitable gpu will be chosen automatically later. + // NOTE: This is not a problem, the most suitable gpu will be chosen automatically later! preferred_gpu = std::nullopt; } const VkPhysicalDeviceFeatures required_features{ - // Add required physical device features here + // Add required physical device features here if desired... }; std::vector required_extensions{ - // Since we want to draw on a window, we need the swapchain extension VK_KHR_SWAPCHAIN_EXTENSION_NAME, }; @@ -382,51 +292,88 @@ ExampleApp::ExampleApp(int argc, char **argv) { m_device = std::make_unique(*m_instance, m_surface->surface(), physical_device, required_features, required_extensions); - m_swapchain = std::make_unique(*m_device, m_surface->surface(), m_window->width(), m_window->height(), - m_vsync_enabled); + m_swapchain = std::make_shared(*m_device, "m_swapchain", m_surface->surface()); m_camera = std::make_unique(glm::vec3(6.0f, 10.0f, 2.0f), 180.0f, 0.0f, static_cast(m_window->width()), static_cast(m_window->height())); + // @TODO Find a balance between exposing too few and too many parameters in Camera class constructor + m_camera->set_near_plane(0.1f); m_camera->set_movement_speed(5.0f); m_camera->set_rotation_speed(0.5f); - load_shaders(); - - m_uniform_buffers.emplace_back(*m_device, "matrices uniform buffer", sizeof(UniformBufferObject)); - - // Create an instance of the resource descriptor builder. - // This allows us to make resource descriptors with the help of a builder pattern. - DescriptorBuilder descriptor_builder(*m_device); - - // Make use of the builder to create a resource descriptor for the uniform buffer. - m_descriptors.emplace_back( - descriptor_builder.add_uniform_buffer(m_uniform_buffers[0].buffer(), 0) - .build("Default uniform buffer")); + m_render_graph = std::make_unique(*m_device, !m_no_cmd_buf_cache); load_octree_geometry(true); generate_octree_indices(); m_window->show(); recreate_swapchain(); + setup_render_graph(); + + m_octree_renderer->set_vertices_and_indices(m_octree_vertices, m_octree_indices); } -void ExampleApp::update_uniform_buffers() { - UniformBufferObject ubo{}; +ExampleApp::~ExampleApp() {} - ubo.model = glm::mat4(1.0f); - ubo.view = m_camera->view_matrix(); - ubo.proj = m_camera->perspective_matrix(); - ubo.proj[1][1] *= -1; +void ExampleApp::render_frame() { + if (m_window_resized) { + m_window_resized = false; + recreate_swapchain(); + return; + } - // TODO: Embed this into the render graph. - m_uniform_buffers[0].update(&ubo, sizeof(ubo)); + m_render_graph->render(); + + if (auto fps_value = m_fps_limiter.get_fps()) { + m_window->set_title("Inexor Vulkan API renderer demo - " + std::to_string(*fps_value) + " FPS"); + } +} + +void ExampleApp::recreate_swapchain() { + m_window->wait_for_focus(); + m_device->wait_idle(); + + // Query the framebuffer size here again although the window width is set during framebuffer resize callback + // The reason for this is that the framebuffer size could already be different again because we missed a poll + // This seems to be an issue on Linux only though + auto [window_width, window_height] = m_window->get_framebuffer_size(); + m_camera->set_aspect_ratio(window_width, window_height); + + m_swapchain->setup_swapchain( + VkExtent2D{static_cast(window_width), static_cast(window_height)}, + m_vsync_enabled); + + // @TODO Update or recreate all swapchain or image attachments! +} + +void ExampleApp::setup_render_graph() { + // Create a depth buffer for octree rendering (ImGui pass does not require it) + m_depth_buffer = m_render_graph->add_texture("m_depth_buffer", TextureUsage::DEPTH_ATTACHMENT, + VK_FORMAT_D32_SFLOAT_S8_UINT, m_swapchain->extent().width, + m_swapchain->extent().height, 1, VK_SAMPLE_COUNT_1_BIT, [&]() { + if (const auto depth_buffer = m_depth_buffer.lock()) { + const auto extent = m_swapchain->extent(); + depth_buffer->request_resize(extent.width, extent.height); + } + }); + + // Initialize the octree renderer + m_octree_renderer = std::make_unique(m_render_graph, m_swapchain, m_depth_buffer, m_camera); + + // Initialize the ImGui renderer + m_imgui_renderer = std::make_unique(m_render_graph, m_swapchain, [&]() { + // This is the external user-defined ImGui update function + update_imgui_overlay(); + }); + + m_render_graph->compile(); } void ExampleApp::update_imgui_overlay() { auto cursor_pos = m_input->kbm_data().get_cursor_pos(); ImGuiIO &io = ImGui::GetIO(); - io.DeltaTime = m_time_passed; + io.DeltaTime = m_fps_limiter.elapsed_seconds(); io.MousePos = ImVec2(static_cast(cursor_pos[0]), static_cast(cursor_pos[1])); io.MouseDown[0] = m_input->kbm_data().is_mouse_button_pressed(GLFW_MOUSE_BUTTON_LEFT); io.MouseDown[1] = m_input->kbm_data().is_mouse_button_pressed(GLFW_MOUSE_BUTTON_RIGHT); @@ -442,8 +389,9 @@ void ExampleApp::update_imgui_overlay() { ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); ImGui::Text("%s", m_device->gpu_name().c_str()); ImGui::Text("Engine version %s (git SHA %s)", ENGINE_VERSION_STR, BUILD_GIT); - ImGui::Text("Vulkan API %d.%d.%d", VK_API_VERSION_MAJOR(VK_API_VERSION_1_2), - VK_API_VERSION_MINOR(VK_API_VERSION_1_2), VK_API_VERSION_PATCH(VK_API_VERSION_1_2)); + ImGui::Text("Vulkan API %d.%d.%d", VK_API_VERSION_MAJOR(Instance::REQUIRED_VK_API_VERSION), + VK_API_VERSION_MINOR(Instance::REQUIRED_VK_API_VERSION), + VK_API_VERSION_PATCH(Instance::REQUIRED_VK_API_VERSION)); const auto cam_pos = m_camera->position(); ImGui::Text("Camera position (%.2f, %.2f, %.2f)", cam_pos.x, cam_pos.y, cam_pos.z); const auto cam_rot = m_camera->rotation(); @@ -457,13 +405,11 @@ void ExampleApp::update_imgui_overlay() { ImGui::Text("Yaw: %.2f pitch: %.2f roll: %.2f", m_camera->yaw(), m_camera->pitch(), m_camera->roll()); const auto cam_fov = m_camera->fov(); ImGui::Text("Field of view: %d", static_cast(cam_fov)); - ImGui::PushItemWidth(150.0f * m_imgui_overlay->scale()); + ImGui::PushItemWidth(150.0f); ImGui::PopItemWidth(); ImGui::End(); ImGui::PopStyleVar(); ImGui::Render(); - - m_imgui_overlay->update(); } void ExampleApp::process_input() { @@ -488,11 +434,11 @@ void ExampleApp::process_input() { m_input->gamepad_data().current_joystick_axes(0)[GLFW_GAMEPAD_AXIS_LEFT_Y] >= 0.15); m_camera->set_movement_state(CameraMovement::RIGHT, m_input->gamepad_data().current_joystick_axes(0)[GLFW_GAMEPAD_AXIS_LEFT_X] >= 0.15); - m_camera->update(m_time_passed); m_camera->set_movement_state(CameraMovement::FORWARD, m_input->kbm_data().is_key_pressed(GLFW_KEY_W)); m_camera->set_movement_state(CameraMovement::LEFT, m_input->kbm_data().is_key_pressed(GLFW_KEY_A)); m_camera->set_movement_state(CameraMovement::BACKWARD, m_input->kbm_data().is_key_pressed(GLFW_KEY_S)); m_camera->set_movement_state(CameraMovement::RIGHT, m_input->kbm_data().is_key_pressed(GLFW_KEY_D)); + m_camera->update(m_fps_limiter.elapsed_seconds()); } void ExampleApp::check_octree_collisions() { @@ -523,18 +469,14 @@ void ExampleApp::run() { m_window->poll(); if (m_fps_limiter.is_next_frame_allowed()) { m_input->update_gamepad_data(); - update_uniform_buffers(); + process_input(); update_imgui_overlay(); render_frame(); - process_input(); if (m_input->kbm_data().was_key_pressed_once(GLFW_KEY_N)) { load_octree_geometry(false); generate_octree_indices(); - m_index_buffer->upload_data(m_octree_indices); - m_vertex_buffer->upload_data(m_octree_vertices); + m_octree_renderer->set_vertices_and_indices(m_octree_vertices, m_octree_indices); } - m_camera->update(m_time_passed); - m_time_passed = m_stopwatch.time_step(); check_octree_collisions(); } } diff --git a/example-app/application.hpp b/example-app/application.hpp index c6890fa57..d129d1e29 100644 --- a/example-app/application.hpp +++ b/example-app/application.hpp @@ -2,47 +2,56 @@ #include "renderer.hpp" -#include "inexor/vulkan-renderer/input/input.hpp" - namespace inexor::vulkan_renderer::octree { // Forward declaration class Cube; } // namespace inexor::vulkan_renderer::octree +namespace inexor::vulkan_renderer::render_modules::octree { +// Forward declaration +class OctreeVertex; +} // namespace inexor::vulkan_renderer::render_modules::octree + +namespace inexor::vulkan_renderer::input { +// Forward declaration +class Input; +} // namespace inexor::vulkan_renderer::input + namespace inexor::vulkan_renderer::wrapper::windows { // Forward declaration -class Window; +enum class WindowMode; } // namespace inexor::vulkan_renderer::wrapper::windows -// Using declarations -using inexor::vulkan_renderer::input::Input; -using inexor::vulkan_renderer::octree::Cube; -using inexor::vulkan_renderer::wrapper::Instance; -using inexor::vulkan_renderer::wrapper::windows::Window; -using inexor::vulkan_renderer::wrapper::windows::WindowSurface; - namespace inexor::example_app { +// Using declarations +using vulkan_renderer::input::Input; +using vulkan_renderer::octree::Cube; +using vulkan_renderer::render_graph::TextureUsage; +using vulkan_renderer::render_modules::octree::OctreeVertex; +using vulkan_renderer::tools::CameraMovement; +using vulkan_renderer::tools::CameraType; +using vulkan_renderer::tools::FPSLimiter; +using vulkan_renderer::tools::InexorException; +using vulkan_renderer::tools::VulkanException; +using vulkan_renderer::wrapper::Instance; +using vulkan_renderer::wrapper::descriptors::DescriptorSetLayoutBuilder; +using vulkan_renderer::wrapper::descriptors::WriteDescriptorSetBuilder; +using vulkan_renderer::wrapper::pipelines::GraphicsPipelineBuilder; +using vulkan_renderer::wrapper::windows::WindowMode; + /// A sample application demonstrating Inexor's vulkan-renderer. class ExampleApp : public ExampleAppBase { private: - std::vector m_vertex_shader_files; - std::vector m_fragment_shader_files; - std::vector m_texture_files; - std::vector m_gltf_model_files; - std::vector m_shader_stages; std::uint32_t m_window_width{0}; std::uint32_t m_window_height{0}; - Mode m_window_mode{Mode::WINDOWED}; + WindowMode m_window_mode; std::string m_window_title; - std::vector m_textures; + bool m_no_cmd_buf_cache{false}; - /// @TODO The TimeStep class can be removed because we have FPSLimiter which delivers the time_passed. - TimeStep m_stopwatch; - /// Necessary for taking into account the relative speed of the system's CPU. - /// @TODO This can also be removed. - float m_time_passed{0.0f}; + std::vector m_octree_vertices; + std::vector m_octree_indices; static VkBool32 validation_layer_debug_messenger_callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, @@ -56,17 +65,18 @@ class ExampleApp : public ExampleAppBase { /// @brief file_name The TOML configuration file. /// @note It was collectively decided not to use JSON for configuration files. void load_toml_configuration_file(const std::string &file_name); - void load_shaders(); /// @param initialize Initialize worlds with a fixed seed, which is useful for benchmarking and testing void load_octree_geometry(bool initialize); void setup_window_and_input_callbacks(); void update_imgui_overlay(); - void update_uniform_buffers(); /// Use the camera's position and view direction vector to check for ray-octree collisions with all octrees. void check_octree_collisions(); void process_input(); void generate_octree_indices(); void initialize_spdlog(); + void recreate_swapchain(); + void render_frame(); + void setup_render_graph(); public: // A wrapper class for mouse, keyboard, and gamepad/joystick input. @@ -74,13 +84,9 @@ class ExampleApp : public ExampleAppBase { public: ExampleApp(int argc, char **argv); + ~ExampleApp(); void run(); }; -// Using declarations -using inexor::vulkan_renderer::tools::FPSLimiter; -using inexor::vulkan_renderer::tools::InexorException; -using inexor::vulkan_renderer::tools::VulkanException; - } // namespace inexor::example_app diff --git a/example-app/renderer.cpp b/example-app/renderer.cpp index aeea55c13..620db4ee0 100644 --- a/example-app/renderer.cpp +++ b/example-app/renderer.cpp @@ -1,106 +1,39 @@ #include "renderer.hpp" -#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" -#include "standard_ubo.hpp" +#include "inexor/vulkan-renderer/wrapper/windows/surface.hpp" +#include "inexor/vulkan-renderer/wrapper/windows/window.hpp" namespace inexor::example_app { -void ExampleAppBase::setup_render_graph() { - m_back_buffer = m_render_graph->add("back buffer", TextureUsage::BACK_BUFFER); - m_back_buffer->set_format(m_swapchain->image_format()); - - auto *depth_buffer = m_render_graph->add("depth buffer", TextureUsage::DEPTH_STENCIL_BUFFER); - depth_buffer->set_format(VK_FORMAT_D32_SFLOAT_S8_UINT); - - m_index_buffer = m_render_graph->add("index buffer", BufferUsage::INDEX_BUFFER); - m_index_buffer->upload_data(m_octree_indices); - - m_vertex_buffer = m_render_graph->add("vertex buffer", BufferUsage::VERTEX_BUFFER); - m_vertex_buffer->add_vertex_attribute(VK_FORMAT_R32G32B32_SFLOAT, offsetof(OctreeGpuVertex, position)); // NOLINT - m_vertex_buffer->add_vertex_attribute(VK_FORMAT_R32G32B32_SFLOAT, offsetof(OctreeGpuVertex, color)); // NOLINT - m_vertex_buffer->upload_data(m_octree_vertices); - - auto *main_stage = m_render_graph->add("main stage"); - main_stage->writes_to(m_back_buffer); - main_stage->writes_to(depth_buffer); - main_stage->reads_from(m_index_buffer); - main_stage->reads_from(m_vertex_buffer); - main_stage->bind_buffer(m_vertex_buffer, 0); - main_stage->set_clears_screen(true); - main_stage->set_depth_options(true, true); - main_stage->set_on_record([&](const PhysicalStage &physical, const CommandBuffer &cmd_buf) { - cmd_buf.bind_descriptor_sets(m_descriptors[0].descriptor_sets(), physical.m_pipeline->pipeline_layout()); - cmd_buf.draw_indexed(static_cast(m_octree_indices.size())); - }); - - for (const auto &shader : m_shaders) { - main_stage->uses_shader(shader); - } - - main_stage->add_descriptor_layout(m_descriptors[0].descriptor_set_layout()); -} - -void ExampleAppBase::recreate_swapchain() { - m_window->wait_for_focus(); - m_device->wait_idle(); - - // Query the framebuffer size here again although the window width is set during framebuffer resize callback - // The reason for this is that the framebuffer size could already be different again because we missed a poll - // This seems to be an issue on Linux only though - auto [window_width, window_height] = m_window->get_framebuffer_size(); - - // TODO: This should be abstracted itno a method of the Window wrapper. - // TODO: This is quite naive, we don't need to recompile the whole render graph on swapchain invalidation. - m_render_graph.reset(); - // Recreate the swapchain - m_swapchain->setup_swapchain( - VkExtent2D{static_cast(window_width), static_cast(window_height)}, - m_vsync_enabled); - m_render_graph = std::make_unique(*m_device, *m_swapchain); - setup_render_graph(); - - m_camera->set_aspect_ratio(window_width, window_height); - - m_imgui_overlay.reset(); - m_imgui_overlay = std::make_unique(*m_device, *m_swapchain, m_render_graph.get(), m_back_buffer); - m_render_graph->compile(m_back_buffer); -} - -void ExampleAppBase::render_frame() { - if (m_window_resized) { - m_window_resized = false; - recreate_swapchain(); - return; - } - - const auto image_index = m_swapchain->acquire_next_image_index(); - const auto &cmd_buf = m_device->request_command_buffer(VulkanQueueType::QUEUE_TYPE_GRAPHICS, "rendergraph"); - - m_render_graph->render(image_index, cmd_buf); - - const std::array stage_mask{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; - - cmd_buf.submit_and_wait(inexor::vulkan_renderer::wrapper::make_info({ - .waitSemaphoreCount = 1, - .pWaitSemaphores = m_swapchain->image_available_semaphore(), - .pWaitDstStageMask = stage_mask.data(), - .commandBufferCount = 1, - })); - - m_swapchain->present(image_index); - - if (auto fps_value = m_fps_limiter.get_fps()) { - m_window->set_title("Inexor Vulkan API renderer demo - " + std::to_string(*fps_value) + " FPS"); - spdlog::trace("FPS: {}, window size: {} x {}", *fps_value, m_window->width(), m_window->height()); - } -} +ExampleAppBase::ExampleAppBase() {} ExampleAppBase::~ExampleAppBase() { spdlog::trace("Shutting down vulkan renderer"); - if (m_device == nullptr) { - return; - } m_device->wait_idle(); + spdlog::trace("Releasing octree renderer"); + m_octree_renderer.reset(); + spdlog::trace("Releasing imgui renderer"); + m_imgui_renderer.reset(); + spdlog::trace("Resetting render graph"); + if (m_render_graph) { + m_render_graph->reset_graph(); + } + spdlog::trace("Releasing render graph"); + m_render_graph.reset(); + spdlog::trace("Releasing swapchain"); + m_swapchain.reset(); + spdlog::trace("Releasing surface"); + m_surface.reset(); + spdlog::trace("Releasing window"); + m_window.reset(); + spdlog::trace("Releasing camera"); + m_camera.reset(); + spdlog::trace("Releasing device"); + m_device.reset(); + spdlog::trace("Releasing debug callback"); + m_dbg_callback.reset(); + spdlog::trace("Releasing instance"); + m_instance.reset(); } } // namespace inexor::example_app diff --git a/example-app/renderer.hpp b/example-app/renderer.hpp index c3b269228..aac4897f8 100644 --- a/example-app/renderer.hpp +++ b/example-app/renderer.hpp @@ -1,20 +1,18 @@ #pragma once -#include "inexor/vulkan-renderer/imgui.hpp" +// @TODO Forward-declare as much as possible! +#include "inexor/vulkan-renderer/render-graph/render_graph.hpp" +#include "inexor/vulkan-renderer/render-modules/imgui/imgui_renderer.hpp" +#include "inexor/vulkan-renderer/render-modules/octree/octree_renderer.hpp" #include "inexor/vulkan-renderer/tools/camera.hpp" #include "inexor/vulkan-renderer/tools/fps_limiter.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/tools/time_step.hpp" #include "inexor/vulkan-renderer/wrapper/debug_callback.hpp" -#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_builder.hpp" -#include "inexor/vulkan-renderer/wrapper/gpu_texture.hpp" #include "inexor/vulkan-renderer/wrapper/instance.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp" #include "inexor/vulkan-renderer/wrapper/shader.hpp" -#include "inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp" -#include "inexor/vulkan-renderer/wrapper/uniform_buffer.hpp" -#include "inexor/vulkan-renderer/wrapper/windows/surface.hpp" -#include "inexor/vulkan-renderer/wrapper/windows/window.hpp" -#include "octree_gpu_vertex.hpp" #include #include @@ -22,17 +20,10 @@ namespace inexor::vulkan_renderer::wrapper { // Forward declarations class Device; -class GpuTexture; class Instance; class Shader; -class UniformBuffer; } // namespace inexor::vulkan_renderer::wrapper -namespace inexor::vulkan_renderer::wrapper::descriptors { -// Forward declaration -class ResourceDescriptor; -} // namespace inexor::vulkan_renderer::wrapper::descriptors - namespace inexor::vulkan_renderer::wrapper::swapchains { // Forward declaration class Swapchain; @@ -45,77 +36,67 @@ class FPSLimiter; class TimeStep; } // namespace inexor::vulkan_renderer::tools -namespace inexor::vulkan_renderer { -// Forward declarations -class ImGUIOverlay; -class RenderGraph; -} // namespace inexor::vulkan_renderer +namespace inexor::vulkan_renderer::render_modules::imgui { +// Forward declaration +class ImGuiRenderer; +} // namespace inexor::vulkan_renderer::render_modules::imgui -// Using declarations -using inexor::vulkan_renderer::BufferResource; -using inexor::vulkan_renderer::BufferUsage; -using inexor::vulkan_renderer ::GraphicsStage; -using inexor::vulkan_renderer::ImGUIOverlay; -using inexor::vulkan_renderer::PhysicalStage; -using inexor::vulkan_renderer::RenderGraph; -using inexor::vulkan_renderer::TextureResource; -using inexor::vulkan_renderer::TextureUsage; -using inexor::vulkan_renderer::tools::Camera; -using inexor::vulkan_renderer::tools::CameraMovement; -using inexor::vulkan_renderer::tools::CameraType; -using inexor::vulkan_renderer::tools::FPSLimiter; -using inexor::vulkan_renderer::tools::TimeStep; -using inexor::vulkan_renderer::wrapper::Device; -using inexor::vulkan_renderer::wrapper::GpuTexture; -using inexor::vulkan_renderer::wrapper::Instance; -using inexor::vulkan_renderer::wrapper::Shader; -using inexor::vulkan_renderer::wrapper::UniformBuffer; -using inexor::vulkan_renderer::wrapper::VulkanDebugUtilsCallback; -using inexor::vulkan_renderer::wrapper::VulkanQueueType; -using inexor::vulkan_renderer::wrapper::commands::CommandBuffer; -using inexor::vulkan_renderer::wrapper::descriptors::DescriptorBuilder; -using inexor::vulkan_renderer::wrapper::descriptors::ResourceDescriptor; -using inexor::vulkan_renderer::wrapper::swapchains::Swapchain; -using inexor::vulkan_renderer::wrapper::windows::Mode; -using inexor::vulkan_renderer::wrapper::windows::Window; -using inexor::vulkan_renderer::wrapper::windows::WindowSurface; +namespace inexor::vulkan_renderer::render_modules::imgui { +// Forward declaration +class OctreeRenderer; +} // namespace inexor::vulkan_renderer::render_modules::imgui + +namespace inexor::vulkan_renderer::wrapper::windows { +// Forward declaration +class Window; +class WindowSurface; +} // namespace inexor::vulkan_renderer::wrapper::windows namespace inexor::example_app { +// Using declarations +using vulkan_renderer::render_graph::RenderGraph; +using vulkan_renderer::render_modules::imgui::ImGuiRenderer; +using vulkan_renderer::render_modules::octree::OctreeRenderer; +using vulkan_renderer::tools::Camera; +using vulkan_renderer::tools::FPSLimiter; +using vulkan_renderer::wrapper::Device; +using vulkan_renderer::wrapper::Instance; +using vulkan_renderer::wrapper::VulkanDebugUtilsCallback; +using vulkan_renderer::wrapper::swapchains::Swapchain; +using vulkan_renderer::wrapper::windows::Window; +using vulkan_renderer::wrapper::windows::WindowSurface; + /// The base class of the Inexor vulkan-renderer example app. class ExampleAppBase { protected: - // Make sure to declare members in dependency order to ensure correct destruction order. - // Dependencies must be declared before dependents (destroyed in reverse order). + /// NOTE: The declaration order of members determines the order of destruction! + /// This means we must keep the instance before the debug utils messenger callback! std::unique_ptr m_instance; std::unique_ptr m_dbg_callback; - std::unique_ptr m_surface; + std::unique_ptr m_device; + + std::unique_ptr m_surface; + std::shared_ptr m_swapchain; + std::shared_ptr m_render_graph; + std::weak_ptr m_depth_buffer; + + std::shared_ptr m_camera; std::unique_ptr m_window; - std::unique_ptr m_swapchain; - std::unique_ptr m_render_graph; - - void setup_render_graph(); - void recreate_swapchain(); - void render_frame(); - - // TODO Everything below needs to be abstracted further so that it's no longer - // part of ExampleAppBase, meaning the rendergraph requires further abstraction. - TextureResource *m_back_buffer{nullptr}; - BufferResource *m_index_buffer{nullptr}; - BufferResource *m_vertex_buffer{nullptr}; - std::vector m_descriptors; - std::vector m_octree_vertices; - std::vector m_octree_indices; - std::vector m_shaders; + + std::unique_ptr m_imgui_renderer; + std::unique_ptr m_octree_renderer; + bool m_vsync_enabled{false}; - std::unique_ptr m_camera; - std::unique_ptr m_imgui_overlay; + + // @TODO Move to window wrapper! bool m_window_resized{false}; + FPSLimiter m_fps_limiter; - std::vector m_uniform_buffers; public: + ExampleAppBase(); ~ExampleAppBase(); }; diff --git a/include/inexor/vulkan-renderer/input/input.hpp b/include/inexor/vulkan-renderer/input/input.hpp index ebe3a11fd..c0c0b7b57 100644 --- a/include/inexor/vulkan-renderer/input/input.hpp +++ b/include/inexor/vulkan-renderer/input/input.hpp @@ -1,18 +1,22 @@ #pragma once -#include "inexor/vulkan-renderer/input/gamepad_data.hpp" -#include "inexor/vulkan-renderer/input/keyboard_mouse_data.hpp" - #include +#include + namespace inexor::vulkan_renderer::input { + +struct GamepadInputData; +struct KeyboardMouseInputData; + class Input { private: - GamepadInputData m_gamepad_data; - KeyboardMouseInputData m_kbm_data; + std::unique_ptr m_gamepad_data; + std::unique_ptr m_kbm_data; public: - Input() = default; + Input(); + ~Input(); /// @brief Call glfwSetCursorPosCallback. /// @param window The window that received the event. @@ -20,13 +24,9 @@ class Input { /// @param y_pos The new y-coordinate, in screen coordinates, of the cursor. void cursor_position_callback(GLFWwindow *window, double x_pos, double y_pos); - GamepadInputData &gamepad_data() { - return m_gamepad_data; - } + [[nodiscard]] GamepadInputData &gamepad_data(); - KeyboardMouseInputData &kbm_data() { - return m_kbm_data; - } + [[nodiscard]] KeyboardMouseInputData &kbm_data(); /// @brief Call glfwSetKeyCallback. /// @param window The window that received the event. @@ -53,4 +53,5 @@ class Input { void update_gamepad_data(); }; + } // namespace inexor::vulkan_renderer::input diff --git a/include/inexor/vulkan-renderer/render-graph/buffer.hpp b/include/inexor/vulkan-renderer/render-graph/buffer.hpp new file mode 100644 index 000000000..49dadaa39 --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/buffer.hpp @@ -0,0 +1,212 @@ +#pragma once + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::commands { +/// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { +/// Forward declaration +class PipelineBarrierBuilder; +} // namespace inexor::vulkan_renderer::wrapper::synchronization + +namespace inexor::vulkan_renderer::tools { +/// Forward declarations +class InexorException; +class VulkanException; +} // namespace inexor::vulkan_renderer::tools + +namespace inexor::vulkan_renderer::render_graph { + +// Forward declaration +class RenderGraph; +class StagingBuffer; + +// Using declarations +using tools::InexorException; +using tools::VulkanException; +using wrapper::Device; +using wrapper::commands::CommandBuffer; + +/// The supported buffer types +/// Based on the buffer type, the rendergraph will use Vulkan Memory Allocator to create the buffers +enum class BufferType { + VERTEX_BUFFER, + INDEX_BUFFER, + UNIFORM_BUFFER, + // @TODO: Support more buffer types (storage buffer, indirect buffer...) +}; + +enum class BufferUpdateMode { + DEVICE_LOCAL, + PER_FRAME_DEVICE_LOCAL, + PER_FRAME_HOST_VISIBLE, +}; + +struct PendingBufferCopy { + VkBuffer src_buffer{VK_NULL_HANDLE}; + VkBuffer dst_buffer{VK_NULL_HANDLE}; + VkBufferCopy region{}; + VkPipelineStageFlags2 dst_stage_mask{VK_PIPELINE_STAGE_2_NONE}; + VkAccessFlags2 dst_access_mask{VK_ACCESS_2_NONE}; +}; + +class Buffer { + friend render_graph::RenderGraph; + +private: + struct PerFrameBufferResources { + VkBuffer m_buffer{VK_NULL_HANDLE}; + VmaAllocation m_alloc{VK_NULL_HANDLE}; + VmaAllocationInfo m_alloc_info{}; + std::size_t m_buffer_capacity{0}; + VkDescriptorBufferInfo m_descriptor_buffer_info{}; + }; + + /// The device wrapper + const Device &m_device; + /// The internal debug name of the buffer resource + std::string m_name; + + /// The buffer type will be set depending on which constructor of the Buffer wrapper is called by rendergraph. The + /// engine currently supports three different types of buffers in the Buffer wrapper class: vertex buffers, index + /// buffers, and uniform buffers. The instances of the Buffer wrapper class are managed by rendergraph only. One + /// solution to deal with the different buffer types would be to use a BufferBase class and to make three distinct + /// classes VertexBuffer, IndexBuffer, and UniformBuffer. However, we aimed for simplicity and wanted to avoid + /// polymorphism in the rendergraph for performance reasons. We also refrained from using templates for this use + /// case. Therefore, we have chosen to use only one Buffer wrapper class which contains members for all three + /// different buffer types. The type of the buffer will be set depending on which Buffer constructor is called by + /// rendergraph. The actual memory management for the buffers is done by Vulkan Memory Allocator (VMA) internally. + BufferType m_buffer_type; + + BufferUpdateMode m_update_mode{BufferUpdateMode::DEVICE_LOCAL}; + + /// The buffer update function which is called by rendergraph to update the buffer's data. This update function is + /// called, no matter what the type of the buffer is. With the currently supported buffer types (vertex-, index-, + /// and uniform buffers) there is always a discussion about whether some update lambdas can be made std::optional. + /// For example we could have one vertex buffer with an index buffer and the index buffer is updated together with + /// the vertex buffer in the update function of the vertex buffer. From the design of the engine there is no + /// limitation which buffer is updated in which update function, as long as the handle to that buffer has been + /// created in rendergraph. In our example, the update function of the index buffer could be std::nullopt. In this + /// case, rendergraph could separate all buffers into those which require an update and those who do not. For + /// simplicity however, we made the update function not std::optional. + std::function m_on_check_for_update; + + /// NOTE: It's the responsibility of the programmer to make sure the data m_src_data points to is still valid when + /// update_buffer() is called! + void *m_src_data{nullptr}; + std::size_t m_src_data_size{0}; + bool m_update_requested{false}; + bool m_descriptor_resource_changed{true}; + + std::vector m_slots{1}; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + /// Create the buffer using Vulkan Memory Allocator (VMA) library + /// @param pending_buffer_copies A list of copy requests to be recorded later in a batched form + /// @param upload_buffer The shared upload arena buffer + /// @param upload_alloc The shared upload arena allocation + /// @param upload_offset Current write offset inside the shared upload arena buffer + /// @param pending_releases Deferred resource releases that must happen after GPU completion + void create(std::vector &pending_buffer_copies, StagingBuffer &staging_buffer, + std::size_t &upload_offset, std::vector> &pending_releases); + + [[nodiscard]] bool can_update_without_command_buffer() const; + + void update_without_command_buffer(); + + void create_per_frame_buffer_resources(PerFrameBufferResources &resources, + std::vector &pending_buffer_copies, + StagingBuffer &staging_buffer, std::size_t &upload_offset, + std::vector> &pending_releases, + std::size_t slot_index); + + void destroy_per_frame_buffer_resources(PerFrameBufferResources &resources); + + [[nodiscard]] PerFrameBufferResources ¤t_frame_resources(); + + [[nodiscard]] const PerFrameBufferResources ¤t_frame_resources() const; + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot); + + /// Destroy the buffer and the staging buffer + void destroy_all(); + +public: + /// Default constructor + /// @param device The device wrapper + /// @param name The buffer name + /// @param type The buffer type + /// @param on_update The buffer update function + Buffer(const Device &device, std::string name, BufferType type, std::function on_update, + BufferUpdateMode update_mode = BufferUpdateMode::DEVICE_LOCAL); + + Buffer(const Buffer &) = delete; + Buffer(Buffer &&other) noexcept; + + ~Buffer(); + + Buffer &operator=(const Buffer &) = delete; + Buffer &operator=(Buffer &&) = delete; + + [[nodiscard]] auto buffer() const { + return current_frame_resources().m_buffer; + } + + [[nodiscard]] const auto *buffer_address() const { + return ¤t_frame_resources().m_buffer; + } + + [[nodiscard]] const auto *descriptor_buffer_info() const { + return ¤t_frame_resources().m_descriptor_buffer_info; + } + + [[nodiscard]] auto name() const { + return m_name; + } + + /// Request a buffer update + /// @param src_data A pointer to the data to copy the updated data from + /// @warning It is the responsibility of the programmer to make sure src_data still points to valid memory when + /// update_buffer() is called! + /// @param src_data_size The size of the data to copy + void request_update(void *src_data, std::size_t src_data_size); + + /// Request a buffer update + /// @tparam BufferDataType + /// @param data + template + void request_update(BufferDataType &data) { + return request_update(std::addressof(data), sizeof(data)); + } + + /// Request a buffer update + /// @tparam BufferDataType + /// @param data + template + void request_update(std::vector &data) { + return request_update(data.data(), sizeof(BufferDataType) * data.size()); + } + + [[nodiscard]] auto type() const { + return m_buffer_type; + } + + [[nodiscard]] auto update_mode() const { + return m_update_mode; + } +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/buffer_copy_batch_builder.hpp b/include/inexor/vulkan-renderer/render-graph/buffer_copy_batch_builder.hpp new file mode 100644 index 000000000..3118334c4 --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/buffer_copy_batch_builder.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { +class PipelineBarrierBatchBuilder; +} // namespace inexor::vulkan_renderer::wrapper::synchronization + +namespace inexor::vulkan_renderer::render_graph { + +class BufferCopyBatchBuilder { +private: + struct BatchKey { + VkBuffer src_buffer{VK_NULL_HANDLE}; + VkBuffer dst_buffer{VK_NULL_HANDLE}; + + bool operator==(const BatchKey &) const = default; + }; + + struct BatchKeyHash { + std::size_t operator()(const BatchKey &key) const noexcept; + }; + + struct BatchValue { + std::vector regions; + VkPipelineStageFlags2 dst_stage_mask{VK_PIPELINE_STAGE_2_NONE}; + VkAccessFlags2 dst_access_mask{VK_ACCESS_2_NONE}; + VkDeviceSize min_offset{std::numeric_limits::max()}; + VkDeviceSize max_end{0}; + }; + + std::unordered_map m_batches; + std::vector m_scratch_regions; + bool m_needs_queue_family_ownership_transfer{false}; + std::uint32_t m_transfer_family_index{VK_QUEUE_FAMILY_IGNORED}; + std::uint32_t m_graphics_family_index{VK_QUEUE_FAMILY_IGNORED}; + +public: + BufferCopyBatchBuilder() = default; + + void set_queue_family_ownership_transfer(bool needs_transfer, std::uint32_t transfer_family_index, + std::uint32_t graphics_family_index); + + void add(const PendingBufferCopy ©_request); + void add(std::span copy_requests); + + [[nodiscard]] bool empty() const; + + void flush(const wrapper::commands::CommandBuffer &cmd_buf, + wrapper::synchronization::PipelineBarrierBatchBuilder &post_copy_barriers, + wrapper::synchronization::PipelineBarrierBatchBuilder &queue_family_acquire_barriers); + + void reset(); +}; + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/render-graph/frame_sync_manager.hpp b/include/inexor/vulkan-renderer/render-graph/frame_sync_manager.hpp new file mode 100644 index 000000000..2a4a448aa --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/frame_sync_manager.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::render_graph { + +class FrameSyncManager { +private: + const wrapper::Device &m_device; + std::vector m_frame_slot_submission_fences{VK_NULL_HANDLE}; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + struct DeferredRelease { + std::vector fences; + std::function release; + }; + + std::vector m_deferred_releases; + +public: + explicit FrameSyncManager(const wrapper::Device &device); + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot); + + void mark_frame_slot_submission_fence(VkFence fence); + + void defer_release(std::span fences, std::function release); + + void defer_release(VkFence fence, std::function release); + + void process_deferred_releases(bool wait_all); + + void clear(); + + [[nodiscard]] const std::vector &frame_slot_submission_fences() const { + return m_frame_slot_submission_fences; + } + + [[nodiscard]] std::size_t frame_slot_count() const { + return m_frame_slot_count; + } + + [[nodiscard]] std::size_t current_frame_slot() const { + return m_current_frame_slot; + } +}; + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/render-graph/graphics_pass.hpp b/include/inexor/vulkan-renderer/render-graph/graphics_pass.hpp new file mode 100644 index 000000000..a781e249e --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/graphics_pass.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include + +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" +#include "inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp" + +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::render_graph { + +// Forward declaration +class RenderGraph; + +// Using declarations +using wrapper::CommandBuffer; +using wrapper::DebugLabelColor; +using wrapper::descriptors::DescriptorSetLayout; +using wrapper::swapchains::Swapchain; + +/// A wrapper for graphics passes inside of rendergraph +class GraphicsPass { +private: + // Onyl rendergraph and swapchain management code are allowed to access the rendering info and related data + friend class RenderGraph; + friend class SwapchainManager; + + /// The name of the graphics pass + std::string m_name; + /// The command buffer recording function of the graphics pass + std::function m_on_record_cmd_buffer{[](auto &) {}}; + /// The descriptor set layout of the pass (this will be created by rendergraph) + std::unique_ptr m_descriptor_set_layout; + /// The descriptor set of the pass (this will be created by rendergraph) + VkDescriptorSet m_descriptor_set{VK_NULL_HANDLE}; + /// The color of the debug label region (visible in graphics debuggers like RenderDoc) + std::array m_debug_label_color; + /// The graphics pass image extent + VkExtent2D m_extent{0, 0}; + + /// The buffers which are read by this graphics pass + std::vector> m_buffer_reads; + /// The texture attachments of this pass (unified means color, depth, stencil attachment or a swapchain) + std::vector, std::optional>> m_texture_writes; + /// The swapchains this graphics pass writes to + std::vector, std::optional>> m_swapchain_writes; + + // All the data below will be filled and used by rendergraph only + + /// The rendering info will be filled during rendergraph compilation so we don't have to do this while rendering. + /// This means we must make sure that the memory of the attachment infos below is still valid during rendering, + /// which is why we store them as members here. + VkRenderingInfo m_rendering_info{}; + /// The color attachments inside of m_rendering_info + std::vector m_color_attachments{}; + /// The depth attachment inside of m_rendering_info + std::optional m_depth_attachment{std::nullopt}; + /// The stencil attachment inside of m_rendering_info + std::optional m_stencil_attachment{std::nullopt}; + + struct CachedAttachmentState { + VkImageView image_view{VK_NULL_HANDLE}; + VkImageLayout image_layout{VK_IMAGE_LAYOUT_UNDEFINED}; + VkExtent2D extent{0, 0}; + std::optional clear_value{std::nullopt}; + TextureUsage usage{TextureUsage::DEFAULT}; + }; + + bool m_rendering_info_dirty{true}; + std::size_t m_cached_texture_color_attachment_count{0}; + VkExtent2D m_cached_render_extent{0, 0}; + std::optional m_cached_texture_render_extent{std::nullopt}; + std::vector m_cached_texture_attachment_states{}; + std::vector m_cached_swapchain_attachment_states{}; + /// Cached storage for texture attachment formats when recording command buffers. + std::vector m_cached_texture_color_attachment_formats{}; + /// Cached storage for all color attachment formats when recording command buffers. + std::vector m_cached_color_attachment_formats{}; + VkFormat m_cached_depth_attachment_format{VK_FORMAT_UNDEFINED}; + VkFormat m_cached_stencil_attachment_format{VK_FORMAT_UNDEFINED}; + /// Reused scratch storage for RenderGraph::fill_graphics_pass_rendering_info() to avoid a heap allocation + /// every frame (this function runs unconditionally once per pass per frame) + std::vector m_scratch_current_texture_states{}; + /// @copydoc m_scratch_current_texture_states + std::vector m_scratch_current_swapchain_states{}; + + /// Reset the rendering info + void reset_rendering_info(); + +public: + /// Default constructor + /// @param name The name of the graphics pass + /// @param on_record_cmd_buffer The command buffer recording function of the graphics pass + /// @param buffer_reads The buffers which are read by this graphics pass + /// @param texture_writes The textures which are written to by this graphics pass + /// @param swapchain_writes The swapchains which are written to by this graphics pass + /// @param pass_debug_label_color The debug label of the pass (visible in graphics debuggers like RenderDoc) + GraphicsPass(std::string name, std::function on_record_cmd_buffer, + std::vector> buffer_reads, + std::vector, std::optional>> texture_writes, + std::vector, std::optional>> swapchain_writes, + DebugLabelColor pass_debug_label_color); + + GraphicsPass(const GraphicsPass &) = delete; + GraphicsPass(GraphicsPass &&other) noexcept; + ~GraphicsPass() = default; + + GraphicsPass &operator=(const GraphicsPass &) = delete; + GraphicsPass &operator=(GraphicsPass &&) = delete; +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/graphics_pass_builder.hpp b/include/inexor/vulkan-renderer/render-graph/graphics_pass_builder.hpp new file mode 100644 index 000000000..52a03890c --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/graphics_pass_builder.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/graphics_pass.hpp" +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp" + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::render_graph { + +// Using declarations +using wrapper::DebugLabelColor; +using wrapper::commands::CommandBuffer; +using wrapper::swapchains::Swapchain; + +/// A builder class for graphics passes in the rendergraph +class GraphicsPassBuilder { +private: + /// The command buffer recording function + std::function m_on_record_cmd_buffer; + /// The textures to which this graphics pass writes to + std::vector, std::optional>> m_texture_writes; + /// The swapchains to which this graphics pass writes to + std::vector, std::optional>> m_swapchain_writes; + /// The buffers which are read by this graphics pass + std::vector> m_buffer_reads; + /// The buffers which are written to by this graphics pass + std::vector> m_buffer_writes; + + /// Reset the data of the graphics pass builder + void reset(); + +public: + GraphicsPassBuilder(); + + GraphicsPassBuilder(const GraphicsPassBuilder &) = delete; + GraphicsPassBuilder(GraphicsPassBuilder &&) noexcept; + + /// Build the graphics pass + /// @param name The name of the graphics pass + /// @param color The debug label color (debug labels are specified per pass and are visible in RenderDoc debugger) + /// @return The graphics pass that was just created + [[nodiscard]] std::shared_ptr build(std::string name, DebugLabelColor color); + + /// Specify that this graphics pass reads from a buffer + /// @param buffer The buffer which is read by this graphics pass + /// @return A const reference to the this pointer (allowing method calls to be chained) + [[nodiscard]] GraphicsPassBuilder &reads_from(std::weak_ptr buffer); + + /// Set the function which will be called when the command buffer for rendering of the pass is being recorded + /// @param on_record_cmd_buffer The command buffer recording function + /// @return A const reference to the this pointer (allowing method calls to be chained) + [[nodiscard]] GraphicsPassBuilder &set_on_record(std::function on_record_cmd_buffer); + + /// Specify that this graphics pass writes to a buffer + /// @brief buffer The buffer that is written to + /// @note This feature might be used in the future if a pass writes to a buffer + /// @return A const reference to the this pointer (allowing method calls to be chained) + [[nodiscard]] GraphicsPassBuilder &writes_to(std::weak_ptr buffer); + + /// Specify that this graphics pass writes to an either a std::weak_ptr or a std::weak_ptr + /// @param attachment The attachment (either a std::weak_ptr or a std::weak_ptr) + /// @param clear_value The optional clear value of the attachment (``std::nullopt`` by default) + /// @return A const reference to the this pointer (allowing method calls to be chained) + [[nodiscard]] GraphicsPassBuilder & + writes_to(std::variant, std::weak_ptr> write_attachment, + std::optional clear_value = std::nullopt); +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/render_graph.hpp b/include/inexor/vulkan-renderer/render-graph/render_graph.hpp new file mode 100644 index 000000000..0dc851bec --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/render_graph.hpp @@ -0,0 +1,257 @@ +#pragma once + +// @TODO Forward-declare as much as possible! +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/buffer_copy_batch_builder.hpp" +#include "inexor/vulkan-renderer/render-graph/frame_sync_manager.hpp" +#include "inexor/vulkan-renderer/render-graph/graphics_pass.hpp" +#include "inexor/vulkan-renderer/render-graph/graphics_pass_builder.hpp" +#include "inexor/vulkan-renderer/render-graph/resource_descriptor_manager.hpp" +#include "inexor/vulkan-renderer/render-graph/staging_buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/swapchain_manager.hpp" +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/render-graph/texture_copy_batch_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer_cache.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::render_graph { + +// Using declarations +using wrapper::DebugLabelColor; +using wrapper::commands::CommandBufferCache; +using wrapper::descriptors::PerFrameDescriptorSets; +using wrapper::pipelines::GraphicsPipelineBuilder; +using wrapper::pipelines::PipelineCache; +using wrapper::synchronization::PipelineBarrierBuilder; +using wrapper::synchronization::Semaphore; + +// @TODO How to handle optional texture update depending on texture type? +// @TODO By implementing textures which are not updated, but only initliazed, we could save memory! +class RenderGraph { +private: + // The device wrapper + Device &m_device; + + /// -------------------------------------------------------------------------------------------------- + /// BUFFERS + /// -------------------------------------------------------------------------------------------------- + + /// The vertex buffers, index buffers, and uniform buffers + std::vector> m_buffers; + + /// -------------------------------------------------------------------------------------------------- + /// TEXTURES + /// -------------------------------------------------------------------------------------------------- + + /// The textures, back buffers, and depth buffers + std::vector> m_textures; + + /// The resource descriptor manager of the rendergraph + ResourceDescriptorManager m_resource_descriptors; + + /// -------------------------------------------------------------------------------------------------- + /// GRAPHICS PIPELINES + /// -------------------------------------------------------------------------------------------------- + + /// The graphics pipeline builder + GraphicsPipelineBuilder m_graphics_pipeline_builder; + /// A using declaration for graphics pipeline create functions + using OnBuildGraphicsPipeline = std::function; + /// The graphics pipeline create functions registered to the rendergraph + std::vector m_graphics_pipeline_create_functions; + + /// -------------------------------------------------------------------------------------------------- + /// GRAPHICS PASSES + /// -------------------------------------------------------------------------------------------------- + + /// The graphics pass builder + GraphicsPassBuilder m_graphics_pass_builder; + // A using declaration for graphics pass create functions + using OnBuildGraphicsPass = std::function(GraphicsPassBuilder &)>; + /// The graphics passes registered to the rendergraph + std::vector> m_graphics_passes; + + /// -------------------------------------------------------------------------------------------------- + + SwapchainManager m_swapchain_manager; + CommandBufferCache m_command_buffer_cache; + std::unique_ptr m_upload_finished; + bool m_upload_submission_pending{false}; + VkPipelineStageFlags2 m_upload_wait_stage_mask{VK_PIPELINE_STAGE_2_NONE}; + std::function m_inline_update_commands; + std::vector> m_inline_update_pending_releases; + + /// Queue family ownership transfer barriers to be replayed as "acquire" operations on the graphics queue, + /// matching the "release" barriers already recorded on the transfer queue submission. Only populated when + /// buffer/texture updates were uploaded via a dedicated transfer queue whose family differs from the graphics + /// queue family (VK_SHARING_MODE_EXCLUSIVE resources require an explicit ownership transfer in that case). + PipelineBarrierBatchBuilder m_pending_queue_ownership_acquire_barriers; + BufferCopyBatchBuilder m_buffer_copy_batch_builder; + TextureCopyBatchBuilder m_texture_copy_batch_builder; + FrameSyncManager m_frame_sync_manager; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + StagingBuffer m_staging_buffer; + + std::vector m_scratch_pending_buffer_copies; + std::vector m_scratch_pending_texture_copies; + std::vector> m_scratch_pending_releases; + std::vector m_scratch_color_attachment_formats; + /// Reused scratch storage for render() to avoid a heap allocation every frame + std::vector m_scratch_render_wait_semaphores; + + void defer_release(std::span fences, std::function release); + + void synchronize_frame_context(); + + void mark_graphics_pass_secondary_cmd_buffers_dirty(); + void mark_graphics_passes_using_texture_dirty(const Texture &texture); + + /// -------------------------------------------------------------------------------------------------- + + /// @TODO Implement! + void sort_graphics_passes_by_order(); + + /// Update textures and buffers + void update_resources(); + + /// Create the graphics pipelines + void create_graphics_pipelines(); + + /// Ensure that rendergraph is a directed acyclic graph (DAG) + void check_for_cycles(); + + /// Rebuild the static texture attachment part of VkRenderingInfo for a graphics pass. + /// @param pass The graphics pass + void rebuild_graphics_pass_texture_rendering_info(GraphicsPass &pass); + + /// Refresh the per-frame swapchain attachment part of VkRenderingInfo for a graphics pass. + /// @param pass The graphics pass + void refresh_graphics_pass_swapchain_rendering_info(GraphicsPass &pass); + + /// Record the command buffer of a pass. After a lot of discussions about the API design of rendergraph, we came to + /// the conclusion that it's the full responsibility of the programmer to manually bind pipelines, descriptors sets, + /// and buffers inside of the on_record function instead of attempting to abstract all of this in rendergraph. This + /// means rendergraph will not automatically bind pipelines, buffers, or descriptor sets! The reason for this is + /// that there could be complex rendering going on inside of the on_record function with an arbitrary number of + /// pipelines descriptor sets, and buffers being bound in a nontrivial order or under conditional cases. We then + /// refrained from designing a simple API inside of rendergraph which automatically binds one graphics pipeline, + /// descriptor set, or a set of buffers at the beginning of rendering before calling on_record because it would + /// cause confusion about the correct API usage for the advanced use cases. Nonetheless, the creation of buffers, + /// descriptors, or pipelines is still the full responsibility of the rendergraph, but you need to use them manually + /// inside of the on_record function. + /// @param cmd_buf The command buffer to record the pass into + /// @param pass The graphics pass to record the command buffer for + void record_command_buffer_for_pass(const CommandBuffer &cmd_buf, GraphicsPass &pass); + +public: + /// Default constructor + /// @param device The device wrapper + /// @param use_secondary_command_buffers Whether graphics passes should be recorded into cached secondary command + /// buffers or directly into the primary command buffer. + RenderGraph(Device &device, bool use_secondary_command_buffers = true); + + ~RenderGraph(); + + /// Add a buffer to the rendergraph + /// @param name The buffer name + /// @param type The buffer type + /// @param on_update The buffer update function + /// @param update_mode How the buffer should be updated and allocated + [[nodiscard]] std::weak_ptr add_buffer(std::string name, BufferType type, std::function on_update, + BufferUpdateMode update_mode = BufferUpdateMode::DEVICE_LOCAL); + + /// Add a graphics pass to the rendergraph + /// @param graphics_pass The graphics pass which was created + /// @note There is no name parameter here because the OnBuildGraphicsPass callback will use GraphicsPassBuilder to + /// build a GraphicsPass, and inside of the builder the name will be set already. + /// @return A weak pointer to the graphics pass which was created + [[nodiscard]] std::weak_ptr add_graphics_pass(OnBuildGraphicsPass on_build_graphics_pass); + + /// Add a graphics pipeline to rendergraph + /// @param on_build_graphics_pipeline The graphics pipeline which will be created + /// @note There is no name parameter here because the OnCreateGraphicsPipeline callback will use + /// GraphicsPipelineBuilder to build a GraphicsPipeline, and inside of the builder the name will be set already. + /// @note This API was chosen so inside of the OnCreateGraphicsPipeline lambda, we build the graphics pipeline with + /// the help of the rendergraph's graphics pipeline builder and store the created graphics pipeline in the renderer + /// class which uses this method. The created graphics pipeline is stored via reference capture of the lambda. + /// An alternative design approach would be to somehow use pointers to smart pointers, passing the smart pointer to + /// OnCreateGraphicsPipeline lambda, which seemed like a design flaw to us. We could write wrappers for this, but + /// there is a tradeoff between simplicity of the code by not using such a wrapper compared with the simplicity of + /// code by not using these lambdas. Also, if we would use such wrappers (or pointers to smart pointer), we would + /// make object lifetime even more complex, which we should avoid at all cost. + void add_graphics_pipeline(OnBuildGraphicsPipeline on_build_graphics_pipeline); + + /// Add a resource descriptor to the rendergraph + /// @param name + /// @param on_build_descriptor_set_layout Builds and returns the descriptor set layout + /// @param on_build_write_descriptor_set Builds the write descriptor sets for one frame slot + [[nodiscard]] std::weak_ptr + add_resource_descriptor(std::string name, + ResourceDescriptorManager::OnBuildDescriptorSetLayout on_build_descriptor_set_layout, + ResourceDescriptorManager::OnBuildWriteDescriptorSet on_build_write_descriptor_set); + + /// Add a buffer resource descriptor with an inferred layout and descriptor write. + [[nodiscard]] std::weak_ptr add_resource_descriptor(std::weak_ptr resource, + VkShaderStageFlags stage); + + /// Add a texture resource descriptor with an inferred layout and descriptor write. + [[nodiscard]] std::weak_ptr add_resource_descriptor(std::weak_ptr resource, + VkShaderStageFlags stage); + + /// Add a texture to the rendergraph + /// @param name The texture name + /// @param usage The texture usage + /// @param format The texture format + /// @param width The texture width + /// @param height The texture height + /// @param channels The number of channels + /// @param sample_count The number of samples + /// @param on_update The texture update function + /// @return A weak pointer to the texture which was created + [[nodiscard]] std::weak_ptr add_texture(std::string name, TextureUsage usage, VkFormat format, + std::uint32_t width, std::uint32_t height, + std::uint32_t channels = 1, + VkSampleCountFlagBits sample_count = VK_SAMPLE_COUNT_1_BIT, + std::optional> on_update = std::nullopt); + + /// Compile the rendergraph + /// Ideally, this should only be done once at startup and all changes in the system will be reported to rendergraph. + void compile(); + + /// Since we need to pass the rendergraph to every render module anyways, + /// there is no need to pass the device wrapper every time as well + [[nodiscard]] const auto &device() const { + return m_device; + } + + /// Render a frame while dealing automatically with all frames in flight internally + void render(); + + /// Reset the entire rendergraph + /// @note We avoid to name it reset() because this would be ambiguous with smart pointer methods + void reset_graph(); +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/resource_descriptor_manager.hpp b/include/inexor/vulkan-renderer/render-graph/resource_descriptor_manager.hpp new file mode 100644 index 000000000..9c1016ac4 --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/resource_descriptor_manager.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +class ResourceDescriptorManager { +public: + using OnBuildDescriptorSetLayout = + std::function; + using OnBuildWriteDescriptorSet = std::function( + wrapper::descriptors::WriteDescriptorSetBuilder &, VkDescriptorSet)>; + +private: + struct ResourceDescriptor { + std::string name; + std::shared_ptr resource; + OnBuildDescriptorSetLayout on_build_descriptor_set_layout; + OnBuildWriteDescriptorSet on_build_write_descriptor_sets; + VkDescriptorSetLayout descriptor_set_layout{VK_NULL_HANDLE}; + }; + + wrapper::Device &m_device; + wrapper::descriptors::DescriptorSetLayoutBuilder m_descriptor_set_layout_builder; + wrapper::descriptors::DescriptorSetAllocator m_descriptor_set_allocator; + wrapper::descriptors::WriteDescriptorSetBuilder m_write_descriptor_set_builder; + std::vector m_write_descriptor_sets; + std::vector m_resource_descriptors; + std::vector m_descriptor_sets_slot_dirty; + std::vector> m_descriptor_resource_slot_dirty; + bool m_descriptor_sets_dirty{true}; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + void sync_descriptor_resource_dirty_tracking(); + +public: + explicit ResourceDescriptorManager(wrapper::Device &device); + + [[nodiscard]] std::weak_ptr + add_resource_descriptor(std::string name, OnBuildDescriptorSetLayout on_build_descriptor_set_layout, + OnBuildWriteDescriptorSet on_build_write_descriptor_set); + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot); + + void create_descriptor_set_layouts(); + + void mark_descriptor_sets_dirty(); + + [[nodiscard]] bool descriptor_sets_dirty() const { + return m_descriptor_sets_dirty; + } + + [[nodiscard]] bool update_write_descriptor_sets(); + + void clear(); +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/staging_buffer.hpp b/include/inexor/vulkan-renderer/render-graph/staging_buffer.hpp new file mode 100644 index 000000000..3f6b651f7 --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/staging_buffer.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::tools { +/// Forward declarations +class InexorException; +class VulkanException; +} // namespace inexor::vulkan_renderer::tools + +namespace inexor::vulkan_renderer::render_graph { + +using tools::InexorException; +using tools::VulkanException; +using wrapper::Device; + +class StagingBuffer { +private: + struct PerFrameStagingBufferResources { + VkBuffer m_buffer{VK_NULL_HANDLE}; + VmaAllocation m_alloc{VK_NULL_HANDLE}; + VmaAllocationInfo m_alloc_info{}; + std::size_t m_capacity{0}; + }; + + const Device &m_device; + std::string m_name; + std::vector m_slots{1}; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + [[nodiscard]] PerFrameStagingBufferResources ¤t_frame_resources(); + [[nodiscard]] const PerFrameStagingBufferResources ¤t_frame_resources() const; + + void destroy_per_frame_resources(PerFrameStagingBufferResources &resources); + +public: + StagingBuffer(const Device &device, std::string name); + StagingBuffer(const StagingBuffer &) = delete; + StagingBuffer(StagingBuffer &&) noexcept = delete; + ~StagingBuffer(); + + StagingBuffer &operator=(const StagingBuffer &) = delete; + StagingBuffer &operator=(StagingBuffer &&) = delete; + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot); + + void ensure_capacity(std::size_t required_bytes, std::vector> &pending_releases); + + void reset(); + + [[nodiscard]] VkBuffer buffer() const; + [[nodiscard]] VmaAllocation allocation() const; + [[nodiscard]] void *mapped_data() const; +}; + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/render-graph/swapchain_manager.hpp b/include/inexor/vulkan-renderer/render-graph/swapchain_manager.hpp new file mode 100644 index 000000000..0d33a60bd --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/swapchain_manager.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" +#include "inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp" + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +class GraphicsPass; + +using wrapper::commands::CommandBuffer; +using wrapper::swapchains::Swapchain; + +class SwapchainManager { +private: + struct SwapchainCacheEntry { + VkSwapchainKHR handle{VK_NULL_HANDLE}; + std::weak_ptr swapchain; + }; + + wrapper::Device &m_device; + std::vector m_cached_swapchains; + bool m_swapchain_cache_dirty{true}; + std::vector> m_frame_swapchains; + std::vector m_swapchains_imgs_available; + std::vector m_swapchain_rendering_finished; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + void rebuild_swapchain_cache(const std::vector> &graphics_passes); + +public: + explicit SwapchainManager(wrapper::Device &device); + + void mark_swapchain_cache_dirty(); + + void clear(); + + void collect_frame_swapchains(const std::vector> &graphics_passes); + + [[nodiscard]] bool acquire_next_images(); + + void synchronize_frame_context(); + + [[nodiscard]] const std::vector &image_available_semaphores() const { + return m_swapchains_imgs_available; + } + + [[nodiscard]] const std::vector &rendering_finished_semaphores() const { + return m_swapchain_rendering_finished; + } + + [[nodiscard]] const std::vector> &frame_swapchains() const { + return m_frame_swapchains; + } + + void prepare_swapchains_for_rendering(const CommandBuffer &cmd_buf) const; + + void prepare_swapchains_for_presenting(const CommandBuffer &cmd_buf) const; + + void mark_frame_swapchains_in_flight(VkFence fence) const; + + void present(std::span wait_semaphores) const; + + [[nodiscard]] std::size_t frame_slot_count() const { + return m_frame_slot_count; + } + + [[nodiscard]] std::size_t current_frame_slot() const { + return m_current_frame_slot; + } +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/texture.hpp b/include/inexor/vulkan-renderer/render-graph/texture.hpp new file mode 100644 index 000000000..1d6084c2e --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/texture.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::images { +// Forward declarations +class Image; +class Sampler; +} // namespace inexor::vulkan_renderer::wrapper::images + +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { +/// Forward declaration +class PipelineBarrierBatchBuilder; +} // namespace inexor::vulkan_renderer::wrapper::synchronization + +namespace inexor::vulkan_renderer::tools { +/// Forward declarations +class InexorException; +class VulkanException; +} // namespace inexor::vulkan_renderer::tools + +namespace inexor::vulkan_renderer::render_graph { + +// Forward declaration +class RenderGraph; +class StagingBuffer; + +// Using declarations +using tools::InexorException; +using tools::VulkanException; +using wrapper::Device; +using wrapper::images::Image; +using wrapper::images::Sampler; +using wrapper::synchronization::PipelineBarrierBatchBuilder; + +/// Specifies the use of the texture +enum class TextureUsage { + DEFAULT, + COLOR_ATTACHMENT, + DEPTH_ATTACHMENT, + STENCIL_ATTACHMENT, +}; + +/// +struct PendingTextureCopy { + VkBuffer src_buffer{VK_NULL_HANDLE}; + VkImage dst_image{VK_NULL_HANDLE}; + VkBufferImageCopy region{}; + VkImageMemoryBarrier2 post_copy_barrier{}; +}; + +class Texture { +private: + friend class RenderGraph; + + // The device wrapper + const Device &m_device; + // The texture name + std::string m_name; + // The texture type + const TextureUsage m_usage; + /// By definition, if this is not std::nullopt, this is a dynamic texture + std::optional> m_on_update; + + /// The format of the texture + VkFormat m_format{VK_FORMAT_UNDEFINED}; + /// The width of the texture + std::uint32_t m_width{0}; + /// The height of the texture + std::uint32_t m_height{0}; + /// The channel count of the texture (4 by default) + // TODO: Can we determine the number of channels based on the given format? + std::uint32_t m_channels{4}; + /// The sample count of the MSAA image (if MSAA is enabled) + VkSampleCountFlagBits m_samples{VK_SAMPLE_COUNT_1_BIT}; + + std::unique_ptr m_default_sampler; + + struct PerFrameTextureResources { + std::shared_ptr m_image; + std::shared_ptr m_msaa_image; + VkDescriptorImageInfo m_descriptor_img_info{}; + }; + + // This is used for initializing textures and for updating dynamic textures + bool m_update_requested{true}; + void *m_src_texture_data{nullptr}; + std::size_t m_src_texture_data_size{0}; + std::vector m_per_frame_texture_resources{1}; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + + void create_all(); + + void destroy_all(); + + void create_per_frame_resources(PerFrameTextureResources &frame_resource, std::size_t frame_index); + + void destroy_per_frame_resources(PerFrameTextureResources &frame_resource); + + [[nodiscard]] PerFrameTextureResources ¤t_frame_resources(); + + [[nodiscard]] const PerFrameTextureResources ¤t_frame_resources() const; + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot); + + /// Collect pending upload copy regions and post-copy barriers for this texture. + /// @param upload_buffer The shared upload arena buffer + /// @param upload_alloc The shared upload arena allocation + /// @param upload_offset Current write offset inside the shared upload arena buffer + /// @param pending_texture_copies Collected copy jobs and image barriers + void collect_update_copies(StagingBuffer &staging_buffer, std::size_t &upload_offset, + std::vector> &pending_releases, + std::vector &pending_texture_copies); + + /// Record the barriers that prepare the texture image for upload. + void prepare_update_barriers(PipelineBarrierBatchBuilder &barrier_builder); + + /// Record initial image-layout barriers for newly created attachment textures. + void prepare_initial_layout_barriers(PipelineBarrierBatchBuilder &barrier_builder); + +public: + /// Default constructor + /// @param device The device wrapper + /// @param name The texture name + /// @param usage The texture usage + /// @param format The image format + /// @param width The texture width + /// @param height The texture height + /// @param channels The number of channels + /// @param samples The number of samples + /// @param on_update The texture update function + Texture(const Device &device, std::string name, TextureUsage usage, VkFormat format, std::uint32_t width, + std::uint32_t height, std::uint32_t channels, VkSampleCountFlagBits samples, + std::optional> on_update); + + Texture(const Texture &) = delete; + Texture(Texture &&) noexcept; + + ~Texture(); + + Texture &operator=(const Texture &) = delete; + Texture &operator=(Texture &&) = delete; + + [[nodiscard]] const auto *descriptor_image_info() const { + return ¤t_frame_resources().m_descriptor_img_info; + } + + [[nodiscard]] VkExtent2D extent() const { + return { + .width = m_width, + .height = m_height, + }; + } + + [[nodiscard]] VkFormat format() const { + return m_format; + } + + [[nodiscard]] VkImageView image_view() const; + + [[nodiscard]] const auto &name() const { + return m_name; + } + + /// Resize this texture and request recreation on the next render-graph texture update. + /// This is primarily used for attachment textures that must track swapchain resize. + void request_resize(std::uint32_t width, std::uint32_t height); + + void request_update(void *src_texture_data, std::size_t src_texture_data_size); + + [[nodiscard]] auto usage() const { + return m_usage; + } +}; + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/include/inexor/vulkan-renderer/render-graph/texture_copy_batch_builder.hpp b/include/inexor/vulkan-renderer/render-graph/texture_copy_batch_builder.hpp new file mode 100644 index 000000000..30d70a32b --- /dev/null +++ b/include/inexor/vulkan-renderer/render-graph/texture_copy_batch_builder.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-graph/texture.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { +class PipelineBarrierBatchBuilder; +} // namespace inexor::vulkan_renderer::wrapper::synchronization + +namespace inexor::vulkan_renderer::render_graph { + +class TextureCopyBatchBuilder { +private: + struct BatchKey { + VkBuffer src_buffer{VK_NULL_HANDLE}; + VkImage dst_image{VK_NULL_HANDLE}; + + bool operator==(const BatchKey &) const = default; + }; + + struct BatchKeyHash { + std::size_t operator()(const BatchKey &key) const noexcept; + }; + + struct BatchValue { + std::vector copies; + }; + + std::unordered_map m_batches; + std::vector m_scratch_regions; + bool m_needs_queue_family_ownership_transfer{false}; + std::uint32_t m_transfer_family_index{VK_QUEUE_FAMILY_IGNORED}; + std::uint32_t m_graphics_family_index{VK_QUEUE_FAMILY_IGNORED}; + +public: + TextureCopyBatchBuilder() = default; + + void set_queue_family_ownership_transfer(bool needs_transfer, std::uint32_t transfer_family_index, + std::uint32_t graphics_family_index); + + void add(const PendingTextureCopy ©_request); + void add(std::span copy_requests); + + [[nodiscard]] bool empty() const; + + void flush(const wrapper::commands::CommandBuffer &cmd_buf, + wrapper::synchronization::PipelineBarrierBatchBuilder &post_copy_barriers, + wrapper::synchronization::PipelineBarrierBatchBuilder &queue_family_acquire_barriers); + + void reset(); +}; + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/render-modules/imgui/imgui_renderer.hpp b/include/inexor/vulkan-renderer/render-modules/imgui/imgui_renderer.hpp new file mode 100644 index 000000000..01a5b530c --- /dev/null +++ b/include/inexor/vulkan-renderer/render-modules/imgui/imgui_renderer.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/graphics_pass.hpp" +#include "inexor/vulkan-renderer/render-graph/render_graph.hpp" +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" + +#include +#include + +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declarations +class Device; +class Shader; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::swapchains { +// Forward declaration +class Swapchain; +} // namespace inexor::vulkan_renderer::wrapper::swapchains + +namespace inexor::vulkan_renderer::wrapper::descriptors { +// Forward declaration +class PerFrameDescriptorSets; +} // namespace inexor::vulkan_renderer::wrapper::descriptors + +namespace inexor::vulkan_renderer::render_modules::imgui { + +// @TODO: Simplify the using declarations! Which ones do we need? + +// Using declarations +using render_graph::Buffer; +using render_graph::BufferType; +using render_graph::DebugLabelColor; +using render_graph::GraphicsPass; +using render_graph::GraphicsPipelineBuilder; +using render_graph::RenderGraph; +using render_graph::Texture; +using wrapper::Device; +using wrapper::Shader; +using wrapper::commands::CommandBuffer; +using wrapper::descriptors::DescriptorSetAllocator; +using wrapper::descriptors::DescriptorSetLayoutBuilder; +using wrapper::descriptors::DescriptorType; +using wrapper::descriptors::PerFrameDescriptorSets; +using wrapper::descriptors::WriteDescriptorSetBuilder; +using wrapper::pipelines::GraphicsPipeline; +using wrapper::swapchains::Swapchain; + +// ImGui user interface integration +class ImGuiRenderer { +private: + std::weak_ptr m_vertex_buffer; + std::weak_ptr m_index_buffer; + std::weak_ptr m_imgui_pass; + std::weak_ptr m_imgui_texture; + std::shared_ptr m_imgui_pipeline; + std::weak_ptr m_swapchain; + std::weak_ptr m_descriptor_set; + + bool m_imgui_font_texture_initialized2{false}; + VkDeviceSize m_upload_size{0}; + unsigned char *m_font_texture_data{}; + int m_font_texture_width{0}; + int m_font_texture_height{0}; + + // The external user-defined ImGui update function + std::function m_on_update_user_imgui_data; + + std::shared_ptr m_vertex_shader; + std::shared_ptr m_fragment_shader; + + std::vector m_index_data; + std::vector m_vertex_data; + + struct PushConstBlock { + glm::vec2 scale; + glm::vec2 translate{glm::vec2(-1.0f)}; + } m_push_const_block{}; + +public: + /// Constructor + /// @param render_graph + /// @param swapchain + /// @param on_update_user_imgui_data + ImGuiRenderer(std::shared_ptr render_graph, std::weak_ptr swapchain, + std::function on_update_user_imgui_data); + + ~ImGuiRenderer(); +}; + +} // namespace inexor::vulkan_renderer::render_modules::imgui diff --git a/include/inexor/vulkan-renderer/render-modules/octree/octree_renderer.hpp b/include/inexor/vulkan-renderer/render-modules/octree/octree_renderer.hpp new file mode 100644 index 000000000..3714bdd3e --- /dev/null +++ b/include/inexor/vulkan-renderer/render-modules/octree/octree_renderer.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-modules/octree/octree_vertex.hpp" + +#include + +#include + +namespace inexor::vulkan_renderer::render_graph { +// Forward declarations +class Buffer; +class Texture; +class GraphicsPass; +class RenderGraph; +} // namespace inexor::vulkan_renderer::render_graph + +namespace inexor::vulkan_renderer::tools { +// Forward declarations +class Camera; +} // namespace inexor::vulkan_renderer::tools + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Shader; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::descriptors { +// Forward declaration +class PerFrameDescriptorSets; +} // namespace inexor::vulkan_renderer::wrapper::descriptors + +namespace inexor::vulkan_renderer::wrapper::swapchains { +// Forward declaration +class Swapchain; +} // namespace inexor::vulkan_renderer::wrapper::swapchains + +namespace inexor::vulkan_renderer::wrapper::pipelines { +// Forward declaration +class GraphicsPipeline; +} // namespace inexor::vulkan_renderer::wrapper::pipelines + +namespace inexor::vulkan_renderer::render_modules::octree { + +// Using declaration +using render_graph::Buffer; +using render_graph::GraphicsPass; +using render_graph::RenderGraph; +using render_graph::Texture; +using vulkan_renderer::tools::Camera; +using wrapper::Shader; +using wrapper::descriptors::PerFrameDescriptorSets; +using wrapper::pipelines::GraphicsPipeline; +using wrapper::swapchains::Swapchain; + +/// @note We can exactly match the definition in the shader using data types in GLM. +/// The data in the matrices is binary compatible with the way the shader expects +/// it, so we can later just memcpy a UniformBufferObject to a VkBuffer. +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +/// A simple renderer for octree geometry +class OctreeRenderer { +private: + // The vertex shader and fragment shader for octree rendering + std::shared_ptr m_vertex_shader; + std::shared_ptr m_fragment_shader; + + // The vertex buffer and index buffer for octree rendering + std::weak_ptr m_vertex_buffer; + std::weak_ptr m_index_buffer; + + // The matrix for model, view, and projection + std::weak_ptr m_mvp_matrix; + + // The graphics pipeline for octree rendering + std::shared_ptr m_octree_pipeline; + + // The graphics pass for octree rendering + std::weak_ptr m_octree_pass; + std::weak_ptr m_descriptor_set; + + std::weak_ptr m_camera; + + std::weak_ptr m_swapchain; + std::weak_ptr m_depth_buffer; + + std::vector m_octree_vertices; + std::vector m_octree_indices; + + UniformBufferObject m_ubo; + + /// Flag to track if octree geometry has been updated and needs GPU buffer refresh + bool m_geometry_updated{true}; // Initially true to upload geometry on first frame + +public: + /// Constructor + /// @param render_graph The rendergraph to use for octree rendering + /// @param swapchain The swapchain to use for octree rendering + /// @param depth_buffer The depth buffer to use for octree rendering + OctreeRenderer(std::shared_ptr render_graph, std::weak_ptr swapchain, + std::weak_ptr depth_buffer, std::shared_ptr camera); + + void set_vertices_and_indices(std::vector vertices, std::vector indices); +}; + +} // namespace inexor::vulkan_renderer::render_modules::octree diff --git a/include/inexor/vulkan-renderer/render-modules/octree/octree_vertex.hpp b/include/inexor/vulkan-renderer/render-modules/octree/octree_vertex.hpp new file mode 100644 index 000000000..3ee7e35f3 --- /dev/null +++ b/include/inexor/vulkan-renderer/render-modules/octree/octree_vertex.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace inexor::vulkan_renderer::render_modules::octree { + +struct OctreeVertex { + glm::vec3 position; + glm::vec3 color; + + OctreeVertex(glm::vec3 position, glm::vec3 color) : position(position), color(color) {} +}; + +// inline to suppress clang-tidy warning. +inline bool operator==(const OctreeVertex &lhs, const OctreeVertex &rhs) { + return lhs.position == rhs.position && lhs.color == rhs.color; +} + +} // namespace inexor::vulkan_renderer::render_modules::octree + +namespace std { + +template <> +struct hash { + std::size_t operator()(const inexor::vulkan_renderer::render_modules::octree::OctreeVertex &vertex) const { + const auto h1 = std::hash{}(vertex.position); + const auto h2 = std::hash{}(vertex.color); + return h1 ^ h2; + } +}; + +} // namespace std diff --git a/include/inexor/vulkan-renderer/tools/camera.hpp b/include/inexor/vulkan-renderer/tools/camera.hpp index 9c3fe351d..c0e504e5b 100644 --- a/include/inexor/vulkan-renderer/tools/camera.hpp +++ b/include/inexor/vulkan-renderer/tools/camera.hpp @@ -68,7 +68,7 @@ class Camera { /// The sensitivity of the mouse. float m_mouse_sensitivity{0.005f}; /// The camera's near plane. - float m_near_plane{0.001f}; + float m_near_plane{0.1f}; /// The camera's far plane. float m_far_plane{1000.0f}; diff --git a/include/inexor/vulkan-renderer/tools/enumerate.hpp b/include/inexor/vulkan-renderer/tools/enumerate.hpp index 30dd14e47..a88dec115 100644 --- a/include/inexor/vulkan-renderer/tools/enumerate.hpp +++ b/include/inexor/vulkan-renderer/tools/enumerate.hpp @@ -28,9 +28,9 @@ namespace inexor::vulkan_renderer::tools { /// @return A std::vector of all physical devices which are available on the system (this can be empty!) [[nodiscard]] std::vector get_physical_devices(VkInstance inst); -/// Call vkGetPhysicalDeviceQueueFamilyProperties +/// Call vkGetPhysicalDeviceQueueFamilyProperties2 /// @param physical_device The physical device to get all extension properties form -/// @exception VulkanException vkGetPhysicalDeviceQueueFamilyProperties call failed +/// @exception VulkanException vkGetPhysicalDeviceQueueFamilyProperties2 call failed /// @return A std::vector of all queue families which are available on the system (this can be empty!) [[nodiscard]] std::vector get_queue_family_properties(VkPhysicalDevice physical_device); diff --git a/include/inexor/vulkan-renderer/tools/fps_limiter.hpp b/include/inexor/vulkan-renderer/tools/fps_limiter.hpp index 1094e8fb6..2500c696f 100644 --- a/include/inexor/vulkan-renderer/tools/fps_limiter.hpp +++ b/include/inexor/vulkan-renderer/tools/fps_limiter.hpp @@ -6,32 +6,45 @@ namespace inexor::vulkan_renderer::tools { -/// A wrapper class for counting and limiting frames per second. +/// Counts rendered frames and limits the maximum frame rate. +/// +/// Call is_next_frame_allowed() before rendering a frame. +/// Call get_fps() exactly once per rendered frame. class FPSLimiter { -private: - std::uint32_t m_max_fps{DEFAULT_FPS}; - std::chrono::milliseconds m_frame_time{DEFAULT_FPS / 1000}; - std::chrono::time_point m_last_time; - std::chrono::time_point m_last_fps_update_time; - std::chrono::milliseconds m_fps_update_interval{1000}; - std::uint32_t m_frames{0}; +public: + using Clock = std::chrono::steady_clock; + using Duration = std::chrono::duration; - // The requested max_fps will be clamped in between these limits. static constexpr std::uint32_t MIN_FPS{1}; - static constexpr std::uint32_t MAX_FPS{2000}; + static constexpr std::uint32_t MAX_FPS{10000}; + static constexpr std::uint32_t DEFAULT_FPS{4000}; -public: - static constexpr std::uint32_t DEFAULT_FPS{1000}; - - FPSLimiter(std::uint32_t max_fps = DEFAULT_FPS); + explicit FPSLimiter(std::uint32_t max_fps = DEFAULT_FPS); void set_max_fps(std::uint32_t max_fps); - /// Ask if the next frame is allowed to be rendered. + /// Returns true when enough time has elapsed for the next frame. [[nodiscard]] bool is_next_frame_allowed(); - /// Return the fps every second, std::nullopt otherwise. + /// Records one rendered frame and returns an updated FPS value periodically. [[nodiscard]] std::optional get_fps(); + + /// Returns the time elapsed since the last allowed frame, in seconds. + [[nodiscard]] double elapsed_seconds() const noexcept { + return m_frame_elapsed.count(); + } + +private: + static constexpr Duration FPS_UPDATE_INTERVAL{1.0}; + + std::uint32_t m_max_fps{DEFAULT_FPS}; + Duration m_frame_time{1.0 / static_cast(DEFAULT_FPS)}; + + Clock::time_point m_last_frame_time; + Clock::time_point m_last_fps_update_time; + + Duration m_frame_elapsed{0.0}; + std::uint32_t m_frames{0}; }; -} // namespace inexor::vulkan_renderer::tools +} // namespace inexor::vulkan_renderer::tools \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/tools/make_info.hpp b/include/inexor/vulkan-renderer/tools/make_info.hpp new file mode 100644 index 000000000..3cc0719b0 --- /dev/null +++ b/include/inexor/vulkan-renderer/tools/make_info.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace inexor::vulkan_renderer::tools { + +/// @brief A small helper function that return vulkan create infos with sType already set +/// @code{.cpp} +/// auto render_pass_ci = make_info(); +/// @endcode +/// @note Also zeros the returned struct +template +[[nodiscard]] T make_info(T = {}); + +} // namespace inexor::vulkan_renderer::tools diff --git a/include/inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp b/include/inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp index a79137d7c..9ebe6d775 100644 --- a/include/inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp +++ b/include/inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp @@ -1,6 +1,9 @@ #pragma once -#include "inexor/vulkan-renderer/wrapper/gpu_memory_buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/images/image.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" #include "inexor/vulkan-renderer/wrapper/synchronization/fence.hpp" #include @@ -11,6 +14,9 @@ namespace inexor::vulkan_renderer::wrapper { // Forward declaration class Device; + +// Forward declaration +struct QueueSemaphoreWait; } // namespace inexor::vulkan_renderer::wrapper namespace inexor::vulkan_renderer::wrapper::synchronization { @@ -18,79 +24,127 @@ namespace inexor::vulkan_renderer::wrapper::synchronization { class Fence; } // namespace inexor::vulkan_renderer::wrapper::synchronization +namespace inexor::vulkan_renderer::wrapper::images { +// Forward declaration +class Image; +} // namespace inexor::vulkan_renderer::wrapper::images + +namespace inexor::vulkan_renderer::wrapper::pipelines { +// Forward declaration +class GraphicsPipeline; +} // namespace inexor::vulkan_renderer::wrapper::pipelines + +namespace inexor::vulkan_renderer::wrapper::descriptors { +// Forward declaration +class PerFrameDescriptorSets; +} // namespace inexor::vulkan_renderer::wrapper::descriptors + +namespace inexor::vulkan_renderer::render_graph { +// Forward declaration +class Buffer; +} // namespace inexor::vulkan_renderer::render_graph + namespace inexor::vulkan_renderer::wrapper::commands { // Using declarations +using render_graph::Buffer; +using render_graph::BufferType; +using tools::InexorException; using wrapper::Device; +using wrapper::descriptors::PerFrameDescriptorSets; +using wrapper::images::Image; +using wrapper::pipelines::GraphicsPipeline; using wrapper::synchronization::Fence; -/// @brief RAII wrapper class for VkCommandBuffer. -/// @todo Make trivially copyable (this class doesn't really "own" the command buffer, more just an OOP wrapper). +/// RAII wrapper class for VkCommandBuffer. +/// @TODO Restrict access to commands which only RenderGraph should have access to (use private and friend class). +/// @TODO Switch to taking in OOP wrappers when we have them (e.g. bind_vertex_buffers takes in a VertexBuffer) +/// @TODO Make trivially copyable (this class doesn't really "own" the command buffer, more just an OOP wrapper). class CommandBuffer { - VkCommandBuffer m_command_buffer{VK_NULL_HANDLE}; - const Device &m_device; - std::string m_name; - std::unique_ptr m_wait_fence; - // The Device wrapper must be able to call begin_command_buffer and end_command_buffer friend class Device; - - /// The staging buffers which are maybe used in the command buffer - /// This vector of staging buffers will be cleared every time ``begin_command_buffer`` is called - /// @note We are not recycling staging buffers. Once they are used and the command buffer handle has reached the end - /// of its lifetime, the staging bufers will be cleared. We trust Vulkan Memory Allocator (VMA) in managing the - /// memory for staging buffers. - mutable std::vector m_staging_bufs; - friend class CommandPool; +private: + VkCommandBuffer m_cmd_buf{VK_NULL_HANDLE}; + const Device &m_device; + mutable std::string m_name; + std::unique_ptr m_wait_fence; + mutable bool m_has_been_submitted{false}; + mutable std::vector m_wait_submit_infos_scratch; + mutable std::vector m_signal_submit_infos_scratch; + /// Call vkBeginCommandBuffer /// @param flags The command buffer usage flags, ``VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`` by default const CommandBuffer & // NOLINT begin_command_buffer(VkCommandBufferUsageFlags flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) const; - /// Create a new staging buffer which will be stored internally for a copy operation - /// @param data A raw pointer to the data to copy (must not be ``nullptr``) - /// @param data_size The size of the data to copy (must be greater than ``0``) - /// @param name The internal name of the staging buffer (must not be empty) - /// @return A VkBuffer which contains the staging buffer data - [[nodiscard]] VkBuffer create_staging_buffer(const void *data, const VkDeviceSize data_size, - const std::string &name) const { - assert(data); - assert(data_size > 0); - assert(!name.empty()); - - // Create a staging buffer for the copy operation and keep it until the CommandBuffer exceeds its lifetime - m_staging_bufs.emplace_back(m_device, name, data_size, data, data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - VMA_MEMORY_USAGE_CPU_ONLY); - - return m_staging_bufs.back().buffer(); - } - - /// Create a new staging buffer which will be stored internally for a copy operation - /// @tparam The data type of the staging buffer - /// @param data A std::span of the source data - /// @param name The internal name of the staging buffer (must not be empty) - /// @return The staging buffer's VkBuffer - template - [[nodiscard]] VkBuffer create_staging_buffer(const std::span data, const std::string &name) const { - return create_staging_buffer(data.data(), static_cast(sizeof(data) * data.size()), name); - } - /// Call vkEndCommandBuffer /// @return A const reference to the this pointer (allowing method calls to be chained) const CommandBuffer &end_command_buffer() const; // NOLINT /// Set the internal debug name of the command buffer. /// @param name The name of the command buffer. - void set_debug_name(const std::string &name); + void set_debug_name(const std::string &name) const; + + /// Call vkQueueSubmit2 + /// @param queue_type The queue type to submit the command buffer to + /// @param wait_semaphore_infos The semaphores to wait for together with stage masks + /// @param signal_semaphore_infos The semaphores to signal together with stage masks + /// @TODO Implement submitting multiple command buffers in one batched call to vkQueueSubmit2! + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphore_infos, + std::span signal_semaphore_infos) const; + + /// Call vkQueueSubmit2 + /// @param queue_type The queue type to submit the command buffer to + /// @param wait_semaphore_infos The semaphores to wait for together with stage masks + /// @param signal_semaphores The semaphores to signal + /// @TODO Implement submitting multiple command buffers in one batched call to vkQueueSubmit2! + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphore_infos, + std::span signal_semaphores = {}) const; + + /// Overload with plain wait semaphores and explicit signal semaphore infos. + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphores, + std::span signal_semaphore_infos) const; + + /// Compatibility overload that applies one default stage mask for all waits based on queue type. + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphores = {}, + std::span signal_semaphores = {}) const; + + /// Compatibility overload that accepts explicit stage masks per wait semaphore. + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphores, + std::span signal_semaphores = {}) const; + + /// Compatibility overload that accepts explicit stage masks per wait semaphore and per signal semaphore. + void submit(const VkQueueFlagBits queue_type, std::span wait_semaphores, + std::span signal_semaphore_infos) const; + + /// + void reset_fence() const; + + /// + VkResult status() const; + + [[nodiscard]] VkFence submission_fence() const { + return m_wait_fence->fence(); + } + + [[nodiscard]] bool was_submitted() const { + return m_has_been_submitted; + } + + /// Block until this command buffer's submission fence is signaled. + void wait_fence() const; + + void reset() const; public: /// Default constructor /// @param device A const reference to the device wrapper class /// @param cmd_pool The command pool from which the command buffer will be allocated /// @param name The internal debug marker name of the command buffer (must not be empty) - CommandBuffer(const Device &device, VkCommandPool cmd_pool, std::string name); + CommandBuffer(const Device &device, VkCommandPool cmd_pool, std::string name, + VkCommandBufferLevel level = VK_COMMAND_BUFFER_LEVEL_PRIMARY); CommandBuffer(const CommandBuffer &) = delete; CommandBuffer(CommandBuffer &&) noexcept; @@ -100,12 +154,51 @@ class CommandBuffer { CommandBuffer &operator=(const CommandBuffer &) = delete; CommandBuffer &operator=(CommandBuffer &&) = delete; - /// Call vkCmdBeginRenderPass - /// @param render_pass_bi The renderpass begin info - /// @param subpass_contents The subpass contents (``VK_SUBPASS_CONTENTS_INLINE`` by default) + [[nodiscard]] VkCommandBuffer command_buffer() const { + return m_cmd_buf; + } + + [[nodiscard]] const std::string &name() const { + return m_name; + } + + /// Begin recording this command buffer as a secondary command buffer. + /// @param inheritance_info Inheritance info including dynamic-rendering inheritance chain. + /// @param flags Recording usage flags. + const CommandBuffer & + begin_secondary_command_buffer(const VkCommandBufferInheritanceInfo &inheritance_info, + VkCommandBufferUsageFlags flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT | + VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) const; + + /// End recording this command buffer. + const CommandBuffer &end_recording() const; + + /// Reset this command buffer before recording again. + void reset_recording() const; + + /// Execute secondary command buffers from this (primary) command buffer. + const CommandBuffer & + execute_secondary_command_buffers(std::span secondary_cmd_buffers) const; + + /// Call vkCmdBeginDebugUtilsLabelEXT + /// @param name The name of the debug label + /// @param color The color of the debug label /// @return A const reference to the this pointer (allowing method calls to be chained) - const CommandBuffer &begin_render_pass(const VkRenderPassBeginInfo &render_pass_bi, // NOLINT - VkSubpassContents subpass_contents = VK_SUBPASS_CONTENTS_INLINE) const; + const CommandBuffer &begin_debug_label_region(const std::string &name, std::array color) const; + + /// Call vkCmdBindDescriptorSets to bind one single descriptor set + /// @note Binding multiple descriptor sets would require implementing bind_descriptor_sets, which is not required + /// for now. + /// @return A const reference to the this pointer (allowing method calls to be chained) + /// @param descriptor_set The descriptor set to bind + /// @param pipeline The graphics pipeline whose pipeline layout will be used + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &bind_descriptor_set(VkDescriptorSet desc_set, std::weak_ptr pipeline) const; + + const CommandBuffer &bind_descriptor_set(std::weak_ptr descriptor_sets) const; + + const CommandBuffer &bind_descriptor_set(std::weak_ptr descriptor_sets, + std::weak_ptr pipeline) const; /// Call vkCmdBindDescriptorSets /// @param desc_sets The descriptor sets to bind @@ -120,6 +213,15 @@ class CommandBuffer { std::uint32_t first_set = 0, std::span dyn_offsets = {}) const; + /// Call vkCmdBindIndexBuffer + /// @param buffer The index buffer to bind + /// @param index_type The index type to use (``VK_INDEX_TYPE_UINT32`` by default) + /// @param offset The offset (``0`` by default) + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &bind_index_buffer(std::weak_ptr buffer, + VkIndexType index_type = VK_INDEX_TYPE_UINT32, // NOLINT + VkDeviceSize offset = 0) const; + /// Call vkCmdBindIndexBuffer /// @param buf The index buffer to bind /// @param index_type The index type to use (``VK_INDEX_TYPE_UINT32`` by default) @@ -128,6 +230,11 @@ class CommandBuffer { const CommandBuffer &bind_index_buffer(VkBuffer buf, VkIndexType index_type = VK_INDEX_TYPE_UINT32, // NOLINT VkDeviceSize offset = 0) const; + /// Call vkCmdBindPipeline + /// @param graphics_pipeline + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &bind_pipeline(std::weak_ptr graphics_pipeline) const; + /// Call vkCmdBindPipeline /// @param pipeline The graphics pipeline to bind /// @param bind_point The pipeline bind point (``VK_PIPELINE_BIND_POINT_GRAPHICS`` by default) @@ -135,6 +242,21 @@ class CommandBuffer { const CommandBuffer &bind_pipeline(VkPipeline pipeline, // NOLINT VkPipelineBindPoint bind_point = VK_PIPELINE_BIND_POINT_GRAPHICS) const; + const CommandBuffer &bind_vertex_buffer(const std::weak_ptr buffer) const { + const auto buffer_ref = buffer.lock(); + if (!buffer_ref) { + throw InexorException("Error: Parameter 'buffer' is an invalid pointer!"); + } + if (buffer_ref->type() != BufferType::VERTEX_BUFFER) { + throw InexorException("Error: Rendergraph buffer resource " + buffer_ref->name() + + " is not a vertex buffer!"); + } + const auto vk_buffer = buffer_ref->buffer(); + const VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(m_cmd_buf, 0, 1, &vk_buffer, &offset); + return *this; + } + /// Call vkCmdBindVertexBuffers /// @param bufs The vertex buffers to bind /// @param first_binding The first binding (``0`` by default) @@ -171,7 +293,7 @@ class CommandBuffer { /// @param dst_mask The destination pipeline stage flags (``VK_PIPELINE_STAGE_ALL_COMMANDS_BIT`` by default) /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) const CommandBuffer & // NOLINT - change_image_layout(VkImage image, VkImageLayout old_layout, VkImageLayout new_layout, + change_image_layout(VkImage image, VkFormat format, VkImageLayout old_layout, VkImageLayout new_layout, std::uint32_t mip_level_count = 1, std::uint32_t array_layer_count = 1, std::uint32_t base_mip_level = 0, std::uint32_t base_array_layer = 0, VkPipelineStageFlags src_mask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, @@ -220,6 +342,13 @@ class CommandBuffer { const CommandBuffer ©_buffer_to_image(VkBuffer src_buf, VkImage dst_img, // NOLINT const VkBufferImageCopy ©_region) const; + /// + /// @param buffer + /// @param img + /// @param extent + /// @return + const CommandBuffer ©_buffer_to_image(VkBuffer buffer, VkImage img, VkExtent3D extent) const; + /// Call vkCmdCopyBuffer /// @param data A raw pointer to the data to copy /// @param data_size The size of the data to copy @@ -232,21 +361,11 @@ class CommandBuffer { VkImage dst_img, const VkBufferImageCopy ©_region, const std::string &name) const; - /// Call vkCmdCopyBuffer - /// @param data A std::span of the source data - /// @note A staging buffer for the copy operation will be created automatically from ``data`` - /// @param dst_img The destination image (must not be ``VK_NULL_HANDLE``) - /// @note The destination image is always expected to be in layout ``VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`` for the - /// copy operation - /// @param name The internal name of the staging buffer (must not be empty) + /// Call vkCmdCopyBufferToImage + /// @param src_buffer The source buffer + /// @param img The image to copy the buffer into /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) - template - const CommandBuffer ©_buffer_to_image(const std::span data, // NOLINT - VkImage dst_img, const VkBufferImageCopy ©_region, - const std::string &name) const { - return copy_buffer_to_image(create_staging_buffer(data, name), dst_img, - static_cast(sizeof(data) * data.size()), copy_region, name); - } + const CommandBuffer ©_buffer_to_image(VkBuffer src_buffer, std::weak_ptr img) const; /// Call vkCmdDraw /// @param vert_count The number of vertices to draw @@ -269,51 +388,61 @@ class CommandBuffer { std::uint32_t first_index = 0, std::int32_t vert_offset = 0, std::uint32_t first_inst = 0) const; - /// Call vkCmdEndRenderPass + /// Call vkCmdBeginRendering + /// @note We don't need to call it ``vkCmdBeginRenderingKHR`` anymore since it's part of Vulkan 1.3's core + /// @note ``begin_render_pass`` has been deprecated because of dynamic rendering (``VK_KHR_dynamic_rendering``) + /// @param rendering_info The info for dynamic rendering /// @return A const reference to the this pointer (allowing method calls to be chained) - const CommandBuffer &end_render_pass() const; // NOLINT + const CommandBuffer &begin_rendering(const VkRenderingInfo &rendering_info) const; - [[nodiscard]] VkResult fence_status() const { - return m_wait_fence->status(); - } + /// Call vkCmdEndDebugUtilsLabelEXT + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &end_debug_label_region() const; - /// Call vkCmdPipelineBarrier - /// @param src_stage_flags The the source stage flags - /// @param dst_stage_flags The destination stage flags - /// @param img_mem_barriers The image memory barriers - /// @note We start with image memory barriers as no-default parameter, since it's the most common use case - /// @param mem_barriers The memory barriers (empty by default) - /// @param buf_mem_barriers The buffer memory barriers (empty by default) - /// @param dep_flags The dependency flags (``0`` by default) + // TODO: Make begin_rendering and end_rendering private and allow only rendergraph to access it! + + /// Call vkCmdEndRendering + /// @note We don't need to call it ``vkCmdEndRenderingKHR`` anymore since it's part of Vulkan 1.3's core + /// @note ``end_render_pass`` has been deprecated because of dynamic rendering (``VK_KHR_dynamic_rendering``) + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &end_rendering() const; + + /// Call vkCmdPipelineBarrier2 + /// @param dependency_info Fully specified dependency info /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) - const CommandBuffer &pipeline_barrier(VkPipelineStageFlags src_stage_flags, // NOLINT - VkPipelineStageFlags dst_stage_flags, - std::span img_mem_barriers, - std::span mem_barriers = {}, - std::span buf_mem_barriers = {}, - VkDependencyFlags dep_flags = 0) const; + const CommandBuffer &pipeline_barrier(const VkDependencyInfo &dependency_info) const; - /// Call vkCmdPipelineBarrier - /// @param src_stage_flags The the source stage flags - /// @param dst_stage_flags The destination stage flags + /// Call vkCmdPipelineBarrier2 with one buffer memory barrier + /// @param buffer_mem_barrier The buffer memory barrier + /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) + const CommandBuffer &pipeline_buffer_memory_barrier(const VkBufferMemoryBarrier2 &buffer_mem_barrier) const; + + /// Call vkCmdPipelineBarrier2 with one image memory barrier /// @param barrier The image memory barrier /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) - const CommandBuffer &pipeline_image_memory_barrier(VkPipelineStageFlags src_stage_flags, // NOLINT - VkPipelineStageFlags dst_stage_flags, - const VkImageMemoryBarrier &barrier) const; + const CommandBuffer &pipeline_image_memory_barrier(const VkImageMemoryBarrier2 &barrier) const; - /// Call vkCmdPipelineBarrier - /// @param src_stage_flags The the source stage flags - /// @param dst_stage_flags The destination stage flags + /// Call vkCmdPipelineBarrier2 with one memory barrier /// @param barrier The memory barrier /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) - const CommandBuffer &pipeline_memory_barrier(VkPipelineStageFlags src_stage_flags, // NOLINT - VkPipelineStageFlags dst_stage_flags, - const VkMemoryBarrier &barrier) const; + const CommandBuffer &pipeline_memory_barrier(const VkMemoryBarrier2 &barrier) const; - /// Call vkCmdPipelineBarrier to place a full memory barrier - /// @warning You should avoid full barriers since they are not the most performant solution in most cases - const CommandBuffer &full_barrier() const; + /// Call vkCmdPipelineBarrier2 for a transfer-write -> shader-read dependency. + /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) + const CommandBuffer &barrier_transfer_write_to_shader_read() const; + + /// Call vkCmdPipelineBarrier2 for a color-attachment-write -> shader-read dependency. + /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) + const CommandBuffer &barrier_color_attachment_write_to_shader_read() const; + + /// Call vkCmdPipelineBarrier2 for a depth-stencil-attachment-write -> shader-read dependency. + /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) + const CommandBuffer &barrier_depth_stencil_write_to_shader_read() const; + + /// Call vkCmdInsertDebugUtilsLabelEXT + /// @param name The name of the debug label to insert + /// @return A const reference to the dereferenced ``this`` pointer (allowing for method calls to be chained) + const CommandBuffer &insert_debug_label(const std::string &name, std::array color) const; /// Call vkCmdPushConstants /// @param layout The pipeline layout @@ -338,41 +467,33 @@ class CommandBuffer { return push_constants(layout, stage, sizeof(data), &data, offset); } - // Graphics commands - // TODO(): Switch to taking in OOP wrappers when we have them (e.g. bind_vertex_buffers takes in a VertexBuffer) - - [[nodiscard]] auto cmd_buffer() const { - return m_command_buffer; - } - - [[nodiscard]] const Fence &get_wait_fence() const { - return *m_wait_fence; + /// Call vkCmdPushConstants + /// @tparam T the data type of the push constant + /// @param pipeline The graphics pipeline + /// @param data A const reference to the data + /// @param stage The shader stage that will be accepting the push constants + /// @param offset The offset value (``0`` by default) + /// @return A const reference to the this pointer (allowing method calls to be chained) + template + const CommandBuffer &push_constant(const std::weak_ptr pipeline, + const T &data, // NOLINT + const VkShaderStageFlags stage, const VkDeviceSize offset = 0) const { + const auto pipeline_ref = pipeline.lock(); + if (!pipeline_ref) { + throw InexorException("Error: Parameter 'pipeline' is an invalid pointer!"); + } + return push_constants(pipeline_ref->pipeline_layout(), stage, sizeof(data), &data, offset); } - /// Call the reset method of the Fence member - const CommandBuffer &reset_fence() const; + /// @brief + /// @param scissor + /// @return + const CommandBuffer &set_scissor(VkRect2D scissor) const; - /// Call vkQueueSubmit - /// @param submit_infos The submit infos - const CommandBuffer &submit(std::span submit_infos) const; // NOLINT - - /// Call vkQueueSubmit - /// @param submit_info The submit info - const CommandBuffer &submit(VkSubmitInfo submit_infos) const; // NOLINT - - /// Call vkQueueSubmit - const CommandBuffer &submit() const; // NOLINT - - /// Call vkQueueSubmit and use a fence to wait for command buffer submission and execution to complete - /// @param submit_infos The submit infos - const CommandBuffer &submit_and_wait(std::span submit_infos) const; // NOLINT - - /// Call vkQueueSubmit and use a fence to wait for command buffer submission and execution to complete - /// @param submit_info The submit info - const CommandBuffer &submit_and_wait(VkSubmitInfo submit_info) const; // NOLINT - - /// Call vkQueueSubmit and use a fence to wait for command buffer submission and execution to complete - const CommandBuffer &submit_and_wait() const; // NOLINT + /// Set the viewport + /// @param viewport The viewport + /// @return A const reference to the this pointer (allowing method calls to be chained) + const CommandBuffer &set_viewport(VkViewport viewport) const; }; } // namespace inexor::vulkan_renderer::wrapper::commands diff --git a/include/inexor/vulkan-renderer/wrapper/commands/command_buffer_cache.hpp b/include/inexor/vulkan-renderer/wrapper/commands/command_buffer_cache.hpp new file mode 100644 index 000000000..c7fb5f4aa --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/commands/command_buffer_cache.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::commands { + +/// Caches RenderGraph-owned secondary command buffer recording state. +class CommandBufferCache { +private: + bool m_use_secondary_command_buffers{true}; + struct SecondaryCommandBufferState { + VkExtent2D cached_render_extent{0, 0}; + std::vector dirty_by_frame_slot{true}; + }; + + Device &m_device; + std::size_t m_frame_slot_count{1}; + std::size_t m_current_frame_slot{0}; + std::vector m_frame_slot_submission_fences{VK_NULL_HANDLE}; + std::unordered_map m_secondary_command_buffers; + + SecondaryCommandBufferState &state_for_pass(const std::string &pass_name); + +public: + explicit CommandBufferCache(Device &device, bool use_secondary_command_buffers = true); + + void set_frame_context(std::size_t frame_slot_count, std::size_t current_frame_slot, + std::span frame_slot_submission_fences); + + void invalidate_all_secondary_command_buffers(); + + void record_secondary_command_buffer(const CommandBuffer &primary_cmd_buf, const std::string &pass_name, + std::array debug_label_color, VkExtent2D render_extent, + const VkCommandBufferInheritanceInfo &inheritance_info, + const VkRenderingInfo &rendering_info, + std::function on_record); +}; + +} // namespace inexor::vulkan_renderer::wrapper::commands \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/wrapper/commands/command_pool.hpp b/include/inexor/vulkan-renderer/wrapper/commands/command_pool.hpp index fbb4c651e..5e1b60efe 100644 --- a/include/inexor/vulkan-renderer/wrapper/commands/command_pool.hpp +++ b/include/inexor/vulkan-renderer/wrapper/commands/command_pool.hpp @@ -2,30 +2,51 @@ #include -#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include +#include +#include namespace inexor::vulkan_renderer::wrapper { // Forward declaration class Device; } // namespace inexor::vulkan_renderer::wrapper +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + namespace inexor::vulkan_renderer::wrapper::commands { -/// @brief RAII wrapper class for VkCommandPool. +/// RAII wrapper class for VkCommandPool class CommandPool { std::string m_name; const Device &m_device; VkCommandPool m_cmd_pool{VK_NULL_HANDLE}; - + VkQueueFlagBits m_queue_type; /// The command buffers which can be requested by the current thread std::vector> m_cmd_bufs; + /// The secondary command buffers which can be requested by the current thread + std::vector m_secondary_cmd_bufs; + /// Ring cursor used when all command buffers are currently in-flight. + std::size_t m_next_reuse_index{0}; + /// Ring cursor used when all secondary command buffers are currently in-flight. + std::size_t m_next_secondary_reuse_index{0}; + + static constexpr std::size_t MAX_IN_FLIGHT_SUBMISSIONS = 3; + + /// Get the internal debug name of the command pool + /// @note We use the queue type to generate a human-readable name for the command pool, but we dont want to use + /// representation wrapper for this. + std::string get_pool_name(const VkQueueFlagBits queue_type) const; public: /// Default constructor - /// @param device The device wrapper instance. - /// @param queue_family_index The queue family index to use. - /// @param name The internal debug marker name which will be assigned to this command pool. - CommandPool(const Device &device, std::uint32_t queue_family_index, std::string name); + /// @param device The device wrapper + /// @param queue_type The queue type + /// @param queue_family_index The queue family index + /// @param name The internal debug marker name which will be assigned to this command pool + CommandPool(const Device &device, VkQueueFlagBits queue_type, std::uint32_t queue_family_index, std::string name); CommandPool(const CommandPool &) = delete; CommandPool(CommandPool &&) noexcept; @@ -35,14 +56,18 @@ class CommandPool { CommandPool &operator=(const CommandPool &) = delete; CommandPool &operator=(CommandPool &&) = delete; - [[nodiscard]] auto cmd_pool() const { - return m_cmd_pool; - } - /// Request a command buffer /// @param name The internal debug name which will be assigned to this command buffer (must not be empty) /// @return A command buffer handle instance which allows access to the requested command buffer [[nodiscard]] const CommandBuffer &request_command_buffer(const std::string &name); + + /// Request a secondary command buffer + /// @param name The internal debug name which will be assigned to this command buffer (must not be empty) + /// @return A command buffer handle instance which allows access to the requested command buffer + [[nodiscard]] const CommandBuffer &request_secondary_command_buffer(const std::string &name); + + /// Wait until all submitted command buffers in this pool have finished execution. + void wait_for_all_submissions() const; }; } // namespace inexor::vulkan_renderer::wrapper::commands diff --git a/include/inexor/vulkan-renderer/wrapper/debug_callback.hpp b/include/inexor/vulkan-renderer/wrapper/debug_callback.hpp index 78fcd8efd..5cb80472c 100644 --- a/include/inexor/vulkan-renderer/wrapper/debug_callback.hpp +++ b/include/inexor/vulkan-renderer/wrapper/debug_callback.hpp @@ -19,6 +19,8 @@ class VulkanDebugUtilsCallback { /// @param inst The Vulkan instance. /// @param debug_callback A pointer to the debug utils messenger callback. VulkanDebugUtilsCallback(const Instance &inst, PFN_vkDebugUtilsMessengerCallbackEXT debug_callback); + + /// Call vkDestroyDebugUtilsMessengerEXT ~VulkanDebugUtilsCallback(); }; diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool.hpp new file mode 100644 index 000000000..677a622fc --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Forward declaration +class DescriptorPoolAllocator; + +/// RAII wrapper for VkDescriptorPool +/// For internal use inside of rendergraph only! +class DescriptorPool { +private: + const Device &m_device; + VkDescriptorPool m_descriptor_pool{VK_NULL_HANDLE}; + std::vector m_pool_sizes; + std::string m_name; + +public: + /// Default constructor is private so only DescriptorPoolAllocator can access it + /// @param device The device wrapper + /// @param pool_sizes The descriptor pool sizes (must not be empty!) + /// @param max_sets The max descriptor set count + /// @param name The internal debug name of this descriptor pool (must not be empty!) + /// @exception std::invalid_argument Internal debug name for descriptor pool must not be empty + /// @exception std::invalid_argument Descriptor pool sizes must not be empty + /// @exception VulkanException vkCreateDescriptorPool call failed + DescriptorPool(const Device &device, std::vector pool_sizes, std::uint32_t max_sets, + std::string name); + + DescriptorPool(const DescriptorPool &) = delete; + DescriptorPool(DescriptorPool &&) noexcept; + + /// Call vkDestroyDescriptorPool + ~DescriptorPool(); + + DescriptorPool &operator=(const DescriptorPool &) = delete; + DescriptorPool &operator=(DescriptorPool &&) = delete; + + [[nodiscard]] auto descriptor_pool() const noexcept { + return m_descriptor_pool; + } +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.hpp new file mode 100644 index 000000000..33debee04 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool.hpp" + +#include + +#include + +// Forward declaration +namespace inexor::vulkan_renderer::wrapper { +class Device; +} + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Forward declaration +class DescriptorSetAllocator; + +/// Allocator for the DescriptorPool class +class DescriptorPoolAllocator { + friend DescriptorSetAllocator; + +private: + /// The device wrapper + const Device &m_device; + /// The descriptor pools + std::vector m_pools; + + /// Default constructor + /// @param device The device wrapper + explicit DescriptorPoolAllocator(const Device &device); + + /// Return a descriptor pool from ``m_pools`` and in case all pools are used up, create a new one + /// @note If we run out of descriptor pools, we simply create one new descriptor pool (not multiple ones!) + /// @return A new descriptor pool that has not been used yet + [[nodiscard]] VkDescriptorPool request_new_descriptor_pool(); + +public: + DescriptorPoolAllocator(const DescriptorPoolAllocator &) = delete; + DescriptorPoolAllocator(DescriptorPoolAllocator &&) noexcept; + + ~DescriptorPoolAllocator() = default; + + DescriptorPoolAllocator &operator=(const DescriptorPoolAllocator &) = delete; + // TODO: Implement me! + DescriptorPoolAllocator &operator=(DescriptorPoolAllocator &&) noexcept; +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.hpp new file mode 100644 index 000000000..ac74efef9 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.hpp" + +#include + +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Forward declaration +class DescriptorBuilder; + +/// This classes manages descriptors by allocating VkDescriptorPools and VkDescriptorSetLayouts. +/// It is also responsible for caching VkDescriptorSetLayouts, meaning we do not create duplicates +/// For internal use inside of rendergraph only! +class DescriptorSetAllocator { + friend class DescriptorBuilder; + +private: + /// The device wrapper + const Device &m_device; + // The descriptor pool currently in use (handled by a DescriptorPool instance) + VkDescriptorPool m_current_pool{VK_NULL_HANDLE}; + std::uint32_t m_current_pool_set_allocations{0}; + + static constexpr std::uint32_t MAX_DESCRIPTOR_SETS_PER_POOL = 4096; + /// The descriptor pool allocator + DescriptorPoolAllocator m_descriptor_pool_allocator; + +public: + /// Default constructor + /// @note This is private because descriptor allocators are for internal use in rendergraph only! + /// @param device The device wrapper + explicit DescriptorSetAllocator(const Device &device); + + DescriptorSetAllocator(const DescriptorSetAllocator &) = delete; + DescriptorSetAllocator(DescriptorSetAllocator &&) noexcept; + + ~DescriptorSetAllocator() = default; + + DescriptorSetAllocator &operator=(const DescriptorSetAllocator &) = delete; + DescriptorSetAllocator &operator=(DescriptorSetAllocator &&) = delete; + + /// Allocate a new descriptor set + /// @note We are currently not batching calls vkAllocateDescriptorSets, which would allow multiple descriptor sets + /// to be allcoated in one vkAllocateDescriptorSets call. The problem is that batching could lead to running out of + /// memory in the VkDescriptorPool, so a new descriptor pool would be created. + /// @param name The name of the descriptor set layout + /// @param descriptor_set_layout The descriptor set layout to allocate the descriptor set with + /// @return The descriptor set which was allocated + [[nodiscard]] VkDescriptorSet allocate(const std::string &name, VkDescriptorSetLayout descriptor_set_layout); +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp new file mode 100644 index 000000000..360380ef8 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +// Forward declaration +namespace inexor::vulkan_renderer::wrapper { +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Forward declaration +class DescriptorSetLayoutCache; + +/// RAII wrapper for VkDescriptorPool +/// For internal use inside of rendergraph only! +class DescriptorSetLayout { +private: + friend DescriptorSetLayoutCache; + // TODO: Make RenderGraph friend + + const Device &m_device; + std::string m_name; + VkDescriptorSetLayout m_descriptor_set_layout{VK_NULL_HANDLE}; + +public: + // TODO: Move me into private again and make rendergraph a friend! + + /// Default constructor + /// @param device The device wrapper + /// @param descriptor_set_layout_ci The descriptor set layout create info + /// @param name The internal debug name of the descriptor set layout + DescriptorSetLayout(const Device &device, VkDescriptorSetLayoutCreateInfo descriptor_set_layout_ci, + std::string name); + + DescriptorSetLayout(const DescriptorSetLayout &) = delete; + DescriptorSetLayout(DescriptorSetLayout &&) noexcept; + + /// Call vkDestroyDescriptorSetLayout + ~DescriptorSetLayout(); + + DescriptorSetLayout &operator=(const DescriptorSetLayout &) = delete; + DescriptorSetLayout &operator=(DescriptorSetLayout &&) = delete; +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.hpp new file mode 100644 index 000000000..cc583db34 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.hpp" + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Using declaration +using tools::InexorException; + +/// A simplified helper enum for descriptor types +enum class DescriptorType { + SAMPLER, + COMBINED_IMAGE_SAMPLER, + SAMPLED_IMAGE, + STORAGE_IMAGE, + UNIFORM_TEXEL_BUFFER, + STORAGE_TEXEL_BUFFER, + UNIFORM_BUFFER, + STORAGE_BUFFER, + UNIFORM_BUFFER_DYNAMIC, + STORAGE_BUFFER_DYNAMIC, + INPUT_ATTACHMENT +}; + +/// A builder for descriptors +class DescriptorSetLayoutBuilder { +private: + const Device &m_device; + /// All instances of DescriptorSetLayoutBuilder have the same DescriptorSetLayoutCache instance! + DescriptorSetLayoutCache m_descriptor_set_layout_cache; + std::vector m_bindings; + std::uint32_t m_binding = 0; + +public: + /// Default constructor + /// @param device The device wrapper + DescriptorSetLayoutBuilder(const Device &device); + + /// Add a new descriptor + /// @param type The type of the descriptor + /// @param stage The shader stage + /// @param count The number of descriptors + /// @note It was deliberately decided not to guess the shader stage based on DescriptorType, simply because one + /// descriptor type could be used in various shader stages. Another approach would have been to make the shader + /// stage an std::optional, and if it's specified as std::nullopt, decide which shader stage is most likely based on + /// the DescriptorType. However, this approach is not clearly defined, because one descriptor could be used in a + /// variety of shader stages. + [[nodiscard]] auto &add(DescriptorType type, VkShaderStageFlags stage, std::uint32_t count = 1) { + m_bindings.emplace_back(VkDescriptorSetLayoutBinding{ + .binding = m_binding++, // Use current binding, then increment + .descriptorType = + [&]() { + switch (type) { + case DescriptorType::SAMPLER: + return VK_DESCRIPTOR_TYPE_SAMPLER; + case DescriptorType::COMBINED_IMAGE_SAMPLER: + return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + case DescriptorType::SAMPLED_IMAGE: + return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + case DescriptorType::STORAGE_IMAGE: + return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + case DescriptorType::UNIFORM_TEXEL_BUFFER: + return VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + case DescriptorType::STORAGE_TEXEL_BUFFER: + return VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; + case DescriptorType::UNIFORM_BUFFER: + return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + case DescriptorType::STORAGE_BUFFER: + return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + case DescriptorType::UNIFORM_BUFFER_DYNAMIC: + return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + case DescriptorType::STORAGE_BUFFER_DYNAMIC: + return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + case DescriptorType::INPUT_ATTACHMENT: + return VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT; + default: + throw InexorException("Unknown DescriptorType!"); + } + }(), + .descriptorCount = count, + .stageFlags = stage, + }); + return *this; + } + + // @TODO Enforce a return type for the build() method so the rendergraph only accepts fully built descriptor set + // layouts, avoiding the descriptor set layout builder to be in incomplete, invalid state! + + /// Build the descriptor set layout + /// @param name The name of the descriptor set layout + /// @return The descriptor set layout that was created + [[nodiscard]] VkDescriptorSetLayout build(std::string name); +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.hpp new file mode 100644 index 000000000..60a69bc6f --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp" + +#include + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::render_graph { +// Forward declaration +class RenderGraph; +} // namespace inexor::vulkan_renderer::render_graph + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Forward declaration +class DescriptorBuilder; + +/// A metadata struct for information on descriptor set layouts +struct DescriptorSetLayoutInfo { + std::vector bindings; + [[nodiscard]] bool operator==(const DescriptorSetLayoutInfo &other) const; + [[nodiscard]] std::size_t hash() const; +}; + +/// A hash object for descriptor set layouts +struct DescriptorSetLayoutHash { + std::size_t operator()(const DescriptorSetLayoutInfo &k) const { + return k.hash(); + } +}; + +/// A class for caching VkDescriptorSetLayouts with the help of std::unordered_map and a hashing function +/// For internal use inside of rendergraph only! +class DescriptorSetLayoutCache { + friend DescriptorBuilder; + friend render_graph::RenderGraph; + +private: + /// The device wrapper + const Device &m_device; + + /// The actual descriptor set layout cache + /// Note that std::unordered_map can accept a third template parameter which is the hash function + /// Also note that the lifetime of the VkDescriptorSetLayout objects is bound to the lifetime of this unordered_map + /// The destructor of the DescriptorSetLayout wrapper instances will be called when DescriptorSetLayoutCache's + /// destructor is called + std::unordered_map m_cache; + +public: + /// Default constructor + /// @param device The device wrapper + DescriptorSetLayoutCache(const Device &device); + + DescriptorSetLayoutCache(const DescriptorSetLayoutCache &) = delete; + DescriptorSetLayoutCache(DescriptorSetLayoutCache &&) noexcept; + + ~DescriptorSetLayoutCache() = default; + + DescriptorSetLayoutCache &operator=(const DescriptorSetLayoutCache &) = delete; + DescriptorSetLayoutCache &operator=(DescriptorSetLayoutCache &&) = delete; + + /// Create a descriptor set layout with the help of the cache + /// @param descriptor_set_layout_ci The descriptor set layout create info + /// @param name The name of the descriptor set layout + /// @return The descriptor set layout that was created + [[nodiscard]] VkDescriptorSetLayout + create_descriptor_set_layout(VkDescriptorSetLayoutCreateInfo descriptor_set_layout_ci, std::string name); +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp new file mode 100644 index 000000000..c562a26ee --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +class PerFrameDescriptorSets { +private: + VkDescriptorSetLayout m_descriptor_set_layout{VK_NULL_HANDLE}; + VkPipelineLayout m_pipeline_layout{VK_NULL_HANDLE}; + std::vector m_descriptor_sets{1, VK_NULL_HANDLE}; + std::size_t m_current_frame_slot{0}; + +public: + void set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot) { + const auto slot_count = std::max(1, frame_slot_count); + if (m_descriptor_sets.size() != slot_count) { + m_descriptor_sets.resize(slot_count, VK_NULL_HANDLE); + } + m_current_frame_slot = std::min(current_frame_slot, m_descriptor_sets.size() - 1); + } + + void set_descriptor_set(const std::size_t slot_index, const VkDescriptorSet descriptor_set) { + if (slot_index >= m_descriptor_sets.size()) { + m_descriptor_sets.resize(slot_index + 1, VK_NULL_HANDLE); + } + m_descriptor_sets[slot_index] = descriptor_set; + } + + [[nodiscard]] VkDescriptorSet descriptor_set(const std::size_t slot_index) const { + return m_descriptor_sets.at(slot_index); + } + + [[nodiscard]] VkDescriptorSet current_descriptor_set() const { + return m_descriptor_sets.at(m_current_frame_slot); + } + + [[nodiscard]] std::size_t frame_slot_count() const { + return m_descriptor_sets.size(); + } + + // TODO: Remove me again! + void set_layout(VkDescriptorSetLayout descriptor_set_layout) { + m_descriptor_set_layout = descriptor_set_layout; + } + + [[nodiscard]] auto layout() const { + return m_descriptor_set_layout; + } + + void set_pipeline_layout(const VkPipelineLayout pipeline_layout) { + m_pipeline_layout = pipeline_layout; + } + + [[nodiscard]] VkPipelineLayout pipeline_layout() const { + return m_pipeline_layout; + } +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors \ No newline at end of file diff --git a/include/inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp b/include/inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp new file mode 100644 index 000000000..0e3f5f043 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::render_graph { +// Forward declarations +class Buffer; +enum class BufferType; +class Texture; +} // namespace inexor::vulkan_renderer::render_graph + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +// Using declarations +using render_graph::Buffer; +using render_graph::BufferType; +using render_graph::Texture; +using tools::InexorException; +using tools::make_info; + +/// A wrapper class for batching calls to vkUpdateDescriptorSets +class WriteDescriptorSetBuilder { +private: + const Device &m_device; + std::vector m_write_descriptor_sets; + +public: + /// Default constructor + /// @param device The device wrapper + explicit WriteDescriptorSetBuilder(const Device &device); + + /// Add a new entry to the write descriptor set builder + /// @param descriptor_set The descriptor set + /// @param descriptor_data Either a buffer or a texture + /// @param descriptor_count The descriptor count (``1`` by default) + [[nodiscard]] WriteDescriptorSetBuilder & + add(const VkDescriptorSet descriptor_set, + std::variant, std::weak_ptr> descriptor_data, std::uint32_t dst_binding, + std::uint32_t descriptor_count = 1) { + if (!descriptor_set) { + throw InexorException("Error: Parameter 'descriptor_set' is invalid!"); + } + + auto write_descriptor_set = make_info({ + .dstSet = descriptor_set, + .dstBinding = dst_binding, + .dstArrayElement = 0, + .descriptorCount = descriptor_count, + }); + + // This short code is presented to you by the magic of variant-based polymorphism :) + // Handle the variant type (either Texture or Buffer) + std::visit( + [&write_descriptor_set](auto &&descriptor) { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + if (auto texture = descriptor.lock(); texture) { + write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write_descriptor_set.pImageInfo = texture->descriptor_image_info(); + } else { + throw InexorException("Error: Texture is invalid!"); + } + } else if constexpr (std::is_same_v>) { + if (auto buffer = descriptor.lock(); buffer) { + // @TODO: Distinguish type by buffer->type()! (uniform ? storage? ..) + write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write_descriptor_set.pBufferInfo = buffer->descriptor_buffer_info(); + } else { + throw InexorException("Error: Buffer is invalid!"); + } + } else { + // @TODO: Support more descriptor types + throw InexorException("Error: Invalid descriptor type in std::variant!"); + } + }, + descriptor_data); + + m_write_descriptor_sets.push_back(write_descriptor_set); + return *this; + } + + /// Return the write descriptor sets and reset the builder + /// @return A std::vector of VkWriteDescriptorSet + [[nodiscard]] std::vector build(); +}; + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/include/inexor/vulkan-renderer/wrapper/device.hpp b/include/inexor/vulkan-renderer/wrapper/device.hpp index 37766e568..743ec520a 100644 --- a/include/inexor/vulkan-renderer/wrapper/device.hpp +++ b/include/inexor/vulkan-renderer/wrapper/device.hpp @@ -1,42 +1,79 @@ #pragma once #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/tools/representation.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" #include "inexor/vulkan-renderer/wrapper/commands/command_pool.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include #include +#include #include #include +#include #include +#include #include +namespace inexor::vulkan_renderer::wrapper::pipelines { +// Forward declaration +class PipelineCache; +} // namespace inexor::vulkan_renderer::wrapper::pipelines + namespace inexor::vulkan_renderer::wrapper { // Forward declaration class Instance; // Using declarations +using commands::CommandBuffer; +using commands::CommandPool; using tools::InexorException; using tools::VulkanException; -using wrapper::commands::CommandBuffer; -using wrapper::commands::CommandPool; - -/// An enum for the supported queue types -enum class VulkanQueueType { - QUEUE_TYPE_GRAPHICS, - QUEUE_TYPE_COMPUTE, - QUEUE_TYPE_TRANSFER, - QUEUE_TYPE_SPARSE_BINDING, +using wrapper::pipelines::PipelineCache; + +/// Debug label colors +enum class DebugLabelColor { + RED, + BLUE, + GREEN, + YELLOW, + PURPLE, + ORANGE, + MAGENTA, + CYAN, + BROWN, + PINK, + LIME, + TURQUOISE, + BEIGE, + MAROON, + OLIVE, + NAVY, + TEAL, +}; + +struct QueueSemaphoreWait { + VkSemaphore semaphore{VK_NULL_HANDLE}; + VkPipelineStageFlags2 stage_mask{VK_PIPELINE_STAGE_2_NONE}; }; +/// Convert a DebugLabelColor to a rgba value +/// @param color The debug label color +/// @return The converted rgba values +[[nodiscard]] std::array get_debug_label_color(const DebugLabelColor color); + /// RAII wrapper class for `VkDevice`, `VkPhysicalDevice` and `VkQueue`. /// @note There is no method ``is_layer_supported`` in this wrapper class because device layers are deprecated. class Device { + friend class CommandBuffer; + friend class CommandPool; + private: VkDevice m_device{VK_NULL_HANDLE}; VkPhysicalDevice m_physical_device{VK_NULL_HANDLE}; + std::unique_ptr m_pipeline_cache; VmaAllocator m_allocator{VK_NULL_HANDLE}; std::string m_gpu_name; VkPhysicalDeviceFeatures m_enabled_features{}; @@ -46,7 +83,6 @@ class Device { VkQueue m_transfer_queue{VK_NULL_HANDLE}; VkQueue m_compute_queue{VK_NULL_HANDLE}; VkQueue m_sparse_binding_queue{VK_NULL_HANDLE}; - VkQueue m_present_queue{VK_NULL_HANDLE}; std::optional m_graphics_queue_family_index{0}; std::optional m_compute_queue_family_index{0}; @@ -62,7 +98,7 @@ class Device { /// Get the thread_local command pool. /// @param queue_type The Vulkan queue type /// @note This method will create a command pool for the thread if it doesn't already exist. - CommandPool &get_thread_command_pool(VulkanQueueType queue_type) const; + CommandPool &get_thread_command_pool(VkQueueFlagBits queue_type) const; // @TODO Implement get_thread_command_pool with "transfer if available, graphics otherwise" for copy operations. @@ -82,14 +118,8 @@ class Device { Device(const Instance &inst, VkSurfaceKHR surface, VkPhysicalDevice physical_device, const VkPhysicalDeviceFeatures &required_features, std::span required_extensions); - Device(const Device &) = delete; - Device(Device &&) noexcept; - ~Device(); - Device &operator=(const Device &) = delete; - Device &operator=(Device &&) = delete; - [[nodiscard]] auto device() const { return m_device; } @@ -110,13 +140,76 @@ class Device { [[nodiscard]] bool is_presentation_supported(VkSurfaceKHR surface, std::uint32_t queue_family_index) const; /// A wrapper method for beginning, ending and submitting command buffers. This method calls the request method for - /// the given command pool, begins the command buffer, executes the lambda, ends recording the command buffer, - /// submits it and waits for it. - /// @param name The internal debug name of the command buffer (must not be empty). - /// @param queue_type The queue type which determines which command pool is used. - /// @param cmd_lambda The command lambda to execute. - void execute(const std::string &name, const VulkanQueueType queue_type, - const std::function &cmd_lambda) const; + /// the given command pool, begins the command buffer, invokes the recording function, ends recording the command + /// buffer, and submits it on the specified queue. The returned fence can be used by callers for synchronization. + /// Using this execute method is the preferred way of + /// using command buffers in the engine. There is no need to request a command buffer manually, which is why this + /// method in CommandPool is not public. + /// @param name The internal debug name of the command buffer (must not be empty) + /// @param queue_type The queue type to submit the command buffer to + /// @param dbg_label_color The color of the debug label when calling ``begin_debug_label_region`` + /// @note Debug label colors are only visible in graphics debuggers such as RenderDoc + /// @param on_record The command buffer recording function to invoke after starting recording + /// @note It's technically allowed that the command buffer recording function is empty or a function which does not + /// do any vkCmd command calls, but this makes no real sense because an empty command buffer will be submitted. It + /// will not be checked if any commands have been recorded into the command buffer, although this could be + /// implemented using CommandBuffer wrapper. However, this would be a case for validation layers though. + /// @param wait_semaphores The semaphores to wait on before starting command buffer execution (empty by default) + /// @param signal_semaphores The semaphores to signal once command buffer execution will finish (empty by default) + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, + const std::function &on_record, + std::span wait_semaphores = {}, + std::span signal_semaphores = {}, + std::source_location source_location = std::source_location::current()) const; + + template + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, OnRecord &&on_record, + std::span wait_semaphores = {}, + std::span signal_semaphores = {}, + std::source_location source_location = std::source_location::current()) const { + const auto &cmd_buf = + get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphores); + return cmd_buf.submission_fence(); + } + + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, + const std::function &on_record, + std::span wait_semaphores, + std::span signal_semaphore_infos, + std::source_location source_location = std::source_location::current()) const; + + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, + const std::function &on_record, + std::span wait_semaphores, + std::span signal_semaphores = {}, + std::source_location source_location = std::source_location::current()) const; + + template + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, OnRecord &&on_record, + std::span wait_semaphores, + std::span signal_semaphores = {}, + std::source_location source_location = std::source_location::current()) const { + const auto &cmd_buf = + get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphores); + return cmd_buf.submission_fence(); + } + + [[nodiscard]] VkFence execute(VkQueueFlagBits queue_type, DebugLabelColor dbg_label_color, + const std::function &on_record, + std::span wait_semaphores, + std::span signal_semaphore_infos, + std::source_location source_location = std::source_location::current()) const; [[nodiscard]] VkPhysicalDevice physical_device() const { return m_physical_device; @@ -134,6 +227,8 @@ class Device { return m_gpu_name; } + [[nodiscard]] VkPipelineCache pipeline_cache() const; + /// Get the pipeline cache UUID for the physical device /// @return A span view of the pipeline cache UUID bytes [[nodiscard]] std::span pipeline_cache_uuid() const { @@ -148,13 +243,18 @@ class Device { return m_transfer_queue != VK_NULL_HANDLE; } + [[nodiscard]] bool transfer_queue_shares_graphics_family() const { + return m_transfer_queue_family_index.has_value() && m_graphics_queue_family_index.has_value() && + m_transfer_queue_family_index.value() == m_graphics_queue_family_index.value(); + } + [[nodiscard]] bool has_any_sparse_binding_queue() const { return m_transfer_queue != VK_NULL_HANDLE; } // TODO: Move to command buffer wrapper! [[nodiscard]] VkQueue compute_queue() const { - return m_graphics_queue; + return m_compute_queue; } // TODO: Move to command buffer wrapper! @@ -162,22 +262,42 @@ class Device { return m_graphics_queue; } - // TODO: Move to command buffer wrapper! - [[nodiscard]] VkQueue present_queue() const { - return m_present_queue; - } - // TODO: Move to command buffer wrapper! [[nodiscard]] VkQueue transfer_queue() const { return m_transfer_queue; } + [[nodiscard]] std::uint32_t graphics_queue_family_index() const { + if (!m_graphics_queue_family_index.has_value()) { + throw std::runtime_error("Error: Graphics queue family index is not available!"); + } + return m_graphics_queue_family_index.value(); + } + + [[nodiscard]] std::uint32_t transfer_queue_family_index() const { + if (!m_transfer_queue_family_index.has_value()) { + throw std::runtime_error("Error: Transfer queue family index is not available!"); + } + return m_transfer_queue_family_index.value(); + } + /// Request a command buffer from the thread_local command pool. /// @param queue_type The Vulkan queue type which is required because a command pool is created with a queue family /// index associated with it. /// @param name The name which will be assigned to the command buffer. /// @return A command buffer from the thread_local command pool. - [[nodiscard]] const CommandBuffer &request_command_buffer(VulkanQueueType queue_type, const std::string &name); + [[nodiscard]] const CommandBuffer &request_command_buffer(VkQueueFlagBits queue_type, const std::string &name); + + /// Request a secondary command buffer from the thread_local command pool. + /// @param queue_type The Vulkan queue type which is required because a command pool is created with a queue family + /// index associated with it. + /// @param name The name which will be assigned to the command buffer. + /// @return A secondary command buffer from the thread_local command pool. + [[nodiscard]] const CommandBuffer &request_secondary_command_buffer(VkQueueFlagBits queue_type, + const std::string &name); + + /// Wait until all submitted command buffers in the current thread's pool for a queue type are complete. + void wait_for_submissions(VkQueueFlagBits queue_type) const; /// Check if a surface supports a certain image usage. /// @param surface The window surface. @@ -198,7 +318,7 @@ class Device { throw InexorException("Error: Parameter 'vk_object' is invalid!"); } - const auto dbg_obj_name = wrapper::make_info({ + const auto dbg_obj_name = tools::make_info({ .objectType = tools::get_vk_object_type(vk_object), .objectHandle = reinterpret_cast(vk_object), .pObjectName = name.c_str(), @@ -209,6 +329,10 @@ class Device { } } + /// Call vkUpdateDescriptorSets + /// @param write_descriptor_sets The write descriptor sets + void update_descriptor_sets(std::span write_descriptor_sets); + /// Call `vkDeviceWaitIdle` or `vkQueueWaitIdle` depending on whether `queue` is specified. /// @warning Avoid using those methods because they result in bad gpu performance due to global stalls! /// @param queue (`VK_NULL_HANDLE` by default). diff --git a/include/inexor/vulkan-renderer/wrapper/images/image.hpp b/include/inexor/vulkan-renderer/wrapper/images/image.hpp new file mode 100644 index 000000000..8e03fc285 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/images/image.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include + +#include +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::render_graph { +// Forward declarations +class RenderGraph; +class Texture; +} // namespace inexor::vulkan_renderer::render_graph + +// Using declarations +using inexor::vulkan_renderer::render_graph::Texture; +using inexor::vulkan_renderer::wrapper::Device; +using inexor::vulkan_renderer::wrapper::commands::CommandBuffer; + +namespace inexor::vulkan_renderer::wrapper::images { + +/// RAII wrapper for VkImage and VkImageView +class Image { +private: + friend Texture; + + /// The device wrapper + const Device &m_device; + /// The internal debug name of the image + std::string m_name; + + VkImage m_img{VK_NULL_HANDLE}; + VkImageCreateInfo m_img_ci{}; + + VkImageView m_img_view{VK_NULL_HANDLE}; + VkImageViewCreateInfo m_img_view_ci{}; + + VmaAllocation m_alloc{VK_NULL_HANDLE}; + VmaAllocationInfo m_alloc_info{}; + + /// Create the image and the image view + /// @param img_ci The image create info + /// @param img_view_ci The image view create info + void create(VkImageCreateInfo img_ci, VkImageViewCreateInfo img_view_ci); + + /// Destroy the image view, the image, and the sampler + void destroy(); + +public: + /// Default constructor + /// @param device The device wrapper + /// @param name The name of the Image + Image(const Device &device, std::string name); + + Image(const Image &) = delete; + Image(Image &&) noexcept; + + ~Image(); + + Image &operator=(const Image &) = delete; + Image &operator=(Image &&) = delete; + + [[nodiscard]] auto height() const { + return m_img_ci.extent.height; + } + + [[nodiscard]] auto image() const { + return m_img; + } + + [[nodiscard]] auto image_view() const { + return m_img_view; + } + + [[nodiscard]] auto width() const { + return m_img_ci.extent.width; + } +}; + +} // namespace inexor::vulkan_renderer::wrapper::images diff --git a/include/inexor/vulkan-renderer/wrapper/images/image_view.hpp b/include/inexor/vulkan-renderer/wrapper/images/image_view.hpp new file mode 100644 index 000000000..e69de29bb diff --git a/include/inexor/vulkan-renderer/wrapper/images/sampler.hpp b/include/inexor/vulkan-renderer/wrapper/images/sampler.hpp new file mode 100644 index 000000000..d0db3d3dc --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/images/sampler.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "inexor/vulkan-renderer/tools/make_info.hpp" + +#include + +#include + +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + +namespace inexor::vulkan_renderer::wrapper::images { + +/// RAII wrapper class for VkSampler +class Sampler { +private: + const Device &m_device; + VkSampler m_sampler{VK_NULL_HANDLE}; + std::string m_name; + +public: + /// Default constructor + /// @param device The device wrapper + /// @param name The internal debug name of the sampler + /// @param sampler_ci The sampler create info + Sampler(const Device &device, std::string name, + const VkSamplerCreateInfo &sampler_ci = tools::make_info({ + // NOTE: These are the default sampler settings + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_FALSE, + .compareOp = VK_COMPARE_OP_ALWAYS, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + .borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK, + .unnormalizedCoordinates = VK_FALSE, + })); + + Sampler(const Sampler &) = delete; + Sampler(Sampler &&) noexcept; + + ~Sampler(); + + Sampler &operator=(const Sampler &) = delete; + Sampler &operator=(Sampler &&) noexcept; + + [[nodiscard]] VkSampler sampler() const { + return m_sampler; + } +}; + +} // namespace inexor::vulkan_renderer::wrapper::images diff --git a/include/inexor/vulkan-renderer/wrapper/instance.hpp b/include/inexor/vulkan-renderer/wrapper/instance.hpp index afa8feb42..e831d31ed 100644 --- a/include/inexor/vulkan-renderer/wrapper/instance.hpp +++ b/include/inexor/vulkan-renderer/wrapper/instance.hpp @@ -46,7 +46,7 @@ class Instance { public: /// This is the version of Vulkan API that we use in the entire engine. - static constexpr std::uint32_t REQUIRED_VK_API_VERSION{VK_API_VERSION_1_2}; + static constexpr std::uint32_t REQUIRED_VK_API_VERSION{VK_API_VERSION_1_3}; /// Default constructor. /// @param instance_layers The required instance layers (can be empty). diff --git a/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp b/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp index fbf2a2271..205739b44 100644 --- a/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp +++ b/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp @@ -1,9 +1,11 @@ #pragma once -#include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include +#include +#include #include #include @@ -18,7 +20,8 @@ class CommandBuffer; } // namespace inexor::vulkan_renderer::wrapper::commands namespace inexor::vulkan_renderer::wrapper::pipelines { -class PipelineCache; +// Forward declarations +class PipelineLayout; } // namespace inexor::vulkan_renderer::wrapper::pipelines namespace inexor::vulkan_renderer::render_graph { @@ -26,51 +29,81 @@ namespace inexor::vulkan_renderer::render_graph { class RenderGraph; } // namespace inexor::vulkan_renderer::render_graph +namespace inexor::vulkan_renderer::wrapper::descriptors { +// Forward declaration +class PerFrameDescriptorSets; +} // namespace inexor::vulkan_renderer::wrapper::descriptors + namespace inexor::vulkan_renderer::wrapper::pipelines { // TODO: Implement RAII wrapper for ComputePipeline +// Using declaration +using tools::make_info; + +/// When creating a graphics pipeline, the lifetime of certain data which is used to create the pipeline must be +/// ensured. In particular, the VkGraphicsPipelineCreateInfo struct must not be stored, however, the memory to which the +/// pointers inside of VkGraphicsPipelineCreateInfo point to must be stored. For example, VkGraphicsPipelineCreateInfo +/// has a member VkPipelineViewportStateCreateInfo, which itself has a pointer that point to VkViewport data, for +/// example. This means we must make sure the lifetime of all data that the pointers point to must be preserved. +/// Initially, we collected all the data to create the graphics pipeline in GraphicsPipelineBuilder, and reset all the +/// data of the builder after the build() method has been called. However, this is wrong, because the lifetime of the +/// data ends with calling reset(). This causes some bugs which are hard to find. +/// +/// @TODO: Implement move constructor for GraphicsPipelineSetupData +struct GraphicsPipelineSetupData { + // This is the underlying data for the create info structures + std::vector shader_stages{}; + std::vector vertex_input_binding_descriptions{}; + std::vector vertex_input_attribute_descriptions{}; + std::vector color_blend_attachment_states{}; + std::vector viewports{}; + std::vector scissors{}; + std::vector push_constant_ranges{}; + std::vector descriptor_set_layouts{}; + VkFormat depth_attachment_format{}; + VkFormat stencil_attachment_format{}; + std::vector color_attachments{}; + std::vector dynamic_states{}; + VkPipelineLayout pipeline_layout{VK_NULL_HANDLE}; + std::vector> associated_descriptor_sets{}; + + // These are the create info structures required to fill the VkGraphicsPipelineCreateInfo + VkPipelineVertexInputStateCreateInfo vertex_input_sci{make_info()}; + VkPipelineInputAssemblyStateCreateInfo input_assembly_sci{make_info()}; + VkPipelineTessellationStateCreateInfo tesselation_sci{make_info()}; + VkPipelineViewportStateCreateInfo viewport_sci{make_info()}; + VkPipelineRasterizationStateCreateInfo rasterization_sci{make_info()}; + VkPipelineDepthStencilStateCreateInfo depth_stencil_sci{make_info()}; + VkPipelineRenderingCreateInfo pipeline_rendering_ci{make_info()}; + VkPipelineMultisampleStateCreateInfo multisample_sci{make_info()}; + VkPipelineColorBlendStateCreateInfo color_blend_sci{make_info()}; + VkPipelineDynamicStateCreateInfo dynamic_states_sci{make_info()}; +}; + /// RAII wrapper for graphics pipelines class GraphicsPipeline { - friend class commands::CommandBuffer; - friend class render_graph::RenderGraph; - private: const Device &m_device; std::string m_name; - VkPipeline m_pipeline; std::unique_ptr m_pipeline_layout; public: /// Default constructor /// @param device The device wrapper - /// @param pipeline_cache The Vulkan pipeline cache - /// @param descriptor_set_layouts The descriptor set layouts in the pipeline layout - /// @param push_constant_ranges The push constant ranges in the pipeline layout - /// @param pipeline_ci The pipeline create info + /// @param setup_data The graphics pipeline setup data /// @param name The internal debug name of the graphics pipeline - GraphicsPipeline(const Device &device, const PipelineCache &pipeline_cache, - std::span descriptor_set_layouts, - std::span push_constant_ranges, - VkGraphicsPipelineCreateInfo pipeline_ci, std::string name); - - GraphicsPipeline(const GraphicsPipeline &) = delete; - GraphicsPipeline(GraphicsPipeline &&) noexcept; + GraphicsPipeline(const Device &device, GraphicsPipelineSetupData setup_data, std::string name); /// Call vkDestroyPipeline ~GraphicsPipeline(); - GraphicsPipeline &operator=(const GraphicsPipeline &) = delete; - GraphicsPipeline &operator=(GraphicsPipeline &&) = delete; - - [[nodicsard]] auto pipeline() const { + [[nodiscard]] VkPipeline pipeline() const { return m_pipeline; } - [[nodicsard]] auto pipeline_layout() const { - return m_pipeline_layout->m_pipeline_layout; - } + [[nodiscard]] VkPipelineLayout pipeline_layout() const; }; } // namespace inexor::vulkan_renderer::wrapper::pipelines diff --git a/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp b/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp index ea8aa52d6..f65683ab6 100644 --- a/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp +++ b/include/inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp @@ -1,8 +1,8 @@ #pragma once -#include - -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" #include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" #include "inexor/vulkan-renderer/wrapper/shader.hpp" @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace inexor::vulkan_renderer::wrapper { @@ -20,6 +21,11 @@ class Device; class Shader; } // namespace inexor::vulkan_renderer::wrapper +namespace inexor::vulkan_renderer::wrapper::descriptors { +// Forward declaration +class PerFrameDescriptorSets; +} // namespace inexor::vulkan_renderer::wrapper::descriptors + namespace inexor::vulkan_renderer::render_graph { // Forward declaration class RenderGraph; @@ -27,89 +33,18 @@ class RenderGraph; namespace inexor::vulkan_renderer::wrapper::pipelines { -// Forward declaration -class PipelineCache; - // TODO: ComputePipelineBuilder /// Builder class for VkPipelineCreateInfo for graphics pipelines which use dynamic rendering /// @note This builder pattern does not perform any checks which are already covered by validation layers. /// This means if you forget to specify viewport for example, creation of the graphics pipeline will fail. /// It is the reponsibility of the programmer to use validation layers to check for problems. +/// @TODO Although we initially did not want to implement checks which mimic the validation layers, it might be worth it +/// to implement checks in case required fields are not set in this builder. class GraphicsPipelineBuilder { private: - /// The device wrapper reference const Device &m_device; - - /// The Vulkan pipeline cache - const PipelineCache &m_pipeline_cache; - - // We are not using member initializers here: - // Note that all members are initialized in the reset() method - // This method is also called after the graphics pipeline has been created, - // allowing one instance of GraphicsPipelineBuilder to be reused - - // With the builder we can either call add_shader or set_shaders - std::vector m_shader_stages; - - std::vector m_vertex_input_binding_descriptions; - std::vector m_vertex_input_attribute_descriptions; - // With the builder we can fill vertex binding descriptions and vertex attribute descriptions in here - VkPipelineVertexInputStateCreateInfo m_vertex_input_sci; - - // With the builder we can set topology in here - VkPipelineInputAssemblyStateCreateInfo m_input_assembly_sci; - - // With the builder we can set the patch control point count in here - VkPipelineTessellationStateCreateInfo m_tesselation_sci; - - std::vector m_viewports; - std::vector m_scissors; - // With the builder we can set viewport(s) and scissor(s) in here - VkPipelineViewportStateCreateInfo m_viewport_sci; - - // With the builder we can set polygon mode, cull mode, front face, and line width - // TODO: Implement methods to enable depth bias and for setting the depth bias parameters - VkPipelineRasterizationStateCreateInfo m_rasterization_sci; - - // With the builder we can't set individial fields of this struct, - // since it's easier to specify an entire VkPipelineDepthStencilStateCreateInfo struct to the builder instead - VkPipelineDepthStencilStateCreateInfo m_depth_stencil_sci; - - VkRenderPass m_render_pass; - - /// This is used for dynamic rendering - VkFormat m_depth_attachment_format; - VkFormat m_stencil_attachment_format; - std::vector m_color_attachments; - - VkPipelineRenderingCreateInfo m_pipeline_rendering_ci; - - // With the builder we can set rasterization samples and min sample shading - // TODO: Expose more multisampling parameters if desired - VkPipelineMultisampleStateCreateInfo m_multisample_sci; - - // With the builder we can't set individial fields of this struct, - // since it's easier to specify an entire VkPipelineColorBlendStateCreateInfo struct to the builder instead - VkPipelineColorBlendStateCreateInfo m_color_blend_sci; - - std::vector m_dynamic_states; - // This will be filled in the build() method - VkPipelineDynamicStateCreateInfo m_dynamic_states_sci; - - /// The layout of the graphics pipeline - VkPipelineLayout m_pipeline_layout; - - // With the builder we can either call add_color_blend_attachment or set_color_blend_attachments - std::vector m_color_blend_attachment_states; - - /// The push constant ranges of the graphics pass - std::vector m_push_constant_ranges; - - std::vector m_descriptor_set_layouts; - - /// Reset all data in this class so the builder can be re-used - /// @note This is called by the constructor + GraphicsPipelineSetupData m_data; void reset(); public: @@ -117,189 +52,345 @@ class GraphicsPipelineBuilder { /// Default constructor /// @param device The device wrapper - /// @param pipeline_cache The Vulkan pipeline cache - GraphicsPipelineBuilder(const Device &device, const PipelineCache &pipeline_cache); - - GraphicsPipelineBuilder(const GraphicsPipelineBuilder &) = delete; - GraphicsPipelineBuilder(GraphicsPipelineBuilder &&other) noexcept; - - ~GraphicsPipelineBuilder() = default; - - GraphicsPipelineBuilder &operator=(const GraphicsPipelineBuilder &) = delete; - GraphicsPipelineBuilder &operator=(GraphicsPipelineBuilder &&) = delete; + GraphicsPipelineBuilder(const Device &device); /// Adds a color attachment /// @param format The format of the color attachment - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &add_color_attachment_format(VkFormat format); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &add_color_attachment_format(const VkFormat format) { + // @TODO How does this relate to add_color_blend_attachment? + m_data.color_attachments.push_back(format); + return *this; + } /// Add a color blend attachment /// @param attachment The color blend attachment - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - add_color_blend_attachment(const VkPipelineColorBlendAttachmentState &attachment); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &add_color_blend_attachment(const VkPipelineColorBlendAttachmentState &attachment) { + m_data.color_blend_attachment_states.push_back(attachment); + return *this; + } - /// Add the default color blend attachment - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &add_default_color_blend_attachment(); + /// Add the standard alpha blend attachment + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] GraphicsPipelineBuilder &add_standard_alpha_blend_attachment(); /// Add a push constant range to the graphics pass /// @param shader_stage The shader stage for the push constant range /// @param size The size of the push constant - /// @param offset The offset in the push constant range - /// @return A const reference to the this pointer (allowing method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &add_push_constant_range(VkShaderStageFlags shader_stage, std::uint32_t size, - std::uint32_t offset = 0); + /// @param offset The offset in the push constant range (``0`` by default) + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &add_push_constant_range(const VkShaderStageFlags shader_stage, const std::uint32_t size, + const std::uint32_t offset = 0) { + m_data.push_constant_ranges.emplace_back(VkPushConstantRange{ + .stageFlags = shader_stage, + .offset = offset, + .size = size, + }); + return *this; + } + + /// Add a shader to the graphics pipeline + /// @param shader The shader to add + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &add_shader(std::weak_ptr shader) { + if (shader.expired()) { + throw InexorException("Error: Parameter 'shader' is invalid!"); + } + m_data.shader_stages.emplace_back(tools::make_info({ + .stage = shader.lock()->shader_stage(), + .module = shader.lock()->shader_module(), + .pName = shader.lock()->entry_point().c_str(), + })); + return *this; + } /// Build the graphics pipeline with specified pipeline create flags /// @param name The debug name of the graphics pipeline - /// @return The unique pointer instance of ``GraphicsPipeline`` that was created + /// @TODO Remove this and use only dynamic rendering! + /// @param use_dynamic_rendering + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained [[nodiscard]] std::shared_ptr build(std::string name); /// Set the color blend manually /// @param color_blend The color blend - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_color_blend(const VkPipelineColorBlendStateCreateInfo &color_blend); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_color_blend(const VkPipelineColorBlendStateCreateInfo &color_blend) { + m_data.color_blend_sci = color_blend; + return *this; + } /// Set all color blend attachments manually /// @note You should prefer to use ``add_color_blend_attachment`` instead /// @param attachments The color blend attachments - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_color_blend_attachments(const std::vector &attachments); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto & + set_color_blend_attachments(const std::vector &attachments) { + m_data.color_blend_attachment_states = attachments; + return *this; + } /// Enable or disable culling /// @warning Disabling culling will have a significant performance impact /// @param culling_enabled ``true`` if culling is enabled - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_culling_mode(VkBool32 culling_enabled); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_culling_mode(const VkBool32 culling_enabled) { + if (culling_enabled == VK_FALSE) { + spdlog::warn("Culling is disabled, which could have negative effects on the performance!"); + } + m_data.rasterization_sci.cullMode = culling_enabled == VK_TRUE ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE; + return *this; + } /// Set the deptch attachment format /// @param format The format of the depth attachment - /// @return A const reference to the this pointer (allowing method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_depth_attachment_format(VkFormat format); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_depth_attachment_format(const VkFormat format) { + m_data.depth_attachment_format = format; + return *this; + } /// Set the descriptor set layout /// @param descriptor_set_layouts The descriptor set layout - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_descriptor_set_layouts(std::vector descriptor_set_layouts); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_descriptor_set_layout(const VkDescriptorSetLayout descriptor_set_layout) { + if (!descriptor_set_layout) { + throw InexorException("Error: Parameter 'descriptor_set_layout' is invalid!"); + } + m_data.descriptor_set_layouts = {descriptor_set_layout}; + return *this; + } + + /// Set the descriptor set layouts + /// @param descriptor_set_layouts The descriptor set layout + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_descriptor_set_layouts(std::vector descriptor_set_layouts) { + if (descriptor_set_layouts.empty()) { + throw InexorException("Error: Parameter 'descriptor_set_layouts' is empty!"); + } + m_data.descriptor_set_layouts = std::move(descriptor_set_layouts); + return *this; + } + + /// Associate a descriptor set resource with the graphics pipeline. + /// The pipeline layout will be linked to this descriptor set automatically after build. + [[nodiscard]] auto &add_descriptor_set(std::weak_ptr descriptor_set) { + if (descriptor_set.expired()) { + throw InexorException("Error: Parameter 'descriptor_set' is invalid!"); + } + m_data.associated_descriptor_sets.emplace_back(std::move(descriptor_set)); + return *this; + } /// Set the depth stencil /// @warning Disabling culling can have performance impacts! /// @param depth_stencil The depth stencil - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_depth_stencil(const VkPipelineDepthStencilStateCreateInfo &depth_stencil); - - /// Set the dynamic states - /// @param dynamic_states The dynamic states - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_dynamic_states(const std::vector &dynamic_states); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_depth_stencil(const VkPipelineDepthStencilStateCreateInfo &depth_stencil) { + m_data.depth_stencil_sci = depth_stencil; + return *this; + } + + /// Set the standard depth stencil state + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] GraphicsPipelineBuilder &set_standard_depth_stencil(); + + /// Set scissor state as dynamic + /// @todo Implement multiple dynamic scissors with VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_dynamic_scissor() { + m_data.dynamic_states.push_back(VK_DYNAMIC_STATE_SCISSOR); + m_data.scissors = {}; + m_data.viewport_sci.scissorCount = 1; + m_data.viewport_sci.pScissors = nullptr; + return *this; + } + + /// Set viewport state as dynamic + /// @todo Implement multiple dynamic scissors with VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_dynamic_viewport() { + m_data.dynamic_states.push_back(VK_DYNAMIC_STATE_VIEWPORT); + m_data.viewports = {}; + m_data.viewport_sci.viewportCount = 1; + m_data.viewport_sci.pViewports = nullptr; + return *this; + } /// Set the stencil attachment format /// @param format The format of the stencil attachment - /// @return A const reference to the this pointer (allowing method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_stencil_attachment_format(VkFormat format); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_stencil_attachment_format(const VkFormat format) { + m_data.stencil_attachment_format = format; + return *this; + } /// Set the input assembly state create info /// @note If you just want to set the triangle topology, call ``set_triangle_topology`` instead, because this is the /// most powerful method of this method in case you really need to overwrite it /// @param input_assembly The pipeline input state create info - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_input_assembly(const VkPipelineInputAssemblyStateCreateInfo &input_assembly); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_input_assembly(const VkPipelineInputAssemblyStateCreateInfo &input_assembly) { + m_data.input_assembly_sci = input_assembly; + return *this; + } /// Set the line width of rasterization /// @param line_width The line width used in rasterization - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_line_width(float width); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_line_width(const float width) { + m_data.rasterization_sci.lineWidth = width; + return *this; + } /// Set the most important MSAA settings /// @param sample_count The number of samples used in rasterization - /// @param min_sample_shading A minimum fraction of sample shading - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_multisampling(VkSampleCountFlagBits sample_count, - std::optional min_sample_shading = std::nullopt); + /// @param min_sample_shading A minimum fraction of sample shading (``std::nullopt`` by default) + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_multisampling(const VkSampleCountFlagBits sample_count, + const std::optional min_sample_shading = std::nullopt) { + m_data.multisample_sci.rasterizationSamples = sample_count; + if (min_sample_shading) { + m_data.multisample_sci.minSampleShading = min_sample_shading.value(); + } + return *this; + } /// Store the pipeline layout /// @param layout The pipeline layout - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_pipeline_layout(VkPipelineLayout layout); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_pipeline_layout(const VkPipelineLayout layout) { + if (layout) { + throw InexorException("Error: Parameter 'layout' is invalid!"); + } + m_data.pipeline_layout = layout; + return *this; + } /// Set the triangle topology /// @param topology the primitive topology - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_primitive_topology(VkPrimitiveTopology topology); - - /// Set the renderpass - /// @param render_pass The renderpass - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_render_pass(const VkRenderPass &render_pass); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_primitive_topology(const VkPrimitiveTopology topology) { + m_data.input_assembly_sci.topology = topology; + return *this; + } /// Set the push constant ranges /// @param push_constant_ranges The push constant ranges - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_push_constant_ranges(std::vector push_constant_ranges); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_push_constant_ranges(std::vector push_constant_ranges) { + if (!push_constant_ranges.empty()) { + throw InexorException("Error: Parameter 'push_constant_ranges' is empty!"); + } + m_data.push_constant_ranges = std::move(push_constant_ranges); + return *this; + } /// Set the rasterization state of the graphics pipeline manually /// @param rasterization The rasterization state - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_rasterization(const VkPipelineRasterizationStateCreateInfo &rasterization); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_rasterization(const VkPipelineRasterizationStateCreateInfo &rasterization) { + m_data.rasterization_sci = rasterization; + return *this; + } /// Set the scissor data in VkPipelineViewportStateCreateInfo /// There is another method called set_scissors in case multiple scissors will be used /// @param scissors The scissors in in VkPipelineViewportStateCreateInfo - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_scissor(const VkRect2D &scissor); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_scissor(const VkRect2D &scissor) { + m_data.scissors = {scissor}; + m_data.viewport_sci.scissorCount = 1; + m_data.viewport_sci.pScissors = m_data.scissors.data(); + return *this; + } /// Set the scissor data in VkPipelineViewportStateCreateInfo (convert VkExtent2D to VkRect2D) /// @param extent The extent of the scissor - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_scissor(const VkExtent2D &extent); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_scissor(const VkExtent2D &extent) { + return set_scissor({ + // Convert VkExtent2D to VkRect2D + .extent = extent, + }); + } /// Set the shader modules /// @param shaders The shader stage create infos - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_shaders(std::vector shaders); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_shaders(std::vector shaders) { + if (shaders.empty()) { + throw InexorException("Error: Parameter 'shaders' is empty!"); + } + m_data.shader_stages = std::move(shaders); + return *this; + } /// Set the tesselation control point count /// @note This is not used in the code so far, because we are not using tesselation /// @param control_point_count The tesselation control point count - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_tesselation_control_point_count(std::uint32_t control_point_count); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_tesselation_control_point_count(const std::uint32_t control_point_count) { + m_data.tesselation_sci.patchControlPoints = control_point_count; + return *this; + } /// Set the vertex input attribute descriptions manually /// @note As of C++23, there is no mechanism to do so called reflection in C++, meaning we can't get any information - /// about the members of a struct, which would allow us to determine vertex input attributes automatically. + /// about the members of a struct at runtime, which would allow us to determine vertex input attributes + /// automatically. Reflection was introduced in C++26, and once we switch to a newer C++ standard, we can use it. /// @param descriptions The vertex input attribute descriptions - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_vertex_input_attributes(const std::vector &descriptions); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto & + set_vertex_input_attributes(const std::vector &descriptions) { + if (descriptions.empty()) { + throw InexorException("Error: Parameter 'descriptions' is empty!"); + } + m_data.vertex_input_attribute_descriptions = descriptions; + return *this; + } /// Set the vertex input binding descriptions manually /// @param descriptions The vertex input binding descriptions - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder & - set_vertex_input_bindings(const std::vector &descriptions); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_vertex_input_bindings(const std::vector &descriptions) { + if (descriptions.empty()) { + throw InexorException("Error: Parameter 'descriptions' is empty!"); + } + m_data.vertex_input_binding_descriptions = descriptions; + return *this; + } /// Set the viewport in VkPipelineViewportStateCreateInfo /// There is another method called set_viewports in case multiple viewports will be used /// @param viewport The viewport in VkPipelineViewportStateCreateInfo - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_viewport(const VkViewport &viewport); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_viewport(const VkViewport &viewport) { + m_data.viewports = {viewport}; + m_data.viewport_sci.viewportCount = 1; + m_data.viewport_sci.pViewports = m_data.viewports.data(); + return *this; + } /// Set the viewport in VkPipelineViewportStateCreateInfo (convert VkExtent2D to VkViewport) /// @param extent The extent of the viewport - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_viewport(const VkExtent2D &extent); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_viewport(const VkExtent2D &extent) { + return set_viewport({ + // Convert VkExtent2D to VkViewport + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .maxDepth = 1.0f, + }); + } /// Set the wireframe mode /// @param wireframe ``true`` if wireframe is enabled - /// @return A reference to the dereferenced this pointer (allows method calls to be chained) - [[nodiscard]] GraphicsPipelineBuilder &set_wireframe(VkBool32 wireframe); + /// @return A const reference to the ``this`` pointer, which allows method calls to be chained + [[nodiscard]] auto &set_wireframe(const VkBool32 wireframe) { + m_data.rasterization_sci.polygonMode = (wireframe == VK_TRUE) ? VK_POLYGON_MODE_LINE : VK_POLYGON_MODE_FILL; + return *this; + } }; } // namespace inexor::vulkan_renderer::wrapper::pipelines diff --git a/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp b/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp index ac7cc2c32..c3344c207 100644 --- a/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp +++ b/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp @@ -7,24 +7,27 @@ #include #include +namespace inexor::vulkan_renderer::wrapper { +// Forward declaration +class Device; +} // namespace inexor::vulkan_renderer::wrapper + namespace inexor::vulkan_renderer::wrapper::pipelines { -// Forward declaration -class GraphicsPipeline; +// Using declaration +using wrapper::Device; /// RAII wrapper class for VkPipelineCache /// We use one pipeline cache for all pipelines (no matter what type: graphics or compute) class PipelineCache { private: // We prefer friend declarations over public get methods - friend class GraphicsPipeline; + friend class Device; // The device wrapper const Device &m_device; - // We need to store the file name of the pipeline cache because we will overwrite it on save std::string m_cache_file_name; - /// NOTE: It could be that the pipeline cache is missing (at first start) or invalid for some reason /// (e.g. driver update), in which case this Vulkan handle remains as VK_NULL_HANLDE. VkPipelineCache m_pipeline_cache{VK_NULL_HANDLE}; @@ -35,9 +38,11 @@ class PipelineCache { /// Save the Vulkan pipeline cache to disk void save_cache_data_to_disk(); -public: - // TODO: Define the file name of the pipeline cache by hashing the gpu! Caches are gpu specific! + auto cache() const { + return m_pipeline_cache; + } +public: /// Default constructor /// @param cache_file_name The name of the pipeline cache file to load PipelineCache(const Device &device); diff --git a/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp b/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp index d2d44534a..ae2c9ad6e 100644 --- a/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp +++ b/include/inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp @@ -52,11 +52,15 @@ class PipelineLayout { PipelineLayout(const PipelineLayout &) = delete; PipelineLayout(PipelineLayout &&) noexcept; - PipelineLayout &operator=(const PipelineLayout &) = delete; - PipelineLayout &operator=(PipelineLayout &&other) noexcept; - /// Call vkDestroyPipelineLayout ~PipelineLayout(); + + PipelineLayout &operator=(const PipelineLayout &) = delete; + PipelineLayout &operator=(PipelineLayout &&) noexcept; + + [[nodiscard]] auto pipeline_layout() const { + return m_pipeline_layout; + } }; } // namespace inexor::vulkan_renderer::wrapper::pipelines diff --git a/include/inexor/vulkan-renderer/wrapper/shader.hpp b/include/inexor/vulkan-renderer/wrapper/shader.hpp index 7689e7228..e52117d15 100644 --- a/include/inexor/vulkan-renderer/wrapper/shader.hpp +++ b/include/inexor/vulkan-renderer/wrapper/shader.hpp @@ -10,32 +10,21 @@ namespace inexor::vulkan_renderer::wrapper { // Forward declaration class Device; -/// @brief RAII wrapper class for VkShaderModules. +/// RAII wrapper class for VkShaderModules class Shader { const Device &m_device; std::string m_name; std::string m_entry_point; - VkShaderStageFlagBits m_type; + VkShaderStageFlagBits m_shader_stage; VkShaderModule m_shader_module{VK_NULL_HANDLE}; public: - /// @brief Construct a shader module from a block of SPIR-V memory. + /// Construct a shader module from a SPIR-V file. /// @param device The const reference to a device RAII wrapper instance. - /// @param type The shader type. - /// @param name The internal debug marker name of the VkShaderModule. - /// @param code The memory block of the SPIR-V shader. + /// @param shader_stage The shader type. + /// @param shader_file_name The name of the SPIR-V shader file to load. /// @param entry_point The name of the entry point, "main" by default. - Shader(const Device &m_device, VkShaderStageFlagBits type, const std::string &name, const std::vector &code, - const std::string &entry_point = "main"); - - /// @brief Construct a shader module from a SPIR-V file. - /// This constructor loads the file content and just calls the other constructor. - /// @param device The const reference to a device RAII wrapper instance. - /// @param type The shader type. - /// @param name The internal debug marker name of the VkShaderModule. - /// @param file_name The name of the SPIR-V shader file to load. - /// @param entry_point The name of the entry point, "main" by default. - Shader(const Device &m_device, VkShaderStageFlagBits type, const std::string &name, const std::string &file_name, + Shader(const Device &m_device, VkShaderStageFlagBits shader_stage, const std::string &shader_file_name, const std::string &entry_point = "main"); Shader(const Shader &) = delete; @@ -58,8 +47,8 @@ class Shader { return m_shader_module; } - [[nodiscard]] VkShaderStageFlagBits type() const { - return m_type; + [[nodiscard]] VkShaderStageFlagBits shader_stage() const { + return m_shader_stage; } }; diff --git a/include/inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp b/include/inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp index eeb734211..291d4b63b 100644 --- a/include/inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp +++ b/include/inexor/vulkan-renderer/wrapper/swapchains/swapchain.hpp @@ -1,5 +1,6 @@ #pragma once +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" #include "inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp" #include @@ -9,21 +10,27 @@ #include #include -namespace inexor::vulkan_renderer::wrapper::synchronization { -// Forward declaration -class Semaphore; -} // namespace inexor::vulkan_renderer::wrapper::synchronization - namespace inexor::vulkan_renderer::wrapper { // Forward declaration class Device; } // namespace inexor::vulkan_renderer::wrapper +namespace inexor::vulkan_renderer::wrapper::commands { +// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { +// Forward declaration +class Semaphore; +} // namespace inexor::vulkan_renderer::wrapper::synchronization + namespace inexor::vulkan_renderer::wrapper::swapchains { // Using declaration using synchronization::Semaphore; using wrapper::Device; +using wrapper::commands::CommandBuffer; /// RAII wrapper class for swapchains class Swapchain { @@ -35,47 +42,79 @@ class Swapchain { std::vector m_imgs; std::vector m_img_views; VkExtent2D m_current_extent{}; - std::unique_ptr m_img_available; + std::vector> m_img_available; + std::vector> m_rendering_finished; + std::vector m_imgs_in_flight; + std::vector m_frame_slot_submission_fences; + std::string m_name; bool m_vsync_enabled{false}; + VkFormat m_format{VK_FORMAT_UNDEFINED}; + VkImage m_current_swapchain_img{VK_NULL_HANDLE}; + VkImageView m_current_swapchain_img_view{VK_NULL_HANDLE}; + std::uint32_t m_current_frame_slot{0}; + std::uint32_t m_current_img_index{0}; /// Call vkGetSwapchainImagesKHR /// @exception inexor::vulkan_renderer::VulkanException vkGetSwapchainImagesKHR call failed /// @return A std::vector of swapchain images (this can be empty!) [[nodiscard]] std::vector get_swapchain_images(); + std::uint32_t m_frame_index{0}; + public: /// Default constructor /// @param device The device wrapper + /// @param name The name of the swapchain /// @param surface The surface - /// @param width The swapchain image width - /// @param height The swapchain image height - /// @param vsync_enabled ``true`` if vertical synchronization is enabled - Swapchain(const Device &device, VkSurfaceKHR surface, std::uint32_t width, std::uint32_t height, - bool vsync_enabled); - - Swapchain(const Swapchain &) = delete; - Swapchain(Swapchain &&) noexcept; + Swapchain(const Device &device, std::string name, VkSurfaceKHR surface); ~Swapchain(); - Swapchain &operator=(const Swapchain &) = delete; - Swapchain &operator=(Swapchain &&) = delete; - /// Call vkAcquireNextImageKHR - /// @param timeout (``std::numeric_limits::max()`` by default) - /// @exception VulkanException vkAcquireNextImageKHR call failed - /// @return The index of the next image - [[nodiscard]] std::uint32_t - acquire_next_image_index(std::uint64_t timeout = std::numeric_limits::max()); + /// @exception VulkanException vkAcquireNextImageKHR failed + [[nodiscard]] VkResult acquire_next_image(); + + /// Change the image layout with a pipeline barrier to prepare for rendering + /// @param cmd_buf The command buffer used for recording + void change_image_layout_to_prepare_for_rendering(const CommandBuffer &cmd_buf); + + /// Change the image layout with a pipeline barrier to prepare to call vkQueuePresentKHR + /// @param cmd_buf The command buffer used for recording + void change_image_layout_to_prepare_for_presenting(const CommandBuffer &cmd_buf); + + [[nodiscard]] auto current_swapchain_image_view() const { + return m_current_swapchain_img_view; + } [[nodiscard]] VkExtent2D extent() const { return m_current_extent; } - [[nodiscard]] const VkSemaphore *image_available_semaphore() const { - return m_img_available->semaphore(); + [[nodiscard]] const VkSemaphore image_available_semaphore() const { + return m_img_available[m_current_frame_slot % m_img_available.size()]->semaphore(); + } + + [[nodiscard]] VkSemaphore rendering_finished_semaphore() const { + return m_rendering_finished[m_current_img_index % m_rendering_finished.size()]->semaphore(); + } + + [[nodiscard]] std::uint32_t current_frame_slot() const { + return m_img_available.empty() ? 0u : m_current_frame_slot; } + [[nodiscard]] std::uint32_t frame_slot_count() const { + return static_cast(m_img_available.size()); + } + + /// Wait for the fence associated with the currently acquired swapchain image, if any. + void wait_for_current_image_if_in_flight() const; + + /// Mark the currently acquired swapchain image as owned by the given submission fence. + void mark_current_image_in_flight(VkFence fence); + + /// Mark the current frame slot as using the given submission fence. + void mark_current_frame_slot_in_flight(VkFence fence); + [[nodiscard]] std::uint32_t image_count() const { return static_cast(m_imgs.size()); } @@ -88,10 +127,11 @@ class Swapchain { return m_img_views; } - /// Call vkQueuePresentKHR - /// @param img_index The image index - /// @exception VulkanException vkQueuePresentKHR call failed - void present(std::uint32_t img_index); + [[nodiscard]] const auto &name() const { + return m_name; + } + + void present(std::span wait_semaphores); /// Setup the swapchain /// @param extent The extent of the swapchain. @@ -100,8 +140,8 @@ class Swapchain { /// @exception VulkanException vkGetPhysicalDeviceSurfaceSupportKHR call failed void setup_swapchain(VkExtent2D extent, bool vsync_enabled); - [[nodiscard]] const VkSwapchainKHR *swapchain() const { - return &m_swapchain; + [[nodiscard]] const VkSwapchainKHR swapchain() const { + return m_swapchain; } }; diff --git a/include/inexor/vulkan-renderer/wrapper/synchronization/fence.hpp b/include/inexor/vulkan-renderer/wrapper/synchronization/fence.hpp index e6bd558ef..61dccb7c4 100644 --- a/include/inexor/vulkan-renderer/wrapper/synchronization/fence.hpp +++ b/include/inexor/vulkan-renderer/wrapper/synchronization/fence.hpp @@ -3,7 +3,6 @@ #include #include -#include #include namespace inexor::vulkan_renderer::wrapper { @@ -13,8 +12,9 @@ class Device; namespace inexor::vulkan_renderer::wrapper::synchronization { -/// @brief A RAII wrapper for VkFences. +/// A RAII wrapper for VkFence class Fence { +private: const Device &m_device; std::string m_name; VkFence m_fence{VK_NULL_HANDLE}; @@ -23,9 +23,7 @@ class Fence { /// @brief Default constructor. /// @param device The const reference to a device RAII wrapper instance. /// @param name The internal debug marker name of the VkFence. - /// @param in_signaled_state True if the VkFence will be constructed in signaled state, false otherwise. - /// @warning Make sure to specify in_signaled_state correctly as needed, otherwise synchronization problems occur. - Fence(const Device &device, const std::string &name, bool in_signaled_state); + Fence(const Device &device, const std::string &name); Fence(const Fence &) = delete; Fence(Fence &&) noexcept; @@ -35,16 +33,14 @@ class Fence { Fence &operator=(const Fence &) = delete; Fence &operator=(Fence &&) = delete; - /// @brief Block fence by calling vkWaitForFences and wait until fence condition is fulfilled. - /// @param timeout_limit The time to wait in milliseconds. If no time is specified, the numeric maximum value - /// is used. - void block(std::uint64_t timeout_limit = std::numeric_limits::max()) const; + /// Call vkCmdWaitForFences + void wait() const; [[nodiscard]] auto fence() const { return m_fence; } - /// @brief Call vkResetFences. + /// Call vkResetFences void reset() const; /// Call vkGetFenceStatus diff --git a/include/inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp b/include/inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp new file mode 100644 index 000000000..d4ad6c049 --- /dev/null +++ b/include/inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { +/// Forward declaration +class CommandBuffer; +} // namespace inexor::vulkan_renderer::wrapper::commands + +namespace inexor::vulkan_renderer::wrapper::synchronization { + +/// A builder pattern for Vulkan pipeline barriers. +/// Collects barriers and submits them in a single vkCmdPipelineBarrier2 call on flush. +class PipelineBarrierBatchBuilder { +private: + std::vector m_memory_barriers; + std::vector m_buffer_barriers; + std::vector m_image_barriers; + +public: + PipelineBarrierBatchBuilder() = default; + + template + auto &add(const BarrierType &barrier) { + // Put the barrier into the correct vector depending on its type + using U = std::remove_cvref_t; + if constexpr (std::is_same_v) { + m_memory_barriers.push_back(barrier); + } else if constexpr (std::is_same_v) { + m_buffer_barriers.push_back(barrier); + } else if constexpr (std::is_same_v) { + m_image_barriers.push_back(barrier); + } else { + static_assert(std::is_same_v, "Unsupported Vulkan barrier type"); + } + return *this; + } + + template + auto &add(std::span barriers) { + // Put the barriers into the correct vector depending on its type + using U = std::remove_cv_t; + if constexpr (std::is_same_v) { + m_memory_barriers.insert(m_memory_barriers.end(), barriers.begin(), barriers.end()); + } else if constexpr (std::is_same_v) { + m_buffer_barriers.insert(m_buffer_barriers.end(), barriers.begin(), barriers.end()); + } else if constexpr (std::is_same_v) { + m_image_barriers.insert(m_image_barriers.end(), barriers.begin(), barriers.end()); + } else { + static_assert(std::is_same_v, "Unsupported Vulkan barrier type"); + } + return *this; + } + + [[nodiscard]] bool empty() const; + + /// Flushes only when at least one barrier has been queued. + void flush_if_not_empty(const wrapper::commands::CommandBuffer &cmd_buf); + + /// Flush queued barriers. In debug builds this asserts if called while empty. + void flush(const wrapper::commands::CommandBuffer &cmd_buf); + void reset(); +}; + +} // namespace inexor::vulkan_renderer::wrapper::synchronization diff --git a/include/inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp b/include/inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp index 3eb6c9f05..a147b9350 100644 --- a/include/inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp +++ b/include/inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp @@ -14,8 +14,8 @@ namespace inexor::vulkan_renderer::wrapper::synchronization { /// RAII wrapper class for VkSemaphore class Semaphore { const Device &m_device; - VkSemaphore m_semaphore{VK_NULL_HANDLE}; std::string m_name; + VkSemaphore m_semaphore{VK_NULL_HANDLE}; public: /// Default constructor @@ -32,7 +32,7 @@ class Semaphore { Semaphore &operator=(Semaphore &&) = delete; [[nodiscard]] auto semaphore() const { - return &m_semaphore; + return m_semaphore; } }; diff --git a/include/inexor/vulkan-renderer/wrapper/windows/surface.hpp b/include/inexor/vulkan-renderer/wrapper/windows/surface.hpp index a82bc469d..26fbd4606 100644 --- a/include/inexor/vulkan-renderer/wrapper/windows/surface.hpp +++ b/include/inexor/vulkan-renderer/wrapper/windows/surface.hpp @@ -7,13 +7,13 @@ struct GLFWwindow; namespace inexor::vulkan_renderer::wrapper::windows { -/// @brief RAII wrapper class for VkSurfaceKHR. +/// RAII wrapper class for VkSurfaceKHR class WindowSurface { VkInstance m_instance{VK_NULL_HANDLE}; VkSurfaceKHR m_surface{VK_NULL_HANDLE}; public: - /// @brief Default constructor. + /// Default constructor /// @param instance The Vulkan instance which will be associated with this surface. /// @param window The window which will be associated with this surface. WindowSurface(VkInstance instance, GLFWwindow *window); diff --git a/include/inexor/vulkan-renderer/wrapper/windows/window.hpp b/include/inexor/vulkan-renderer/wrapper/windows/window.hpp index cc4d8a42f..cdd40c39d 100644 --- a/include/inexor/vulkan-renderer/wrapper/windows/window.hpp +++ b/include/inexor/vulkan-renderer/wrapper/windows/window.hpp @@ -8,14 +8,18 @@ namespace inexor::vulkan_renderer::wrapper::windows { -enum class Mode { WINDOWED, FULLSCREEN, WINDOWED_FULLSCREEN }; +enum class WindowMode { + WINDOWED, + FULLSCREEN, + WINDOWED_FULLSCREEN, +}; /// @brief RAII wrapper class for GLFW windows. class Window { private: std::uint32_t m_width; std::uint32_t m_height; - Mode m_mode; + WindowMode m_mode; GLFWwindow *m_window{nullptr}; public: @@ -26,7 +30,7 @@ class Window { /// @param visible True if the window is visible after creation, false otherwise. /// @param resizable True if the window should be resizable, false otherwise. Window(const std::string &title, std::uint32_t width, std::uint32_t height, bool visible, bool resizable, - Mode mode); + WindowMode mode); ~Window(); @@ -41,7 +45,7 @@ class Window { return m_height; } - [[nodiscard]] Mode mode() const { + [[nodiscard]] auto mode() const { return m_mode; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 77a29c2e5..c1718a298 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,62 +1,83 @@ set(INEXOR_SOURCE_FILES - vulkan-renderer/imgui.cpp - vulkan-renderer/render_graph.cpp - vulkan-renderer/input/gamepad_data.cpp vulkan-renderer/input/input.cpp vulkan-renderer/input/keyboard_mouse_data.cpp + vulkan-renderer/octree/serialization/byte_stream.cpp + vulkan-renderer/octree/serialization/nxoc_parser.cpp + + vulkan-renderer/octree/collision_query.cpp + vulkan-renderer/octree/collision.cpp + vulkan-renderer/octree/cube.cpp + vulkan-renderer/octree/indentation.cpp + + vulkan-renderer/render-graph/buffer_copy_batch_builder.cpp + vulkan-renderer/render-graph/buffer.cpp + vulkan-renderer/render-graph/frame_sync_manager.cpp + vulkan-renderer/render-graph/graphics_pass_builder.cpp + vulkan-renderer/render-graph/graphics_pass.cpp + vulkan-renderer/render-graph/render_graph.cpp + vulkan-renderer/render-graph/resource_descriptor_manager.cpp + vulkan-renderer/render-graph/staging_buffer.cpp + vulkan-renderer/render-graph/swapchain_manager.cpp + vulkan-renderer/render-graph/texture_copy_batch_builder.cpp + vulkan-renderer/render-graph/texture.cpp + + vulkan-renderer/render-modules/imgui/imgui_renderer.cpp + + vulkan-renderer/render-modules/octree/octree_renderer.cpp + vulkan-renderer/render-modules/octree/octree_vertex.cpp + + vulkan-renderer/tools/allocators/pool_allocator.cpp + vulkan-renderer/tools/camera.cpp vulkan-renderer/tools/device_info.cpp vulkan-renderer/tools/enumerate.cpp vulkan-renderer/tools/exception.cpp vulkan-renderer/tools/file.cpp vulkan-renderer/tools/fps_limiter.cpp + vulkan-renderer/tools/make_info.cpp vulkan-renderer/tools/queue_selection.cpp vulkan-renderer/tools/random.cpp vulkan-renderer/tools/representation.cpp vulkan-renderer/tools/time_step.cpp - vulkan-renderer/tools/allocators/pool_allocator.cpp - - vulkan-renderer/wrapper/cpu_texture.cpp - vulkan-renderer/wrapper/debug_callback.cpp - vulkan-renderer/wrapper/device.cpp - vulkan-renderer/wrapper/framebuffer.cpp - vulkan-renderer/wrapper/gpu_memory_buffer.cpp - vulkan-renderer/wrapper/gpu_texture.cpp - vulkan-renderer/wrapper/image.cpp - vulkan-renderer/wrapper/instance.cpp - vulkan-renderer/wrapper/make_info.cpp - vulkan-renderer/wrapper/shader.cpp - vulkan-renderer/wrapper/uniform_buffer.cpp - + vulkan-renderer/wrapper/commands/command_buffer_cache.cpp vulkan-renderer/wrapper/commands/command_buffer.cpp vulkan-renderer/wrapper/commands/command_pool.cpp - vulkan-renderer/wrapper/descriptors/descriptor.cpp - vulkan-renderer/wrapper/descriptors/descriptor_builder.cpp + vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.cpp + vulkan-renderer/wrapper/descriptors/descriptor_pool.cpp + vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.cpp + vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.cpp + vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.cpp + vulkan-renderer/wrapper/descriptors/descriptor_set_layout.cpp + vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.cpp + vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.cpp + + vulkan-renderer/wrapper/images/image_view.cpp + vulkan-renderer/wrapper/images/image.cpp + vulkan-renderer/wrapper/images/sampler.cpp - vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.cpp + vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp vulkan-renderer/wrapper/pipelines/pipeline_cache.cpp vulkan-renderer/wrapper/pipelines/pipeline_layout.cpp - vulkan-renderer/wrapper/windows/window.cpp - vulkan-renderer/wrapper/windows/surface.cpp - vulkan-renderer/wrapper/swapchains/swapchain.cpp vulkan-renderer/wrapper/swapchains/swapchain_utils.cpp + vulkan-renderer/wrapper/swapchains/swapchain.cpp vulkan-renderer/wrapper/synchronization/fence.cpp + vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.cpp vulkan-renderer/wrapper/synchronization/semaphore.cpp - vulkan-renderer/octree/collision.cpp - vulkan-renderer/octree/collision_query.cpp - vulkan-renderer/octree/cube.cpp - vulkan-renderer/octree/indentation.cpp + vulkan-renderer/wrapper/windows/surface.cpp + vulkan-renderer/wrapper/windows/window.cpp - vulkan-renderer/octree/serialization/byte_stream.cpp - vulkan-renderer/octree/serialization/nxoc_parser.cpp + vulkan-renderer/wrapper/debug_callback.cpp + vulkan-renderer/wrapper/device.cpp + vulkan-renderer/wrapper/instance.cpp + vulkan-renderer/wrapper/shader.cpp ) foreach(FILE ${INEXOR_SOURCE_FILES}) @@ -156,14 +177,6 @@ target_link_libraries(inexor-vulkan-renderer-core-lib PUBLIC VulkanMemoryAllocator ) -# suppress all compiler warnings for third-party dependencies -foreach(_dep glfw glm imgui tinygltf) - target_compile_options(${_dep} PRIVATE - $<$:-w> - $<$:/W0> - ) -endforeach() - if(MSVC) # group dependencies in Visual Studio set(THIRD_PARTY_TARGETS diff --git a/src/vulkan-renderer/input/input.cpp b/src/vulkan-renderer/input/input.cpp index d7edc1090..c7df9c880 100644 --- a/src/vulkan-renderer/input/input.cpp +++ b/src/vulkan-renderer/input/input.cpp @@ -1,11 +1,29 @@ #include "inexor/vulkan-renderer/input/input.hpp" +#include "inexor/vulkan-renderer/input/gamepad_data.hpp" +#include "inexor/vulkan-renderer/input/keyboard_mouse_data.hpp" + #include namespace inexor::vulkan_renderer::input { +Input::Input() { + m_gamepad_data = std::make_unique(); + m_kbm_data = std::make_unique(); +} + +Input ::~Input() = default; + void Input::cursor_position_callback(GLFWwindow * /*window*/, double x_pos, double y_pos) { - m_kbm_data.set_cursor_pos(x_pos, y_pos); + m_kbm_data->set_cursor_pos(x_pos, y_pos); +} + +GamepadInputData &Input::gamepad_data() { + return *m_gamepad_data; +} + +KeyboardMouseInputData &Input::kbm_data() { + return *m_kbm_data; } void Input::key_callback(GLFWwindow * /*window*/, int key, int /*scancode*/, int action, int /*mods*/) { @@ -15,10 +33,10 @@ void Input::key_callback(GLFWwindow * /*window*/, int key, int /*scancode*/, int switch (action) { case GLFW_PRESS: - m_kbm_data.press_key(key); + m_kbm_data->press_key(key); break; case GLFW_RELEASE: - m_kbm_data.release_key(key); + m_kbm_data->release_key(key); break; default: break; @@ -32,10 +50,10 @@ void Input::mouse_button_callback(GLFWwindow * /*window*/, int button, int actio switch (action) { case GLFW_PRESS: - m_kbm_data.press_mouse_button(button); + m_kbm_data->press_mouse_button(button); break; case GLFW_RELEASE: - m_kbm_data.release_mouse_button(button); + m_kbm_data->release_mouse_button(button); break; default: break; @@ -43,7 +61,7 @@ void Input::mouse_button_callback(GLFWwindow * /*window*/, int button, int actio } void Input::mouse_scroll_callback(GLFWwindow * /*window*/, double /*x_offset*/, double y_offset) { - m_kbm_data.set_mouse_wheel_offset(y_offset); + m_kbm_data->set_mouse_wheel_offset(y_offset); } void Input::update() { @@ -59,14 +77,14 @@ void Input::update_gamepad_data() { if (glfwGetGamepadState(GLFW_JOYSTICK_1, &state) == 1) { for (int i = 0; i < GLFW_GAMEPAD_BUTTON_LAST; i++) { if (state.buttons[i] == 1) { - m_gamepad_data.press_button(i); + m_gamepad_data->press_button(i); } else { - m_gamepad_data.release_button(i); + m_gamepad_data->release_button(i); } } for (int i = 0; i < 2; i++) { - m_gamepad_data.set_joystick_axis(i, state.axes[i]); - m_gamepad_data.set_joystick_axis(i, state.axes[i + 2], 1); + m_gamepad_data->set_joystick_axis(i, state.axes[i]); + m_gamepad_data->set_joystick_axis(i, state.axes[i + 2], 1); } } } diff --git a/src/vulkan-renderer/render-graph/buffer.cpp b/src/vulkan-renderer/render-graph/buffer.cpp new file mode 100644 index 000000000..90915c879 --- /dev/null +++ b/src/vulkan-renderer/render-graph/buffer.cpp @@ -0,0 +1,271 @@ +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" + +#include "inexor/vulkan-renderer/render-graph/staging_buffer.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +Buffer::Buffer(const Device &device, std::string buffer_name, BufferType buffer_type, std::function on_update, + const BufferUpdateMode update_mode) + : m_device(device), m_name(std::move(buffer_name)), m_on_check_for_update(std::move(on_update)), + m_buffer_type(buffer_type), m_update_mode(update_mode) { + if (m_name.empty()) { + throw InexorException("Error: Parameter 'buffer_name' is an empty string!"); + } +} + +Buffer::Buffer(Buffer &&other) noexcept : m_device(other.m_device) { + m_name = std::move(other.m_name); + m_buffer_type = other.m_buffer_type; + m_update_mode = other.m_update_mode; + m_on_check_for_update = std::move(other.m_on_check_for_update); + m_src_data = std::exchange(other.m_src_data, nullptr); + m_src_data_size = other.m_src_data_size; + m_update_requested = other.m_update_requested; + m_descriptor_resource_changed = other.m_descriptor_resource_changed; + m_slots = std::move(other.m_slots); + m_frame_slot_count = other.m_frame_slot_count; + m_current_frame_slot = other.m_current_frame_slot; +} + +Buffer::~Buffer() { + destroy_all(); +} + +bool Buffer::can_update_without_command_buffer() const { + if (m_update_mode != BufferUpdateMode::PER_FRAME_HOST_VISIBLE) { + return false; + } + + for (const auto &slot : m_slots) { + if (slot.m_buffer == VK_NULL_HANDLE || slot.m_alloc_info.pMappedData == nullptr || + slot.m_buffer_capacity < m_src_data_size) { + return false; + } + } + + return true; +} + +void Buffer::update_without_command_buffer() { + auto &slot = current_frame_resources(); + if (const auto result = + vmaCopyMemoryToAllocation(m_device.allocator(), m_src_data, slot.m_alloc, 0, m_src_data_size); + result != VK_SUCCESS) { + throw VulkanException("Error: vmaCopyMemoryToAllocation failed for buffer!", result, m_name); + } + + m_descriptor_resource_changed = false; + m_update_requested = false; + m_src_data = nullptr; + m_src_data_size = 0; +} + +void Buffer::create(std::vector &pending_buffer_copies, StagingBuffer &staging_buffer, + std::size_t &upload_offset, std::vector> &pending_releases) { + if (can_update_without_command_buffer()) { + update_without_command_buffer(); + return; + } + + m_descriptor_resource_changed = false; + + auto &slot = current_frame_resources(); + const auto slot_index = m_current_frame_slot; + create_per_frame_buffer_resources(slot, pending_buffer_copies, staging_buffer, upload_offset, pending_releases, + slot_index); + + m_update_requested = false; + m_src_data = nullptr; + m_src_data_size = 0; +} + +void Buffer::create_per_frame_buffer_resources(PerFrameBufferResources &slot, + std::vector &pending_buffer_copies, + StagingBuffer &staging_buffer, std::size_t &upload_offset, + std::vector> &pending_releases, + const std::size_t slot_index) { + + auto grow_capacity = [](const std::size_t current_capacity, const std::size_t required_capacity) { + if (current_capacity >= required_capacity) { + return current_capacity; + } + auto capacity = current_capacity == 0 ? required_capacity : current_capacity; + while (capacity < required_capacity) { + capacity *= 2; + } + return capacity; + }; + + const std::unordered_map buffer_usage{ + {BufferType::UNIFORM_BUFFER, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, + {BufferType::VERTEX_BUFFER, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT}, + {BufferType::INDEX_BUFFER, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT}, + }; + + const auto slot_name = m_slots.size() > 1 ? m_name + "[slot " + std::to_string(slot_index) + "]" : m_name; + const auto required_buffer_capacity = grow_capacity(slot.m_buffer_capacity, m_src_data_size); + + const bool host_visible_mapped = m_update_mode == BufferUpdateMode::PER_FRAME_HOST_VISIBLE; + + const VmaAllocationCreateInfo alloc_ci{ + .flags = host_visible_mapped + ? static_cast(VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT) + : 0, + .usage = host_visible_mapped ? VMA_MEMORY_USAGE_AUTO : VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, + .priority = + (m_buffer_type == BufferType::INDEX_BUFFER || m_buffer_type == BufferType::VERTEX_BUFFER) ? 0.9f : 0.7f, + }; + + const bool needs_fresh_device_local_buffer = m_update_mode == BufferUpdateMode::DEVICE_LOCAL; + + if (slot.m_buffer == VK_NULL_HANDLE || slot.m_buffer_capacity < m_src_data_size || + needs_fresh_device_local_buffer) { + const auto old_buffer = std::exchange(slot.m_buffer, VK_NULL_HANDLE); + const auto old_alloc = std::exchange(slot.m_alloc, VK_NULL_HANDLE); + slot.m_alloc_info = {}; + slot.m_descriptor_buffer_info = {}; + + const auto buffer_ci = tools::make_info({ + .size = required_buffer_capacity, + .usage = buffer_usage.at(m_buffer_type), + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }); + + if (const auto result = vmaCreateBuffer(m_device.allocator(), &buffer_ci, &alloc_ci, &slot.m_buffer, + &slot.m_alloc, &slot.m_alloc_info); + result != VK_SUCCESS) { + throw VulkanException("Error: vmaCreateBuffer failed!", result, m_name); + } + + vmaSetAllocationName(m_device.allocator(), slot.m_alloc, slot_name.c_str()); + m_device.set_debug_name(slot.m_buffer, slot_name); + slot.m_buffer_capacity = required_buffer_capacity; + m_descriptor_resource_changed = true; + + if (old_buffer != VK_NULL_HANDLE) { + const auto allocator = m_device.allocator(); + pending_releases.push_back( + [allocator, old_buffer, old_alloc] { vmaDestroyBuffer(allocator, old_buffer, old_alloc); }); + } + } + + VkMemoryPropertyFlags mem_prop_flags{}; + vmaGetAllocationMemoryProperties(m_device.allocator(), slot.m_alloc, &mem_prop_flags); + + if (mem_prop_flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) { + if (const auto result = + vmaCopyMemoryToAllocation(m_device.allocator(), m_src_data, slot.m_alloc, 0, m_src_data_size); + result != VK_SUCCESS) { + throw VulkanException("Error: vmaCopyMemoryToAllocation failed for buffer!", result, m_name); + } + } else { + upload_offset = (upload_offset + 15) & ~std::size_t(15); + const auto src_offset = static_cast(upload_offset); + std::memcpy(static_cast(staging_buffer.mapped_data()) + upload_offset, m_src_data, + m_src_data_size); + upload_offset += m_src_data_size; + + const auto [dst_stage_mask, dst_access_mask] = [&]() -> std::pair { + switch (m_buffer_type) { + case BufferType::INDEX_BUFFER: + return {VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT, VK_ACCESS_2_INDEX_READ_BIT}; + case BufferType::VERTEX_BUFFER: + return {VK_PIPELINE_STAGE_2_VERTEX_ATTRIBUTE_INPUT_BIT, VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT}; + case BufferType::UNIFORM_BUFFER: + default: + return {VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + VK_ACCESS_2_UNIFORM_READ_BIT}; + } + }(); + + pending_buffer_copies.push_back({ + .src_buffer = staging_buffer.buffer(), + .dst_buffer = slot.m_buffer, + .region = + { + .srcOffset = src_offset, + .dstOffset = 0, + .size = m_src_data_size, + }, + .dst_stage_mask = dst_stage_mask, + .dst_access_mask = dst_access_mask, + }); + } + + slot.m_descriptor_buffer_info = { + .buffer = slot.m_buffer, + .offset = 0, + .range = m_src_data_size, + }; +} + +void Buffer::destroy_all() { + for (auto &slot : m_slots) { + destroy_per_frame_buffer_resources(slot); + } +} + +void Buffer::destroy_per_frame_buffer_resources(PerFrameBufferResources &slot) { + if (slot.m_buffer == VK_NULL_HANDLE && slot.m_alloc == VK_NULL_HANDLE) { + return; + } + + vmaDestroyBuffer(m_device.allocator(), slot.m_buffer, slot.m_alloc); + slot.m_buffer = VK_NULL_HANDLE; + slot.m_alloc = VK_NULL_HANDLE; + slot.m_alloc_info = {}; + slot.m_buffer_capacity = 0; + slot.m_descriptor_buffer_info = {}; +} + +Buffer::PerFrameBufferResources &Buffer::current_frame_resources() { + return m_slots.at(m_current_frame_slot); +} + +const Buffer::PerFrameBufferResources &Buffer::current_frame_resources() const { + return m_slots.at(m_current_frame_slot); +} + +void Buffer::set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot) { + const auto desired_slot_count = + (m_update_mode == BufferUpdateMode::DEVICE_LOCAL) ? 1u : std::max(1, frame_slot_count); + + if (desired_slot_count < m_slots.size()) { + for (std::size_t slot_index = desired_slot_count; slot_index < m_slots.size(); ++slot_index) { + destroy_per_frame_buffer_resources(m_slots[slot_index]); + } + } + if (m_slots.size() != desired_slot_count) { + m_slots.resize(desired_slot_count); + } + m_frame_slot_count = desired_slot_count; + m_current_frame_slot = std::min(current_frame_slot, m_slots.size() - 1); +} + +void Buffer::request_update(void *src_data, const std::size_t src_data_size) { + if (src_data == nullptr) { + throw std::runtime_error("Error: Parameter 'src_data' is nullptr!"); + } + if (src_data_size == 0) { + throw std::runtime_error("Error: Parameter 'src_data_size' is 0!"); + } + m_src_data = src_data; + m_src_data_size = src_data_size; + m_update_requested = true; +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/buffer_copy_batch_builder.cpp b/src/vulkan-renderer/render-graph/buffer_copy_batch_builder.cpp new file mode 100644 index 000000000..a5e7d56a1 --- /dev/null +++ b/src/vulkan-renderer/render-graph/buffer_copy_batch_builder.cpp @@ -0,0 +1,164 @@ +#include "inexor/vulkan-renderer/render-graph/buffer_copy_batch_builder.hpp" + +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +namespace { + +template +std::size_t hash_handle(T handle) { + static_assert(std::is_trivially_copyable_v); + + std::uintptr_t value = 0; + std::memcpy(&value, &handle, std::min(sizeof(value), sizeof(handle))); + return std::hash{}(value); +} + +[[nodiscard]] bool can_merge_buffer_copy_regions(const VkBufferCopy &lhs, const VkBufferCopy &rhs) { + return lhs.srcOffset + lhs.size == rhs.srcOffset && lhs.dstOffset + lhs.size == rhs.dstOffset; +} + +void coalesce_buffer_copy_regions(std::vector ®ions, std::vector &scratch) { + if (regions.size() < 2) { + return; + } + + std::sort(regions.begin(), regions.end(), [](const VkBufferCopy &lhs, const VkBufferCopy &rhs) { + if (lhs.srcOffset != rhs.srcOffset) { + return lhs.srcOffset < rhs.srcOffset; + } + if (lhs.dstOffset != rhs.dstOffset) { + return lhs.dstOffset < rhs.dstOffset; + } + return lhs.size < rhs.size; + }); + + scratch.clear(); + scratch.reserve(regions.size()); + for (const auto ®ion : regions) { + if (!scratch.empty() && can_merge_buffer_copy_regions(scratch.back(), region)) { + scratch.back().size += region.size; + continue; + } + scratch.push_back(region); + } + + regions = scratch; +} + +} // namespace + +std::size_t BufferCopyBatchBuilder::BatchKeyHash::operator()(const BatchKey &key) const noexcept { + std::size_t seed = hash_handle(key.src_buffer); + seed ^= hash_handle(key.dst_buffer) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return seed; +} + +void BufferCopyBatchBuilder::set_queue_family_ownership_transfer(const bool needs_transfer, + const std::uint32_t transfer_family_index, + const std::uint32_t graphics_family_index) { + m_needs_queue_family_ownership_transfer = needs_transfer; + m_transfer_family_index = needs_transfer ? transfer_family_index : VK_QUEUE_FAMILY_IGNORED; + m_graphics_family_index = needs_transfer ? graphics_family_index : VK_QUEUE_FAMILY_IGNORED; +} + +void BufferCopyBatchBuilder::add(const PendingBufferCopy ©_request) { + auto &batch = m_batches[BatchKey{.src_buffer = copy_request.src_buffer, .dst_buffer = copy_request.dst_buffer}]; + batch.dst_stage_mask |= copy_request.dst_stage_mask; + batch.dst_access_mask |= copy_request.dst_access_mask; + batch.min_offset = std::min(batch.min_offset, copy_request.region.dstOffset); + batch.max_end = std::max(batch.max_end, copy_request.region.dstOffset + copy_request.region.size); + batch.regions.push_back(copy_request.region); +} + +void BufferCopyBatchBuilder::add(std::span copy_requests) { + for (const auto ©_request : copy_requests) { + add(copy_request); + } +} + +bool BufferCopyBatchBuilder::empty() const { + return m_batches.empty(); +} + +void BufferCopyBatchBuilder::flush( + const wrapper::commands::CommandBuffer &cmd_buf, + wrapper::synchronization::PipelineBarrierBatchBuilder &post_copy_barriers, + wrapper::synchronization::PipelineBarrierBatchBuilder &queue_family_acquire_barriers) { + if (empty()) { + return; + } + + for (auto &[key, batch] : m_batches) { + coalesce_buffer_copy_regions(batch.regions, m_scratch_regions); + if (batch.regions.empty()) { + continue; + } + + cmd_buf.copy_buffer(key.src_buffer, key.dst_buffer, batch.regions); + + const auto dst_stage_mask = batch.dst_stage_mask | VK_PIPELINE_STAGE_2_COPY_BIT; + const auto dst_access_mask = batch.dst_access_mask | VK_ACCESS_2_TRANSFER_WRITE_BIT; + if (dst_stage_mask == VK_PIPELINE_STAGE_2_NONE || dst_access_mask == VK_ACCESS_2_NONE) { + continue; + } + + const auto barrier_size = batch.max_end - batch.min_offset; + if (m_needs_queue_family_ownership_transfer) { + post_copy_barriers.add(VkBufferMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_NONE, + .dstAccessMask = VK_ACCESS_2_NONE, + .srcQueueFamilyIndex = m_transfer_family_index, + .dstQueueFamilyIndex = m_graphics_family_index, + .buffer = key.dst_buffer, + .offset = batch.min_offset, + .size = barrier_size, + }); + + queue_family_acquire_barriers.add(VkBufferMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_NONE, + .srcAccessMask = VK_ACCESS_2_NONE, + .dstStageMask = dst_stage_mask, + .dstAccessMask = dst_access_mask, + .srcQueueFamilyIndex = m_transfer_family_index, + .dstQueueFamilyIndex = m_graphics_family_index, + .buffer = key.dst_buffer, + .offset = batch.min_offset, + .size = barrier_size, + }); + } else { + post_copy_barriers.add(VkBufferMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = dst_stage_mask, + .dstAccessMask = dst_access_mask, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = key.dst_buffer, + .offset = batch.min_offset, + .size = barrier_size, + }); + } + } + + reset(); +} + +void BufferCopyBatchBuilder::reset() { + m_batches.clear(); + m_scratch_regions.clear(); +} + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/src/vulkan-renderer/render-graph/frame_sync_manager.cpp b/src/vulkan-renderer/render-graph/frame_sync_manager.cpp new file mode 100644 index 000000000..460c890cc --- /dev/null +++ b/src/vulkan-renderer/render-graph/frame_sync_manager.cpp @@ -0,0 +1,138 @@ +#include "inexor/vulkan-renderer/render-graph/frame_sync_manager.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +FrameSyncManager::FrameSyncManager(const wrapper::Device &device) : m_device(device) {} + +void FrameSyncManager::set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot) { + m_frame_slot_count = std::max(1, frame_slot_count); + m_current_frame_slot = std::min(current_frame_slot, m_frame_slot_count - 1); + + if (m_frame_slot_submission_fences.size() != m_frame_slot_count) { + m_frame_slot_submission_fences.resize(m_frame_slot_count, VK_NULL_HANDLE); + } +} + +void FrameSyncManager::mark_frame_slot_submission_fence(const VkFence fence) { + if (m_frame_slot_submission_fences.size() != m_frame_slot_count) { + m_frame_slot_submission_fences.resize(m_frame_slot_count, VK_NULL_HANDLE); + } + m_frame_slot_submission_fences.at(m_current_frame_slot) = fence; +} + +void FrameSyncManager::defer_release(const std::span fences, std::function release) { + if (!release) { + return; + } + + std::vector filtered_fences; + filtered_fences.reserve(fences.size()); + for (const auto fence : fences) { + if (fence != VK_NULL_HANDLE) { + filtered_fences.push_back(fence); + } + } + + m_deferred_releases.push_back({ + .fences = std::move(filtered_fences), + .release = std::move(release), + }); +} + +void FrameSyncManager::defer_release(const VkFence fence, std::function release) { + defer_release(std::span{&fence, fence == VK_NULL_HANDLE ? 0u : 1u}, std::move(release)); +} + +void FrameSyncManager::process_deferred_releases(const bool wait_all) { + if (m_deferred_releases.empty()) { + return; + } + + std::vector unique_fences; + unique_fences.reserve(m_deferred_releases.size()); + std::unordered_set seen_fences; + seen_fences.reserve(m_deferred_releases.size()); + + for (const auto &entry : m_deferred_releases) { + for (const auto fence : entry.fences) { + if (fence == VK_NULL_HANDLE) { + continue; + } + if (seen_fences.insert(fence).second) { + unique_fences.push_back(fence); + } + } + } + + if (wait_all) { + if (!unique_fences.empty()) { + if (const auto result = + vkWaitForFences(m_device.device(), static_cast(unique_fences.size()), + unique_fences.data(), VK_TRUE, std::numeric_limits::max()); + result != VK_SUCCESS) { + throw VulkanException("Error: vkWaitForFences failed!", result, + "FrameSyncManager::process_deferred_releases"); + } + } + + for (auto &entry : m_deferred_releases) { + entry.release(); + } + m_deferred_releases.clear(); + return; + } + + std::unordered_map fence_ready; + fence_ready.reserve(unique_fences.size()); + for (const auto fence : unique_fences) { + const auto result = vkWaitForFences(m_device.device(), 1, &fence, VK_TRUE, 0); + if (result == VK_SUCCESS) { + fence_ready.emplace(fence, true); + } else if (result == VK_TIMEOUT) { + fence_ready.emplace(fence, false); + } else { + throw VulkanException("Error: vkWaitForFences failed!", result, + "FrameSyncManager::process_deferred_releases"); + } + } + + std::size_t write_index = 0; + for (std::size_t read_index = 0; read_index < m_deferred_releases.size(); ++read_index) { + auto &entry = m_deferred_releases[read_index]; + + bool ready = true; + for (const auto fence : entry.fences) { + const auto it = fence_ready.find(fence); + if (it == fence_ready.end() || !it->second) { + ready = false; + break; + } + } + + if (ready) { + entry.release(); + continue; + } + + m_deferred_releases[write_index++] = std::move(entry); + } + m_deferred_releases.resize(write_index); +} + +void FrameSyncManager::clear() { + m_deferred_releases.clear(); + m_frame_slot_submission_fences.clear(); + m_frame_slot_count = 1; + m_current_frame_slot = 0; +} + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/src/vulkan-renderer/render-graph/graphics_pass.cpp b/src/vulkan-renderer/render-graph/graphics_pass.cpp new file mode 100644 index 000000000..9f3263bc9 --- /dev/null +++ b/src/vulkan-renderer/render-graph/graphics_pass.cpp @@ -0,0 +1,96 @@ +#include "inexor/vulkan-renderer/render-graph/graphics_pass.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" + +#include + +namespace inexor::vulkan_renderer::render_graph { + +// Using declaration +using tools::make_info; +using wrapper::InexorException; + +GraphicsPass::GraphicsPass( + std::string name, std::function on_record_cmd_buffer, + std::vector> buffer_reads, + std::vector, std::optional>> texture_writes, + std::vector, std::optional>> swapchain_writes, + const DebugLabelColor pass_debug_label_color) { + // Pick any extent and store it, they must be all the same at this point + if (!texture_writes.empty()) { + const auto &attachment = texture_writes[0].first.lock(); + m_extent = { + .width = attachment->extent().width, + .height = attachment->extent().height, + }; + } else if (!swapchain_writes.empty()) { + // No color attachments, so pick the extent from any of the swapchains specified + const auto &swapchain = swapchain_writes[0].first.lock(); + m_extent = swapchain->extent(); + } + // Check if either width or height is 0 + if (m_extent.width == 0) { + throw InexorException("Error: m_extent.width is 0!"); + } + if (m_extent.height == 0) { + throw InexorException("Error: m_extent.height is 0!"); + } + + buffer_reads.reserve(buffer_reads.size()); + texture_writes.reserve(texture_writes.size()); + swapchain_writes.reserve(swapchain_writes.size()); + + m_name = std::move(name); + m_on_record_cmd_buffer = std::move(on_record_cmd_buffer); + m_buffer_reads = std::move(buffer_reads); + m_texture_writes = std::move(texture_writes); + m_swapchain_writes = std::move(swapchain_writes); + m_debug_label_color = wrapper::get_debug_label_color(pass_debug_label_color); + + const auto texture_write_count = m_texture_writes.size(); + const auto swapchain_write_count = m_swapchain_writes.size(); + m_cached_texture_attachment_states.reserve(texture_write_count); + m_cached_swapchain_attachment_states.reserve(swapchain_write_count); + m_scratch_current_texture_states.reserve(texture_write_count); + m_scratch_current_swapchain_states.reserve(swapchain_write_count); + m_cached_texture_color_attachment_formats.reserve(texture_write_count); + m_cached_color_attachment_formats.reserve(texture_write_count + swapchain_write_count); + m_color_attachments.reserve(texture_write_count + swapchain_write_count); +} + +GraphicsPass::GraphicsPass(GraphicsPass &&other) noexcept { + m_name = std::move(other.m_name); + m_on_record_cmd_buffer = std::move(other.m_on_record_cmd_buffer); + m_descriptor_set_layout = std::exchange(other.m_descriptor_set_layout, nullptr); + m_descriptor_set = std::exchange(other.m_descriptor_set, VK_NULL_HANDLE); + m_debug_label_color = other.m_debug_label_color; + m_extent = std::move(other.m_extent); + m_buffer_reads = std::move(other.m_buffer_reads); + m_texture_writes = std::move(other.m_texture_writes); + m_swapchain_writes = std::move(other.m_swapchain_writes); + m_rendering_info = std::move(other.m_rendering_info); + m_color_attachments = std::move(other.m_color_attachments); + m_depth_attachment = std::move(other.m_depth_attachment); + m_stencil_attachment = std::move(other.m_stencil_attachment); + m_rendering_info_dirty = other.m_rendering_info_dirty; + m_cached_render_extent = other.m_cached_render_extent; + m_cached_texture_attachment_states = std::move(other.m_cached_texture_attachment_states); + m_cached_swapchain_attachment_states = std::move(other.m_cached_swapchain_attachment_states); + m_cached_texture_color_attachment_formats = std::move(other.m_cached_texture_color_attachment_formats); + m_cached_color_attachment_formats = std::move(other.m_cached_color_attachment_formats); + m_cached_depth_attachment_format = other.m_cached_depth_attachment_format; + m_cached_stencil_attachment_format = other.m_cached_stencil_attachment_format; + m_scratch_current_texture_states = std::move(other.m_scratch_current_texture_states); + m_scratch_current_swapchain_states = std::move(other.m_scratch_current_swapchain_states); +} + +void GraphicsPass::reset_rendering_info() { + m_rendering_info = make_info(); + m_color_attachments.clear(); + m_depth_attachment = std::nullopt; + m_stencil_attachment = std::nullopt; + m_rendering_info_dirty = true; +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/graphics_pass_builder.cpp b/src/vulkan-renderer/render-graph/graphics_pass_builder.cpp new file mode 100644 index 000000000..24e846669 --- /dev/null +++ b/src/vulkan-renderer/render-graph/graphics_pass_builder.cpp @@ -0,0 +1,89 @@ +#include "inexor/vulkan-renderer/render-graph/graphics_pass_builder.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" + +#include + +namespace inexor::vulkan_renderer::render_graph { + +// Using declaration +using wrapper::InexorException; + +GraphicsPassBuilder::GraphicsPassBuilder() { + reset(); +} + +GraphicsPassBuilder::GraphicsPassBuilder(GraphicsPassBuilder &&other) noexcept { + m_on_record_cmd_buffer = std::move(other.m_on_record_cmd_buffer); + m_swapchain_writes = std::move(other.m_swapchain_writes); + m_texture_writes = std::move(other.m_texture_writes); + m_buffer_reads = std::move(other.m_buffer_reads); + m_buffer_writes = std::move(other.m_buffer_writes); +} + +std::shared_ptr GraphicsPassBuilder::build(std::string name, const DebugLabelColor pass_debug_color) { + auto graphics_pass = + std::make_shared(std::move(name), std::move(m_on_record_cmd_buffer), std::move(m_buffer_reads), + std::move(m_texture_writes), std::move(m_swapchain_writes), pass_debug_color); + // NOTE: We could use RAII here to bind the call of reset() to some destructor call like a scope guard pattern does. + reset(); + return graphics_pass; +} + +GraphicsPassBuilder &GraphicsPassBuilder::reads_from(std::weak_ptr buffer) { + if (buffer.expired()) { + throw InexorException("Error: Parameter 'buffer' is an invalid pointer!"); + } + m_buffer_reads.push_back(std::move(buffer)); + return *this; +} + +void GraphicsPassBuilder::reset() { + m_on_record_cmd_buffer = {}; + m_swapchain_writes.clear(); + m_texture_writes.clear(); + m_buffer_reads.clear(); + m_buffer_writes.clear(); +} + +GraphicsPassBuilder & +GraphicsPassBuilder::set_on_record(std::function on_record_cmd_buffer) { + m_on_record_cmd_buffer = std::move(on_record_cmd_buffer); + return *this; +} + +GraphicsPassBuilder &GraphicsPassBuilder::writes_to(std::weak_ptr buffer) { + if (buffer.expired()) { + throw InexorException("Error: Parameter 'buffer' is an invalid pointer!"); + } + m_buffer_writes.emplace_back(std::move(buffer)); + return *this; +} + +GraphicsPassBuilder & +GraphicsPassBuilder::writes_to(std::variant, std::weak_ptr> write_attachment, + std::optional clear_value) { + // Check if this is a std::weak_ptr + if (std::holds_alternative>(write_attachment)) { + // This is a std::weak_ptr, but we need to check if it's a valid pointer + auto &texture = std::get>(write_attachment); + // Check if the std::weak_ptr is still a valid pointer + if (texture.expired()) { + throw InexorException("Error: Parameter 'write_attachment' is an invalid pointer!"); + } + // It's a std::weak_ptr and the memory is valid + m_texture_writes.emplace_back(std::move(texture), std::move(clear_value)); + } else { + // Otherwise, this must be a std::weak_ptr! No need to check with std::holds_alternative explicitely. + auto &swapchain = std::get>(write_attachment); + // Check if the std::weak_ptr is still a valid pointer + if (swapchain.expired()) { + throw InexorException("Error: Parameter 'write_attachment' is an invalid pointer!"); + } + // It's a std::weak_ptr and the memory is valid + m_swapchain_writes.emplace_back(std::move(swapchain), std::move(clear_value)); + } + return *this; +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/render_graph.cpp b/src/vulkan-renderer/render-graph/render_graph.cpp new file mode 100644 index 000000000..a7f23aea3 --- /dev/null +++ b/src/vulkan-renderer/render-graph/render_graph.cpp @@ -0,0 +1,807 @@ +#include "inexor/vulkan-renderer/render-graph/render_graph.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +// Using declaration +using tools::make_info; +using wrapper::descriptors::DescriptorSetLayoutBuilder; +using wrapper::descriptors::DescriptorType; +using wrapper::descriptors::PerFrameDescriptorSets; +using wrapper::descriptors::WriteDescriptorSetBuilder; + +RenderGraph::RenderGraph(Device &device, const bool use_secondary_command_buffers) + : m_device(device), m_resource_descriptors(device), m_graphics_pipeline_builder(device), + m_swapchain_manager(device), m_command_buffer_cache(device, use_secondary_command_buffers), + m_upload_finished(std::make_unique(device, "render_graph_upload_finished")), + m_frame_sync_manager(device), m_staging_buffer(device, "render_graph_upload_arena") {} + +RenderGraph::~RenderGraph() { + try { + spdlog::trace("RenderGraph::~RenderGraph begin"); + m_device.wait_idle(); + m_frame_sync_manager.process_deferred_releases(true); + for (auto &release : m_inline_update_pending_releases) { + release(); + } + m_inline_update_pending_releases.clear(); + m_inline_update_commands = {}; + m_graphics_passes.clear(); + m_buffers.clear(); + m_textures.clear(); + m_resource_descriptors.clear(); + m_graphics_pipeline_create_functions.clear(); + m_swapchain_manager.clear(); + m_pending_queue_ownership_acquire_barriers.reset(); + m_staging_buffer.reset(); + spdlog::trace("RenderGraph::~RenderGraph end"); + } catch (...) {} +} + +void RenderGraph::synchronize_frame_context() { + m_frame_slot_count = m_swapchain_manager.frame_slot_count(); + m_current_frame_slot = m_swapchain_manager.current_frame_slot(); + + m_frame_sync_manager.set_frame_context(m_frame_slot_count, m_current_frame_slot); + m_command_buffer_cache.set_frame_context(m_frame_slot_count, m_current_frame_slot, + m_frame_sync_manager.frame_slot_submission_fences()); + + m_resource_descriptors.set_frame_context(m_frame_slot_count, m_current_frame_slot); + mark_graphics_pass_secondary_cmd_buffers_dirty(); + m_staging_buffer.set_frame_context(m_frame_slot_count, m_current_frame_slot); + m_buffer_copy_batch_builder.reset(); + m_texture_copy_batch_builder.reset(); + + for (const auto &buffer : m_buffers) { + buffer->set_frame_context(m_frame_slot_count, m_current_frame_slot); + } + for (const auto &texture : m_textures) { + texture->set_frame_context(m_frame_slot_count, m_current_frame_slot); + } +} + +std::weak_ptr RenderGraph::add_buffer(std::string name, const BufferType type, std::function on_update, + const BufferUpdateMode update_mode) { + // Create a shared pointer for the new buffer inside of rendergraph and return a weak pointer to external code + // This memory ownership models dictates that buffer are owned by rendergraph and not by external code! + return m_buffers.emplace_back( + std::make_shared(m_device, std::move(name), type, std::move(on_update), update_mode)); +} + +std::weak_ptr RenderGraph::add_graphics_pass(OnBuildGraphicsPass on_build_graphics_pass) { + // Invoke the graphics pipeline create lambda, insert the shared pointer into vector, and return weak pointer + // This memory ownership models dictates that graphics passes are owned by rendergraph and not by external code! + m_swapchain_manager.mark_swapchain_cache_dirty(); + return m_graphics_passes.emplace_back(std::move(on_build_graphics_pass(m_graphics_pass_builder))); +} + +void RenderGraph::add_graphics_pipeline(OnBuildGraphicsPipeline on_build_graphics_pipeline) { + // Store the graphics pipeline create function so it can be invoked later when pipeline layout is known, + // for which we need to know the descriptor set layouts first. Take a moment to realize this ordering requirement. + m_graphics_pipeline_create_functions.emplace_back(std::move(on_build_graphics_pipeline)); +} + +std::weak_ptr RenderGraph::add_resource_descriptor( + std::string name, ResourceDescriptorManager::OnBuildDescriptorSetLayout on_build_descriptor_set_layout, + ResourceDescriptorManager::OnBuildWriteDescriptorSet on_build_write_descriptor_set) { + auto resource = m_resource_descriptors.add_resource_descriptor( + std::move(name), std::move(on_build_descriptor_set_layout), std::move(on_build_write_descriptor_set)); + mark_graphics_pass_secondary_cmd_buffers_dirty(); + return resource; +} + +std::weak_ptr RenderGraph::add_resource_descriptor(std::weak_ptr resource, + const VkShaderStageFlags stage) { + const auto resource_ref = resource.lock(); + if (!resource_ref) { + throw InexorException("Error: Parameter 'resource' is invalid!"); + } + if (resource_ref->type() != BufferType::UNIFORM_BUFFER) { + throw InexorException("Error: Automatic buffer descriptors currently only support uniform buffers!"); + } + + const auto descriptor_name = resource_ref->name(); + auto build_descriptor_set_layout = [stage, descriptor_name](DescriptorSetLayoutBuilder &builder) { + return builder.add(DescriptorType::UNIFORM_BUFFER, stage).build(descriptor_name); + }; + auto build_write_descriptor_set = [resource](WriteDescriptorSetBuilder &builder, + const VkDescriptorSet descriptor_set) { + return builder.add(descriptor_set, resource, 0).build(); + }; + + return add_resource_descriptor(resource_ref->name(), std::move(build_descriptor_set_layout), + std::move(build_write_descriptor_set)); +} + +std::weak_ptr RenderGraph::add_resource_descriptor(std::weak_ptr resource, + const VkShaderStageFlags stage) { + const auto resource_ref = resource.lock(); + if (!resource_ref) { + throw InexorException("Error: Parameter 'resource' is invalid!"); + } + + const auto descriptor_name = resource_ref->name(); + auto build_descriptor_set_layout = [stage, descriptor_name](DescriptorSetLayoutBuilder &builder) { + return builder.add(DescriptorType::COMBINED_IMAGE_SAMPLER, stage).build(descriptor_name); + }; + auto build_write_descriptor_set = [resource](WriteDescriptorSetBuilder &builder, + const VkDescriptorSet descriptor_set) { + return builder.add(descriptor_set, resource, 0).build(); + }; + + return add_resource_descriptor(resource_ref->name(), std::move(build_descriptor_set_layout), + std::move(build_write_descriptor_set)); +} + +std::weak_ptr RenderGraph::add_texture(std::string name, const TextureUsage usage, const VkFormat format, + const std::uint32_t width, const std::uint32_t height, + const std::uint32_t channels, const VkSampleCountFlagBits sample_count, + std::optional> on_update) { + // Create a shared pointer for the new texture inside of rendergraph and return a weak pointer to external code + // This memory ownership models dictates that textures are owned by rendergraph and not by external code! + return m_textures.emplace_back(std::make_shared(m_device, std::move(name), usage, format, width, height, + channels, sample_count, std::move(on_update))); +} + +void RenderGraph::mark_graphics_pass_secondary_cmd_buffers_dirty() { + m_command_buffer_cache.invalidate_all_secondary_command_buffers(); +} + +void RenderGraph::mark_graphics_passes_using_texture_dirty(const Texture &texture) { + for (const auto &pass : m_graphics_passes) { + bool uses_texture = false; + for (const auto &write_attachment : pass->m_texture_writes) { + const auto attachment = write_attachment.first.lock(); + if (attachment && attachment.get() == &texture) { + uses_texture = true; + break; + } + } + + if (uses_texture) { + pass->m_rendering_info_dirty = true; + } + } +} + +void RenderGraph::create_graphics_pipelines() { + m_resource_descriptors.create_descriptor_set_layouts(); + spdlog::trace("Creating {} graphics pipelines", m_graphics_pipeline_create_functions.size()); + for (const auto &create_func : m_graphics_pipeline_create_functions) { + std::invoke(create_func, m_graphics_pipeline_builder); + } + m_resource_descriptors.mark_descriptor_sets_dirty(); +} + +void RenderGraph::check_for_cycles() { + // @TODO Implement! +} + +void RenderGraph::compile() { + check_for_cycles(); + sort_graphics_passes_by_order(); + synchronize_frame_context(); + create_graphics_pipelines(); + mark_graphics_pass_secondary_cmd_buffers_dirty(); +} + +void RenderGraph::rebuild_graphics_pass_texture_rendering_info(GraphicsPass &pass) { + if (!pass.m_rendering_info_dirty) { + return; + } + + auto clear_values_equal = [](const std::optional &lhs, const std::optional &rhs) { + if (lhs.has_value() != rhs.has_value()) { + return false; + } + if (!lhs.has_value()) { + return true; + } + return std::memcmp(&lhs.value(), &rhs.value(), sizeof(VkClearValue)) == 0; + }; + + auto extents_equal = [](const VkExtent2D &lhs, const VkExtent2D &rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height; + }; + + auto get_image_layout = [](const TextureUsage usage) { + switch (usage) { + case TextureUsage::COLOR_ATTACHMENT: + case TextureUsage::DEFAULT: + return VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + case TextureUsage::DEPTH_ATTACHMENT: + case TextureUsage::STENCIL_ATTACHMENT: + return VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + default: + return VK_IMAGE_LAYOUT_UNDEFINED; + } + }; + + auto make_rendering_attachment_info = [&](const VkImageView image_view, const VkImageLayout image_layout, + const std::optional &clear_value) { + return make_info({ + .imageView = image_view, + .imageLayout = image_layout, + .resolveMode = VK_RESOLVE_MODE_NONE, + .resolveImageView = nullptr, + .loadOp = clear_value ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = clear_value.value_or(VkClearValue{}), + }); + }; + + auto track_attachment_usage = [](const TextureUsage usage, std::size_t &color_attachment_count, + bool &has_depth_attachment, bool &has_stencil_attachment) { + switch (usage) { + case TextureUsage::COLOR_ATTACHMENT: + ++color_attachment_count; + break; + case TextureUsage::DEPTH_ATTACHMENT: + has_depth_attachment = true; + break; + case TextureUsage::STENCIL_ATTACHMENT: + has_stencil_attachment = true; + break; + default: + break; + } + }; + + VkExtent2D render_extent{}; + bool render_extent_initialized = false; + auto shrink_render_extent = [&](const VkExtent2D attachment_extent) { + if (!render_extent_initialized) { + render_extent = attachment_extent; + render_extent_initialized = true; + return; + } + render_extent.width = std::min(render_extent.width, attachment_extent.width); + render_extent.height = std::min(render_extent.height, attachment_extent.height); + }; + + // NOTE: These are reused per-pass scratch vectors (cleared and refilled every call) instead of fresh locals, + // to avoid a heap allocation every frame for every pass. + auto ¤t_texture_states = pass.m_scratch_current_texture_states; + current_texture_states.clear(); + current_texture_states.reserve(pass.m_texture_writes.size()); + + auto &cached_texture_color_attachment_formats = pass.m_cached_texture_color_attachment_formats; + cached_texture_color_attachment_formats.clear(); + cached_texture_color_attachment_formats.reserve(pass.m_texture_writes.size()); + pass.m_cached_depth_attachment_format = VK_FORMAT_UNDEFINED; + pass.m_cached_stencil_attachment_format = VK_FORMAT_UNDEFINED; + + std::size_t color_texture_attachment_count = 0; + bool has_depth_attachment = false; + bool has_stencil_attachment = false; + + for (const auto &write_attachment : pass.m_texture_writes) { + const auto attachment = write_attachment.first.lock(); + if (!attachment) { + throw std::runtime_error("Error: Graphics pass texture attachment expired!"); + } + + current_texture_states.push_back({ + .image_view = attachment->image_view(), + .image_layout = get_image_layout(attachment->usage()), + .extent = attachment->extent(), + .clear_value = write_attachment.second, + .usage = attachment->usage(), + }); + track_attachment_usage(attachment->usage(), color_texture_attachment_count, has_depth_attachment, + has_stencil_attachment); + switch (attachment->usage()) { + case TextureUsage::COLOR_ATTACHMENT: + pass.m_cached_texture_color_attachment_formats.push_back(attachment->format()); + break; + case TextureUsage::DEPTH_ATTACHMENT: + pass.m_cached_depth_attachment_format = attachment->format(); + break; + case TextureUsage::STENCIL_ATTACHMENT: + pass.m_cached_stencil_attachment_format = attachment->format(); + break; + default: + break; + } + shrink_render_extent(attachment->extent()); + } + + if (!pass.m_texture_writes.empty() && + (!render_extent_initialized || render_extent.width == 0 || render_extent.height == 0)) { + throw std::runtime_error("Error: Render pass extent is invalid after attachment resize!"); + } + + auto &texture_states = pass.m_cached_texture_attachment_states; + texture_states = current_texture_states; + pass.m_cached_texture_color_attachment_count = color_texture_attachment_count; + pass.m_cached_texture_render_extent = + render_extent_initialized ? std::optional{render_extent} : std::nullopt; + + pass.reset_rendering_info(); + pass.m_color_attachments.clear(); + pass.m_color_attachments.reserve(color_texture_attachment_count + pass.m_swapchain_writes.size()); + + auto build_rendering_attachment_info = [&](const GraphicsPass::CachedAttachmentState &attachment_state) { + return make_rendering_attachment_info(attachment_state.image_view, attachment_state.image_layout, + attachment_state.clear_value); + }; + + for (const auto &attachment_state : texture_states) { + const auto rendering_attachment = build_rendering_attachment_info(attachment_state); + + switch (attachment_state.usage) { + case TextureUsage::COLOR_ATTACHMENT: + pass.m_color_attachments.push_back(rendering_attachment); + break; + case TextureUsage::DEPTH_ATTACHMENT: + pass.m_depth_attachment = rendering_attachment; + break; + case TextureUsage::STENCIL_ATTACHMENT: + pass.m_stencil_attachment = rendering_attachment; + break; + default: + break; + } + } + + pass.m_cached_render_extent = render_extent_initialized ? render_extent : VkExtent2D{0, 0}; + pass.m_rendering_info = make_info({ + .renderArea = + { + .offset = {0, 0}, + .extent = pass.m_cached_render_extent, + }, + .layerCount = 1, + .colorAttachmentCount = static_cast(pass.m_color_attachments.size()), + .pColorAttachments = pass.m_color_attachments.empty() ? nullptr : pass.m_color_attachments.data(), + .pDepthAttachment = pass.m_depth_attachment.has_value() ? &pass.m_depth_attachment.value() : nullptr, + .pStencilAttachment = pass.m_stencil_attachment.has_value() ? &pass.m_stencil_attachment.value() : nullptr, + }); + pass.m_rendering_info_dirty = false; +} + +void RenderGraph::refresh_graphics_pass_swapchain_rendering_info(GraphicsPass &pass) { + if (pass.m_swapchain_writes.empty()) { + return; + } + + auto make_rendering_attachment_info = [](const VkImageView image_view, const VkImageLayout image_layout, + const std::optional &clear_value) { + return make_info({ + .imageView = image_view, + .imageLayout = image_layout, + .resolveMode = VK_RESOLVE_MODE_NONE, + .resolveImageView = nullptr, + .loadOp = clear_value ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = clear_value.value_or(VkClearValue{}), + }); + }; + + auto ¤t_swapchain_states = pass.m_scratch_current_swapchain_states; + current_swapchain_states.clear(); + current_swapchain_states.reserve(pass.m_swapchain_writes.size()); + + VkExtent2D render_extent = pass.m_cached_texture_render_extent.value_or(VkExtent2D{}); + bool render_extent_initialized = pass.m_cached_texture_render_extent.has_value(); + + for (const auto &write_swapchain : pass.m_swapchain_writes) { + const auto swapchain = write_swapchain.first.lock(); + if (!swapchain) { + throw std::runtime_error("Error: Graphics pass swapchain attachment expired!"); + } + + const auto extent = swapchain->extent(); + current_swapchain_states.push_back({ + .image_view = swapchain->current_swapchain_image_view(), + .image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .extent = extent, + .clear_value = write_swapchain.second, + .usage = TextureUsage::COLOR_ATTACHMENT, + }); + + if (!render_extent_initialized) { + render_extent = extent; + render_extent_initialized = true; + } else { + render_extent.width = std::min(render_extent.width, extent.width); + render_extent.height = std::min(render_extent.height, extent.height); + } + } + + if (!render_extent_initialized || render_extent.width == 0 || render_extent.height == 0) { + throw std::runtime_error("Error: Render pass extent is invalid after attachment resize!"); + } + + pass.m_cached_swapchain_attachment_states = current_swapchain_states; + + auto &cached_color_attachment_formats = pass.m_cached_color_attachment_formats; + cached_color_attachment_formats.clear(); + cached_color_attachment_formats.reserve(pass.m_cached_texture_color_attachment_formats.size() + + current_swapchain_states.size()); + cached_color_attachment_formats.insert(cached_color_attachment_formats.end(), + pass.m_cached_texture_color_attachment_formats.begin(), + pass.m_cached_texture_color_attachment_formats.end()); + + pass.m_color_attachments.clear(); + pass.m_color_attachments.reserve(pass.m_cached_texture_color_attachment_count + current_swapchain_states.size()); + for (const auto &attachment_state : current_swapchain_states) { + pass.m_color_attachments.push_back(make_rendering_attachment_info( + attachment_state.image_view, attachment_state.image_layout, attachment_state.clear_value)); + } + + for (const auto &write_swapchain : pass.m_swapchain_writes) { + const auto swapchain = write_swapchain.first.lock(); + if (!swapchain) { + throw std::runtime_error("Error: Graphics pass swapchain attachment expired!"); + } + cached_color_attachment_formats.push_back(swapchain->image_format()); + } + + pass.m_cached_render_extent = render_extent; + pass.m_rendering_info = make_info({ + .renderArea = + { + .offset = {0, 0}, + .extent = render_extent, + }, + .layerCount = 1, + .colorAttachmentCount = static_cast(pass.m_color_attachments.size()), + .pColorAttachments = pass.m_color_attachments.empty() ? nullptr : pass.m_color_attachments.data(), + .pDepthAttachment = pass.m_depth_attachment.has_value() ? &pass.m_depth_attachment.value() : nullptr, + .pStencilAttachment = pass.m_stencil_attachment.has_value() ? &pass.m_stencil_attachment.value() : nullptr, + }); +} + +void RenderGraph::record_command_buffer_for_pass(const CommandBuffer &cmd_buf, GraphicsPass &pass) { + if (pass.m_rendering_info_dirty) { + rebuild_graphics_pass_texture_rendering_info(pass); + } + if (!pass.m_swapchain_writes.empty()) { + refresh_graphics_pass_swapchain_rendering_info(pass); + } + + const auto inheritance_rendering_info = make_info({ + .colorAttachmentCount = static_cast(pass.m_cached_color_attachment_formats.size()), + .pColorAttachmentFormats = + pass.m_cached_color_attachment_formats.empty() ? nullptr : pass.m_cached_color_attachment_formats.data(), + .depthAttachmentFormat = pass.m_cached_depth_attachment_format, + .stencilAttachmentFormat = pass.m_cached_stencil_attachment_format, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + }); + + const auto inheritance_info = make_info({ + .pNext = &inheritance_rendering_info, + }); + + m_command_buffer_cache.record_secondary_command_buffer(cmd_buf, pass.m_name, pass.m_debug_label_color, + pass.m_cached_render_extent, inheritance_info, + pass.m_rendering_info, pass.m_on_record_cmd_buffer); +} + +void RenderGraph::render() { + m_frame_sync_manager.process_deferred_releases(false); + + m_swapchain_manager.collect_frame_swapchains(m_graphics_passes); + + if (!m_swapchain_manager.acquire_next_images()) { + return; + } + + m_swapchain_manager.synchronize_frame_context(); + synchronize_frame_context(); + update_resources(); + + auto &render_wait_semaphores = m_scratch_render_wait_semaphores; + render_wait_semaphores.clear(); + render_wait_semaphores.reserve(m_swapchain_manager.image_available_semaphores().size() + + (m_upload_submission_pending ? 1u : 0u)); + for (const auto semaphore : m_swapchain_manager.image_available_semaphores()) { + render_wait_semaphores.push_back({ + .semaphore = semaphore, + .stage_mask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }); + } + if (m_upload_submission_pending) { + const auto upload_wait_stage_mask = m_upload_wait_stage_mask == VK_PIPELINE_STAGE_2_NONE + ? VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT + : m_upload_wait_stage_mask; + render_wait_semaphores.push_back({ + .semaphore = m_upload_finished->semaphore(), + .stage_mask = upload_wait_stage_mask, + }); + } + + if (m_resource_descriptors.descriptor_sets_dirty()) { + if (m_resource_descriptors.update_write_descriptor_sets()) { + mark_graphics_pass_secondary_cmd_buffers_dirty(); + } + } + + const auto render_submit_fence = m_device.execute( + VK_QUEUE_GRAPHICS_BIT, DebugLabelColor::CYAN, + [&](const CommandBuffer &cmd_buf) { + if (m_inline_update_commands) { + m_inline_update_commands(cmd_buf); + } + + // Acquire ownership of any buffers/images that were uploaded on a transfer queue whose family differs + // from the graphics queue family, before they are read by any pass below. + m_pending_queue_ownership_acquire_barriers.flush_if_not_empty(cmd_buf); + + m_swapchain_manager.prepare_swapchains_for_rendering(cmd_buf); + for (const auto &pass : m_graphics_passes) { + record_command_buffer_for_pass(cmd_buf, *pass); + } + m_swapchain_manager.prepare_swapchains_for_presenting(cmd_buf); + }, + render_wait_semaphores, m_swapchain_manager.rendering_finished_semaphores()); + + m_upload_submission_pending = false; + + if (!m_inline_update_pending_releases.empty()) { + std::vector release_wait_fences = m_frame_sync_manager.frame_slot_submission_fences(); + release_wait_fences.push_back(render_submit_fence); + for (auto &release : m_inline_update_pending_releases) { + m_frame_sync_manager.defer_release(release_wait_fences, std::move(release)); + } + m_inline_update_pending_releases.clear(); + } + m_inline_update_commands = {}; + + m_frame_sync_manager.mark_frame_slot_submission_fence(render_submit_fence); + m_swapchain_manager.mark_frame_swapchains_in_flight(render_submit_fence); + m_swapchain_manager.present(m_swapchain_manager.rendering_finished_semaphores()); +} + +void RenderGraph::reset_graph() { + m_frame_sync_manager.process_deferred_releases(true); + m_staging_buffer.reset(); + m_buffers.clear(); + m_textures.clear(); + m_graphics_passes.clear(); + m_resource_descriptors.clear(); + m_graphics_pipeline_create_functions.clear(); + m_graphics_passes.clear(); + m_swapchain_manager.clear(); + m_upload_submission_pending = false; + m_upload_wait_stage_mask = VK_PIPELINE_STAGE_2_NONE; + m_inline_update_commands = {}; + m_inline_update_pending_releases.clear(); + m_pending_queue_ownership_acquire_barriers.reset(); + m_frame_sync_manager.clear(); + m_frame_slot_count = 1; + m_current_frame_slot = 0; + m_scratch_pending_buffer_copies.clear(); + m_scratch_pending_texture_copies.clear(); + m_buffer_copy_batch_builder.reset(); + m_texture_copy_batch_builder.reset(); + m_scratch_pending_releases.clear(); + m_scratch_color_attachment_formats.clear(); + m_resource_descriptors.mark_descriptor_sets_dirty(); + mark_graphics_pass_secondary_cmd_buffers_dirty(); +} + +void RenderGraph::sort_graphics_passes_by_order() { + // @TODO Implement this in rendergraph3 + // @TODO Implement ordering mechanisms between passes instead of relying on swapchain write order +} + +void RenderGraph::update_resources() { + m_upload_submission_pending = false; + m_upload_wait_stage_mask = VK_PIPELINE_STAGE_2_NONE; + m_inline_update_commands = {}; + m_inline_update_pending_releases.clear(); + + bool any_buffer_update_required = false; + bool any_buffer_gpu_update_required = false; + bool any_buffer_gpu_update_requires_graphics_queue = false; + std::vector pending_direct_buffer_updates; + std::vector pending_gpu_buffer_updates; + pending_direct_buffer_updates.reserve(m_buffers.size()); + pending_gpu_buffer_updates.reserve(m_buffers.size()); + + for (const auto &buffer : m_buffers) { + std::invoke(buffer->m_on_check_for_update); + if (buffer->m_update_requested) { + any_buffer_update_required = true; + if (!buffer->can_update_without_command_buffer()) { + any_buffer_gpu_update_required = true; + pending_gpu_buffer_updates.push_back(buffer.get()); + if (buffer->update_mode() == BufferUpdateMode::PER_FRAME_DEVICE_LOCAL) { + any_buffer_gpu_update_requires_graphics_queue = true; + } + } else { + pending_direct_buffer_updates.push_back(buffer.get()); + } + } + } + + bool any_texture_update_required = false; + // Attachment-type textures (color/depth/stencil) that need their initial layout transition (or, in principle, a + // CPU-data re-upload) use graphics-pipeline-only pipeline stages (color attachment output, fragment tests) which + // are not valid on a transfer-only queue family. Whenever this is the case, we must not route this frame's + // updates through the transfer queue at all (falls back to the same-queue/inline path further below). + bool any_attachment_texture_layout_prep_required = false; + std::vector pending_texture_updates; + pending_texture_updates.reserve(m_textures.size()); + for (const auto &texture : m_textures) { + if (texture->m_on_update) { + std::invoke(texture->m_on_update.value()); + } + if (texture->m_update_requested) { + any_texture_update_required = true; + pending_texture_updates.push_back(texture.get()); + if (texture->usage() != TextureUsage::DEFAULT) { + const bool needs_initial_layout_prep = + !texture->current_frame_resources().m_image || + texture->current_frame_resources().m_image->image() == VK_NULL_HANDLE; + if (needs_initial_layout_prep || texture->m_src_texture_data_size != 0) { + any_attachment_texture_layout_prep_required = true; + } + } + } + } + + if (!any_buffer_update_required && !any_texture_update_required) { + return; + } + + auto &pending_releases = m_scratch_pending_releases; + pending_releases.clear(); + + for (auto *buffer : pending_direct_buffer_updates) { + buffer->update_without_command_buffer(); + } + + for (auto *texture : pending_texture_updates) { + if (!texture->current_frame_resources().m_image || + texture->current_frame_resources().m_image->image() == VK_NULL_HANDLE) { + m_resource_descriptors.mark_descriptor_sets_dirty(); + mark_graphics_passes_using_texture_dirty(*texture); + } + } + + if (!any_buffer_gpu_update_required && !any_texture_update_required) { + return; + } + + std::size_t required_upload_bytes = 0; + constexpr std::size_t upload_alignment = 16; + const auto align_up = [](const std::size_t value, const std::size_t alignment) { + return (value + alignment - 1) & ~(alignment - 1); + }; + + for (const auto *buffer : pending_gpu_buffer_updates) { + for (std::size_t slot_index = 0; slot_index < buffer->m_slots.size(); ++slot_index) { + required_upload_bytes += align_up(buffer->m_src_data_size, upload_alignment); + } + } + + for (const auto *texture : pending_texture_updates) { + if (texture->m_src_texture_data_size > 0) { + for (std::size_t slot_index = 0; slot_index < texture->m_per_frame_texture_resources.size(); ++slot_index) { + required_upload_bytes += align_up(texture->m_src_texture_data_size, upload_alignment); + } + } + } + + m_staging_buffer.ensure_capacity(required_upload_bytes, pending_releases); + + // Keep uploads on the graphics queue so the frame uses one submit path end-to-end. This avoids the extra + // transfer-queue submit and the queue-family ownership transfer overhead, at the cost of giving up async upload + // overlap with rendering. + const bool use_transfer_queue = false && !any_attachment_texture_layout_prep_required; + const bool needs_queue_family_ownership_transfer = + use_transfer_queue && !m_device.transfer_queue_shares_graphics_family(); + const std::uint32_t transfer_family_index = + needs_queue_family_ownership_transfer ? m_device.transfer_queue_family_index() : VK_QUEUE_FAMILY_IGNORED; + const std::uint32_t graphics_family_index = + needs_queue_family_ownership_transfer ? m_device.graphics_queue_family_index() : VK_QUEUE_FAMILY_IGNORED; + + wrapper::synchronization::PipelineBarrierBatchBuilder pre_copy_barriers; + wrapper::synchronization::PipelineBarrierBatchBuilder post_copy_barriers; + m_scratch_pending_buffer_copies.clear(); + m_scratch_pending_texture_copies.clear(); + m_buffer_copy_batch_builder.reset(); + m_texture_copy_batch_builder.reset(); + m_buffer_copy_batch_builder.set_queue_family_ownership_transfer(needs_queue_family_ownership_transfer, + transfer_family_index, graphics_family_index); + m_texture_copy_batch_builder.set_queue_family_ownership_transfer(needs_queue_family_ownership_transfer, + transfer_family_index, graphics_family_index); + std::size_t upload_offset = 0; + + for (auto *buffer : pending_gpu_buffer_updates) { + buffer->create(m_scratch_pending_buffer_copies, m_staging_buffer, upload_offset, pending_releases); + if (buffer->m_descriptor_resource_changed) { + m_resource_descriptors.mark_descriptor_sets_dirty(); + mark_graphics_pass_secondary_cmd_buffers_dirty(); + } + } + + m_buffer_copy_batch_builder.add(m_scratch_pending_buffer_copies); + + for (auto *texture : pending_texture_updates) { + bool texture_was_created = false; + if (!texture->current_frame_resources().m_image || + texture->current_frame_resources().m_image->image() == VK_NULL_HANDLE) { + texture->create_all(); + texture_was_created = true; + } + if (texture_was_created) { + texture->prepare_initial_layout_barriers(pre_copy_barriers); + mark_graphics_passes_using_texture_dirty(*texture); + mark_graphics_pass_secondary_cmd_buffers_dirty(); + } + texture->prepare_update_barriers(pre_copy_barriers); + texture->collect_update_copies(m_staging_buffer, upload_offset, pending_releases, + m_scratch_pending_texture_copies); + } + + m_texture_copy_batch_builder.add(m_scratch_pending_texture_copies); + + VkPipelineStageFlags2 upload_wait_stage_mask = VK_PIPELINE_STAGE_2_NONE; + for (const auto ©_request : m_scratch_pending_buffer_copies) { + upload_wait_stage_mask |= copy_request.dst_stage_mask; + } + for (const auto ©_request : m_scratch_pending_texture_copies) { + upload_wait_stage_mask |= copy_request.post_copy_barrier.dstStageMask; + } + + auto record_update_commands = [this, any_buffer_gpu_update_required, any_texture_update_required, + needs_queue_family_ownership_transfer, transfer_family_index, graphics_family_index, + pre_copy_barriers = std::move(pre_copy_barriers), + post_copy_barriers = + std::move(post_copy_barriers)](const CommandBuffer &cmd_buf) mutable { + // Phase 1: Emit all pre-copy transitions at once. + pre_copy_barriers.flush_if_not_empty(cmd_buf); + + if (any_buffer_gpu_update_required) { + m_buffer_copy_batch_builder.flush(cmd_buf, post_copy_barriers, m_pending_queue_ownership_acquire_barriers); + } + + if (any_texture_update_required) { + m_texture_copy_batch_builder.flush(cmd_buf, post_copy_barriers, m_pending_queue_ownership_acquire_barriers); + } + + // Phase 2: Emit all post-copy visibility/layout barriers at once. + post_copy_barriers.flush_if_not_empty(cmd_buf); + }; + + if (use_transfer_queue) { + const std::array upload_signal_semaphore = {m_upload_finished->semaphore()}; + const auto update_fence = + m_device.execute(VK_QUEUE_TRANSFER_BIT, DebugLabelColor::MAGENTA, record_update_commands, + std::span{}, std::span(upload_signal_semaphore)); + + std::vector release_wait_fences = m_frame_sync_manager.frame_slot_submission_fences(); + release_wait_fences.push_back(update_fence); + + m_upload_submission_pending = true; + m_upload_wait_stage_mask = upload_wait_stage_mask; + if (!pending_releases.empty()) { + for (auto &release : pending_releases) { + m_frame_sync_manager.defer_release(release_wait_fences, std::move(release)); + } + } + return; + } + + // Same-queue path: merge update recording into the main render submission to avoid an extra submit. + m_inline_update_commands = std::move(record_update_commands); + m_inline_update_pending_releases = std::move(pending_releases); +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/resource_descriptor_manager.cpp b/src/vulkan-renderer/render-graph/resource_descriptor_manager.cpp new file mode 100644 index 000000000..c3dba5f41 --- /dev/null +++ b/src/vulkan-renderer/render-graph/resource_descriptor_manager.cpp @@ -0,0 +1,170 @@ +#include "inexor/vulkan-renderer/render-graph/resource_descriptor_manager.hpp" + +#include + +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +ResourceDescriptorManager::ResourceDescriptorManager(wrapper::Device &device) + : m_device(device), m_descriptor_set_layout_builder(device), m_descriptor_set_allocator(device), + m_write_descriptor_set_builder(device) {} + +std::weak_ptr +ResourceDescriptorManager::add_resource_descriptor(std::string name, + OnBuildDescriptorSetLayout on_build_descriptor_set_layout, + OnBuildWriteDescriptorSet on_build_write_descriptor_set) { + auto resource = std::make_shared(); + resource->set_frame_context(m_frame_slot_count, m_current_frame_slot); + + m_resource_descriptors.emplace_back(ResourceDescriptor{ + .name = std::move(name), + .resource = resource, + .on_build_descriptor_set_layout = std::move(on_build_descriptor_set_layout), + .on_build_write_descriptor_sets = std::move(on_build_write_descriptor_set), + }); + + sync_descriptor_resource_dirty_tracking(); + for (auto &slot_dirty : m_descriptor_resource_slot_dirty) { + if (!slot_dirty.empty()) { + slot_dirty.back() = true; + } + } + mark_descriptor_sets_dirty(); + + return resource; +} + +void ResourceDescriptorManager::set_frame_context(const std::size_t frame_slot_count, + const std::size_t current_frame_slot) { + m_frame_slot_count = std::max(1, frame_slot_count); + m_current_frame_slot = std::min(current_frame_slot, m_frame_slot_count - 1); + + if (m_descriptor_sets_slot_dirty.size() != m_frame_slot_count) { + m_descriptor_sets_slot_dirty.resize(m_frame_slot_count, true); + } + + sync_descriptor_resource_dirty_tracking(); + + for (const auto &descriptor : m_resource_descriptors) { + descriptor.resource->set_frame_context(m_frame_slot_count, m_current_frame_slot); + } +} + +void ResourceDescriptorManager::create_descriptor_set_layouts() { + spdlog::trace("Creating {} descriptor set layouts", m_resource_descriptors.size()); + for (auto &descriptor : m_resource_descriptors) { + descriptor.descriptor_set_layout = + std::invoke(descriptor.on_build_descriptor_set_layout, m_descriptor_set_layout_builder); + descriptor.resource->set_layout(descriptor.descriptor_set_layout); + } +} + +void ResourceDescriptorManager::mark_descriptor_sets_dirty() { + if (m_descriptor_sets_slot_dirty.size() != m_frame_slot_count) { + m_descriptor_sets_slot_dirty.assign(m_frame_slot_count, true); + } else { + std::fill(m_descriptor_sets_slot_dirty.begin(), m_descriptor_sets_slot_dirty.end(), true); + } + + sync_descriptor_resource_dirty_tracking(); + for (auto &slot_dirty : m_descriptor_resource_slot_dirty) { + std::fill(slot_dirty.begin(), slot_dirty.end(), true); + } + + m_descriptor_sets_dirty = true; +} +bool ResourceDescriptorManager::update_write_descriptor_sets() { + sync_descriptor_resource_dirty_tracking(); + m_write_descriptor_sets.clear(); + + const auto slot_index = m_current_frame_slot; + auto &slot_resource_dirty = m_descriptor_resource_slot_dirty.at(slot_index); + + std::size_t dirty_descriptor_count = 0; + for (const auto is_dirty : slot_resource_dirty) { + if (is_dirty) { + ++dirty_descriptor_count; + } + } + + spdlog::trace("Updating descriptor sets [slot={} total={} dirty={}]", slot_index, m_resource_descriptors.size(), + dirty_descriptor_count); + + bool any_descriptor_changes = false; + for (std::size_t descriptor_index = 0; descriptor_index < m_resource_descriptors.size(); ++descriptor_index) { + auto &descriptor = m_resource_descriptors[descriptor_index]; + descriptor.resource->set_frame_context(m_frame_slot_count, m_current_frame_slot); + + if (descriptor.resource->descriptor_set(slot_index) == VK_NULL_HANDLE) { + descriptor.resource->set_descriptor_set( + slot_index, + m_descriptor_set_allocator.allocate(descriptor.name + "[slot " + std::to_string(slot_index) + "]", + descriptor.descriptor_set_layout)); + slot_resource_dirty[descriptor_index] = true; + any_descriptor_changes = true; + spdlog::trace("Descriptor set allocated [{}, slot={}]", descriptor.name, slot_index); + } + + if (!slot_resource_dirty[descriptor_index]) { + continue; + } + + auto write_descriptor_sets = + std::invoke(descriptor.on_build_write_descriptor_sets, m_write_descriptor_set_builder, + descriptor.resource->descriptor_set(slot_index)); + + spdlog::trace("Descriptor writes built [{}, slot={}, writes={}]", descriptor.name, slot_index, + write_descriptor_sets.size()); + + std::move(write_descriptor_sets.begin(), write_descriptor_sets.end(), + std::back_inserter(m_write_descriptor_sets)); + + slot_resource_dirty[descriptor_index] = false; + any_descriptor_changes = true; + } + + if (!m_write_descriptor_sets.empty()) { + spdlog::trace("Descriptor writes submitted [slot={} writes={}]", slot_index, m_write_descriptor_sets.size()); + m_device.update_descriptor_sets(m_write_descriptor_sets); + } + + m_descriptor_sets_slot_dirty.at(slot_index) = VK_FALSE; + + m_descriptor_sets_dirty = std::any_of(m_descriptor_sets_slot_dirty.begin(), m_descriptor_sets_slot_dirty.end(), + [](const VkBool32 dirty) { return dirty != VK_FALSE; }); + return any_descriptor_changes; +} + +void ResourceDescriptorManager::clear() { + m_write_descriptor_sets.clear(); + m_resource_descriptors.clear(); + m_descriptor_sets_slot_dirty.clear(); + m_descriptor_resource_slot_dirty.clear(); + m_descriptor_sets_dirty = true; + m_frame_slot_count = 1; + m_current_frame_slot = 0; +} + +void ResourceDescriptorManager::sync_descriptor_resource_dirty_tracking() { + const auto resource_count = m_resource_descriptors.size(); + + if (m_descriptor_resource_slot_dirty.size() != m_frame_slot_count) { + m_descriptor_resource_slot_dirty.assign(m_frame_slot_count, std::vector(resource_count, VK_TRUE)); + return; + } + + for (auto &slot_dirty : m_descriptor_resource_slot_dirty) { + if (slot_dirty.size() < resource_count) { + const auto previous_size = slot_dirty.size(); + slot_dirty.resize(resource_count, VK_TRUE); + std::fill(slot_dirty.begin() + static_cast(previous_size), slot_dirty.end(), VK_TRUE); + } else if (slot_dirty.size() > resource_count) { + slot_dirty.resize(resource_count); + } + } +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/staging_buffer.cpp b/src/vulkan-renderer/render-graph/staging_buffer.cpp new file mode 100644 index 000000000..3a7a60379 --- /dev/null +++ b/src/vulkan-renderer/render-graph/staging_buffer.cpp @@ -0,0 +1,128 @@ +#include "inexor/vulkan-renderer/render-graph/staging_buffer.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +using tools::make_info; + +StagingBuffer::StagingBuffer(const Device &device, std::string name) : m_device(device), m_name(std::move(name)) { + if (m_name.empty()) { + throw InexorException("Error: Parameter 'name' is an empty string!"); + } +} + +StagingBuffer::~StagingBuffer() { + reset(); +} + +void StagingBuffer::ensure_capacity(const std::size_t required_bytes, + std::vector> &pending_releases) { + if (required_bytes == 0) { + return; + } + + auto &resources = current_frame_resources(); + if (resources.m_buffer != VK_NULL_HANDLE && resources.m_capacity >= required_bytes) { + return; + } + + const auto capacity = required_bytes; + + const auto old_buffer = std::exchange(resources.m_buffer, VK_NULL_HANDLE); + const auto old_alloc = std::exchange(resources.m_alloc, VK_NULL_HANDLE); + resources.m_alloc_info = {}; + + const auto buffer_ci = make_info({ + .size = capacity, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }); + + const VmaAllocationCreateInfo alloc_ci{ + .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT, + .usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST, + .priority = 0.1f, + }; + + if (const auto result = vmaCreateBuffer(m_device.allocator(), &buffer_ci, &alloc_ci, &resources.m_buffer, + &resources.m_alloc, &resources.m_alloc_info); + result != VK_SUCCESS) { + throw VulkanException("Error: vmaCreateBuffer failed!", result, m_name); + } + + resources.m_capacity = capacity; + const auto slot_name = m_slots.size() > 1 ? m_name + "[slot " + std::to_string(m_current_frame_slot) + "]" : m_name; + vmaSetAllocationName(m_device.allocator(), resources.m_alloc, slot_name.c_str()); + m_device.set_debug_name(resources.m_buffer, slot_name); + + if (old_buffer != VK_NULL_HANDLE) { + const auto allocator = m_device.allocator(); + pending_releases.push_back( + [allocator, old_buffer, old_alloc] { vmaDestroyBuffer(allocator, old_buffer, old_alloc); }); + } +} + +void StagingBuffer::reset() { + for (auto &resources : m_slots) { + destroy_per_frame_resources(resources); + } +} + +VkBuffer StagingBuffer::buffer() const { + return current_frame_resources().m_buffer; +} + +VmaAllocation StagingBuffer::allocation() const { + return current_frame_resources().m_alloc; +} + +void *StagingBuffer::mapped_data() const { + return current_frame_resources().m_alloc_info.pMappedData; +} + +void StagingBuffer::set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot) { + const auto desired_slot_count = std::max(1, frame_slot_count); + + if (desired_slot_count < m_slots.size()) { + for (std::size_t slot_index = desired_slot_count; slot_index < m_slots.size(); ++slot_index) { + destroy_per_frame_resources(m_slots[slot_index]); + } + } + if (m_slots.size() != desired_slot_count) { + m_slots.resize(desired_slot_count); + } + + m_frame_slot_count = desired_slot_count; + m_current_frame_slot = std::min(current_frame_slot, m_slots.size() - 1); +} + +StagingBuffer::PerFrameStagingBufferResources &StagingBuffer::current_frame_resources() { + return m_slots.at(m_current_frame_slot); +} + +const StagingBuffer::PerFrameStagingBufferResources &StagingBuffer::current_frame_resources() const { + return m_slots.at(m_current_frame_slot); +} + +void StagingBuffer::destroy_per_frame_resources(PerFrameStagingBufferResources &resources) { + if (resources.m_buffer == VK_NULL_HANDLE && resources.m_alloc == VK_NULL_HANDLE) { + return; + } + + vmaDestroyBuffer(m_device.allocator(), resources.m_buffer, resources.m_alloc); + resources.m_buffer = VK_NULL_HANDLE; + resources.m_alloc = VK_NULL_HANDLE; + resources.m_alloc_info = {}; + resources.m_capacity = 0; +} + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/src/vulkan-renderer/render-graph/swapchain_manager.cpp b/src/vulkan-renderer/render-graph/swapchain_manager.cpp new file mode 100644 index 000000000..5fe7a8f1e --- /dev/null +++ b/src/vulkan-renderer/render-graph/swapchain_manager.cpp @@ -0,0 +1,144 @@ +#include "inexor/vulkan-renderer/render-graph/swapchain_manager.hpp" + +#include "inexor/vulkan-renderer/render-graph/graphics_pass.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" + +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +SwapchainManager::SwapchainManager(wrapper::Device &device) : m_device(device) {} + +void SwapchainManager::mark_swapchain_cache_dirty() { + m_swapchain_cache_dirty = true; +} + +void SwapchainManager::clear() { + m_cached_swapchains.clear(); + m_swapchain_cache_dirty = true; + m_frame_swapchains.clear(); + m_swapchains_imgs_available.clear(); + m_swapchain_rendering_finished.clear(); + m_frame_slot_count = 1; + m_current_frame_slot = 0; +} + +void SwapchainManager::rebuild_swapchain_cache(const std::vector> &graphics_passes) { + if (!m_swapchain_cache_dirty) { + return; + } + + m_cached_swapchains.clear(); + + std::unordered_set seen_handles; + seen_handles.reserve(graphics_passes.size()); + + for (const auto &pass : graphics_passes) { + for (const auto &write : pass->m_swapchain_writes) { + const auto swapchain = write.first.lock(); + if (!swapchain) { + continue; + } + + const auto handle = swapchain->swapchain(); + if (!seen_handles.insert(handle).second) { + continue; + } + + m_cached_swapchains.push_back({ + .handle = handle, + .swapchain = swapchain, + }); + } + } + + m_swapchain_cache_dirty = false; +} + +void SwapchainManager::collect_frame_swapchains(const std::vector> &graphics_passes) { + rebuild_swapchain_cache(graphics_passes); + + m_frame_swapchains.clear(); + m_frame_swapchains.reserve(m_cached_swapchains.size()); + for (const auto &entry : m_cached_swapchains) { + if (const auto swapchain = entry.swapchain.lock()) { + m_frame_swapchains.push_back(swapchain); + } + } +} + +bool SwapchainManager::acquire_next_images() { + m_swapchains_imgs_available.clear(); + m_swapchain_rendering_finished.clear(); + m_swapchains_imgs_available.reserve(m_frame_swapchains.size()); + m_swapchain_rendering_finished.reserve(m_frame_swapchains.size()); + + for (const auto &swapchain : m_frame_swapchains) { + const auto result = swapchain->acquire_next_image(); + if (result != VK_SUCCESS) { + m_swapchains_imgs_available.clear(); + m_swapchain_rendering_finished.clear(); + return false; + } + + swapchain->wait_for_current_image_if_in_flight(); + m_swapchains_imgs_available.emplace_back(swapchain->image_available_semaphore()); + m_swapchain_rendering_finished.emplace_back(swapchain->rendering_finished_semaphore()); + } + + return true; +} + +void SwapchainManager::synchronize_frame_context() { + std::size_t frame_slot_count = 1; + std::size_t current_frame_slot = 0; + bool first_swapchain = true; + + for (const auto &swapchain : m_frame_swapchains) { + const auto swapchain_slot_count = static_cast(swapchain->frame_slot_count()); + const auto swapchain_frame_slot = static_cast(swapchain->current_frame_slot()); + + if (first_swapchain) { + frame_slot_count = std::max(1, swapchain_slot_count); + current_frame_slot = swapchain_frame_slot; + first_swapchain = false; + continue; + } + + if (frame_slot_count != swapchain_slot_count || current_frame_slot != swapchain_frame_slot) { + throw std::runtime_error("Error: RenderGraph requires all swapchains to share the same frame-slot state!"); + } + } + + m_frame_slot_count = frame_slot_count; + m_current_frame_slot = std::min(current_frame_slot, m_frame_slot_count - 1); +} + +void SwapchainManager::prepare_swapchains_for_rendering(const CommandBuffer &cmd_buf) const { + for (const auto &swapchain : m_frame_swapchains) { + swapchain->change_image_layout_to_prepare_for_rendering(cmd_buf); + } +} + +void SwapchainManager::prepare_swapchains_for_presenting(const CommandBuffer &cmd_buf) const { + for (const auto &swapchain : m_frame_swapchains) { + swapchain->change_image_layout_to_prepare_for_presenting(cmd_buf); + } +} + +void SwapchainManager::mark_frame_swapchains_in_flight(const VkFence fence) const { + for (const auto &swapchain : m_frame_swapchains) { + swapchain->mark_current_image_in_flight(fence); + swapchain->mark_current_frame_slot_in_flight(fence); + } +} + +void SwapchainManager::present(std::span wait_semaphores) const { + for (const auto &swapchain : m_frame_swapchains) { + swapchain->present(wait_semaphores); + } +} + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/src/vulkan-renderer/render-graph/texture.cpp b/src/vulkan-renderer/render-graph/texture.cpp new file mode 100644 index 000000000..3f53064ee --- /dev/null +++ b/src/vulkan-renderer/render-graph/texture.cpp @@ -0,0 +1,476 @@ +#include "inexor/vulkan-renderer/render-graph/texture.hpp" + +#include "inexor/vulkan-renderer/render-graph/staging_buffer.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" +#include "inexor/vulkan-renderer/wrapper/images/image.hpp" +#include "inexor/vulkan-renderer/wrapper/images/sampler.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" + +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +Texture::Texture(const Device &device, std::string name, const TextureUsage usage, const VkFormat format, + const std::uint32_t width, const std::uint32_t height, const std::uint32_t channels, + const VkSampleCountFlagBits samples, std::optional> on_update) + : m_device(device), m_name(std::move(name)), m_usage(usage), m_on_update(std::move(on_update)), m_format(format), + m_width(width), m_height(height), m_channels(channels), m_samples(samples) { + if (m_name.empty()) { + throw InexorException("Error: Parameter 'name' is an empty string!"); + } + m_default_sampler = std::make_unique(m_device, "Default Sampler"); +} + +Texture::Texture(Texture &&other) noexcept : m_device(other.m_device), m_usage(other.m_usage) { + m_name = std::move(other.m_name); + m_on_update = std::move(other.m_on_update); + m_format = other.m_format; + m_width = other.m_width; + m_height = other.m_height; + m_channels = other.m_channels; + m_samples = other.m_samples; + m_default_sampler = std::exchange(other.m_default_sampler, nullptr); + m_update_requested = other.m_update_requested; + m_src_texture_data = std::exchange(other.m_src_texture_data, nullptr); + m_src_texture_data_size = other.m_src_texture_data_size; + m_per_frame_texture_resources = std::move(other.m_per_frame_texture_resources); + m_frame_slot_count = other.m_frame_slot_count; + m_current_frame_slot = other.m_current_frame_slot; +} + +Texture::~Texture() { + destroy_all(); +} + +void Texture::collect_update_copies(StagingBuffer &staging_buffer, std::size_t &upload_offset, + std::vector> &pending_releases, + std::vector &pending_texture_copies) { + if (m_src_texture_data_size == 0) { + m_update_requested = false; + m_src_texture_data = nullptr; + m_src_texture_data_size = 0; + return; + } + + for (std::size_t slot_index = 0; slot_index < m_per_frame_texture_resources.size(); ++slot_index) { + auto &slot = m_per_frame_texture_resources[slot_index]; + upload_offset = (upload_offset + 15) & ~std::size_t(15); + const auto src_offset = static_cast(upload_offset); + std::memcpy(static_cast(staging_buffer.mapped_data()) + upload_offset, m_src_texture_data, + m_src_texture_data_size); + upload_offset += m_src_texture_data_size; + + const auto aspect_mask = [&]() -> VkImageAspectFlags { + switch (m_usage) { + case TextureUsage::DEPTH_ATTACHMENT: + switch (m_format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + case VK_FORMAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + } + case TextureUsage::STENCIL_ATTACHMENT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_COLOR_BIT); + } + }(); + + pending_texture_copies.push_back({ + .src_buffer = staging_buffer.buffer(), + .dst_image = slot.m_image->image(), + .region = + { + .bufferOffset = src_offset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = + { + .aspectMask = aspect_mask, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {m_width, m_height, 1}, + }, + .post_copy_barrier = + { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = slot.m_image->m_img, + .subresourceRange = + { + .aspectMask = aspect_mask, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }, + }); + + slot.m_descriptor_img_info = { + .sampler = m_default_sampler->sampler(), + .imageView = slot.m_image->m_img_view, + .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + } + + m_update_requested = false; + m_src_texture_data = nullptr; + m_src_texture_data_size = 0; +} + +void Texture::create_all() { + for (std::size_t frame_index = 0; frame_index < m_per_frame_texture_resources.size(); ++frame_index) { + create_per_frame_resources(m_per_frame_texture_resources[frame_index], frame_index); + } +} + +void Texture::create_per_frame_resources(PerFrameTextureResources &frame_resource, const std::size_t slot_index) { + const auto slot_name = + m_per_frame_texture_resources.size() > 1 ? m_name + "[slot " + std::to_string(slot_index) + "]" : m_name; + if (!frame_resource.m_image) { + frame_resource.m_image = std::make_shared(m_device, slot_name); + } + if (m_samples > VK_SAMPLE_COUNT_1_BIT && !frame_resource.m_msaa_image) { + frame_resource.m_msaa_image = std::make_shared(m_device, slot_name + "|msaa"); + } + + auto img_ci = tools::make_info({ + .imageType = VK_IMAGE_TYPE_2D, + .format = m_format, + .extent = + { + .width = m_width, + .height = m_height, + .depth = 1, + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = [&]() -> VkImageUsageFlags { + switch (m_usage) { + case TextureUsage::DEFAULT: + return VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + case TextureUsage::COLOR_ATTACHMENT: + return VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + default: + return VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + } + }(), + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }); + + const auto img_view_ci = tools::make_info({ + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = m_format, + .subresourceRange = + { + .aspectMask = [&]() -> VkImageAspectFlags { + switch (m_usage) { + case TextureUsage::DEPTH_ATTACHMENT: + switch (m_format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + return VK_IMAGE_ASPECT_DEPTH_BIT; + case VK_FORMAT_S8_UINT: + return VK_IMAGE_ASPECT_STENCIL_BIT; + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + default: + return VK_IMAGE_ASPECT_DEPTH_BIT; + } + default: + return VK_IMAGE_ASPECT_COLOR_BIT; + } + }(), + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + + frame_resource.m_image->create(img_ci, img_view_ci); + frame_resource.m_descriptor_img_info = { + .sampler = m_default_sampler->sampler(), + .imageView = frame_resource.m_image->m_img_view, + .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + + if (m_samples > VK_SAMPLE_COUNT_1_BIT && frame_resource.m_msaa_image) { + img_ci.samples = m_samples; + frame_resource.m_msaa_image->create(img_ci, img_view_ci); + } +} + +Texture::PerFrameTextureResources &Texture::current_frame_resources() { + return m_per_frame_texture_resources.at(m_current_frame_slot); +} + +const Texture::PerFrameTextureResources &Texture::current_frame_resources() const { + return m_per_frame_texture_resources.at(m_current_frame_slot); +} + +void Texture::destroy_all() { + for (auto &slot : m_per_frame_texture_resources) { + destroy_per_frame_resources(slot); + } +} + +void Texture::destroy_per_frame_resources(PerFrameTextureResources &resources) { + if (resources.m_image) { + resources.m_image->destroy(); + resources.m_image.reset(); + } + if (resources.m_msaa_image) { + resources.m_msaa_image->destroy(); + resources.m_msaa_image.reset(); + } + resources.m_descriptor_img_info = {}; +} + +VkImageView Texture::image_view() const { + return current_frame_resources().m_image->image_view(); +} + +void Texture::prepare_initial_layout_barriers(PipelineBarrierBatchBuilder &barrier_builder) { + if (m_src_texture_data_size != 0) { + return; + } + + const auto target_layout = [&]() -> VkImageLayout { + switch (m_usage) { + case TextureUsage::COLOR_ATTACHMENT: + return VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + case TextureUsage::DEPTH_ATTACHMENT: + case TextureUsage::STENCIL_ATTACHMENT: + return VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + default: + return VK_IMAGE_LAYOUT_UNDEFINED; + } + }(); + + if (target_layout == VK_IMAGE_LAYOUT_UNDEFINED) { + return; + } + + const auto target_stage_mask = [&]() -> VkPipelineStageFlags2 { + switch (target_layout) { + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + return VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + default: + return VK_PIPELINE_STAGE_2_NONE; + } + }(); + + const auto target_access_mask = [&]() -> VkAccessFlags2 { + switch (target_layout) { + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + return VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + default: + return VK_ACCESS_2_NONE; + } + }(); + + const auto aspect_mask = [&]() -> VkImageAspectFlags { + switch (m_usage) { + case TextureUsage::DEPTH_ATTACHMENT: + switch (m_format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + case VK_FORMAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + } + case TextureUsage::STENCIL_ATTACHMENT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_COLOR_BIT); + } + }(); + + for (const auto &slot : m_per_frame_texture_resources) { + barrier_builder.add(tools::make_info({ + .srcStageMask = VK_PIPELINE_STAGE_2_NONE, + .srcAccessMask = VK_ACCESS_2_NONE, + .dstStageMask = target_stage_mask, + .dstAccessMask = target_access_mask, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = target_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = slot.m_image->m_img, + .subresourceRange = + { + .aspectMask = aspect_mask, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + })); + } +} + +void Texture::prepare_update_barriers(PipelineBarrierBatchBuilder &barrier_builder) { + if (m_src_texture_data_size == 0) { + return; + } + + const auto old_layout = [&]() { + switch (m_usage) { + case TextureUsage::COLOR_ATTACHMENT: + return VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + case TextureUsage::DEPTH_ATTACHMENT: + case TextureUsage::STENCIL_ATTACHMENT: + return VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + default: + return VK_IMAGE_LAYOUT_UNDEFINED; + } + }(); + + const auto src_stage_mask = [&]() -> VkPipelineStageFlags2 { + switch (old_layout) { + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + return VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + default: + return VK_PIPELINE_STAGE_2_NONE; + } + }(); + + const auto src_access_mask = [&]() -> VkAccessFlags2 { + switch (old_layout) { + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + return VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + default: + return VK_ACCESS_2_NONE; + } + }(); + + for (std::size_t slot_index = 0; slot_index < m_per_frame_texture_resources.size(); ++slot_index) { + auto &slot = m_per_frame_texture_resources[slot_index]; + + barrier_builder.add(tools::make_info({ + .srcStageMask = src_stage_mask, + .srcAccessMask = src_access_mask, + .dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .oldLayout = old_layout, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = slot.m_image->m_img, + .subresourceRange = + { + .aspectMask = [&]() -> VkImageAspectFlags { + switch (m_usage) { + case TextureUsage::DEPTH_ATTACHMENT: + switch (m_format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + case VK_FORMAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT | + VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_DEPTH_BIT); + } + case TextureUsage::STENCIL_ATTACHMENT: + return static_cast(VK_IMAGE_ASPECT_STENCIL_BIT); + default: + return static_cast(VK_IMAGE_ASPECT_COLOR_BIT); + } + }(), + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + })); + } +} + +void Texture::request_resize(const std::uint32_t width, const std::uint32_t height) { + if (width == 0 || height == 0 || (width == m_width && height == m_height)) { + return; + } + + m_width = width; + m_height = height; + + destroy_all(); + + m_src_texture_data = nullptr; + m_src_texture_data_size = 0; + m_update_requested = true; +} + +void Texture::request_update(void *src_texture_data, const std::size_t src_texture_data_size) { + if (src_texture_data == nullptr || src_texture_data_size == 0) { + return; + } + m_src_texture_data = src_texture_data; + m_src_texture_data_size = src_texture_data_size; + m_update_requested = true; +} + +void Texture::set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot) { + const auto desired_slot_count = m_usage == TextureUsage::DEFAULT ? std::max(1, frame_slot_count) : 1u; + + if (desired_slot_count < m_per_frame_texture_resources.size()) { + for (std::size_t slot_index = desired_slot_count; slot_index < m_per_frame_texture_resources.size(); + ++slot_index) { + destroy_per_frame_resources(m_per_frame_texture_resources[slot_index]); + } + } + if (m_per_frame_texture_resources.size() != desired_slot_count) { + m_per_frame_texture_resources.resize(desired_slot_count); + } + m_frame_slot_count = desired_slot_count; + m_current_frame_slot = std::min(current_frame_slot, m_per_frame_texture_resources.size() - 1); +} + +} // namespace inexor::vulkan_renderer::render_graph diff --git a/src/vulkan-renderer/render-graph/texture_copy_batch_builder.cpp b/src/vulkan-renderer/render-graph/texture_copy_batch_builder.cpp new file mode 100644 index 000000000..3244c8665 --- /dev/null +++ b/src/vulkan-renderer/render-graph/texture_copy_batch_builder.cpp @@ -0,0 +1,163 @@ +#include "inexor/vulkan-renderer/render-graph/texture_copy_batch_builder.hpp" + +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" + +#include +#include +#include +#include + +namespace inexor::vulkan_renderer::render_graph { + +namespace { + +template +std::size_t hash_handle(T handle) { + static_assert(std::is_trivially_copyable_v); + + std::uintptr_t value = 0; + std::memcpy(&value, &handle, std::min(sizeof(value), sizeof(handle))); + return std::hash{}(value); +} + +[[nodiscard]] bool same_subresource(const VkBufferImageCopy &lhs, const VkBufferImageCopy &rhs) { + return lhs.imageSubresource.aspectMask == rhs.imageSubresource.aspectMask && + lhs.imageSubresource.mipLevel == rhs.imageSubresource.mipLevel && + lhs.imageSubresource.baseArrayLayer == rhs.imageSubresource.baseArrayLayer && + lhs.imageSubresource.layerCount == rhs.imageSubresource.layerCount; +} + +[[nodiscard]] bool same_image_geometry(const VkBufferImageCopy &lhs, const VkBufferImageCopy &rhs) { + return lhs.imageOffset.x == rhs.imageOffset.x && lhs.imageOffset.y == rhs.imageOffset.y && + lhs.imageOffset.z == rhs.imageOffset.z && lhs.imageExtent.width == rhs.imageExtent.width && + lhs.imageExtent.height == rhs.imageExtent.height && lhs.imageExtent.depth == rhs.imageExtent.depth && + lhs.bufferRowLength == rhs.bufferRowLength && lhs.bufferImageHeight == rhs.bufferImageHeight; +} + +void sort_buffer_image_copies(std::vector ®ions) { + std::sort(regions.begin(), regions.end(), [](const VkBufferImageCopy &lhs, const VkBufferImageCopy &rhs) { + if (lhs.bufferOffset != rhs.bufferOffset) { + return lhs.bufferOffset < rhs.bufferOffset; + } + if (lhs.imageOffset.z != rhs.imageOffset.z) { + return lhs.imageOffset.z < rhs.imageOffset.z; + } + if (lhs.imageOffset.y != rhs.imageOffset.y) { + return lhs.imageOffset.y < rhs.imageOffset.y; + } + if (lhs.imageOffset.x != rhs.imageOffset.x) { + return lhs.imageOffset.x < rhs.imageOffset.x; + } + if (lhs.imageExtent.width != rhs.imageExtent.width) { + return lhs.imageExtent.width < rhs.imageExtent.width; + } + if (lhs.imageExtent.height != rhs.imageExtent.height) { + return lhs.imageExtent.height < rhs.imageExtent.height; + } + return lhs.imageExtent.depth < rhs.imageExtent.depth; + }); +} + +} // namespace + +std::size_t TextureCopyBatchBuilder::BatchKeyHash::operator()(const BatchKey &key) const noexcept { + std::size_t seed = hash_handle(key.src_buffer); + seed ^= hash_handle(key.dst_image) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return seed; +} + +void TextureCopyBatchBuilder::set_queue_family_ownership_transfer(const bool needs_transfer, + const std::uint32_t transfer_family_index, + const std::uint32_t graphics_family_index) { + m_needs_queue_family_ownership_transfer = needs_transfer; + m_transfer_family_index = needs_transfer ? transfer_family_index : VK_QUEUE_FAMILY_IGNORED; + m_graphics_family_index = needs_transfer ? graphics_family_index : VK_QUEUE_FAMILY_IGNORED; +} + +void TextureCopyBatchBuilder::add(const PendingTextureCopy ©_request) { + m_batches[BatchKey{.src_buffer = copy_request.src_buffer, .dst_image = copy_request.dst_image}].copies.push_back( + copy_request); +} + +void TextureCopyBatchBuilder::add(std::span copy_requests) { + for (const auto ©_request : copy_requests) { + add(copy_request); + } +} + +bool TextureCopyBatchBuilder::empty() const { + return m_batches.empty(); +} + +void TextureCopyBatchBuilder::flush( + const wrapper::commands::CommandBuffer &cmd_buf, + wrapper::synchronization::PipelineBarrierBatchBuilder &post_copy_barriers, + wrapper::synchronization::PipelineBarrierBatchBuilder &queue_family_acquire_barriers) { + if (empty()) { + return; + } + + for (auto &[key, batch] : m_batches) { + if (batch.copies.empty()) { + continue; + } + + m_scratch_regions.clear(); + m_scratch_regions.reserve(batch.copies.size()); + for (const auto ©_request : batch.copies) { + m_scratch_regions.push_back(copy_request.region); + } + + sort_buffer_image_copies(m_scratch_regions); + + std::vector deduped_regions; + deduped_regions.reserve(m_scratch_regions.size()); + for (const auto ®ion : m_scratch_regions) { + if (!deduped_regions.empty()) { + const auto &prev = deduped_regions.back(); + if (same_subresource(prev, region) && same_image_geometry(prev, region) && + prev.bufferOffset == region.bufferOffset) { + continue; + } + } + deduped_regions.push_back(region); + } + + if (deduped_regions.empty()) { + continue; + } + + cmd_buf.copy_buffer_to_image(key.src_buffer, key.dst_image, deduped_regions); + + for (const auto ©_request : batch.copies) { + if (m_needs_queue_family_ownership_transfer) { + auto release_barrier = copy_request.post_copy_barrier; + release_barrier.srcQueueFamilyIndex = m_transfer_family_index; + release_barrier.dstQueueFamilyIndex = m_graphics_family_index; + release_barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE; + release_barrier.dstAccessMask = VK_ACCESS_2_NONE; + post_copy_barriers.add(release_barrier); + + auto acquire_barrier = copy_request.post_copy_barrier; + acquire_barrier.srcStageMask = VK_PIPELINE_STAGE_2_NONE; + acquire_barrier.srcAccessMask = VK_ACCESS_2_NONE; + acquire_barrier.srcQueueFamilyIndex = m_transfer_family_index; + acquire_barrier.dstQueueFamilyIndex = m_graphics_family_index; + queue_family_acquire_barriers.add(acquire_barrier); + } else { + post_copy_barriers.add(copy_request.post_copy_barrier); + } + } + } + + reset(); +} + +void TextureCopyBatchBuilder::reset() { + m_batches.clear(); + m_scratch_regions.clear(); +} + +} // namespace inexor::vulkan_renderer::render_graph \ No newline at end of file diff --git a/src/vulkan-renderer/render-modules/imgui/imgui_renderer.cpp b/src/vulkan-renderer/render-modules/imgui/imgui_renderer.cpp new file mode 100644 index 000000000..e8567a373 --- /dev/null +++ b/src/vulkan-renderer/render-modules/imgui/imgui_renderer.cpp @@ -0,0 +1,225 @@ +#include "inexor/vulkan-renderer/render-modules/imgui/imgui_renderer.hpp" + +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" +#include "inexor/vulkan-renderer/wrapper/shader.hpp" + +namespace inexor::vulkan_renderer::render_modules::imgui { + +ImGuiRenderer::ImGuiRenderer(std::shared_ptr render_graph, std::weak_ptr swapchain, + std::function on_update_user_imgui_data) + : m_swapchain(swapchain), m_on_update_user_imgui_data(std::move(on_update_user_imgui_data)) { + spdlog::trace("Creating ImGUI context"); + ImGui::CreateContext(); + + ImGuiStyle &style = ImGui::GetStyle(); + style.Colors[ImGuiCol_TitleBg] = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + style.Colors[ImGuiCol_TitleBgActive] = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.0f, 0.0f, 0.0f, 0.1f); + style.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_Header] = ImVec4(0.8f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_HeaderActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_HeaderHovered] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_FrameBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.8f); + style.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); + style.Colors[ImGuiCol_SliderGrab] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); + style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.1f); + style.Colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.2f); + style.Colors[ImGuiCol_Button] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + style.Colors[ImGuiCol_ButtonHovered] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f); + style.Colors[ImGuiCol_ButtonActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); + + ImGuiIO &io = ImGui::GetIO(); + io.FontGlobalScale = 1.0f; + + spdlog::trace("Loading ImGUI shaders"); + m_vertex_shader = + std::make_shared(render_graph->device(), VK_SHADER_STAGE_VERTEX_BIT, "shaders/ui.vert.spv"); + m_fragment_shader = + std::make_shared(render_graph->device(), VK_SHADER_STAGE_FRAGMENT_BIT, "shaders/ui.frag.spv"); + + // Load font texture + + // @TODO Move this data into a container class; have container class also support bold and italic. + constexpr const char *FONT_FILE_PATH = "assets/fonts/NotoSans-Bold.ttf"; + constexpr float FONT_SIZE = 18.0f; + + spdlog::trace("Loading front {}", FONT_FILE_PATH); + + // NOTE: We do not need to free this pointer because the memory is freed by ImGui internally again + // This illustrates another reason why to use smart pointers instead nowadays: memory ownership clarity + ImFont *font = io.Fonts->AddFontFromFileTTF(FONT_FILE_PATH, FONT_SIZE); + io.Fonts->GetTexDataAsRGBA32(&m_font_texture_data, &m_font_texture_width, &m_font_texture_height); + + if (font == nullptr || m_font_texture_data == nullptr) { + spdlog::error("Unable to load font {}. Falling back to error texture", FONT_FILE_PATH); + // @TODO generate error texture for rendergraph! + } else { + spdlog::trace("Creating ImGUI font texture"); + + // Our font textures always have 4 channels and a single mip level by definition. + constexpr int FONT_TEXTURE_CHANNELS{4}; + constexpr int FONT_MIP_LEVELS{1}; + + m_upload_size = static_cast(m_font_texture_width) * + static_cast(m_font_texture_height) * + static_cast(FONT_TEXTURE_CHANNELS); + + m_imgui_texture = render_graph->add_texture( + "ImGui|Texture", render_graph::TextureUsage::DEFAULT, VK_FORMAT_R8G8B8A8_UNORM, m_font_texture_width, + m_font_texture_height, FONT_TEXTURE_CHANNELS, VK_SAMPLE_COUNT_1_BIT, [&]() { + // Make sure the ImGui font texture is only updated once! + if (!m_imgui_font_texture_initialized2) { + m_imgui_texture.lock()->request_update(m_font_texture_data, m_upload_size); + m_imgui_font_texture_initialized2 = true; + } + }); + } + + m_descriptor_set = render_graph->add_resource_descriptor(m_imgui_texture, VK_SHADER_STAGE_FRAGMENT_BIT); + + // ImGui vertex/index data is rewritten in full every frame, so these buffers use per-frame-slot, + // host-visible mapped memory (one backing allocation per frame-in-flight) instead of a single + // device-local buffer. This avoids a staging-buffer copy and transfer barrier every frame, and avoids + // writing into memory the GPU might still be reading from a previous frame. + m_vertex_buffer = render_graph->add_buffer( + "ImGui vertices", BufferType::VERTEX_BUFFER, + [&]() { + ImDrawData *imgui_draw_data = ImGui::GetDrawData(); + if (!imgui_draw_data || imgui_draw_data->TotalVtxCount == 0) { + return; + } + m_vertex_data.clear(); + m_index_data.clear(); + for (std::size_t i = 0; i < imgui_draw_data->CmdListsCount; i++) { + const ImDrawList *cmd_list = imgui_draw_data->CmdLists[i]; + m_vertex_data.insert(m_vertex_data.end(), cmd_list->VtxBuffer.Data, + cmd_list->VtxBuffer.Data + cmd_list->VtxBuffer.Size); + m_index_data.insert(m_index_data.end(), cmd_list->IdxBuffer.Data, + cmd_list->IdxBuffer.Data + cmd_list->IdxBuffer.Size); + } + if (!m_vertex_data.empty()) { + m_vertex_buffer.lock()->request_update(m_vertex_data); + } + }, + render_graph::BufferUpdateMode::PER_FRAME_HOST_VISIBLE); + + m_index_buffer = render_graph->add_buffer( + "ImGui indices", BufferType::INDEX_BUFFER, + [&]() { + if (!m_index_data.empty()) { + m_index_buffer.lock()->request_update(m_index_data); + } + }, + render_graph::BufferUpdateMode::PER_FRAME_HOST_VISIBLE); + + // Add the ImGui graphics pipeline to rendergraph + render_graph->add_graphics_pipeline([&](GraphicsPipelineBuilder &builder) { + const auto swapchain = m_swapchain.lock(); + const auto descriptor_set = m_descriptor_set.lock(); + m_imgui_pipeline = builder + .set_vertex_input_bindings({ + { + .binding = 0, + .stride = sizeof(ImDrawVert), + .inputRate = VK_VERTEX_INPUT_RATE_VERTEX, + }, + }) + .set_vertex_input_attributes({ + { + .location = 0, + .format = VK_FORMAT_R32G32_SFLOAT, + .offset = offsetof(ImDrawVert, pos), + }, + { + .location = 1, + .format = VK_FORMAT_R32G32_SFLOAT, + .offset = offsetof(ImDrawVert, uv), + }, + { + .location = 2, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .offset = offsetof(ImDrawVert, col), + }, + }) + .add_standard_alpha_blend_attachment() + .add_color_attachment_format(swapchain->image_format()) + .set_dynamic_scissor() + .set_dynamic_viewport() + .add_shader(m_vertex_shader) + .add_shader(m_fragment_shader) + .set_descriptor_set_layout(descriptor_set->layout()) + .add_descriptor_set(m_descriptor_set) + .add_push_constant_range(VK_SHADER_STAGE_VERTEX_BIT, sizeof(m_push_const_block)) + .build("ImGui"); + }); + + using render_graph::GraphicsPassBuilder; + + // Add the ImGui graphics pass to rendergraph + m_imgui_pass = render_graph->add_graphics_pass([&](GraphicsPassBuilder &builder) { + return builder.writes_to(swapchain) + .reads_from(m_vertex_buffer) + .reads_from(m_index_buffer) + .set_on_record([&](const CommandBuffer &cmd_buf) { + ImDrawData *draw_data = ImGui::GetDrawData(); + if (draw_data == nullptr || draw_data->TotalVtxCount == 0 || draw_data->TotalIdxCount == 0) { + return; + } + + const auto vertex_buffer = m_vertex_buffer.lock(); + const auto index_buffer = m_index_buffer.lock(); + if (!vertex_buffer || !index_buffer || vertex_buffer->buffer() == VK_NULL_HANDLE || + index_buffer->buffer() == VK_NULL_HANDLE) { + // During resize/rebuild there can be a transient frame where ImGui draw data exists + // but GPU buffers are not uploaded yet. Skip recording in that case. + return; + } + + const ImGuiIO &io = ImGui::GetIO(); + m_push_const_block.scale = glm::vec2(2.0f / io.DisplaySize.x, 2.0f / io.DisplaySize.y); + + cmd_buf.bind_pipeline(m_imgui_pipeline) + .bind_descriptor_set(m_descriptor_set, m_imgui_pipeline) + .push_constant(m_imgui_pipeline, m_push_const_block, VK_SHADER_STAGE_VERTEX_BIT) + .bind_vertex_buffer(m_vertex_buffer) + .bind_index_buffer(m_index_buffer) + .set_viewport({ + .width = ImGui::GetIO().DisplaySize.x, + .height = ImGui::GetIO().DisplaySize.y, + .minDepth = 0.0f, + .maxDepth = 1.0f, + }); + + std::uint32_t index_offset = 0; + std::int32_t vertex_offset = 0; + for (std::size_t i = 0; i < draw_data->CmdListsCount; i++) { + const ImDrawList *cmd_list = draw_data->CmdLists[i]; + for (std::int32_t j = 0; j < cmd_list->CmdBuffer.Size; j++) { + const ImDrawCmd &draw_cmd = cmd_list->CmdBuffer[j]; + cmd_buf + .set_scissor({ + .offset{ + .x = std::max(static_cast(draw_cmd.ClipRect.x), 0), + .y = std::max(static_cast(draw_cmd.ClipRect.y), 0), + }, + .extent{ + .width = static_cast(draw_cmd.ClipRect.z - draw_cmd.ClipRect.x), + .height = static_cast(draw_cmd.ClipRect.w - draw_cmd.ClipRect.y), + }, + }) + .draw_indexed(draw_cmd.ElemCount, 1, index_offset, vertex_offset); + index_offset += draw_cmd.ElemCount; + } + vertex_offset += cmd_list->VtxBuffer.Size; + } + }) + .build("ImGui", DebugLabelColor::BLUE); + }); +} + +ImGuiRenderer::~ImGuiRenderer() { + ImGui::DestroyContext(); +} + +} // namespace inexor::vulkan_renderer::render_modules::imgui diff --git a/src/vulkan-renderer/render-modules/octree/octree_renderer.cpp b/src/vulkan-renderer/render-modules/octree/octree_renderer.cpp new file mode 100644 index 000000000..47f8e195a --- /dev/null +++ b/src/vulkan-renderer/render-modules/octree/octree_renderer.cpp @@ -0,0 +1,182 @@ +#include "inexor/vulkan-renderer/render-modules/octree/octree_renderer.hpp" + +#include "inexor/vulkan-renderer/render-graph/buffer.hpp" +#include "inexor/vulkan-renderer/render-graph/render_graph.hpp" +#include "inexor/vulkan-renderer/render-graph/texture.hpp" +#include "inexor/vulkan-renderer/tools/camera.hpp" +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp" +#include "inexor/vulkan-renderer/wrapper/shader.hpp" + +#include +#include +#include + +#include +#include + +namespace inexor::vulkan_renderer::render_modules::octree { + +OctreeRenderer::OctreeRenderer(std::shared_ptr render_graph, std::weak_ptr swapchain, + std::weak_ptr depth_buffer, std::shared_ptr camera) + : m_swapchain(std::move(swapchain)), m_depth_buffer(std::move(depth_buffer)), m_camera(std::move(camera)) { + // Using declarations + using render_graph::BufferType; + using render_graph::GraphicsPassBuilder; + using tools::InexorException; + using tools::make_info; + using wrapper::DebugLabelColor; + using wrapper::pipelines::GraphicsPipelineBuilder; + + if (!render_graph) { + throw InexorException("Error: Parameter 'render_graph' is invalid!"); + } + if (m_swapchain.expired()) { + throw InexorException("Error: Parameter 'swapchain' is invalid!"); + } + if (m_depth_buffer.expired()) { + throw InexorException("Error: Parameter 'depth_buffer' is invalid!"); + } + if (m_camera.expired()) { + throw InexorException("Error: Parameter 'camera' is invalid!"); + } + + // @TODO Maybe it is a good idea to let rendergraph load shaders and to abstract shader loading in it? + + // Load vertex and fragment shader for octree rendering + // @TODO Use spirv-cross to load shaders and determine type automatically + m_vertex_shader = + std::make_shared(render_graph->device(), VK_SHADER_STAGE_VERTEX_BIT, "shaders/main.vert.spv"); + m_fragment_shader = + std::make_shared(render_graph->device(), VK_SHADER_STAGE_FRAGMENT_BIT, "shaders/main.frag.spv"); + + m_mvp_matrix = render_graph->add_buffer( + "model/view/proj", BufferType::UNIFORM_BUFFER, + [&]() { + // @TODO Restrict external code from access to request_update method by implementing something like a + // BufferUpdateBuilder, which is given to the lambda as a parameter. Maybe this can even help us to simplify + // the pipeline barrier placement! + m_ubo.model = glm::mat4(1.0f); + m_ubo.view = m_camera.lock()->view_matrix(); + m_ubo.proj = m_camera.lock()->perspective_matrix(); + m_ubo.proj[1][1] *= -1; + m_mvp_matrix.lock()->request_update(m_ubo); + }, + render_graph::BufferUpdateMode::PER_FRAME_HOST_VISIBLE); + + // Descriptor management for the model/view/projection uniform buffer + m_descriptor_set = render_graph->add_resource_descriptor(m_mvp_matrix, VK_SHADER_STAGE_VERTEX_BIT); + + m_vertex_buffer = render_graph->add_buffer("vertex buffer", BufferType::VERTEX_BUFFER, [&]() { + // Upload once after render-graph/swapchain recreation as well, because the new buffer starts null. + const auto vertex_buffer = m_vertex_buffer.lock(); + const bool needs_initial_upload = vertex_buffer && vertex_buffer->buffer() == VK_NULL_HANDLE; + if ((m_geometry_updated || needs_initial_upload) && !m_octree_vertices.empty()) { + vertex_buffer->request_update(m_octree_vertices); + } + }); + + m_index_buffer = render_graph->add_buffer("index buffer", BufferType::INDEX_BUFFER, [&]() { + // Upload once after render-graph/swapchain recreation as well, because the new buffer starts null. + const auto index_buffer = m_index_buffer.lock(); + const bool needs_initial_upload = index_buffer && index_buffer->buffer() == VK_NULL_HANDLE; + if ((m_geometry_updated || needs_initial_upload) && !m_octree_indices.empty()) { + index_buffer->request_update(m_octree_indices); + // Reset after index update has been requested so vertex+index uploads stay in sync. + m_geometry_updated = false; + } + }); + + // Add the graphics pipeline for the octree renderer + render_graph->add_graphics_pipeline([&](GraphicsPipelineBuilder &builder) { + const auto pipeline_extent = m_swapchain.lock()->extent(); + const auto descriptor_set = m_descriptor_set.lock(); + + // The octree graphics pipeline is stored in the octree renderer + // It is being build in this lambda by reference capture + m_octree_pipeline = builder.add_shader(m_vertex_shader) + .add_shader(m_fragment_shader) + .set_vertex_input_bindings({{ + .binding = 0, + .stride = sizeof(OctreeVertex), + .inputRate = VK_VERTEX_INPUT_RATE_VERTEX, + }}) + .set_vertex_input_attributes({ + { + .location = 0, + .format = VK_FORMAT_R32G32B32_SFLOAT, + .offset = offsetof(OctreeVertex, position), + }, + { + .location = 1, + .format = VK_FORMAT_R32G32B32_SFLOAT, + .offset = offsetof(OctreeVertex, color), + }, + }) + .add_standard_alpha_blend_attachment() + .set_depth_attachment_format(m_depth_buffer.lock()->format()) + .set_standard_depth_stencil() + .add_color_attachment_format(m_swapchain.lock()->image_format()) + .set_dynamic_scissor() + .set_dynamic_viewport() + .set_viewport({ + .width = static_cast(pipeline_extent.width), + .height = static_cast(pipeline_extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }) + .set_scissor({ + .extent = pipeline_extent, + }) + .set_descriptor_set_layout(descriptor_set->layout()) + .add_descriptor_set(m_descriptor_set) + .build("Octree"); + }); + + // Add the graphics pass for the octree renderer + m_octree_pass = render_graph->add_graphics_pass([&](GraphicsPassBuilder &builder) { + return builder.writes_to(m_swapchain, VkClearValue{0.0f, 0.0f, 0.0f}) + .writes_to(m_depth_buffer, VkClearValue{.depthStencil = {.depth = 1.0f, .stencil = 0}}) + .reads_from(m_vertex_buffer) + .writes_to(m_index_buffer) + .reads_from(m_index_buffer) + .set_on_record([&](const CommandBuffer &cmd_buf) { + const auto vertex_buffer = m_vertex_buffer.lock(); + const auto index_buffer = m_index_buffer.lock(); + const auto swapchain = m_swapchain.lock(); + if (!vertex_buffer || !index_buffer || vertex_buffer->buffer() == VK_NULL_HANDLE || + index_buffer->buffer() == VK_NULL_HANDLE || m_octree_indices.empty() || !swapchain) { + return; + } + const auto swapchain_extent = swapchain->extent(); + cmd_buf.bind_pipeline(m_octree_pipeline) + .bind_descriptor_set(m_descriptor_set) + .bind_vertex_buffer(m_vertex_buffer) + .bind_index_buffer(m_index_buffer) + .set_viewport({ + .width = static_cast(swapchain_extent.width), + .height = static_cast(swapchain_extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }) + .set_scissor({ + .extent = swapchain_extent, + }) + .draw_indexed(static_cast(m_octree_indices.size())); + }) + .build("Octree", DebugLabelColor::GREEN); + }); +} + +void OctreeRenderer::set_vertices_and_indices(std::vector vertices, std::vector indices) { + if (m_octree_vertices == vertices && m_octree_indices == indices) { + return; + } + + m_octree_vertices = std::move(vertices); + m_octree_indices = std::move(indices); + m_geometry_updated = true; +} + +} // namespace inexor::vulkan_renderer::render_modules::octree diff --git a/src/vulkan-renderer/render-modules/octree/octree_vertex.cpp b/src/vulkan-renderer/render-modules/octree/octree_vertex.cpp new file mode 100644 index 000000000..bff2533e6 --- /dev/null +++ b/src/vulkan-renderer/render-modules/octree/octree_vertex.cpp @@ -0,0 +1 @@ +#include "inexor/vulkan-renderer/render-modules/octree/octree_vertex.hpp" diff --git a/src/vulkan-renderer/tools/device_info.cpp b/src/vulkan-renderer/tools/device_info.cpp index 8b880dedb..1197592b5 100644 --- a/src/vulkan-renderer/tools/device_info.cpp +++ b/src/vulkan-renderer/tools/device_info.cpp @@ -13,14 +13,20 @@ namespace inexor::vulkan_renderer::tools { DeviceInfo build_device_info(const VkPhysicalDevice physical_device, const VkSurfaceKHR surface) { - VkPhysicalDeviceProperties properties{}; - vkGetPhysicalDeviceProperties(physical_device, &properties); + VkPhysicalDeviceProperties2 properties2{}; + properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + vkGetPhysicalDeviceProperties2(physical_device, &properties2); + const auto &properties = properties2.properties; - VkPhysicalDeviceMemoryProperties memory_properties{}; - vkGetPhysicalDeviceMemoryProperties(physical_device, &memory_properties); + VkPhysicalDeviceMemoryProperties2 memory_properties2{}; + memory_properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2; + vkGetPhysicalDeviceMemoryProperties2(physical_device, &memory_properties2); + const auto &memory_properties = memory_properties2.memoryProperties; - VkPhysicalDeviceFeatures features{}; - vkGetPhysicalDeviceFeatures(physical_device, &features); + VkPhysicalDeviceFeatures2 features2{}; + features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + vkGetPhysicalDeviceFeatures2(physical_device, &features2); + const auto &features = features2.features; VkDeviceSize total_device_local = 0; for (std::size_t i = 0; i < memory_properties.memoryHeapCount; i++) { @@ -95,9 +101,10 @@ std::vector get_device_features_as_vector(const VkPhysicalDeviceFeatur std::string get_physical_device_name(const VkPhysicalDevice physical_device) { assert(physical_device); - VkPhysicalDeviceProperties properties{}; - vkGetPhysicalDeviceProperties(physical_device, &properties); - return properties.deviceName; + VkPhysicalDeviceProperties2 properties2{}; + properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + vkGetPhysicalDeviceProperties2(physical_device, &properties2); + return properties2.properties.deviceName; } bool is_gpu_suitable(const DeviceInfo &info, const VkPhysicalDeviceFeatures &required_features, diff --git a/src/vulkan-renderer/tools/enumerate.cpp b/src/vulkan-renderer/tools/enumerate.cpp index b2895b128..ce33ddb84 100644 --- a/src/vulkan-renderer/tools/enumerate.cpp +++ b/src/vulkan-renderer/tools/enumerate.cpp @@ -115,12 +115,25 @@ std::vector get_queue_family_properties(const VkPhysica if (physical_device == VK_NULL_HANDLE) { throw InexorException("Error: Parameter 'physical_device' is invalid!"); } + if (vkGetPhysicalDeviceQueueFamilyProperties2 == nullptr) { + throw InexorException("Error: Function pointer 'vkGetPhysicalDeviceQueueFamilyProperties2' is not available!"); + } + std::uint32_t props_count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &props_count, nullptr); + vkGetPhysicalDeviceQueueFamilyProperties2(physical_device, &props_count, nullptr); if (props_count != 0) { // We must call resize here, not reserve! + std::vector queue_families2(props_count); + for (auto &props2 : queue_families2) { + props2.sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; + } + + vkGetPhysicalDeviceQueueFamilyProperties2(physical_device, &props_count, queue_families2.data()); + queue_families.resize(props_count); - vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &props_count, queue_families.data()); + for (std::size_t i = 0; i < props_count; ++i) { + queue_families[i] = queue_families2[i].queueFamilyProperties; + } } return queue_families; } diff --git a/src/vulkan-renderer/tools/fps_limiter.cpp b/src/vulkan-renderer/tools/fps_limiter.cpp index aba87a7ee..16ce09bbb 100644 --- a/src/vulkan-renderer/tools/fps_limiter.cpp +++ b/src/vulkan-renderer/tools/fps_limiter.cpp @@ -4,38 +4,44 @@ namespace inexor::vulkan_renderer::tools { -FPSLimiter::FPSLimiter(const std::uint32_t max_fps) { +FPSLimiter::FPSLimiter(const std::uint32_t max_fps) + : m_last_frame_time(Clock::now()), m_last_fps_update_time(m_last_frame_time) { set_max_fps(max_fps); } -void FPSLimiter::set_max_fps(std::uint32_t max_fps) { +void FPSLimiter::set_max_fps(const std::uint32_t max_fps) { m_max_fps = std::clamp(max_fps, MIN_FPS, MAX_FPS); - m_frame_time = std::chrono::milliseconds(1000) / m_max_fps; + m_frame_time = Duration{1.0 / static_cast(m_max_fps)}; } bool FPSLimiter::is_next_frame_allowed() { - const auto current_time = std::chrono::high_resolution_clock::now(); - const auto elapsed_ms = std::chrono::duration_cast(current_time - m_last_time); - if (elapsed_ms >= m_frame_time) { - m_last_time = current_time; - return true; + const auto current_time = Clock::now(); + m_frame_elapsed = current_time - m_last_frame_time; + + if (m_frame_elapsed < m_frame_time) { + return false; } - return false; + + m_last_frame_time = current_time; + return true; } std::optional FPSLimiter::get_fps() { - m_frames++; - const auto current_time = std::chrono::high_resolution_clock::now(); - const auto elapsed_ms = - std::chrono::duration_cast(current_time - m_last_fps_update_time); - if (elapsed_ms >= m_fps_update_interval) { - m_last_fps_update_time = current_time; - auto fps_value = static_cast(m_frames * 1000.0f / elapsed_ms.count()); - m_frames = 0; - return fps_value; + ++m_frames; + + const auto current_time = Clock::now(); + const Duration fps_elapsed = current_time - m_last_fps_update_time; + + if (fps_elapsed < FPS_UPDATE_INTERVAL) { + return std::nullopt; } - return std::nullopt; + const auto fps = static_cast(static_cast(m_frames) / fps_elapsed.count()); + + m_frames = 0; + m_last_fps_update_time = current_time; + + return fps; } -} // namespace inexor::vulkan_renderer::tools +} // namespace inexor::vulkan_renderer::tools \ No newline at end of file diff --git a/src/vulkan-renderer/tools/make_info.cpp b/src/vulkan-renderer/tools/make_info.cpp new file mode 100644 index 000000000..63704294b --- /dev/null +++ b/src/vulkan-renderer/tools/make_info.cpp @@ -0,0 +1,337 @@ +#include "inexor/vulkan-renderer/tools/make_info.hpp" + +#include + +namespace inexor::vulkan_renderer::tools { + +template <> +VkApplicationInfo make_info(VkApplicationInfo info) { + info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + return info; +} + +template <> +VkBufferCreateInfo make_info(VkBufferCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + return info; +} + +template <> +VkBufferMemoryBarrier make_info(VkBufferMemoryBarrier info) { + info.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + return info; +} + +template <> +VkBufferMemoryBarrier2 make_info(VkBufferMemoryBarrier2 info) { + info.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; + return info; +} + +template <> +VkCommandBufferAllocateInfo make_info(VkCommandBufferAllocateInfo info) { + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + return info; +} + +template <> +VkCommandBufferInheritanceInfo make_info(VkCommandBufferInheritanceInfo info) { + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; + return info; +} + +template <> +VkCommandBufferInheritanceRenderingInfo make_info(VkCommandBufferInheritanceRenderingInfo info) { + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO; + return info; +} + +template <> +VkCommandBufferBeginInfo make_info(VkCommandBufferBeginInfo info) { + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + return info; +} + +template <> +VkCommandPoolCreateInfo make_info(VkCommandPoolCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + return info; +} + +template <> +VkPhysicalDeviceDynamicRenderingFeaturesKHR make_info(VkPhysicalDeviceDynamicRenderingFeaturesKHR info) { + info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; + return info; +} + +template <> +VkDebugUtilsLabelEXT make_info(VkDebugUtilsLabelEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + return info; +} + +template <> +VkDebugMarkerMarkerInfoEXT make_info(VkDebugMarkerMarkerInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT; + return info; +} + +template <> +VkDebugMarkerObjectNameInfoEXT make_info(VkDebugMarkerObjectNameInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_OBJECT_NAME_INFO_EXT; + return info; +} + +template <> +VkDebugMarkerObjectTagInfoEXT make_info(VkDebugMarkerObjectTagInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_OBJECT_TAG_INFO_EXT; + return info; +} + +template <> +VkDebugReportCallbackCreateInfoEXT make_info(VkDebugReportCallbackCreateInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT; + return info; +} + +template <> +VkDebugUtilsMessengerCreateInfoEXT make_info(VkDebugUtilsMessengerCreateInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + return info; +} + +template <> +VkDebugUtilsObjectNameInfoEXT make_info(VkDebugUtilsObjectNameInfoEXT info) { + info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; + return info; +} + +template <> +VkDescriptorPoolCreateInfo make_info(VkDescriptorPoolCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + return info; +} + +template <> +VkDescriptorSetAllocateInfo make_info(VkDescriptorSetAllocateInfo info) { + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + return info; +} + +template <> +VkDescriptorSetLayoutCreateInfo make_info(VkDescriptorSetLayoutCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + return info; +} + +template <> +VkDeviceCreateInfo make_info(VkDeviceCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + return info; +} + +template <> +VkDeviceQueueCreateInfo make_info(VkDeviceQueueCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + return info; +} + +template <> +VkFenceCreateInfo make_info(VkFenceCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + return info; +} + +template <> +VkFramebufferCreateInfo make_info(VkFramebufferCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + return info; +} + +template <> +VkGraphicsPipelineCreateInfo make_info(VkGraphicsPipelineCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + return info; +} + +template <> +VkImageCreateInfo make_info(VkImageCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + return info; +} + +template <> +VkImageMemoryBarrier make_info(VkImageMemoryBarrier info) { + info.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + return info; +} + +template <> +VkImageMemoryBarrier2 make_info(VkImageMemoryBarrier2 info) { + info.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + return info; +} + +template <> +VkImageViewCreateInfo make_info(VkImageViewCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + return info; +} + +template <> +VkInstanceCreateInfo make_info(VkInstanceCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + return info; +} + +template <> +VkMemoryBarrier make_info(VkMemoryBarrier info) { + info.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + return info; +} + +template <> +VkPipelineCacheCreateInfo make_info(VkPipelineCacheCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + return info; +} + +template <> +VkPipelineColorBlendStateCreateInfo make_info(VkPipelineColorBlendStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineDepthStencilStateCreateInfo make_info(VkPipelineDepthStencilStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineDynamicStateCreateInfo make_info(VkPipelineDynamicStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineInputAssemblyStateCreateInfo make_info(VkPipelineInputAssemblyStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineLayoutCreateInfo make_info(VkPipelineLayoutCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + return info; +} + +template <> +VkPipelineMultisampleStateCreateInfo make_info(VkPipelineMultisampleStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineRasterizationStateCreateInfo make_info(VkPipelineRasterizationStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineRenderingCreateInfo make_info(VkPipelineRenderingCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + return info; +} + +template <> +VkPipelineShaderStageCreateInfo make_info(VkPipelineShaderStageCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + return info; +} + +template <> +VkPipelineTessellationStateCreateInfo make_info(VkPipelineTessellationStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineVertexInputStateCreateInfo make_info(VkPipelineVertexInputStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + return info; +} + +template <> +VkPipelineViewportStateCreateInfo make_info(VkPipelineViewportStateCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + return info; +} + +template <> +VkPresentInfoKHR make_info(VkPresentInfoKHR info) { + info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + return info; +} + +template <> +VkRenderingAttachmentInfo make_info(VkRenderingAttachmentInfo info) { + info.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + return info; +} + +template <> +VkRenderingInfo make_info(VkRenderingInfo info) { + info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + return info; +} + +template <> +VkRenderPassBeginInfo make_info(VkRenderPassBeginInfo info) { + info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + return info; +} + +template <> +VkRenderPassCreateInfo make_info(VkRenderPassCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + return info; +} + +template <> +VkSamplerCreateInfo make_info(VkSamplerCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + return info; +} + +template <> +VkSemaphoreCreateInfo make_info(VkSemaphoreCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + return info; +} + +template <> +VkShaderModuleCreateInfo make_info(VkShaderModuleCreateInfo info) { + info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + return info; +} + +template <> +VkSubmitInfo make_info(VkSubmitInfo info) { + info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + return info; +} + +template <> +VkSwapchainCreateInfoKHR make_info(VkSwapchainCreateInfoKHR info) { + info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + return info; +} + +template <> +VkWriteDescriptorSet make_info(VkWriteDescriptorSet info) { + info.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + return info; +} + +} // namespace inexor::vulkan_renderer::tools diff --git a/src/vulkan-renderer/tools/queue_selection.cpp b/src/vulkan-renderer/tools/queue_selection.cpp index 018b4f8fe..a04486b3b 100644 --- a/src/vulkan-renderer/tools/queue_selection.cpp +++ b/src/vulkan-renderer/tools/queue_selection.cpp @@ -1,6 +1,6 @@ #include "inexor/vulkan-renderer/tools/queue_selection.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include #include @@ -77,7 +77,7 @@ QueueFamilyIndexCandidates determine_queue_family_indices(const std::vector({ + return_value.queues_to_create.push_back(make_info({ .queueFamilyIndex = queue_family_index.value(), .queueCount = 1, .pQueuePriorities = &DEFAULT_QUEUE_PRIORITY, diff --git a/src/vulkan-renderer/wrapper/commands/command_buffer.cpp b/src/vulkan-renderer/wrapper/commands/command_buffer.cpp index 4c342ee33..e366046f4 100644 --- a/src/vulkan-renderer/wrapper/commands/command_buffer.cpp +++ b/src/vulkan-renderer/wrapper/commands/command_buffer.cpp @@ -1,8 +1,12 @@ #include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" + +#include #include #include @@ -10,47 +14,128 @@ namespace inexor::vulkan_renderer::wrapper::commands { +// Using declarations +using tools::make_info; using tools::VulkanException; -CommandBuffer::CommandBuffer(const Device &device, const VkCommandPool cmd_pool, std::string name) +CommandBuffer::CommandBuffer(const Device &device, const VkCommandPool cmd_pool, std::string name, + const VkCommandBufferLevel level) : m_device(device), m_name(std::move(name)) { + const auto cmd_buf_ai = make_info({ .commandPool = cmd_pool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .level = level, .commandBufferCount = 1, }); - if (const auto result = vkAllocateCommandBuffers(m_device.device(), &cmd_buf_ai, &m_command_buffer); + if (const auto result = vkAllocateCommandBuffers(m_device.device(), &cmd_buf_ai, &m_cmd_buf); result != VK_SUCCESS) { throw VulkanException("Error: vkAllocateCommandBuffers failed!", result, m_name); } - - m_device.set_debug_name(m_command_buffer, m_name); - - m_wait_fence = std::make_unique(m_device, m_name, false); + m_device.set_debug_name(m_cmd_buf, m_name); + m_wait_fence = std::make_unique(m_device, m_name); } CommandBuffer::CommandBuffer(CommandBuffer &&other) noexcept : m_device(other.m_device) { - m_command_buffer = std::exchange(other.m_command_buffer, VK_NULL_HANDLE); + m_cmd_buf = std::exchange(other.m_cmd_buf, VK_NULL_HANDLE); m_name = std::move(other.m_name); - m_wait_fence = std::exchange(other.m_wait_fence, nullptr); - m_staging_bufs = std::move(other.m_staging_bufs); + m_wait_fence = std::move(other.m_wait_fence); + m_has_been_submitted = other.m_has_been_submitted; + m_wait_submit_infos_scratch = std::move(other.m_wait_submit_infos_scratch); + m_signal_submit_infos_scratch = std::move(other.m_signal_submit_infos_scratch); } const CommandBuffer &CommandBuffer::begin_command_buffer(const VkCommandBufferUsageFlags flags) const { const auto begin_info = make_info({ .flags = flags, }); - vkBeginCommandBuffer(m_command_buffer, &begin_info); + vkBeginCommandBuffer(m_cmd_buf, &begin_info); + return *this; +} + +const CommandBuffer & +CommandBuffer::begin_secondary_command_buffer(const VkCommandBufferInheritanceInfo &inheritance_info, + const VkCommandBufferUsageFlags flags) const { + const auto begin_info = make_info({ + .flags = flags, + .pInheritanceInfo = &inheritance_info, + }); + vkBeginCommandBuffer(m_cmd_buf, &begin_info); + return *this; +} + +const CommandBuffer &CommandBuffer::end_recording() const { + return end_command_buffer(); +} + +void CommandBuffer::reset_recording() const { + reset(); +} + +const CommandBuffer & +CommandBuffer::execute_secondary_command_buffers(const std::span secondary_cmd_buffers) const { + assert(!secondary_cmd_buffers.empty()); + vkCmdExecuteCommands(m_cmd_buf, static_cast(secondary_cmd_buffers.size()), + secondary_cmd_buffers.data()); + return *this; +} + +const CommandBuffer &CommandBuffer::begin_debug_label_region(const std::string &name, + std::array color) const { + if (name.empty()) { + // NOTE: Despite Vulkan spec allowing name to be empty, we strictly enforce this rule in our code base! + throw InexorException("Error: Parameter 'name' is empty!"); + } + auto label = make_info({ + .pLabelName = name.c_str(), + .color = {color[0], color[1], color[2], color[3]}, + }); + vkCmdBeginDebugUtilsLabelEXT(m_cmd_buf, &label); + return *this; +} - // We must clear the staging buffers which could be left over from previous use of this command buffer - m_staging_bufs.clear(); +const CommandBuffer &CommandBuffer::bind_descriptor_set(const VkDescriptorSet descriptor_set, + const std::weak_ptr pipeline) const { + if (!descriptor_set) { + throw InexorException("Error: Parameter 'descriptor_set' is invalid!"); + } + const auto pipeline_ref = pipeline.lock(); + if (!pipeline_ref) { + throw InexorException("Error: Parameter 'pipeline' is an invalid pointer!"); + } + vkCmdBindDescriptorSets(m_cmd_buf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_ref->pipeline_layout(), 0, 1, + &descriptor_set, 0, nullptr); return *this; } -const CommandBuffer &CommandBuffer::begin_render_pass(const VkRenderPassBeginInfo &render_pass_bi, - const VkSubpassContents subpass_contents) const { - vkCmdBeginRenderPass(m_command_buffer, &render_pass_bi, subpass_contents); +const CommandBuffer & +CommandBuffer::bind_descriptor_set(const std::weak_ptr descriptor_sets, + const std::weak_ptr pipeline) const { + const auto descriptor_sets_ref = descriptor_sets.lock(); + if (!descriptor_sets_ref) { + throw InexorException("Error: Parameter 'descriptor_set' is an invalid pointer!"); + } + return bind_descriptor_set(descriptor_sets_ref->current_descriptor_set(), pipeline); +} + +const CommandBuffer & +CommandBuffer::bind_descriptor_set(const std::weak_ptr descriptor_sets) const { + const auto desc_sets = descriptor_sets.lock(); + if (!desc_sets) { + throw InexorException("Error: Parameter 'descriptor_set' is an invalid pointer!"); + } + const auto descriptor_set = desc_sets->current_descriptor_set(); + if (!descriptor_set) { + throw InexorException("Error: Current descriptor set is invalid!"); + } + + const auto pipeline_layout = desc_sets->pipeline_layout(); + if (!pipeline_layout) { + throw InexorException("Error: Descriptor set pipeline layout is invalid!"); + } + + vkCmdBindDescriptorSets(m_cmd_buf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, + nullptr); return *this; } @@ -61,23 +146,45 @@ const CommandBuffer &CommandBuffer::bind_descriptor_sets(const std::span dyn_offsets) const { assert(layout); assert(!desc_sets.empty()); - vkCmdBindDescriptorSets(m_command_buffer, bind_point, layout, first_set, - static_cast(desc_sets.size()), desc_sets.data(), - static_cast(dyn_offsets.size()), dyn_offsets.data()); + vkCmdBindDescriptorSets(m_cmd_buf, bind_point, layout, first_set, static_cast(desc_sets.size()), + desc_sets.data(), static_cast(dyn_offsets.size()), dyn_offsets.data()); + return *this; +} + +const CommandBuffer & +CommandBuffer::bind_index_buffer(const std::weak_ptr buffer, + const VkIndexType index_type, const VkDeviceSize offset) const { + const auto buffer_ref = buffer.lock(); + if (!buffer_ref) { + throw InexorException("Error: Parameter 'buffer' is an invalid pointer!"); + } + if (buffer_ref->type() != inexor::vulkan_renderer::render_graph::BufferType::INDEX_BUFFER) { + throw InexorException("Error: Rendergraph buffer resource " + buffer_ref->name() + " is not an index buffer!"); + } + vkCmdBindIndexBuffer(m_cmd_buf, buffer_ref->buffer(), offset, index_type); return *this; } const CommandBuffer &CommandBuffer::bind_index_buffer(const VkBuffer buf, const VkIndexType index_type, const VkDeviceSize offset) const { assert(buf); - vkCmdBindIndexBuffer(m_command_buffer, buf, offset, index_type); + vkCmdBindIndexBuffer(m_cmd_buf, buf, offset, index_type); return *this; } +const CommandBuffer &CommandBuffer::bind_pipeline( + std::weak_ptr pipeline) const { + const auto pipeline_ref = pipeline.lock(); + if (!pipeline_ref) { + throw InexorException("Error: Parameter 'pipeline' is an invalid pointer!"); + } + return bind_pipeline(pipeline_ref->pipeline()); +} + const CommandBuffer &CommandBuffer::bind_pipeline(const VkPipeline pipeline, const VkPipelineBindPoint bind_point) const { assert(pipeline); - vkCmdBindPipeline(m_command_buffer, bind_point, pipeline); + vkCmdBindPipeline(m_cmd_buf, bind_point, pipeline); return *this; } @@ -85,8 +192,18 @@ const CommandBuffer &CommandBuffer::bind_vertex_buffers(const std::span offsets) const { assert(!bufs.empty()); - vkCmdBindVertexBuffers(m_command_buffer, first_binding, static_cast(bufs.size()), bufs.data(), - offsets.empty() ? std::vector(bufs.size(), 0).data() : offsets.data()); + if (!offsets.empty()) { + vkCmdBindVertexBuffers(m_cmd_buf, first_binding, static_cast(bufs.size()), bufs.data(), + offsets.data()); + return *this; + } + // NOTE: When no offsets are specified, all buffers are bound at offset 0. We use a fixed-size stack array here + // instead of a temporary std::vector to avoid a heap allocation on every call. + constexpr std::size_t MAX_STACK_BINDINGS = 16; + assert(bufs.size() <= MAX_STACK_BINDINGS && "Too many vertex buffer bindings for the fixed-size stack buffer!"); + const std::array zero_offsets{}; + vkCmdBindVertexBuffers(m_cmd_buf, first_binding, static_cast(bufs.size()), bufs.data(), + zero_offsets.data()); return *this; } @@ -95,38 +212,44 @@ const CommandBuffer &CommandBuffer::change_image_layout(const VkImage image, con const VkImageSubresourceRange subres_range, const VkPipelineStageFlags src_mask, const VkPipelineStageFlags dst_mask) const { + if (image == VK_NULL_HANDLE) { + throw std::invalid_argument("Error: Parameter 'image' is an invalid pointer!"); + } assert(new_layout != old_layout); - auto barrier = make_info({ + auto barrier = VkImageMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .srcStageMask = static_cast(src_mask), + .dstStageMask = static_cast(dst_mask), .oldLayout = old_layout, .newLayout = new_layout, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = image, .subresourceRange = subres_range, - }); + }; switch (old_layout) { case VK_IMAGE_LAYOUT_UNDEFINED: barrier.srcAccessMask = 0; break; case VK_IMAGE_LAYOUT_PREINITIALIZED: - barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + barrier.srcAccessMask = VK_ACCESS_2_HOST_WRITE_BIT; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: - barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: - barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: - barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.srcAccessMask = VK_ACCESS_2_SHADER_READ_BIT; break; default: break; @@ -134,41 +257,64 @@ const CommandBuffer &CommandBuffer::change_image_layout(const VkImage image, con switch (new_layout) { case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: - barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: - barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: - barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: - barrier.dstAccessMask = barrier.dstAccessMask | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = barrier.dstAccessMask | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: if (barrier.srcAccessMask == 0) { - barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.srcAccessMask = VK_ACCESS_2_HOST_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT; } - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT; break; default: break; } - return pipeline_image_memory_barrier(src_mask, dst_mask, barrier); + return pipeline_image_memory_barrier(barrier); } const CommandBuffer & -CommandBuffer::change_image_layout(const VkImage image, const VkImageLayout old_layout, const VkImageLayout new_layout, - const std::uint32_t mip_level_count, const std::uint32_t array_layer_count, - const std::uint32_t base_mip_level, const std::uint32_t base_array_layer, - const VkPipelineStageFlags src_mask, const VkPipelineStageFlags dst_mask) const { +CommandBuffer::change_image_layout(const VkImage image, const VkFormat format, const VkImageLayout old_layout, + const VkImageLayout new_layout, const std::uint32_t mip_level_count, + const std::uint32_t array_layer_count, const std::uint32_t base_mip_level, + const std::uint32_t base_array_layer, const VkPipelineStageFlags src_mask, + const VkPipelineStageFlags dst_mask) const { + + auto deduce_aspect_mask = [&]() -> VkImageAspectFlags { + switch (format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_D32_SFLOAT: + return VK_IMAGE_ASPECT_DEPTH_BIT; + + case VK_FORMAT_S8_UINT: + return VK_IMAGE_ASPECT_STENCIL_BIT; + + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + + default: + return VK_IMAGE_ASPECT_COLOR_BIT; + } + }(); + return change_image_layout(image, old_layout, new_layout, - {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = base_mip_level, - .levelCount = mip_level_count, - .baseArrayLayer = base_array_layer, - .layerCount = array_layer_count}, + { + .aspectMask = deduce_aspect_mask, + .baseMipLevel = base_mip_level, + .levelCount = mip_level_count, + .baseArrayLayer = base_array_layer, + .layerCount = array_layer_count, + }, src_mask, dst_mask); } @@ -177,8 +323,7 @@ const CommandBuffer &CommandBuffer::copy_buffer(const VkBuffer src_buf, const Vk assert(src_buf); assert(dst_buf); assert(!copy_regions.empty()); - vkCmdCopyBuffer(m_command_buffer, src_buf, dst_buf, static_cast(copy_regions.size()), - copy_regions.data()); + vkCmdCopyBuffer(m_cmd_buf, src_buf, dst_buf, static_cast(copy_regions.size()), copy_regions.data()); return *this; } @@ -196,83 +341,165 @@ const CommandBuffer &CommandBuffer::copy_buffer_to_image(const VkBuffer src_buf, const std::span copy_regions) const { assert(src_buf); assert(dst_img); - vkCmdCopyBufferToImage(m_command_buffer, src_buf, dst_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + vkCmdCopyBufferToImage(m_cmd_buf, src_buf, dst_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(copy_regions.size()), copy_regions.data()); return *this; } const CommandBuffer &CommandBuffer::copy_buffer_to_image(const VkBuffer src_buf, const VkImage dst_img, const VkBufferImageCopy ©_region) const { - assert(src_buf); - assert(dst_img); - vkCmdCopyBufferToImage(m_command_buffer, src_buf, dst_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); + if (!src_buf) { + throw InexorException("Error: Parameter 'src_buf' is invalid!"); + } + if (!dst_img) { + throw InexorException("Error: Parameter 'dst_img' is invalid!"); + } + vkCmdCopyBufferToImage(m_cmd_buf, src_buf, dst_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); return *this; } -const CommandBuffer &CommandBuffer::copy_buffer_to_image(const void *data, - const VkDeviceSize data_size, // NOLINT - const VkImage dst_img, const VkBufferImageCopy ©_region, - const std::string &name) const { - return copy_buffer_to_image(create_staging_buffer(data, data_size, name), dst_img, copy_region); +const CommandBuffer &CommandBuffer::copy_buffer_to_image(const VkBuffer buffer, const VkImage img, + const VkExtent3D extent) const { + // NOTE: We delegate error checks to the other function overload + return copy_buffer_to_image(buffer, img, + { + .imageSubresource = + { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .layerCount = 1, + }, + .imageExtent = + { + .width = extent.width, + .height = extent.height, + .depth = 1, + }, + }); +} + +const CommandBuffer &CommandBuffer::copy_buffer_to_image(const VkBuffer src_buf, const std::weak_ptr img) const { + // NOTE: We delegate error checks to the other function overload + const auto image = img.lock(); + if (!image) { + throw InexorException("Error: Parameter 'img' is an invalid pointer!"); + } + return copy_buffer_to_image(src_buf, image->image(), + { + .width = image->width(), + .height = image->height(), + .depth = 1, + }); } const CommandBuffer &CommandBuffer::draw(const std::uint32_t vert_count, const std::uint32_t inst_count, const std::uint32_t first_vert, const std::uint32_t first_inst) const { - vkCmdDraw(m_command_buffer, vert_count, inst_count, first_vert, first_inst); + vkCmdDraw(m_cmd_buf, vert_count, inst_count, first_vert, first_inst); return *this; } const CommandBuffer &CommandBuffer::draw_indexed(const std::uint32_t index_count, const std::uint32_t inst_count, const std::uint32_t first_index, const std::int32_t vert_offset, const std::uint32_t first_inst) const { - vkCmdDrawIndexed(m_command_buffer, index_count, inst_count, first_index, vert_offset, first_inst); + vkCmdDrawIndexed(m_cmd_buf, index_count, inst_count, first_index, vert_offset, first_inst); return *this; } const CommandBuffer &CommandBuffer::end_command_buffer() const { - vkEndCommandBuffer(m_command_buffer); + vkEndCommandBuffer(m_cmd_buf); return *this; } -const CommandBuffer &CommandBuffer::end_render_pass() const { - vkCmdEndRenderPass(m_command_buffer); +const CommandBuffer &CommandBuffer::begin_rendering(const VkRenderingInfo &rendering_info) const { + vkCmdBeginRendering(m_cmd_buf, &rendering_info); + return *this; +}; + +const CommandBuffer &CommandBuffer::end_debug_label_region() const { + vkCmdEndDebugUtilsLabelEXT(m_cmd_buf); return *this; } -const CommandBuffer &CommandBuffer::pipeline_barrier(const VkPipelineStageFlags src_stage_flags, - const VkPipelineStageFlags dst_stage_flags, - const std::span img_mem_barriers, - const std::span mem_barriers, - const std::span buf_mem_barriers, - const VkDependencyFlags dep_flags) const { - // One barrier must be set at least - assert(!(img_mem_barriers.empty() && mem_barriers.empty()) && buf_mem_barriers.empty()); +const CommandBuffer &CommandBuffer::end_rendering() const { + vkCmdEndRendering(m_cmd_buf); + return *this; +} - vkCmdPipelineBarrier(m_command_buffer, src_stage_flags, dst_stage_flags, dep_flags, - static_cast(mem_barriers.size()), mem_barriers.data(), - static_cast(buf_mem_barriers.size()), buf_mem_barriers.data(), - static_cast(img_mem_barriers.size()), img_mem_barriers.data()); +const CommandBuffer &CommandBuffer::pipeline_barrier(const VkDependencyInfo &dependency_info) const { + vkCmdPipelineBarrier2(m_cmd_buf, &dependency_info); return *this; } -const CommandBuffer &CommandBuffer::pipeline_image_memory_barrier(const VkPipelineStageFlags src_stage_flags, - const VkPipelineStageFlags dst_stage_flags, - const VkImageMemoryBarrier &img_barrier) const { - return pipeline_barrier(src_stage_flags, dst_stage_flags, {&img_barrier, 1}); +const CommandBuffer & +CommandBuffer::pipeline_buffer_memory_barrier(const VkBufferMemoryBarrier2 &buffer_mem_barrier) const { + const auto dependency_info = VkDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &buffer_mem_barrier, + }; + return pipeline_barrier(dependency_info); +} + +const CommandBuffer &CommandBuffer::pipeline_image_memory_barrier(const VkImageMemoryBarrier2 &img_barrier) const { + const auto dependency_info = VkDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &img_barrier, + }; + return pipeline_barrier(dependency_info); +} + +const CommandBuffer &CommandBuffer::pipeline_memory_barrier(const VkMemoryBarrier2 &mem_barrier) const { + const auto dependency_info = VkDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .memoryBarrierCount = 1, + .pMemoryBarriers = &mem_barrier, + }; + return pipeline_barrier(dependency_info); +} + +const CommandBuffer &CommandBuffer::barrier_transfer_write_to_shader_read() const { + return pipeline_memory_barrier({ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, + }); } -const CommandBuffer &CommandBuffer::pipeline_memory_barrier(VkPipelineStageFlags src_stage_flags, - VkPipelineStageFlags dst_stage_flags, - const VkMemoryBarrier &mem_barrier) const { - return pipeline_barrier(src_stage_flags, dst_stage_flags, {}, {&mem_barrier, 1}); +const CommandBuffer &CommandBuffer::barrier_color_attachment_write_to_shader_read() const { + return pipeline_memory_barrier({ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, + }); +} + +const CommandBuffer &CommandBuffer::barrier_depth_stencil_write_to_shader_read() const { + return pipeline_memory_barrier({ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, + .srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, + }); } -const CommandBuffer &CommandBuffer::full_barrier() const { - return pipeline_memory_barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - make_info({ - .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, - .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, - })); +const CommandBuffer &CommandBuffer::insert_debug_label(const std::string &name, std::array color) const { + if (name.empty()) { + throw InexorException("Error: Parameter 'name' is an empty string!"); + } + auto label = make_info({ + .pLabelName = name.c_str(), + .color = {color[0], color[1], color[2], color[3]}, + }); + vkCmdInsertDebugUtilsLabelEXT(m_cmd_buf, &label); + return *this; } const CommandBuffer &CommandBuffer::push_constants(const VkPipelineLayout layout, const VkShaderStageFlags stage, @@ -281,57 +508,195 @@ const CommandBuffer &CommandBuffer::push_constants(const VkPipelineLayout layout assert(layout); assert(size > 0); assert(data); - vkCmdPushConstants(m_command_buffer, layout, stage, static_cast(offset), size, data); + vkCmdPushConstants(m_cmd_buf, layout, stage, static_cast(offset), size, data); return *this; } -const CommandBuffer &CommandBuffer::reset_fence() const { - m_wait_fence->reset(); +const CommandBuffer &CommandBuffer::set_scissor(const VkRect2D scissor) const { + vkCmdSetScissor(m_cmd_buf, 0, 1, &scissor); return *this; } -const CommandBuffer &CommandBuffer::submit(const std::span submit_infos) const { - assert(!submit_infos.empty()); - end_command_buffer(); +const CommandBuffer &CommandBuffer::set_viewport(const VkViewport viewport) const { + vkCmdSetViewport(m_cmd_buf, 0, 1, &viewport); + return *this; +} - if (const auto result = vkQueueSubmit(m_device.graphics_queue(), static_cast(submit_infos.size()), - submit_infos.data(), m_wait_fence->fence())) { - throw VulkanException("Error: vkQueueSubmit failed!", result); +void CommandBuffer::submit(const VkQueueFlagBits queue_type, + const std::span wait_semaphore_infos, + const std::span signal_semaphore_infos) const { + const auto command_buffer_info = VkCommandBufferSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .commandBuffer = m_cmd_buf, + .deviceMask = 0, + }; + + const auto submit_info = VkSubmitInfo2{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .waitSemaphoreInfoCount = static_cast(wait_semaphore_infos.size()), + .pWaitSemaphoreInfos = wait_semaphore_infos.empty() ? nullptr : wait_semaphore_infos.data(), + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &command_buffer_info, + .signalSemaphoreInfoCount = static_cast(signal_semaphore_infos.size()), + .pSignalSemaphoreInfos = signal_semaphore_infos.empty() ? nullptr : signal_semaphore_infos.data(), + }; + + auto get_queue = [&]() { + switch (queue_type) { + case VK_QUEUE_TRANSFER_BIT: { + return m_device.m_transfer_queue; + } + case VK_QUEUE_COMPUTE_BIT: { + return m_device.m_compute_queue; + } + case VK_QUEUE_SPARSE_BINDING_BIT: { + return m_device.m_sparse_binding_queue; + } + default: { + return m_device.m_graphics_queue; + } + } + }; + + if (const auto result = vkQueueSubmit2(get_queue(), 1, &submit_info, m_wait_fence->fence())) { + throw VulkanException("Error: vkQueueSubmit2 failed!", result, m_name); } - return *this; + m_has_been_submitted = true; +} + +void CommandBuffer::submit(const VkQueueFlagBits queue_type, + const std::span wait_semaphore_infos, + const std::span signal_semaphores) const { + const auto stage_mask = [&]() -> VkPipelineStageFlags2 { + switch (queue_type) { + case VK_QUEUE_TRANSFER_BIT: + return VK_PIPELINE_STAGE_2_TRANSFER_BIT; + case VK_QUEUE_COMPUTE_BIT: + return VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + case VK_QUEUE_SPARSE_BINDING_BIT: + return VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + default: + return VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT; + } + }(); + + m_signal_submit_infos_scratch.resize(signal_semaphores.size()); + for (std::size_t index = 0; index < signal_semaphores.size(); ++index) { + m_signal_submit_infos_scratch[index] = VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = signal_semaphores[index], + .stageMask = stage_mask, + }; + } + + submit(queue_type, wait_semaphore_infos, m_signal_submit_infos_scratch); +} + +void CommandBuffer::submit(const VkQueueFlagBits queue_type, const std::span wait_semaphores, + const std::span signal_semaphore_infos) const { + const auto default_wait_stage_mask = [&]() -> VkPipelineStageFlags2 { + switch (queue_type) { + case VK_QUEUE_TRANSFER_BIT: + return VK_PIPELINE_STAGE_2_TRANSFER_BIT; + case VK_QUEUE_COMPUTE_BIT: + return VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + case VK_QUEUE_SPARSE_BINDING_BIT: + return VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + default: + return VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT; + } + }(); + + m_wait_submit_infos_scratch.resize(wait_semaphores.size()); + for (std::size_t index = 0; index < wait_semaphores.size(); ++index) { + m_wait_submit_infos_scratch[index] = VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = wait_semaphores[index], + .stageMask = default_wait_stage_mask, + }; + } + + submit(queue_type, m_wait_submit_infos_scratch, signal_semaphore_infos); +} + +void CommandBuffer::submit(const VkQueueFlagBits queue_type, const std::span wait_semaphores, + const std::span signal_semaphores) const { + const auto default_wait_stage_mask = [&]() -> VkPipelineStageFlags2 { + switch (queue_type) { + case VK_QUEUE_TRANSFER_BIT: + return VK_PIPELINE_STAGE_2_TRANSFER_BIT; + case VK_QUEUE_COMPUTE_BIT: + return VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + case VK_QUEUE_SPARSE_BINDING_BIT: + return VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + default: + return VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT; + } + }(); + + m_wait_submit_infos_scratch.resize(wait_semaphores.size()); + for (std::size_t index = 0; index < wait_semaphores.size(); ++index) { + m_wait_submit_infos_scratch[index] = VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = wait_semaphores[index], + .stageMask = default_wait_stage_mask, + }; + } + + submit(queue_type, m_wait_submit_infos_scratch, signal_semaphores); } -const CommandBuffer &CommandBuffer::submit(const VkSubmitInfo submit_info) const { - return submit({&submit_info, 1}); +void CommandBuffer::submit(const VkQueueFlagBits queue_type, + const std::span wait_semaphores, + const std::span signal_semaphores) const { + m_wait_submit_infos_scratch.resize(wait_semaphores.size()); + for (std::size_t index = 0; index < wait_semaphores.size(); ++index) { + const auto &wait_semaphore = wait_semaphores[index]; + m_wait_submit_infos_scratch[index] = VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = wait_semaphore.semaphore, + .stageMask = wait_semaphore.stage_mask, + }; + } + + submit(queue_type, m_wait_submit_infos_scratch, signal_semaphores); } -const CommandBuffer &CommandBuffer::submit() const { - return submit(make_info({ - .commandBufferCount = 1, - .pCommandBuffers = &m_command_buffer, - })); +void CommandBuffer::submit(const VkQueueFlagBits queue_type, + const std::span wait_semaphores, + const std::span signal_semaphore_infos) const { + m_wait_submit_infos_scratch.resize(wait_semaphores.size()); + for (std::size_t index = 0; index < wait_semaphores.size(); ++index) { + const auto &wait_semaphore = wait_semaphores[index]; + m_wait_submit_infos_scratch[index] = VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = wait_semaphore.semaphore, + .stageMask = wait_semaphore.stage_mask, + }; + } + + submit(queue_type, m_wait_submit_infos_scratch, signal_semaphore_infos); } -const CommandBuffer &CommandBuffer::submit_and_wait(const std::span submit_infos) const { - submit(submit_infos); - m_wait_fence->block(); - return *this; +void CommandBuffer::set_debug_name(const std::string &name) const { + m_name = name; + m_device.set_debug_name(m_cmd_buf, name); } -const CommandBuffer &CommandBuffer::submit_and_wait(VkSubmitInfo submit_info) const { - submit_info.pCommandBuffers = &m_command_buffer; - return submit_and_wait({&submit_info, 1}); +void CommandBuffer::reset() const { + vkResetCommandBuffer(m_cmd_buf, 0); } -const CommandBuffer &CommandBuffer::submit_and_wait() const { - return submit_and_wait(make_info({ - .commandBufferCount = 1, - .pCommandBuffers = &m_command_buffer, - })); +void CommandBuffer::reset_fence() const { + m_wait_fence->reset(); +} + +VkResult CommandBuffer::status() const { + return m_wait_fence->status(); } -void CommandBuffer::set_debug_name(const std::string &name) { - m_device.set_debug_name(m_command_buffer, name); +void CommandBuffer::wait_fence() const { + m_wait_fence->wait(); } } // namespace inexor::vulkan_renderer::wrapper::commands diff --git a/src/vulkan-renderer/wrapper/commands/command_buffer_cache.cpp b/src/vulkan-renderer/wrapper/commands/command_buffer_cache.cpp new file mode 100644 index 000000000..2f5874e9f --- /dev/null +++ b/src/vulkan-renderer/wrapper/commands/command_buffer_cache.cpp @@ -0,0 +1,103 @@ +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer_cache.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::commands { + +using tools::VulkanException; + +CommandBufferCache::CommandBufferCache(Device &device, const bool use_secondary_command_buffers) + : m_device(device), m_use_secondary_command_buffers(use_secondary_command_buffers) {} + +CommandBufferCache::SecondaryCommandBufferState &CommandBufferCache::state_for_pass(const std::string &pass_name) { + auto &state = m_secondary_command_buffers[pass_name]; + if (state.dirty_by_frame_slot.size() != m_frame_slot_count) { + state.dirty_by_frame_slot.assign(m_frame_slot_count, true); + } + return state; +} + +void CommandBufferCache::set_frame_context(const std::size_t frame_slot_count, const std::size_t current_frame_slot, + const std::span frame_slot_submission_fences) { + const bool frame_slot_count_changed = m_frame_slot_count != frame_slot_count; + m_frame_slot_count = frame_slot_count == 0 ? 1 : frame_slot_count; + m_current_frame_slot = current_frame_slot < m_frame_slot_count ? current_frame_slot : 0; + m_frame_slot_submission_fences.assign(frame_slot_submission_fences.begin(), frame_slot_submission_fences.end()); + if (m_frame_slot_submission_fences.size() != m_frame_slot_count) { + m_frame_slot_submission_fences.resize(m_frame_slot_count, VK_NULL_HANDLE); + } + + if (!frame_slot_count_changed) { + return; + } + + for (auto &[_, state] : m_secondary_command_buffers) { + state.dirty_by_frame_slot.assign(m_frame_slot_count, true); + } +} + +void CommandBufferCache::invalidate_all_secondary_command_buffers() { + for (auto &[_, state] : m_secondary_command_buffers) { + if (state.dirty_by_frame_slot.size() != m_frame_slot_count) { + state.dirty_by_frame_slot.assign(m_frame_slot_count, true); + } else { + std::fill(state.dirty_by_frame_slot.begin(), state.dirty_by_frame_slot.end(), true); + } + } +} + +void CommandBufferCache::record_secondary_command_buffer( + const CommandBuffer &primary_cmd_buf, const std::string &pass_name, std::array debug_label_color, + const VkExtent2D render_extent, const VkCommandBufferInheritanceInfo &inheritance_info, + const VkRenderingInfo &rendering_info, std::function on_record) { + if (!m_use_secondary_command_buffers) { + primary_cmd_buf.begin_debug_label_region(pass_name, debug_label_color); + primary_cmd_buf.begin_rendering(rendering_info); + std::invoke(on_record, primary_cmd_buf); + primary_cmd_buf.end_rendering(); + primary_cmd_buf.end_debug_label_region(); + return; + } + + auto &state = state_for_pass(pass_name); + + const bool extent_changed = state.cached_render_extent.width != render_extent.width || + state.cached_render_extent.height != render_extent.height; + if (extent_changed) { + std::fill(state.dirty_by_frame_slot.begin(), state.dirty_by_frame_slot.end(), true); + } + + const auto slot_index = m_current_frame_slot; + const auto &secondary_cmd = m_device.request_secondary_command_buffer( + VK_QUEUE_GRAPHICS_BIT, pass_name + "[slot " + std::to_string(slot_index) + "]|secondary"); + + const bool secondary_cmd_dirty = extent_changed || state.dirty_by_frame_slot[slot_index]; + + if (secondary_cmd_dirty) { + secondary_cmd.reset_recording(); + secondary_cmd.begin_secondary_command_buffer(inheritance_info, + VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT); + std::invoke(on_record, secondary_cmd); + secondary_cmd.end_recording(); + state.cached_render_extent = render_extent; + state.dirty_by_frame_slot[slot_index] = false; + } + + auto rendering_info_with_secondary = rendering_info; + rendering_info_with_secondary.flags |= VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT; + + primary_cmd_buf.begin_debug_label_region(pass_name, debug_label_color); + primary_cmd_buf.begin_rendering(rendering_info_with_secondary); + const VkCommandBuffer secondary_handle = secondary_cmd.command_buffer(); + primary_cmd_buf.execute_secondary_command_buffers(std::span(&secondary_handle, 1)); + primary_cmd_buf.end_rendering(); + primary_cmd_buf.end_debug_label_region(); +} + +} // namespace inexor::vulkan_renderer::wrapper::commands \ No newline at end of file diff --git a/src/vulkan-renderer/wrapper/commands/command_pool.cpp b/src/vulkan-renderer/wrapper/commands/command_pool.cpp index c85b7648c..0907f0e18 100644 --- a/src/vulkan-renderer/wrapper/commands/command_pool.cpp +++ b/src/vulkan-renderer/wrapper/commands/command_pool.cpp @@ -1,8 +1,10 @@ #include "inexor/vulkan-renderer/wrapper/commands/command_pool.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/tools/representation.hpp" +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include @@ -12,11 +14,11 @@ namespace inexor::vulkan_renderer::wrapper::commands { -using tools::VulkanException; +CommandPool::CommandPool(const Device &device, const VkQueueFlagBits queue_type, const std::uint32_t queue_family_index, + std::string name) + : m_device(device), m_name(std::move(name)), m_queue_type(queue_type) { -CommandPool::CommandPool(const Device &device, const std::uint32_t queue_family_index, std::string name) - : m_device(device), m_name(std::move(name)) { - const auto cmd_pool_ci = make_info({ + const auto cmd_pool_ci = tools::make_info({ .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT | VK_COMMAND_POOL_CREATE_TRANSIENT_BIT, .queueFamilyIndex = queue_family_index, }); @@ -24,7 +26,7 @@ CommandPool::CommandPool(const Device &device, const std::uint32_t queue_family_ // Converting the thread ID to std::string is more complicated than you might think, we need a stringstream. std::ostringstream oss; oss << std::this_thread::get_id(); - spdlog::trace("Creating command pool for thread ID {}", oss.str()); + spdlog::trace("Creating command pool [thread ID: {}, type: {}]", oss.str(), tools::as_string(queue_type)); if (const auto result = vkCreateCommandPool(m_device.device(), &cmd_pool_ci, nullptr, &m_cmd_pool); result != VK_SUCCESS) { @@ -34,32 +36,82 @@ CommandPool::CommandPool(const Device &device, const std::uint32_t queue_family_ } CommandPool::CommandPool(CommandPool &&other) noexcept : m_device(other.m_device) { + m_name = std::move(other.m_name); m_cmd_pool = std::exchange(other.m_cmd_pool, nullptr); + m_queue_type = other.m_queue_type; + m_cmd_bufs = std::move(other.m_cmd_bufs); + m_secondary_cmd_bufs = std::move(other.m_secondary_cmd_bufs); + m_next_reuse_index = other.m_next_reuse_index; + m_next_secondary_reuse_index = other.m_next_secondary_reuse_index; } CommandPool::~CommandPool() { vkDestroyCommandPool(m_device.device(), m_cmd_pool, nullptr); } +std::string CommandPool::get_pool_name(const VkQueueFlagBits queue_type) const { + switch (queue_type) { + case VK_QUEUE_GRAPHICS_BIT: + return "graphics"; + case VK_QUEUE_COMPUTE_BIT: + return "compute"; + case VK_QUEUE_TRANSFER_BIT: + return "transfer"; + case VK_QUEUE_SPARSE_BINDING_BIT: + return "sparse binding"; + default: + return "unknown queue type"; + } +} + const CommandBuffer &CommandPool::request_command_buffer(const std::string &name) { - // Try to find a command buffer which is currently not used - for (const auto &cmd_buf : m_cmd_bufs) { - if (cmd_buf->fence_status() == VK_SUCCESS) { - cmd_buf->reset_fence(); - cmd_buf->set_debug_name(name); - cmd_buf->begin_command_buffer(); - return *cmd_buf; - } + // Allocate additional command buffers only up to the configured in-flight limit. + if (m_cmd_bufs.size() < MAX_IN_FLIGHT_SUBMISSIONS) { + const auto cmd_buffer_number = std::to_string(m_cmd_bufs.size()); + const auto cmd_pool_name = get_pool_name(m_queue_type); + std::string cmd_buf_name = "[" + cmd_pool_name + "] Command Buffer " + cmd_buffer_number; + m_cmd_bufs.emplace_back(std::make_unique(m_device, m_cmd_pool, cmd_buf_name)); + spdlog::trace("Creating command buffer [type=primary, index={}, pool={}]", cmd_buffer_number, cmd_pool_name); + m_cmd_bufs.back()->set_debug_name(name); + m_cmd_bufs.back()->begin_command_buffer(); + return *m_cmd_bufs.back(); } - // We need to create a new command buffer because no free one was found - // Note that there is currently no method for shrinking m_cmd_bufs, but this should not be a problem - m_cmd_bufs.emplace_back(std::make_unique(m_device, m_cmd_pool, "command buffer")); + // All command buffers are currently in-flight: wait for one slot and reuse it. + auto &cmd_buf = m_cmd_bufs[m_next_reuse_index]; + cmd_buf->wait_fence(); + cmd_buf->reset_fence(); + cmd_buf->set_debug_name(name); + cmd_buf->reset(); + cmd_buf->begin_command_buffer(); + m_next_reuse_index = (m_next_reuse_index + 1) % m_cmd_bufs.size(); + return *cmd_buf; +} - spdlog::trace("Creating new command buffer #{}", m_cmd_bufs.size()); +const CommandBuffer &CommandPool::request_secondary_command_buffer(const std::string &name) { + for (auto &cmd_buf : m_secondary_cmd_bufs) { + if (cmd_buf.name() == name) { + cmd_buf.set_debug_name(name); + return cmd_buf; + } + } + + // Create a dedicated secondary command buffer for each unique pass name. + const auto cmd_buffer_number = std::to_string(m_secondary_cmd_bufs.size()); + const auto cmd_pool_name = get_pool_name(m_queue_type); + std::string cmd_buf_name = "[" + cmd_pool_name + "] Secondary Command Buffer " + cmd_buffer_number; + m_secondary_cmd_bufs.emplace_back(m_device, m_cmd_pool, cmd_buf_name, VK_COMMAND_BUFFER_LEVEL_SECONDARY); + spdlog::trace("Creating command buffer [type=secondary, index={}, pool={}]", cmd_buffer_number, cmd_pool_name); + m_secondary_cmd_bufs.back().set_debug_name(name); + return m_secondary_cmd_bufs.back(); +} - m_cmd_bufs.back()->begin_command_buffer(); - return *m_cmd_bufs.back(); +void CommandPool::wait_for_all_submissions() const { + for (const auto &cmd_buf : m_cmd_bufs) { + if (cmd_buf->was_submitted()) { + cmd_buf->wait_fence(); + } + } } } // namespace inexor::vulkan_renderer::wrapper::commands diff --git a/src/vulkan-renderer/wrapper/debug_callback.cpp b/src/vulkan-renderer/wrapper/debug_callback.cpp index 2d4c257d7..43a0115b2 100644 --- a/src/vulkan-renderer/wrapper/debug_callback.cpp +++ b/src/vulkan-renderer/wrapper/debug_callback.cpp @@ -1,13 +1,18 @@ #include "inexor/vulkan-renderer/wrapper/debug_callback.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/instance.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include namespace inexor::vulkan_renderer::wrapper { +// Using declarations +using tools::InexorException; +using tools::make_info; +using tools::VulkanException; + VulkanDebugUtilsCallback::VulkanDebugUtilsCallback(const Instance &inst, const PFN_vkDebugUtilsMessengerCallbackEXT debug_callback) : m_instance(inst) { @@ -15,15 +20,15 @@ VulkanDebugUtilsCallback::VulkanDebugUtilsCallback(const Instance &inst, // If they are not available, you either forgot to specify VK_EXT_debug_utils instance extension // or your system does not support VK_EXT_debug_utils. if (vkCreateDebugUtilsMessengerEXT == nullptr) { - throw tools::InexorException("Error: vkCreateDebugUtilsMessengerEXT was not found! Make sure to enable " - "VK_EXT_debug_utils instance extension (if available on the system)!"); + throw InexorException("Error: vkCreateDebugUtilsMessengerEXT was not found! Make sure to enable " + "VK_EXT_debug_utils instance extension (if available on the system)!"); } if (vkDestroyDebugUtilsMessengerEXT == nullptr) { - throw tools::InexorException("Error: vkDestroyDebugUtilsMessengerEXT was not found! Make sure to enable " - "VK_EXT_debug_utils instance extension (if available on the system)!"); + throw InexorException("Error: vkDestroyDebugUtilsMessengerEXT was not found! Make sure to enable " + "VK_EXT_debug_utils instance extension (if available on the system)!"); } if (debug_callback == nullptr) { - throw tools::InexorException("Error: Parameter 'debug_callback' is invalid!"); + throw InexorException("Error: Parameter 'debug_callback' is invalid!"); } const auto dbg_messenger_ci = make_info({ @@ -37,10 +42,11 @@ VulkanDebugUtilsCallback::VulkanDebugUtilsCallback(const Instance &inst, }); spdlog::trace("Creating debug utils messenger callback (VK_EXT_debug_utils)"); + if (const auto result = vkCreateDebugUtilsMessengerEXT(m_instance.instance(), &dbg_messenger_ci, nullptr, &m_debug_callback); result != VK_SUCCESS) { - throw tools::VulkanException("Error: vkCreateDebugUtilsMessengerEXT failed!", result); + throw VulkanException("Error: vkCreateDebugUtilsMessengerEXT failed!", result); } } diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_pool.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_pool.cpp new file mode 100644 index 000000000..23830b851 --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_pool.cpp @@ -0,0 +1,44 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorPool::DescriptorPool(const Device &device, std::vector pool_sizes, + const std::uint32_t max_sets, std::string name) + : m_device(device), m_pool_sizes(pool_sizes), m_name(std::move(name)) { + if (m_name.empty()) { + throw InexorException("Error: Parameter 'name' is an empty string!"); + } + if (m_pool_sizes.empty()) { + throw InexorException("Error: Parameter 'pool_sizes' is empty!"); + } + + const auto descriptor_pool_ci = tools::make_info({ + .maxSets = max_sets, + .poolSizeCount = static_cast(m_pool_sizes.size()), + .pPoolSizes = m_pool_sizes.data(), + }); + + if (const auto result = vkCreateDescriptorPool(m_device.device(), &descriptor_pool_ci, nullptr, &m_descriptor_pool); + result != VK_SUCCESS) { + throw VulkanException("Error: vkCreateDescriptorPool failed!", result, m_name); + } + m_device.set_debug_name(m_descriptor_pool, m_name); +} + +DescriptorPool::DescriptorPool(DescriptorPool &&other) noexcept : m_device(other.m_device) { + m_name = std::move(other.m_name); + m_descriptor_pool = std::exchange(other.m_descriptor_pool, VK_NULL_HANDLE); + m_pool_sizes = std::move(other.m_pool_sizes); +} + +DescriptorPool::~DescriptorPool() { + vkDestroyDescriptorPool(m_device.device(), m_descriptor_pool, nullptr); +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.cpp new file mode 100644 index 000000000..e6d37da11 --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.cpp @@ -0,0 +1,41 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_pool_allocator.hpp" + +#include + +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorPoolAllocator::DescriptorPoolAllocator(const Device &device) : m_device(device) {} + +DescriptorPoolAllocator::DescriptorPoolAllocator(DescriptorPoolAllocator &&other) noexcept : m_device(other.m_device) { + m_pools = std::move(other.m_pools); +} + +VkDescriptorPool DescriptorPoolAllocator::request_new_descriptor_pool() { + // TODO: Expose this as a parameter! + // When creating a new descriptor pool we use these pool sizes as default values + // Adapt to other pool types as needed in the future + const std::vector DEFAULT_POOL_SIZES{ + { + .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = 4096, + }, + { + .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = 4096, + }, + }; + + // TODO: Maybe rendergraph can reason about descriptor pool sizes ahead of descriptor pool allocation? + + // When creating a new descriptor pool, we specify a maximum of 1024 descriptor sets to be used + const std::uint32_t DEFAULT_MAX_DESCRIPTOR_COUNT{4096}; + + // This might fail because there's not enough memory left for creating the new descriptor pool + // In this case, DescriptorPool wrapper will throw a VulkanException + return m_pools.emplace_back(m_device, DEFAULT_POOL_SIZES, DEFAULT_MAX_DESCRIPTOR_COUNT, "descriptor pool") + .descriptor_pool(); +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.cpp new file mode 100644 index 000000000..d67718d9b --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.cpp @@ -0,0 +1,73 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_allocator.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorSetAllocator::DescriptorSetAllocator(const Device &device) + : m_device(device), m_descriptor_pool_allocator(device) { + m_current_pool = m_descriptor_pool_allocator.request_new_descriptor_pool(); + if (m_current_pool == VK_NULL_HANDLE) { + throw InexorException("Error: Failed to create descriptor pool!"); + } + m_current_pool_set_allocations = 0; +} + +DescriptorSetAllocator::DescriptorSetAllocator(DescriptorSetAllocator &&other) noexcept + : m_device(other.m_device), m_descriptor_pool_allocator(std::move(other.m_descriptor_pool_allocator)) { + m_current_pool = std::exchange(other.m_current_pool, VK_NULL_HANDLE); +} + +VkDescriptorSet DescriptorSetAllocator::allocate(const std::string &name, + const VkDescriptorSetLayout descriptor_set_layout) { + assert(descriptor_set_layout); + + if (m_current_pool_set_allocations >= MAX_DESCRIPTOR_SETS_PER_POOL) { + m_current_pool = m_descriptor_pool_allocator.request_new_descriptor_pool(); + m_current_pool_set_allocations = 0; + } + + auto descriptor_set_ai = tools::make_info({ + .descriptorPool = m_current_pool, + .descriptorSetCount = 1, + .pSetLayouts = &descriptor_set_layout, + }); + // Attempt to allocate a new descriptor set from the current descriptor pool + VkDescriptorSet new_descriptor_set = VK_NULL_HANDLE; + auto result = vkAllocateDescriptorSets(m_device.device(), &descriptor_set_ai, &new_descriptor_set); + + // Do not throw an exception rightaway if this attempt to allocate failed + // It might be the case that we simply ran out of pool memory for the allocation + if (result == VK_ERROR_OUT_OF_POOL_MEMORY || result == VK_ERROR_FRAGMENTED_POOL) { + spdlog::trace("Requesting new descriptor pool"); + // The allocation failed in the first attempt because we did run out of descriptor pool memory! + // We still have a chance to recover from this: Create a new descriptor pool and then try again! + m_current_pool = m_descriptor_pool_allocator.request_new_descriptor_pool(); + m_current_pool_set_allocations = 0; + // Don't forget we are using the new descriptor pool here that we just created + descriptor_set_ai.descriptorPool = m_current_pool; + // Try again with the new descriptor pool that was just created + result = vkAllocateDescriptorSets(m_device.device(), &descriptor_set_ai, &new_descriptor_set); + } + // This is true if either the first or the second attempt to call vkAllocateDescriptorSets failed + if (result != VK_SUCCESS) { + // All attempts failed, but it's not because we did run out of descriptor pool memory + // If this happens, we have a huge problem and here's nothing we can do anymore + // This is a hint that there is something fundamentally wrong with our descriptor management in the engine! + throw VulkanException("Error: All attempts to call vkAllocateDescriptorSets failed!", result); + } + // Assign an internal debug name to the descriptor set that was just created + m_device.set_debug_name(new_descriptor_set, name); + ++m_current_pool_set_allocations; + // At this point, the allocation did work successfully either because we had enough memory in the first attempt to + // call vkAllocateDescriptorSets or it worked on the second attempt because we created a new descriptor pool + return new_descriptor_set; +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.cpp new file mode 100644 index 000000000..cdc44f21a --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.cpp @@ -0,0 +1,34 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorSetLayout::DescriptorSetLayout(const Device &device, + const VkDescriptorSetLayoutCreateInfo descriptor_set_layout_ci, + std::string name) + : m_device(device), m_name(std::move(name)) { + if (m_name.empty()) { + throw InexorException("Error: Parameter 'name' is an empty string!"); + } + if (const auto result = vkCreateDescriptorSetLayout(m_device.device(), &descriptor_set_layout_ci, nullptr, + &m_descriptor_set_layout); + result != VK_SUCCESS) { + throw VulkanException("Error: vkCreateDescriptorSetLayout failed!", result, m_name); + } + m_device.set_debug_name(m_descriptor_set_layout, m_name); +} + +DescriptorSetLayout::DescriptorSetLayout(DescriptorSetLayout &&other) noexcept : m_device(other.m_device) { + m_name = std::move(other.m_name); + m_descriptor_set_layout = std::exchange(other.m_descriptor_set_layout, VK_NULL_HANDLE); +} + +DescriptorSetLayout::~DescriptorSetLayout() { + vkDestroyDescriptorSetLayout(m_device.device(), m_descriptor_set_layout, nullptr); +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.cpp new file mode 100644 index 000000000..ad86f4888 --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.cpp @@ -0,0 +1,29 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_builder.hpp" + +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorSetLayoutBuilder::DescriptorSetLayoutBuilder(const Device &device) + : m_device(device), m_descriptor_set_layout_cache(device) {} + +VkDescriptorSetLayout DescriptorSetLayoutBuilder::build(std::string name) { + const auto descriptor_set_layout_ci = tools::make_info({ + .bindingCount = static_cast(m_bindings.size()), + .pBindings = m_bindings.data(), + }); + + // Create the descriptor set layout using the descriptor set layout cache + const auto descriptor_set_layout = + m_descriptor_set_layout_cache.create_descriptor_set_layout(descriptor_set_layout_ci, std::move(name)); + + // Reset all the data of the builder so the builder can be re-used + m_bindings.clear(); + m_binding = 0; + + // Return the descriptor set layout that was created + return descriptor_set_layout; +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.cpp b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.cpp new file mode 100644 index 000000000..d88b82570 --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.cpp @@ -0,0 +1,92 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/descriptor_set_layout_cache.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include +#include +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +DescriptorSetLayoutCache::DescriptorSetLayoutCache(const Device &device) : m_device(device) {} + +DescriptorSetLayoutCache::DescriptorSetLayoutCache(DescriptorSetLayoutCache &&other) noexcept + : m_device(other.m_device) { + m_cache = std::move(other.m_cache); +} + +VkDescriptorSetLayout +DescriptorSetLayoutCache::create_descriptor_set_layout(const VkDescriptorSetLayoutCreateInfo descriptor_set_layout_ci, + std::string name) { + DescriptorSetLayoutInfo layout_info; + layout_info.bindings.reserve(descriptor_set_layout_ci.bindingCount); + bool is_sorted = true; + int last_binding = -1; + + // Loop through all bindings and ensure that the bindings are in increasing order + for (std::size_t i = 0; i < descriptor_set_layout_ci.bindingCount; i++) { + // Copy the bindings into layout_info + layout_info.bindings.push_back(descriptor_set_layout_ci.pBindings[i]); + + // Check if the descriptor set layout bindings are sorted by binding + if (descriptor_set_layout_ci.pBindings[i].binding < last_binding) { + last_binding = descriptor_set_layout_ci.pBindings[i].binding; + } else { + is_sorted = false; + // As soon as we know it's not sorted, we can stop and start sorting + break; + } + } + // We need to make sure the bindings are sorted because this is important for the hash! + if (!is_sorted) { + std::sort(layout_info.bindings.begin(), layout_info.bindings.end(), [](auto &a, auto &b) { + return a.binding < b.binding; // Sort by binding + }); + } + + // Check if this descriptor set layout does already exist in the cache + if (!m_cache.contains(layout_info)) { + m_cache.emplace(layout_info, DescriptorSetLayout(m_device, descriptor_set_layout_ci, std::move(name))); + } + // TODO: Name descriptor set layout internally! + return m_cache.at(layout_info).m_descriptor_set_layout; +} + +bool DescriptorSetLayoutInfo::operator==(const DescriptorSetLayoutInfo &other) const { + if (other.bindings.size() != bindings.size()) { + return false; + } + // Check if each of the bindings is the same + // Note that we assume the bindings are sorted! + for (std::size_t i = 0; i < bindings.size(); i++) { + if (other.bindings[i].binding != bindings[i].binding) { + return false; + } + if (other.bindings[i].descriptorType != bindings[i].descriptorType) { + return false; + } + if (other.bindings[i].descriptorCount != bindings[i].descriptorCount) { + return false; + } + if (other.bindings[i].stageFlags != bindings[i].stageFlags) { + return false; + } + } + return true; +} + +std::size_t DescriptorSetLayoutInfo::hash() const { + assert(!bindings.empty()); + std::size_t result = std::hash()(bindings.size()); + for (const auto &binding : bindings) { + // Pack binding data into 64 bits + std::size_t binding_hash = + binding.binding | binding.descriptorType << 8 | binding.descriptorCount << 16 | binding.stageFlags << 24; + // shuffle the packed binding data and xor it with the main hash + result ^= std::hash()(binding_hash); + } + return result; +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.cpp b/src/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.cpp new file mode 100644 index 000000000..b3b8bf1fd --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.cpp @@ -0,0 +1,3 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" + +namespace inexor::vulkan_renderer::wrapper::descriptors {} diff --git a/src/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.cpp b/src/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.cpp new file mode 100644 index 000000000..8ed6e362b --- /dev/null +++ b/src/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.cpp @@ -0,0 +1,17 @@ +#include "inexor/vulkan-renderer/wrapper/descriptors/write_descriptor_set_builder.hpp" + +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +namespace inexor::vulkan_renderer::wrapper::descriptors { + +WriteDescriptorSetBuilder::WriteDescriptorSetBuilder(const Device &device) : m_device(device) {} + +std::vector WriteDescriptorSetBuilder::build() { + std::vector write_descriptor_sets; + write_descriptor_sets.swap(m_write_descriptor_sets); + return write_descriptor_sets; +} + +} // namespace inexor::vulkan_renderer::wrapper::descriptors diff --git a/src/vulkan-renderer/wrapper/device.cpp b/src/vulkan-renderer/wrapper/device.cpp index 46b84b050..94803f5ab 100644 --- a/src/vulkan-renderer/wrapper/device.cpp +++ b/src/vulkan-renderer/wrapper/device.cpp @@ -2,9 +2,10 @@ #include "inexor/vulkan-renderer/tools/device_info.hpp" #include "inexor/vulkan-renderer/tools/enumerate.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/tools/queue_selection.hpp" #include "inexor/vulkan-renderer/wrapper/instance.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp" #define VMA_DEBUG_MARGIN 16 #define VMA_DEBUG_DETECT_CORRUPTION 1 @@ -13,10 +14,56 @@ #include +#include +#include #include namespace inexor::vulkan_renderer::wrapper { +/// Convert a DebugLabelColor to a rgba value +/// @param color The debug label color +/// @return The converted rgba values +std::array get_debug_label_color(const DebugLabelColor color) { + switch (color) { + case DebugLabelColor::RED: + return {0.98f, 0.60f, 0.60f, 1.0f}; + case DebugLabelColor::BLUE: + return {0.68f, 0.85f, 0.90f, 1.0f}; + case DebugLabelColor::GREEN: + return {0.73f, 0.88f, 0.73f, 1.0f}; + case DebugLabelColor::YELLOW: + return {0.98f, 0.98f, 0.70f, 1.0f}; + case DebugLabelColor::PURPLE: + return {0.80f, 0.70f, 0.90f, 1.0f}; + case DebugLabelColor::ORANGE: + return {0.98f, 0.75f, 0.53f, 1.0f}; + case DebugLabelColor::MAGENTA: + return {0.96f, 0.60f, 0.76f, 1.0f}; + case DebugLabelColor::CYAN: + return {0.70f, 0.98f, 0.98f, 1.0f}; + case DebugLabelColor::BROWN: + return {0.82f, 0.70f, 0.55f, 1.0f}; + case DebugLabelColor::PINK: + return {0.98f, 0.75f, 0.85f, 1.0f}; + case DebugLabelColor::LIME: + return {0.80f, 0.98f, 0.60f, 1.0f}; + case DebugLabelColor::TURQUOISE: + return {0.70f, 0.93f, 0.93f, 1.0f}; + case DebugLabelColor::BEIGE: + return {0.96f, 0.96f, 0.86f, 1.0f}; + case DebugLabelColor::MAROON: + return {0.76f, 0.50f, 0.50f, 1.0f}; + case DebugLabelColor::OLIVE: + return {0.74f, 0.75f, 0.50f, 1.0f}; + case DebugLabelColor::NAVY: + return {0.53f, 0.70f, 0.82f, 1.0f}; + case DebugLabelColor::TEAL: + return {0.53f, 0.80f, 0.75f, 1.0f}; + default: + return {0.0f, 0.0f, 0.0f, 1.0f}; // Default to opaque black if the color is not recognized + } +} + Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysicalDevice desired_gpu, const VkPhysicalDeviceFeatures &required_features, const std::span required_extensions) : m_enabled_features(required_features) { @@ -44,9 +91,10 @@ Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysica } // Get the device properties - VkPhysicalDeviceProperties device_properties{}; - vkGetPhysicalDeviceProperties(m_physical_device, &device_properties); - std::memcpy(m_pipeline_cache_uuid.data(), device_properties.pipelineCacheUUID, VK_UUID_SIZE); + VkPhysicalDeviceProperties2 device_properties2{}; + device_properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + vkGetPhysicalDeviceProperties2(m_physical_device, &device_properties2); + std::memcpy(m_pipeline_cache_uuid.data(), device_properties2.properties.pipelineCacheUUID, VK_UUID_SIZE); spdlog::trace("Creating Vulkan queues"); @@ -69,11 +117,60 @@ Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysica // Store the enabled features. m_enabled_features = required_features; + std::vector enabled_extensions(required_extensions.begin(), required_extensions.end()); + const auto available_extensions = tools::get_extension_properties(m_physical_device); + const bool memory_priority_ext_supported = + tools::is_extension_supported(available_extensions, VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME); + bool memory_priority_feature_supported = false; + if (memory_priority_ext_supported) { + VkPhysicalDeviceMemoryPriorityFeaturesEXT memory_priority_features{}; + memory_priority_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PRIORITY_FEATURES_EXT; + + VkPhysicalDeviceFeatures2 features2{}; + features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features2.pNext = &memory_priority_features; + vkGetPhysicalDeviceFeatures2(m_physical_device, &features2); + + memory_priority_feature_supported = (memory_priority_features.memoryPriority == VK_TRUE); + } + + const bool memory_priority_supported = memory_priority_ext_supported && memory_priority_feature_supported; + if (memory_priority_supported) { + const auto not_already_enabled = std::find(enabled_extensions.begin(), enabled_extensions.end(), + VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME) == enabled_extensions.end(); + if (not_already_enabled) { + enabled_extensions.push_back(VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME); + } + } + + // We want to use synchronization2 for vkCmdPipelineBarrier2. + VkPhysicalDeviceSynchronization2Features sync2_feature{}; + sync2_feature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES; + sync2_feature.pNext = nullptr; + sync2_feature.synchronization2 = VK_TRUE; + + VkPhysicalDeviceMemoryPriorityFeaturesEXT memory_priority_feature{}; + memory_priority_feature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PRIORITY_FEATURES_EXT; + memory_priority_feature.pNext = nullptr; + memory_priority_feature.memoryPriority = memory_priority_supported ? VK_TRUE : VK_FALSE; + + sync2_feature.pNext = memory_priority_supported ? &memory_priority_feature : nullptr; + + using tools::make_info; + + // We want to use dynamic rendering (VK_KHR_dynamic_rendering). + auto dyn_rendering_feature = make_info({ + .pNext = &sync2_feature, + .dynamicRendering = VK_TRUE, + }); + const auto device_ci = make_info({ + // This is one of those rare cases where pNext is actually not nullptr! + .pNext = &dyn_rendering_feature, // We use dynamic rendering .queueCreateInfoCount = static_cast(optimal_queues.queues_to_create.size()), .pQueueCreateInfos = optimal_queues.queues_to_create.data(), - .enabledExtensionCount = static_cast(required_extensions.size()), - .ppEnabledExtensionNames = required_extensions.data(), + .enabledExtensionCount = static_cast(enabled_extensions.size()), + .ppEnabledExtensionNames = enabled_extensions.data(), .pEnabledFeatures = &m_enabled_features, }); @@ -114,43 +211,22 @@ Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysica // We already checked earlier if the graphics queue family index is not std::nullopt. vkGetDeviceQueue(m_device, m_graphics_queue_family_index.value(), 0, &m_graphics_queue); + set_debug_name(m_graphics_queue, "m_graphics_queue"); // Do we have any queue for compute? if (m_compute_queue_family_index) { vkGetDeviceQueue(m_device, m_compute_queue_family_index.value(), 0, &m_compute_queue); + set_debug_name(m_compute_queue, "m_compute_queue"); } // Do we have any queue for compute? if (m_transfer_queue_family_index) { vkGetDeviceQueue(m_device, m_transfer_queue_family_index.value(), 0, &m_transfer_queue); + set_debug_name(m_transfer_queue, "m_transfer_queue"); } // Do we have any queue for sparse binding? if (m_sparse_binding_queue_family_index) { vkGetDeviceQueue(m_device, m_sparse_binding_queue_family_index.value(), 0, &m_sparse_binding_queue); - } - - // Check if compute or transfer queue can be used for presenting - if (m_compute_queue_family_index && is_presentation_supported(surface, m_compute_queue_family_index.value())) { - spdlog::trace("Using compute queue [queue family index: {}] for vkQueuePresentKHR", - m_compute_queue_family_index.value()); - m_present_queue = m_compute_queue; - } else if (m_transfer_queue_family_index && - is_presentation_supported(surface, m_transfer_queue_family_index.value())) { - spdlog::trace("Using transfer queue [queue family index: {}] for vkQueuePresentKHR", - m_transfer_queue_family_index.value()); - m_present_queue = m_transfer_queue; - } else if (m_sparse_binding_queue_family_index && - is_presentation_supported(surface, m_sparse_binding_queue_family_index.value())) { - spdlog::trace("Using sparse binding queue [queue family index: {}] for vkQueuePresentKHR", - m_sparse_binding_queue_family_index.value()); - m_present_queue = m_transfer_queue; - } else { - if (!is_presentation_supported(surface, m_graphics_queue_family_index.value())) { - // This should be extremely unlikely. - throw std::runtime_error("Error: Graphics queue does not support present on GPU '" + m_gpu_name + "'"); - } - spdlog::trace("Using graphics queue [queue family index: {}] for vkQueuePresentKHR", - m_graphics_queue_family_index.value()); - m_present_queue = m_graphics_queue; + set_debug_name(m_sparse_binding_queue, "m_sparse_binding_queue"); } VmaVulkanFunctions vma_vk_functions{ @@ -158,13 +234,16 @@ Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysica .vkGetDeviceProcAddr = vkGetDeviceProcAddr, }; - const VmaAllocatorCreateInfo vma_ci{ + auto vma_ci = VmaAllocatorCreateInfo{ .physicalDevice = m_physical_device, .device = m_device, .pVulkanFunctions = &vma_vk_functions, .instance = inst.instance(), .vulkanApiVersion = Instance::REQUIRED_VK_API_VERSION, }; + if (memory_priority_supported) { + vma_ci.flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT; + } spdlog::trace("Creating instance of Vulkan Memory Allocator (VMA)"); if (const auto result = vmaCreateAllocator(&vma_ci, &m_allocator); result != VK_SUCCESS) { @@ -172,29 +251,39 @@ Device::Device(const Instance &inst, const VkSurfaceKHR surface, const VkPhysica vkDestroyDevice(m_device, nullptr); throw VulkanException("Error: vmaCreateAllocator failed!", result); } -} -Device::Device(Device &&other) noexcept : m_cmd_pools(std::move(other.m_cmd_pools)) { - // After moving the cmd_pools, the memory is in a valid but unspecified state. - // By calling .clear(), we bring it back into a defined state again. - // This is not strictly necessary, but let's be safe in case we use it again. - other.m_cmd_pools.clear(); + m_pipeline_cache = std::make_unique(*this); - // TODO: Check me - m_device = std::exchange(other.m_device, nullptr); - m_physical_device = std::exchange(other.m_physical_device, nullptr); + // Create command pools at here instead of allocating them lazily at first request during runtime + get_thread_command_pool(VK_QUEUE_GRAPHICS_BIT); + get_thread_command_pool(VK_QUEUE_TRANSFER_BIT); + get_thread_command_pool(VK_QUEUE_COMPUTE_BIT); + get_thread_command_pool(VK_QUEUE_SPARSE_BINDING_BIT); } Device::~Device() { std::scoped_lock locker(m_mutex); - + // Wait for the device to complete ongoing work + spdlog::trace("Device::~Device begin"); + wait_idle(); // Because the device handle must be valid for the destruction of the command pools in the CommandPool destructor, // we must destroy the command pools manually here in order to ensure the right order of destruction m_cmd_pools.clear(); - + // Dump detailed allocator stats before destruction so leaking allocations can be identified by name. + char *vma_stats_string = nullptr; + vmaBuildStatsString(m_allocator, &vma_stats_string, VK_TRUE); + if (vma_stats_string != nullptr) { + spdlog::warn("VMA allocator stats before destruction:\n{}", vma_stats_string); + vmaFreeStatsString(m_allocator, vma_stats_string); + } // Now that we destroyed the command pools, we can destroy the allocator and finally the device itself + spdlog::trace("Device::~Device destroying VMA allocator"); vmaDestroyAllocator(m_allocator); + // Shutdown pipeline cache + m_pipeline_cache.reset(); + // Destroy the device vkDestroyDevice(m_device, nullptr); + spdlog::trace("Device::~Device end"); } bool Device::is_presentation_supported(const VkSurfaceKHR surface, const std::uint32_t queue_family_index) const { @@ -222,80 +311,149 @@ bool Device::surface_supports_usage(const VkSurfaceKHR surface, const VkImageUsa return (capabilities.supportedUsageFlags & usage) != 0u; } -void Device::execute(const std::string &name, const VulkanQueueType queue_type, - const std::function &cmd_lambda) const { - const auto &cmd_buf = get_thread_command_pool(queue_type).request_command_buffer(name); - cmd_lambda(cmd_buf); - cmd_buf.submit_and_wait(); +VkFence Device::execute(const VkQueueFlagBits queue_type, const DebugLabelColor dbg_label_color, + const std::function &on_record, + const std::span wait_semaphores, + const std::span signal_semaphores, + const std::source_location source_location) const { + const auto &cmd_buf = get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphores); + return cmd_buf.submission_fence(); +} + +VkFence Device::execute(const VkQueueFlagBits queue_type, const DebugLabelColor dbg_label_color, + const std::function &on_record, + const std::span wait_semaphores, + const std::span signal_semaphore_infos, + const std::source_location source_location) const { + const auto &cmd_buf = get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphore_infos); + return cmd_buf.submission_fence(); } -CommandPool &Device::get_thread_command_pool(const VulkanQueueType queue_type) const { - // Note that thread_local means that it is implicitely static! +VkFence Device::execute(const VkQueueFlagBits queue_type, const DebugLabelColor dbg_label_color, + const std::function &on_record, + const std::span wait_semaphores, + const std::span signal_semaphores, + const std::source_location source_location) const { + const auto &cmd_buf = get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphores); + return cmd_buf.submission_fence(); +} + +VkFence Device::execute(const VkQueueFlagBits queue_type, const DebugLabelColor dbg_label_color, + const std::function &on_record, + const std::span wait_semaphores, + const std::span signal_semaphore_infos, + const std::source_location source_location) const { + const auto &cmd_buf = get_thread_command_pool(queue_type).request_command_buffer(source_location.function_name()); + cmd_buf.begin_debug_label_region(source_location.function_name(), get_debug_label_color(dbg_label_color)); + std::invoke(on_record, cmd_buf); + cmd_buf.end_debug_label_region(); + cmd_buf.end_command_buffer(); + + cmd_buf.submit(queue_type, wait_semaphores, signal_semaphore_infos); + return cmd_buf.submission_fence(); +} + +CommandPool &Device::get_thread_command_pool(const VkQueueFlagBits queue_type) const { + // NOTE: thread_local keyword means that it is implicitely static! thread_local CommandPool *thread_graphics_cmd_pool = nullptr; // NOLINT thread_local CommandPool *thread_compute_cmd_pool = nullptr; // NOLINT thread_local CommandPool *thread_transfer_cmd_pool = nullptr; // NOLINT thread_local CommandPool *thread_sparse_binding_cmd_pool = nullptr; // NOLINT switch (queue_type) { - case VulkanQueueType::QUEUE_TYPE_GRAPHICS: { + case VK_QUEUE_GRAPHICS_BIT: { if (thread_graphics_cmd_pool == nullptr) { - // Note that we checked during construction that there is a valid queue family index for graphics. - // There is no need for additional error checking here. - auto cmd_pool = - std::make_unique(*this, m_graphics_queue_family_index.value(), "graphics command pool"); + // NOTE: We checked earlier for a valid queue family index for graphics so no error checks required here. + auto cmd_pool = std::make_unique(*this, queue_type, m_graphics_queue_family_index.value(), + "thread_graphics_cmd_pool"); std::unique_lock lock(m_mutex); thread_graphics_cmd_pool = m_cmd_pools.emplace_back(std::move(cmd_pool)).get(); } std::shared_lock lock(m_mutex); return *thread_graphics_cmd_pool; } - case VulkanQueueType::QUEUE_TYPE_COMPUTE: { + case VK_QUEUE_COMPUTE_BIT: { if (thread_compute_cmd_pool == nullptr) { if (!has_any_compute_queue()) { throw std::runtime_error("Error: GPU '" + m_gpu_name + "' has no compute queue!"); } - auto cmd_pool = - std::make_unique(*this, m_compute_queue_family_index.value(), "compute command pool"); + auto cmd_pool = std::make_unique(*this, queue_type, m_compute_queue_family_index.value(), + "thread_compute_cmd_pool"); std::unique_lock lock(m_mutex); thread_compute_cmd_pool = m_cmd_pools.emplace_back(std::move(cmd_pool)).get(); } std::shared_lock lock(m_mutex); return *thread_compute_cmd_pool; } - case VulkanQueueType::QUEUE_TYPE_TRANSFER: { + case VK_QUEUE_TRANSFER_BIT: { if (thread_transfer_cmd_pool == nullptr) { if (!has_any_transfer_queue()) { throw std::runtime_error("Error: GPU '" + m_gpu_name + "' has no transfer queue!"); } - auto cmd_pool = - std::make_unique(*this, m_transfer_queue_family_index.value(), "transfer command pool"); + auto cmd_pool = std::make_unique(*this, queue_type, m_transfer_queue_family_index.value(), + "thread_transfer_cmd_pool"); std::unique_lock lock(m_mutex); thread_transfer_cmd_pool = m_cmd_pools.emplace_back(std::move(cmd_pool)).get(); } std::shared_lock lock(m_mutex); return *thread_transfer_cmd_pool; } - case VulkanQueueType::QUEUE_TYPE_SPARSE_BINDING: { - if (thread_transfer_cmd_pool == nullptr) { + case VK_QUEUE_SPARSE_BINDING_BIT: { + if (thread_sparse_binding_cmd_pool == nullptr) { if (!has_any_sparse_binding_queue()) { throw std::runtime_error("Error: GPU '" + m_gpu_name + "' has no sparse binding queue!"); } - auto cmd_pool = std::make_unique(*this, m_sparse_binding_queue_family_index.value(), - "sparse binding command pool"); + auto cmd_pool = std::make_unique( + *this, queue_type, m_sparse_binding_queue_family_index.value(), "thread_sparse_binding_cmd_pool"); std::unique_lock lock(m_mutex); - thread_transfer_cmd_pool = m_cmd_pools.emplace_back(std::move(cmd_pool)).get(); + thread_sparse_binding_cmd_pool = m_cmd_pools.emplace_back(std::move(cmd_pool)).get(); } std::shared_lock lock(m_mutex); - return *thread_transfer_cmd_pool; + return *thread_sparse_binding_cmd_pool; } } throw std::runtime_error("Error: Unknown VuklkanQueueType!"); } -const CommandBuffer &Device::request_command_buffer(const VulkanQueueType queue_type, const std::string &name) { +VkPipelineCache Device::pipeline_cache() const { + return m_pipeline_cache->cache(); +} + +const CommandBuffer &Device::request_command_buffer(const VkQueueFlagBits queue_type, const std::string &name) { return get_thread_command_pool(queue_type).request_command_buffer(name); } +const CommandBuffer &Device::request_secondary_command_buffer(const VkQueueFlagBits queue_type, + const std::string &name) { + return get_thread_command_pool(queue_type).request_secondary_command_buffer(name); +} + +void Device::wait_for_submissions(const VkQueueFlagBits queue_type) const { + get_thread_command_pool(queue_type).wait_for_all_submissions(); +} + +void Device::update_descriptor_sets(const std::span write_descriptor_sets) { + // NOTE: No error checks are required here because this function is of type void + vkUpdateDescriptorSets(m_device, static_cast(write_descriptor_sets.size()), + write_descriptor_sets.data(), 0, nullptr); +} + void Device::wait_idle(const VkQueue queue) const { if (queue == VK_NULL_HANDLE) { if (const auto result = vkDeviceWaitIdle(m_device); result != VK_SUCCESS) { diff --git a/src/vulkan-renderer/wrapper/images/image.cpp b/src/vulkan-renderer/wrapper/images/image.cpp new file mode 100644 index 000000000..23b58f4a6 --- /dev/null +++ b/src/vulkan-renderer/wrapper/images/image.cpp @@ -0,0 +1,72 @@ +#include "inexor/vulkan-renderer/wrapper/images/image.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +namespace inexor::vulkan_renderer::wrapper::images { + +Image::Image(const Device &device, std::string name) : m_device(device), m_name(std::move(name)) {} + +Image::Image(Image &&other) noexcept : m_device(other.m_device) { + m_name = std::move(other.m_name); + m_img = std::exchange(other.m_img, VK_NULL_HANDLE); + m_img_view = std::exchange(other.m_img_view, VK_NULL_HANDLE); + m_alloc = std::exchange(other.m_alloc, VK_NULL_HANDLE); + m_alloc_info = other.m_alloc_info; +} + +Image::~Image() { + destroy(); +} + +void Image::create(VkImageCreateInfo img_ci, VkImageViewCreateInfo img_view_ci) { + m_img_ci = std::move(img_ci); + m_img_view_ci = std::move(img_view_ci); + + float priority = 0.7f; + if ((m_img_ci.usage & (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_STORAGE_BIT)) != 0u) { + priority = 1.0f; + } + + const VmaAllocationCreateInfo alloc_ci{ + .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, + .priority = priority, + }; + + // Create the image + if (const auto result = vmaCreateImage(m_device.allocator(), &m_img_ci, &alloc_ci, &m_img, &m_alloc, &m_alloc_info); + result != VK_SUCCESS) { + throw VulkanException("Error: vmaCreateImage failed!", result, m_name); + } + m_device.set_debug_name(m_img, m_name); + + // Set the internal debug name of the image in Vulkan Memory Allocator (VMA) + vmaSetAllocationName(m_device.allocator(), m_alloc, m_name.c_str()); + + // Set the image in the VkImageViewCreateInfo + m_img_view_ci.image = m_img; + + // Create the image view + if (const auto result = vkCreateImageView(m_device.device(), &m_img_view_ci, nullptr, &m_img_view); + result != VK_SUCCESS) { + throw VulkanException("Error: vkCreateImageView failed!", result, m_name); + } + m_device.set_debug_name(m_img_view, m_name); +} + +void Image::destroy() { + if (m_img_view == VK_NULL_HANDLE && m_img == VK_NULL_HANDLE && m_alloc == VK_NULL_HANDLE) { + return; + } + + vkDestroyImageView(m_device.device(), m_img_view, nullptr); + m_img_view = VK_NULL_HANDLE; + vmaDestroyImage(m_device.allocator(), m_img, m_alloc); + m_img = VK_NULL_HANDLE; + m_alloc = VK_NULL_HANDLE; +} + +} // namespace inexor::vulkan_renderer::wrapper::images diff --git a/src/vulkan-renderer/wrapper/images/image_view.cpp b/src/vulkan-renderer/wrapper/images/image_view.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/vulkan-renderer/wrapper/images/sampler.cpp b/src/vulkan-renderer/wrapper/images/sampler.cpp new file mode 100644 index 000000000..ab748c90b --- /dev/null +++ b/src/vulkan-renderer/wrapper/images/sampler.cpp @@ -0,0 +1,28 @@ +#include "inexor/vulkan-renderer/wrapper/images/sampler.hpp" + +#include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" + +#include + +namespace inexor::vulkan_renderer::wrapper::images { + +Sampler::Sampler(const Device &device, std::string name, const VkSamplerCreateInfo &sampler_ci) + : m_device(device), m_name(std::move(name)) { + if (const auto result = vkCreateSampler(m_device.device(), &sampler_ci, nullptr, &m_sampler); + result != VK_SUCCESS) { + throw VulkanException("Error: vkCreateSampler failed!", result, m_name); + } + m_device.set_debug_name(m_sampler, m_name); +} + +Sampler::~Sampler() { + vkDestroySampler(m_device.device(), m_sampler, nullptr); +} + +Sampler::Sampler(Sampler &&other) noexcept : m_device(other.m_device) { + m_sampler = std::exchange(other.m_sampler, VK_NULL_HANDLE); + m_name = std::move(other.m_name); +} + +} // namespace inexor::vulkan_renderer::wrapper::images diff --git a/src/vulkan-renderer/wrapper/instance.cpp b/src/vulkan-renderer/wrapper/instance.cpp index 9eaa91430..670025e26 100644 --- a/src/vulkan-renderer/wrapper/instance.cpp +++ b/src/vulkan-renderer/wrapper/instance.cpp @@ -3,7 +3,7 @@ #include "inexor/vulkan-renderer/meta/meta.hpp" #include "inexor/vulkan-renderer/tools/enumerate.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include #include @@ -14,6 +14,7 @@ namespace inexor::vulkan_renderer::wrapper { // Using declarations using tools::InexorException; +using tools::make_info; using tools::VulkanException; bool is_instance_extension_supported(const std::string &extension_name) { diff --git a/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp b/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp index 509a4f2a5..5a178c3b0 100644 --- a/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp +++ b/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline.cpp @@ -1,47 +1,127 @@ #include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" -#include "inexor/vulkan-renderer/render_graph.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/wrapper/descriptors/per_frame_descriptor_sets.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp" +#include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp" + +#include #include namespace inexor::vulkan_renderer::wrapper::pipelines { -GraphicsPipeline::GraphicsPipeline(const Device &device, const PipelineCache &pipeline_cache, - std::span descriptor_set_layouts, - std::span push_constant_ranges, - VkGraphicsPipelineCreateInfo pipeline_ci, std::string name) +GraphicsPipeline::GraphicsPipeline(const Device &device, GraphicsPipelineSetupData setup_data, std::string name) : m_device(device), m_name(std::move(name)) { - // @TODO Should the pipeline layout really be part of graphics pipeline wrapper? + spdlog::trace(" - Building graphics pipeline [{}]", m_name); + + // NOTE: It's important to fill VkGraphicsPipelineCreateInfo in the constructor of GraphicsPipeline! + // GraphicsPipeline wrapper is responsible for keeping the memory alive which is in VkGraphicsPipelineCreateInfo! + // If we would store GraphicsPipelineSetupData in the graphics pipeline builder, the created graphics pipeline would + // become invalidate as soon as the graphics pipeline builder finishes building the pipeline because this will reset + // the graphics pipeline builder so it can be re-used. This is one of the small but extremely important things to + // keep in mind when dealing with Vulkan API: object lifetime! + + setup_data.pipeline_rendering_ci = make_info({ + // TODO: Support multiview rendering and expose viewMask parameter + .colorAttachmentCount = static_cast(setup_data.color_attachments.size()), + .pColorAttachmentFormats = setup_data.color_attachments.data(), + .depthAttachmentFormat = setup_data.depth_attachment_format, + .stencilAttachmentFormat = setup_data.stencil_attachment_format, + }); + + setup_data.vertex_input_sci = make_info({ + .vertexBindingDescriptionCount = + static_cast(setup_data.vertex_input_binding_descriptions.size()), + .pVertexBindingDescriptions = setup_data.vertex_input_binding_descriptions.data(), + .vertexAttributeDescriptionCount = + static_cast(setup_data.vertex_input_attribute_descriptions.size()), + .pVertexAttributeDescriptions = setup_data.vertex_input_attribute_descriptions.data(), + + }); + + setup_data.viewport_sci = make_info({ + .viewportCount = static_cast(setup_data.viewports.size()), + .pViewports = setup_data.viewports.data(), + .scissorCount = static_cast(setup_data.scissors.size()), + .pScissors = setup_data.scissors.data(), + }); + + // If we use VK_DYNAMIC_STATE_SCISSOR, we set scissor count to 1 + /// @TODO Implement multiple dynamic scissors with VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT + if (std::find(setup_data.dynamic_states.begin(), setup_data.dynamic_states.end(), VK_DYNAMIC_STATE_SCISSOR) != + setup_data.dynamic_states.end()) { + // Even with scissor as dynamic state enabled, there must be at least one scissor here! + setup_data.viewport_sci.scissorCount = 1; + setup_data.viewport_sci.pScissors = nullptr; + } + + // If we use VK_DYNAMIC_STATE_VIEWPORT, we set scissor count to 1 + /// @TODO Implement multiple dynamic viewports with VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT + if (std::find(setup_data.dynamic_states.begin(), setup_data.dynamic_states.end(), VK_DYNAMIC_STATE_VIEWPORT) != + setup_data.dynamic_states.end()) { + // Even with viewport as dynamic state enabled, there must be at least one viewport here! + setup_data.viewport_sci.viewportCount = 1; + setup_data.viewport_sci.pViewports = nullptr; + } + + setup_data.color_blend_sci = make_info({ + .attachmentCount = static_cast(setup_data.color_blend_attachment_states.size()), + .pAttachments = setup_data.color_blend_attachment_states.data(), + }); + + setup_data.dynamic_states_sci = make_info({ + .dynamicStateCount = static_cast(setup_data.dynamic_states.size()), + .pDynamicStates = setup_data.dynamic_states.data(), + }); - // Create the graphics pipeline layout - m_pipeline_layout = std::make_unique(m_device, m_name, std::move(descriptor_set_layouts), - std::move(push_constant_ranges)); + // @TODO Expose the pipeline layout as parameter + m_pipeline_layout = std::make_unique(m_device, m_name, setup_data.descriptor_set_layouts, + setup_data.push_constant_ranges); + + for (const auto &descriptor_set : setup_data.associated_descriptor_sets) { + const auto descriptor_set_ref = descriptor_set.lock(); + if (!descriptor_set_ref) { + throw InexorException("Error: Associated descriptor set is invalid!"); + } + descriptor_set_ref->set_pipeline_layout(m_pipeline_layout->pipeline_layout()); + } - // Set the pipeline layout in the pipeline create info struct - pipeline_ci.layout = m_pipeline_layout->m_pipeline_layout; + auto pipeline_ci = make_info({ + // NOTE: This is one of those rare cases where pNext is actually not nullptr! + .pNext = &setup_data.pipeline_rendering_ci, + .stageCount = static_cast(setup_data.shader_stages.size()), + .pStages = setup_data.shader_stages.data(), + .pVertexInputState = &setup_data.vertex_input_sci, + .pInputAssemblyState = &setup_data.input_assembly_sci, + .pTessellationState = &setup_data.tesselation_sci, + .pViewportState = &setup_data.viewport_sci, + .pRasterizationState = &setup_data.rasterization_sci, + .pMultisampleState = &setup_data.multisample_sci, + .pDepthStencilState = &setup_data.depth_stencil_sci, + .pColorBlendState = &setup_data.color_blend_sci, + .pDynamicState = &setup_data.dynamic_states_sci, + .layout = m_pipeline_layout->pipeline_layout(), + .renderPass = VK_NULL_HANDLE, + }); - if (const auto result = vkCreateGraphicsPipelines(m_device.device(), pipeline_cache.m_pipeline_cache, 1, - &pipeline_ci, nullptr, &m_pipeline); + // Create the graphics pipeline + if (const auto result = vkCreateGraphicsPipelines(m_device.device(), m_device.pipeline_cache(), 1, &pipeline_ci, + nullptr, &m_pipeline); result != VK_SUCCESS) { throw VulkanException("Error: vkCreateGraphicsPipelines failed!", result, m_name); } m_device.set_debug_name(m_pipeline, m_name); } -GraphicsPipeline::GraphicsPipeline(GraphicsPipeline &&other) noexcept : m_device(other.m_device) { - // TODO: Check me! - m_pipeline = std::exchange(other.m_pipeline, VK_NULL_HANDLE); - m_pipeline_layout = std::exchange(other.m_pipeline_layout, nullptr); - m_name = std::move(other.m_name); -} - GraphicsPipeline::~GraphicsPipeline() { vkDestroyPipeline(m_device.device(), m_pipeline, nullptr); } +VkPipelineLayout GraphicsPipeline::pipeline_layout() const { + return m_pipeline_layout->pipeline_layout(); +} + } // namespace inexor::vulkan_renderer::wrapper::pipelines diff --git a/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.cpp b/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.cpp index 1c7cd9351..c544abd91 100644 --- a/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.cpp +++ b/src/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.cpp @@ -1,109 +1,20 @@ #include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline_builder.hpp" -#include "inexor/vulkan-renderer/tools/exception.hpp" -#include "inexor/vulkan-renderer/wrapper/device.hpp" - -#include - namespace inexor::vulkan_renderer::wrapper::pipelines { -// Using declaration -using wrapper::InexorException; - -GraphicsPipelineBuilder::GraphicsPipelineBuilder(const Device &device, const PipelineCache &pipeline_cache) - : m_device(device), m_pipeline_cache(pipeline_cache) { +GraphicsPipelineBuilder::GraphicsPipelineBuilder(const Device &device) : m_device(device) { reset(); } -GraphicsPipelineBuilder::GraphicsPipelineBuilder(GraphicsPipelineBuilder &&other) noexcept - : m_device(other.m_device), m_pipeline_cache(other.m_pipeline_cache) { - // TODO: Check me! - m_pipeline_rendering_ci = std::move(other.m_pipeline_rendering_ci); - m_color_attachments = std::move(other.m_color_attachments); - m_depth_attachment_format = other.m_depth_attachment_format; - m_stencil_attachment_format = other.m_stencil_attachment_format; - m_shader_stages = std::move(other.m_shader_stages); - m_vertex_input_binding_descriptions = std::move(other.m_vertex_input_binding_descriptions); - m_vertex_input_attribute_descriptions = std::move(other.m_vertex_input_attribute_descriptions); - m_vertex_input_sci = std::move(other.m_vertex_input_sci); - m_input_assembly_sci = std::move(other.m_input_assembly_sci); - m_tesselation_sci = std::move(other.m_tesselation_sci); - m_viewport_sci = std::move(other.m_viewport_sci); - m_viewports = std::move(other.m_viewports); - m_scissors = std::move(other.m_scissors); - m_rasterization_sci = std::move(m_rasterization_sci); - m_multisample_sci = std::move(other.m_multisample_sci); - m_depth_stencil_sci = std::move(other.m_depth_stencil_sci); - m_color_blend_sci = std::move(other.m_color_blend_sci); - m_dynamic_states = std::move(other.m_dynamic_states); - m_dynamic_states_sci = std::move(other.m_dynamic_states_sci); - m_pipeline_layout = std::exchange(other.m_pipeline_layout, VK_NULL_HANDLE); - m_color_blend_attachment_states = std::move(other.m_color_blend_attachment_states); -} - std::shared_ptr GraphicsPipelineBuilder::build(std::string name) { if (name.empty()) { throw InexorException("Error: Parameter 'name' is an empty string!"); } - // NOTE: Inside of GraphicsPipelineBuilder, we carry out no error checks when it comes to the data which is used to - // build the graphics pipeline. This is because validation of this data is job of the validation layers, and not the - // job of GraphicsPipelineBuilder. We should not mimic the behavious of validation layers here. - - m_pipeline_rendering_ci = make_info({ - // TODO: Support multiview rendering and expose viewMask parameter - .colorAttachmentCount = static_cast(m_color_attachments.size()), - .pColorAttachmentFormats = m_color_attachments.data(), - .depthAttachmentFormat = m_depth_attachment_format, - .stencilAttachmentFormat = m_stencil_attachment_format, - }); - - m_vertex_input_sci = make_info({ - .vertexBindingDescriptionCount = static_cast(m_vertex_input_binding_descriptions.size()), - .pVertexBindingDescriptions = m_vertex_input_binding_descriptions.data(), - .vertexAttributeDescriptionCount = static_cast(m_vertex_input_attribute_descriptions.size()), - .pVertexAttributeDescriptions = m_vertex_input_attribute_descriptions.data(), - - }); - - m_viewport_sci = make_info({ - .viewportCount = static_cast(m_viewports.size()), - .pViewports = m_viewports.data(), - .scissorCount = static_cast(m_scissors.size()), - .pScissors = m_scissors.data(), - }); - - m_color_blend_sci = wrapper::make_info({ - .attachmentCount = static_cast(m_color_blend_attachment_states.size()), - .pAttachments = m_color_blend_attachment_states.data(), - }); - - m_dynamic_states_sci = make_info({ - .dynamicStateCount = static_cast(m_dynamic_states.size()), - .pDynamicStates = m_dynamic_states.data(), - }); + // NOTE: Inside of GraphicsPipelineBuilder, we do not carry out error checks when it comes to the data which is used + // to build the graphics pipeline. This is because validation of this data is job of the validation layers, and not + // the job of GraphicsPipelineBuilder. We should not mimic the behavious of validation layers here. - auto pipeline_ci = make_info({ - // NOTE: This is one of those rare cases where pNext is actually not nullptr! - //.pNext = &m_pipeline_rendering_ci, - .stageCount = static_cast(m_shader_stages.size()), - .pStages = m_shader_stages.data(), - .pVertexInputState = &m_vertex_input_sci, - .pInputAssemblyState = &m_input_assembly_sci, - .pTessellationState = &m_tesselation_sci, - .pViewportState = &m_viewport_sci, - .pRasterizationState = &m_rasterization_sci, - .pMultisampleState = &m_multisample_sci, - .pDepthStencilState = &m_depth_stencil_sci, - .pColorBlendState = &m_color_blend_sci, - .pDynamicState = &m_dynamic_states_sci, - .layout = m_pipeline_layout, - // @TODO Make this VK_NULL_HANDLE and use dynamic rendering! - .renderPass = m_render_pass, - }); - - auto graphics_pipeline = - std::make_shared(m_device, m_pipeline_cache, m_descriptor_set_layouts, m_push_constant_ranges, - std::move(pipeline_ci), std::move(name)); + auto graphics_pipeline = std::make_shared(m_device, std::move(m_data), std::move(name)); // NOTE: We reset the data of the builder here so it can be re-used reset(); @@ -112,68 +23,7 @@ std::shared_ptr GraphicsPipelineBuilder::build(std::string nam return graphics_pipeline; } -void GraphicsPipelineBuilder::reset() { - m_pipeline_rendering_ci = make_info(); - m_color_attachments.clear(); - m_depth_attachment_format = VK_FORMAT_UNDEFINED; - m_stencil_attachment_format = VK_FORMAT_UNDEFINED; - - m_shader_stages.clear(); - m_vertex_input_binding_descriptions.clear(); - m_vertex_input_attribute_descriptions.clear(); - m_vertex_input_sci = make_info(); - - m_input_assembly_sci = make_info({ - .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - .primitiveRestartEnable = VK_FALSE, - }); - - m_tesselation_sci = make_info(); - - m_viewports.clear(); - m_scissors.clear(); - - m_viewport_sci = make_info(); - - m_rasterization_sci = make_info({ - .polygonMode = VK_POLYGON_MODE_FILL, - .cullMode = VK_CULL_MODE_BACK_BIT, - .frontFace = VK_FRONT_FACE_CLOCKWISE, - .lineWidth = 1.0f, - }); - - m_multisample_sci = make_info({ - .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, - .sampleShadingEnable = VK_FALSE, - .minSampleShading = 1.0f, - }); - - m_depth_stencil_sci = make_info(); - m_color_blend_sci = make_info(); - - m_dynamic_states.clear(); - m_dynamic_states_sci = make_info(); - - m_pipeline_layout = VK_NULL_HANDLE; - m_color_blend_attachment_states.clear(); - - m_push_constant_ranges.clear(); -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::add_color_attachment_format(const VkFormat format) { - m_color_attachments.push_back(format); - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::add_color_blend_attachment(const VkPipelineColorBlendAttachmentState &attachment) { - m_color_blend_attachment_states.push_back(attachment); - return *this; -} - -/// Add the default color blend attachment -/// @return A reference to the dereferenced this pointer (allows method calls to be chained) -[[nodiscard]] GraphicsPipelineBuilder &GraphicsPipelineBuilder::add_default_color_blend_attachment() { +GraphicsPipelineBuilder &GraphicsPipelineBuilder::add_standard_alpha_blend_attachment() { return add_color_blend_attachment({ .blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA, @@ -187,174 +37,35 @@ GraphicsPipelineBuilder::add_color_blend_attachment(const VkPipelineColorBlendAt }); } -GraphicsPipelineBuilder &GraphicsPipelineBuilder::add_push_constant_range(const VkShaderStageFlags shader_stage, - const std::uint32_t size, - const std::uint32_t offset) { - m_push_constant_ranges.emplace_back(VkPushConstantRange{ - .stageFlags = shader_stage, - .offset = offset, - .size = size, - }); - return *this; +GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_standard_depth_stencil() { + return set_depth_stencil(make_info({ + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = VK_COMPARE_OP_LESS, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + })); } -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_color_blend(const VkPipelineColorBlendStateCreateInfo &color_blend) { - m_color_blend_sci = color_blend; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_color_blend_attachments( - const std::vector &attachments) { - m_color_blend_attachment_states = attachments; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_culling_mode(const VkBool32 culling_enabled) { - if (culling_enabled == VK_FALSE) { - spdlog::warn("Culling is disabled, which could have negative effects on the performance!"); - } - m_rasterization_sci.cullMode = culling_enabled == VK_TRUE ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_depth_attachment_format(const VkFormat format) { - m_depth_attachment_format = format; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_depth_stencil(const VkPipelineDepthStencilStateCreateInfo &depth_stencil) { - m_depth_stencil_sci = depth_stencil; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_stencil_attachment_format(const VkFormat format) { - m_stencil_attachment_format = format; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_descriptor_set_layouts(std::vector descriptor_set_layouts) { - assert(!descriptor_set_layouts.empty()); - m_descriptor_set_layouts = std::move(descriptor_set_layouts); - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_dynamic_states(const std::vector &dynamic_states) { - assert(!dynamic_states.empty()); - m_dynamic_states = dynamic_states; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_input_assembly(const VkPipelineInputAssemblyStateCreateInfo &input_assembly) { - m_input_assembly_sci = input_assembly; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_line_width(const float width) { - m_rasterization_sci.lineWidth = width; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_multisampling(const VkSampleCountFlagBits sample_count, - const std::optional min_sample_shading) { - m_multisample_sci.rasterizationSamples = sample_count; - if (min_sample_shading) { - m_multisample_sci.minSampleShading = min_sample_shading.value(); - } - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_pipeline_layout(const VkPipelineLayout layout) { - assert(layout); - m_pipeline_layout = layout; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_push_constant_ranges(std::vector push_constant_ranges) { - m_push_constant_ranges = std::move(push_constant_ranges); - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_primitive_topology(const VkPrimitiveTopology topology) { - m_input_assembly_sci.topology = topology; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_rasterization(const VkPipelineRasterizationStateCreateInfo &rasterization) { - m_rasterization_sci = rasterization; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_render_pass(const VkRenderPass &render_pass) { - m_render_pass = render_pass; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_scissor(const VkRect2D &scissor) { - m_scissors = {scissor}; - m_viewport_sci.scissorCount = 1; - m_viewport_sci.pScissors = m_scissors.data(); - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_scissor(const VkExtent2D &extent) { - return set_scissor({ - // Convert VkExtent2D to VkRect2D - .extent = extent, +void GraphicsPipelineBuilder::reset() { + m_data = {}; + // NOTE: This is the implicit default value + m_data.rasterization_sci = make_info({ + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_BACK_BIT, + .frontFace = VK_FRONT_FACE_CLOCKWISE, + .lineWidth = 1.0f, }); -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_shaders(std::vector shaders) { - assert(!shaders.empty()); - m_shader_stages = std::move(shaders); - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_tesselation_control_point_count(const std::uint32_t control_point_count) { - m_tesselation_sci.patchControlPoints = control_point_count; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_vertex_input_attributes( - const std::vector &descriptions) { - assert(!descriptions.empty()); - m_vertex_input_attribute_descriptions = descriptions; - return *this; -} - -GraphicsPipelineBuilder & -GraphicsPipelineBuilder::set_vertex_input_bindings(const std::vector &descriptions) { - assert(!descriptions.empty()); - m_vertex_input_binding_descriptions = descriptions; - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_viewport(const VkViewport &viewport) { - m_viewports = {viewport}; - m_viewport_sci.viewportCount = 1; - m_viewport_sci.pViewports = m_viewports.data(); - return *this; -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_viewport(const VkExtent2D &extent) { - return set_viewport({ - // Convert VkExtent2D to VkViewport - .width = static_cast(extent.width), - .height = static_cast(extent.height), - .maxDepth = 1.0f, + // NOTE: This is the implicit default value + m_data.input_assembly_sci = make_info({ + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = VK_FALSE, }); -} - -GraphicsPipelineBuilder &GraphicsPipelineBuilder::set_wireframe(const VkBool32 wireframe) { - m_rasterization_sci.polygonMode = (wireframe == VK_TRUE) ? VK_POLYGON_MODE_LINE : VK_POLYGON_MODE_FILL; - return *this; + // NOTE: This is the implicit default value + m_data.multisample_sci.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + // NOTE: This is the implicit default value + m_data.multisample_sci.minSampleShading = 1.0f; + // NOTE: This is the implicit default value } } // namespace inexor::vulkan_renderer::wrapper::pipelines diff --git a/src/vulkan-renderer/wrapper/pipelines/pipeline_cache.cpp b/src/vulkan-renderer/wrapper/pipelines/pipeline_cache.cpp index 343aa2b68..70d317acc 100644 --- a/src/vulkan-renderer/wrapper/pipelines/pipeline_cache.cpp +++ b/src/vulkan-renderer/wrapper/pipelines/pipeline_cache.cpp @@ -1,7 +1,8 @@ #include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_cache.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" +#include "inexor/vulkan-renderer/wrapper/device.hpp" #include "inexor/vulkan-renderer/wrapper/pipelines/graphics_pipeline.hpp" #include @@ -60,7 +61,7 @@ PipelineCache::PipelineCache(const Device &device) : m_device(device) { const auto pipeline_cache_data = read_cache_data_from_disk(); // TODO: Do we need to set flag VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT? - const auto pipeline_cache_ci = wrapper::make_info({ + const auto pipeline_cache_ci = tools::make_info({ .initialDataSize = pipeline_cache_data.size(), .pInitialData = pipeline_cache_data.data(), }); @@ -73,6 +74,7 @@ PipelineCache::PipelineCache(const Device &device) : m_device(device) { } PipelineCache::~PipelineCache() { + // @TODO Bug: The destructor is invoked twice? Why? save_cache_data_to_disk(); vkDestroyPipelineCache(m_device.device(), m_pipeline_cache, nullptr); } @@ -97,10 +99,10 @@ std::vector PipelineCache::read_cache_data_from_disk() { // Read the data from the file if (!cache_file_stream.read(reinterpret_cast(pipeline_cache_data.data()), pipeline_cache_data.size())) { - spdlog::error("Error: Could not load Vulkan pipeline cache '{}'!", m_cache_file_name); + spdlog::error("Could not load Vulkan pipeline cache file '{}'!", m_cache_file_name); pipeline_cache_data.clear(); } else { - spdlog::trace("Loaded {} bytes from Vulkan pipeline cache '{}'.", cache_file_size, + spdlog::trace("Loaded {} bytes from Vulkan pipeline cache file '{}'.", cache_file_size, m_cache_file_name); } } @@ -120,7 +122,7 @@ std::vector PipelineCache::read_cache_data_from_disk() { void PipelineCache::save_cache_data_to_disk() { std::size_t cache_size = 0; if (m_pipeline_cache == VK_NULL_HANDLE) { - spdlog::error("Vulkan pipeline cache cannot be saved to disk!"); + spdlog::error("Vulkan pipeline cache is invalid and cannot be saved to a file!"); return; } auto result = vkGetPipelineCacheData(m_device.device(), m_pipeline_cache, &cache_size, nullptr); @@ -140,20 +142,19 @@ void PipelineCache::save_cache_data_to_disk() { m_cache_file_name); } else { // Maybe the file path was set incorrectly? - spdlog::error("Error: Could not create file '{}' to write Vulkan pipeline cache to file!", - m_cache_file_name); + spdlog::error("Could not create file Vulkan pipeline cache file '{}'!", m_cache_file_name); } } else { // This should be a rare error! - spdlog::error("Error: Could not retrieve Vulkan pipeline cache data with vkGetPipelineCacheData!"); + spdlog::error("Could not retrieve Vulkan pipeline cache data with vkGetPipelineCacheData!"); } } else { // In this case, we probably forgot to pass the Vulkan pipeline cache handle during pipeline creation! - spdlog::warn("Warning: Vulkan pipeline cache is empty at application shutdown!"); + spdlog::warn("Vulkan pipeline cache is empty at application shutdown!"); } } else { - // No exception thrown because we are in a destructor! - spdlog::error("Error: vkGetPipelineCacheData returned {}!", tools::as_string(result)); + // NOTE: No exception thrown because we are in a destructor! + spdlog::error("vkGetPipelineCacheData returned {}!", tools::as_string(result)); } } diff --git a/src/vulkan-renderer/wrapper/pipelines/pipeline_layout.cpp b/src/vulkan-renderer/wrapper/pipelines/pipeline_layout.cpp index a37cd72c9..a07d01b08 100644 --- a/src/vulkan-renderer/wrapper/pipelines/pipeline_layout.cpp +++ b/src/vulkan-renderer/wrapper/pipelines/pipeline_layout.cpp @@ -1,8 +1,8 @@ #include "inexor/vulkan-renderer/wrapper/pipelines/pipeline_layout.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include @@ -16,7 +16,7 @@ PipelineLayout::PipelineLayout(const Device &device, std::string name, throw InexorException("Error: Parameter 'name' is an emtpy string!"); } - const auto pipeline_layout_ci = wrapper::make_info({ + const auto pipeline_layout_ci = tools::make_info({ .setLayoutCount = static_cast(descriptor_set_layouts.size()), .pSetLayouts = descriptor_set_layouts.data(), .pushConstantRangeCount = static_cast(push_constant_ranges.size()), diff --git a/src/vulkan-renderer/wrapper/shader.cpp b/src/vulkan-renderer/wrapper/shader.cpp index a659f7d95..c6564aa2e 100644 --- a/src/vulkan-renderer/wrapper/shader.cpp +++ b/src/vulkan-renderer/wrapper/shader.cpp @@ -2,43 +2,43 @@ #include "inexor/vulkan-renderer/tools/exception.hpp" #include "inexor/vulkan-renderer/tools/file.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include #include namespace inexor::vulkan_renderer::wrapper { -Shader::Shader(const Device &device, const VkShaderStageFlagBits type, const std::string &name, - const std::string &file_name, const std::string &entry_point) - : Shader(device, type, name, tools::read_file_binary_data(file_name), entry_point) {} +Shader::Shader(const Device &device, const VkShaderStageFlagBits shader_stage, const std::string &shader_file_name, + const std::string &entry_point) + : m_device(device), m_shader_stage(shader_stage), m_name(shader_file_name), m_entry_point(entry_point) { + if (shader_file_name.empty()) { + throw std::invalid_argument("Error: Parameter 'shader_file_name' is an empty string!"); + } -Shader::Shader(const Device &device, const VkShaderStageFlagBits type, const std::string &name, - const std::vector &code, const std::string &entry_point) - : m_device(device), m_type(type), m_name(name), m_entry_point(entry_point) { - assert(device.device()); - assert(!name.empty()); - assert(!code.empty()); - assert(!entry_point.empty()); + const auto shader_code = tools::read_file_binary_data(shader_file_name); + if (shader_code.empty()) { + throw std::runtime_error("Error: read_file_binary_data(shader_file_name) returned an empty array!"); + } - const auto shader_module_ci = make_info({ - .codeSize = code.size(), + const auto shader_module_ci = tools::make_info({ + .codeSize = shader_code.size(), // When you perform a cast like this, you also need to ensure that the data satisfies the alignment // requirements of std::uint32_t. Lucky for us, the data is stored in an std::vector where the default // allocator already ensures that the data satisfies the worst case alignment requirements. - .pCode = reinterpret_cast(code.data()), // NOLINT + .pCode = reinterpret_cast(shader_code.data()), // NOLINT }); if (const auto result = vkCreateShaderModule(m_device.device(), &shader_module_ci, nullptr, &m_shader_module); result != VK_SUCCESS) { - throw tools::VulkanException("Error: vkCreateShaderModule failed!", result, m_name); + throw VulkanException("Error: vkCreateShaderModule failed!", result, m_name); } m_device.set_debug_name(m_shader_module, m_name); } Shader::Shader(Shader &&other) noexcept : m_device(other.m_device) { - m_type = other.m_type; + m_shader_stage = other.m_shader_stage; m_name = std::move(other.m_name); m_entry_point = std::move(other.m_entry_point); m_shader_module = std::exchange(other.m_shader_module, nullptr); diff --git a/src/vulkan-renderer/wrapper/swapchains/swapchain.cpp b/src/vulkan-renderer/wrapper/swapchains/swapchain.cpp index 58032dfc9..dc4f66316 100644 --- a/src/vulkan-renderer/wrapper/swapchains/swapchain.cpp +++ b/src/vulkan-renderer/wrapper/swapchains/swapchain.cpp @@ -2,9 +2,9 @@ #include "inexor/vulkan-renderer/tools/enumerate.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/tools/representation.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/swapchains/swapchain_utils.hpp" #include "inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp" @@ -17,11 +17,10 @@ namespace inexor::vulkan_renderer::wrapper::swapchains { // Using declaration -using tools::VulkanException; +using tools::make_info; -Swapchain::Swapchain(const Device &device, const VkSurfaceKHR surface, const std::uint32_t width, - const std::uint32_t height, const bool vsync_enabled) - : m_device(device), m_surface(surface), m_vsync_enabled(vsync_enabled) { +Swapchain::Swapchain(const Device &device, std::string name, const VkSurfaceKHR surface) + : m_device(device), m_name(std::move(name)), m_surface(surface) { if (vkCreateSwapchainKHR == nullptr) { throw InexorException("Error: Function pointer 'vkCreateSwapchainKHR' is not available!"); } @@ -37,34 +36,115 @@ Swapchain::Swapchain(const Device &device, const VkSurfaceKHR surface, const std if (vkDestroySwapchainKHR == nullptr) { throw InexorException("Error: Function pointer 'vkDestroySwapchainKHR' is not available!"); } - m_img_available = std::make_unique(m_device, "m_img_available"); - setup_swapchain({width, height}, vsync_enabled); + if (m_name.empty()) { + throw InexorException("Error: Swapchain name invalid!"); + } + spdlog::trace("Creating swapchain '{}'", m_name); } -Swapchain::Swapchain(Swapchain &&other) noexcept : m_device(other.m_device) { - m_swapchain = std::exchange(other.m_swapchain, VK_NULL_HANDLE); - m_surface = std::exchange(other.m_surface, VK_NULL_HANDLE); - m_surface_format = other.m_surface_format; - m_imgs = std::move(other.m_imgs); - m_img_views = std::move(other.m_img_views); - m_current_extent = other.m_current_extent; - m_img_available = std::exchange(other.m_img_available, nullptr); - m_vsync_enabled = other.m_vsync_enabled; +VkResult Swapchain::acquire_next_image() { + if (m_img_available.empty()) { + throw std::runtime_error("Error: Swapchain has no image-available semaphores!"); + } + + const auto slot_count = static_cast(m_img_available.size()); + auto selected_slot = m_frame_index % slot_count; + + if (!m_frame_slot_submission_fences.empty()) { + bool found_ready_slot = false; + for (std::uint32_t offset = 0; offset < slot_count; ++offset) { + const auto slot = (selected_slot + offset) % slot_count; + auto &slot_fence = m_frame_slot_submission_fences[slot]; + if (slot_fence == VK_NULL_HANDLE) { + selected_slot = slot; + found_ready_slot = true; + break; + } + + const auto status = vkWaitForFences(m_device.device(), 1, &slot_fence, VK_TRUE, 0); + if (status == VK_SUCCESS) { + slot_fence = VK_NULL_HANDLE; + selected_slot = slot; + found_ready_slot = true; + break; + } + if (status != VK_TIMEOUT) { + throw VulkanException("Error: vkWaitForFences failed!", status, m_name); + } + } + + if (!found_ready_slot) { + auto &slot_fence = m_frame_slot_submission_fences[selected_slot]; + if (slot_fence != VK_NULL_HANDLE) { + if (const auto result = vkWaitForFences(m_device.device(), 1, &slot_fence, VK_TRUE, + std::numeric_limits::max()); + result != VK_SUCCESS) { + throw VulkanException("Error: vkWaitForFences failed!", result, m_name); + } + slot_fence = VK_NULL_HANDLE; + } + } + } + + m_current_frame_slot = selected_slot; + + const auto result = + vkAcquireNextImageKHR(m_device.device(), m_swapchain, std::numeric_limits::max(), + m_img_available[m_current_frame_slot]->semaphore(), VK_NULL_HANDLE, &m_current_img_index); + if (result == VK_ERROR_OUT_OF_DATE_KHR) { + setup_swapchain(m_current_extent, m_vsync_enabled); + // NOTE: After recreating the swapchain, we can't immediately attempt to acquire the next image index! + // Instead, we must poll and process window events, and skip frames in rendergraph until acquiring + // succeeds. If we realize that swapchain has become invalid in the present() call, we will recreate + // swapchain in the present() function and just continue. Because window events are then polled at the + // beginning of each frame, we then reach the acquire_next_image function again. + return result; + } + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw VulkanException("Error: vkAcquireNextImageKHR failed!", result); + } + + // Store the current swapchain image and current swapchain image view! + m_current_swapchain_img = m_imgs[m_current_img_index]; + m_current_swapchain_img_view = m_img_views[m_current_img_index]; + return VK_SUCCESS; } -std::uint32_t Swapchain::acquire_next_image_index(const std::uint64_t timeout) { - std::uint32_t img_index = 0; - if (const auto result = vkAcquireNextImageKHR(m_device.device(), m_swapchain, timeout, - *m_img_available->semaphore(), VK_NULL_HANDLE, &img_index); - result != VK_SUCCESS) { - if (result == VK_SUBOPTIMAL_KHR) { - // We need to recreate the swapchain. - setup_swapchain(m_current_extent, m_vsync_enabled); - } else { - throw VulkanException("Error: vkAcquireNextImageKHR failed!", result); +void Swapchain::wait_for_current_image_if_in_flight() const { + const auto in_flight_fence = m_imgs_in_flight[m_current_img_index]; + if (in_flight_fence != VK_NULL_HANDLE) { + if (const auto result = vkWaitForFences(m_device.device(), 1, &in_flight_fence, VK_TRUE, + std::numeric_limits::max()); + result != VK_SUCCESS) { + throw VulkanException("Error: vkWaitForFences failed!", result, m_name); } } - return img_index; +} + +void Swapchain::mark_current_image_in_flight(const VkFence fence) { + m_imgs_in_flight[m_current_img_index] = fence; +} + +void Swapchain::mark_current_frame_slot_in_flight(const VkFence fence) { + if (m_img_available.empty()) { + return; + } + if (m_frame_slot_submission_fences.size() != m_img_available.size()) { + m_frame_slot_submission_fences.assign(m_img_available.size(), VK_NULL_HANDLE); + } + m_frame_slot_submission_fences[m_current_frame_slot] = fence; +} + +// @TODO Move to inside of rendergraph +void Swapchain::change_image_layout_to_prepare_for_rendering(const CommandBuffer &cmd_buf) { + cmd_buf.change_image_layout(m_current_swapchain_img, m_format, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +} + +// @TODO Move to inside of rendergraph +void Swapchain::change_image_layout_to_prepare_for_presenting(const CommandBuffer &cmd_buf) { + cmd_buf.change_image_layout(m_current_swapchain_img, m_format, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } std::vector Swapchain::get_swapchain_images() { @@ -81,13 +161,16 @@ std::vector Swapchain::get_swapchain_images() { return imgs; } -void Swapchain::present(const std::uint32_t img_index) { +void Swapchain::present(const std::span rendering_finished) { const auto present_info = make_info({ + .waitSemaphoreCount = static_cast(rendering_finished.size()), + .pWaitSemaphores = rendering_finished.data(), + // @TODO Batch presenting multiple swapchains into one call .swapchainCount = 1, .pSwapchains = &m_swapchain, - .pImageIndices = &img_index, + .pImageIndices = &m_current_img_index, }); - if (const auto result = vkQueuePresentKHR(m_device.present_queue(), &present_info); result != VK_SUCCESS) { + if (const auto result = vkQueuePresentKHR(m_device.graphics_queue(), &present_info); result != VK_SUCCESS) { if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) { // We need to recreate the swapchain setup_swapchain(m_current_extent, m_vsync_enabled); @@ -96,6 +179,9 @@ void Swapchain::present(const std::uint32_t img_index) { throw VulkanException("Error: vkQueuePresentKHR failed!", result); } } + if (!m_img_available.empty()) { + m_frame_index = (m_current_frame_slot + 1) % static_cast(m_img_available.size()); + } } void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vsync_enabled) { @@ -107,8 +193,8 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs const auto available_present_modes = tools::get_surface_present_modes(m_device.physical_device(), m_surface); const VkSwapchainKHR old_swapchain = m_swapchain; - VkFormatProperties format_props; - vkGetPhysicalDeviceFormatProperties(m_device.physical_device(), m_surface_format.format, &format_props); + VkFormatProperties2 format_props{.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2}; + vkGetPhysicalDeviceFormatProperties2(m_device.physical_device(), m_surface_format.format, &format_props); const auto swapchain_ci = make_info({ .surface = m_surface, @@ -117,9 +203,9 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs .imageColorSpace = m_surface_format.colorSpace, .imageExtent = choose_image_extent(requested_extent, caps, m_current_extent), .imageArrayLayers = choose_array_layers(caps), - .imageUsage = choose_image_usage(caps.supportedUsageFlags, format_props.optimalTilingFeatures), - // We use VK_SHARING_MODE_EXCLUSIVE because we consider multi-queue swapchain setups an antipattern. - // There is likely no real use case for VK_SHARING_MODE_EXCLUSIVE which could not be achieved otherwise. + .imageUsage = choose_image_usage(caps.supportedUsageFlags, format_props.formatProperties.optimalTilingFeatures), + // NOTE: We use VK_SHARING_MODE_EXCLUSIVE because we consider multi-queue swapchain setups an antipattern. + // There is likely no real use case for VK_SHARING_MODE_CONCURRENT which could not be achieved otherwise. .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, .preTransform = choose_transform(caps), .compositeAlpha = choose_composite_alpha(caps.supportedCompositeAlpha), @@ -128,6 +214,8 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs .oldSwapchain = old_swapchain, }); + m_format = swapchain_ci.imageFormat; + spdlog::trace("Creating swapchain"); if (const auto result = vkCreateSwapchainKHR(m_device.device(), &swapchain_ci, nullptr, &m_swapchain); @@ -135,8 +223,12 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs throw VulkanException("Error: vkCreateSwapchainKHR failed!", result); } + m_device.set_debug_name(m_swapchain, m_name); + // We must destroy the old swapchain and its image views if specified! if (old_swapchain != VK_NULL_HANDLE) { + // Ensure no in-flight work is still touching old swapchain images/views before destruction. + m_device.wait_idle(m_device.graphics_queue()); for (auto *const img_view : m_img_views) { vkDestroyImageView(m_device.device(), img_view, nullptr); } @@ -145,7 +237,8 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs vkDestroySwapchainKHR(m_device.device(), old_swapchain, nullptr); } - m_current_extent = requested_extent; + // Keep the actual extent selected by Vulkan/capabilities, not the requested window extent. + m_current_extent = swapchain_ci.imageExtent; m_imgs = get_swapchain_images(); @@ -153,6 +246,25 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs throw std::runtime_error("Error: Swapchain image count is 0!"); } + m_rendering_finished.clear(); + m_img_available.clear(); + for (std::size_t img_index = 0; img_index < m_imgs.size(); img_index++) { + // Create one "rendering finished" semaphore for this swapchain image + m_rendering_finished.emplace_back( + std::make_unique(m_device, "m_rendering_finished[" + std::to_string(img_index) + "]")); + // Create one "swapchain image available" semaphore for this swapchain image + m_img_available.emplace_back( + std::make_unique(m_device, "m_img_available[" + std::to_string(img_index) + "]")); + // Name this swapchain image + m_device.set_debug_name(m_imgs[img_index], m_name + "::m_imgs[" + std::to_string(img_index) + "]"); + } + + // Reset per-image ownership tracking after (re)creating the swapchain. + m_imgs_in_flight.assign(m_imgs.size(), VK_NULL_HANDLE); + m_frame_slot_submission_fences.assign(m_imgs.size(), VK_NULL_HANDLE); + m_current_frame_slot = 0; + m_frame_index = 0; + spdlog::trace("Creating {} swapchain image views", m_imgs.size()); m_img_views.resize(m_imgs.size()); @@ -179,7 +291,8 @@ void Swapchain::setup_swapchain(const VkExtent2D requested_extent, const bool vs if (const auto result = vkCreateImageView(m_device.device(), &img_view_ci, nullptr, &m_img_views[img_index]); result != VK_SUCCESS) { - throw VulkanException("Error: vkCreateImageView failed!", result, "swapchain image view " + img_index); + throw VulkanException("Error: vkCreateImageView failed!", result, + "swapchain image view " + std::to_string(img_index)); } m_device.set_debug_name(m_img_views[img_index], "swapchain image view"); } diff --git a/src/vulkan-renderer/wrapper/swapchains/swapchain_utils.cpp b/src/vulkan-renderer/wrapper/swapchains/swapchain_utils.cpp index d7550dd06..7b4d045f9 100644 --- a/src/vulkan-renderer/wrapper/swapchains/swapchain_utils.cpp +++ b/src/vulkan-renderer/wrapper/swapchains/swapchain_utils.cpp @@ -153,15 +153,18 @@ VkPresentModeKHR choose_present_mode(const std::span ava VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_RELAXED_KHR, }; + // @TODO Switch to std::ranges::contains when upgrading to C++23 // Iterate through the preferred present modes and return the first one that is supported. for (auto requested_mode : present_modes_in_preference_order) { if (std::ranges::find(available_present_modes, requested_mode) != available_present_modes.end()) { + spdlog::trace("Selecting swapchain present mode '{}'", tools::as_string(requested_mode)); return requested_mode; } } // If none of the present modes from the priority list are available, fallback to FIFO. } // FIFO is guaranteed to be supported and enforces vsync to be enabled. + spdlog::trace("Selecting swapchain present mode 'VK_PRESENT_MODE_FIFO_KHR'"); return VK_PRESENT_MODE_FIFO_KHR; } @@ -239,7 +242,7 @@ VkSurfaceTransformFlagBitsKHR choose_transform(const VkSurfaceCapabilitiesKHR &c const auto chosen_transform = ((requested_transform & caps.supportedTransforms) != 0u) ? static_cast(requested_transform) : caps.currentTransform; - spdlog::trace("Selecting swapchain image transform '{}'", tools::as_string(chosen_transform)); + spdlog::trace("Selecting swapchain surface transform '{}'", tools::as_string(chosen_transform)); return chosen_transform; } diff --git a/src/vulkan-renderer/wrapper/synchronization/fence.cpp b/src/vulkan-renderer/wrapper/synchronization/fence.cpp index 69eeeaeca..f946ca4b1 100644 --- a/src/vulkan-renderer/wrapper/synchronization/fence.cpp +++ b/src/vulkan-renderer/wrapper/synchronization/fence.cpp @@ -1,25 +1,22 @@ #include "inexor/vulkan-renderer/wrapper/synchronization/fence.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include +#include #include namespace inexor::vulkan_renderer::wrapper::synchronization { -Fence::Fence(const Device &device, const std::string &name, const bool in_signaled_state) - : m_device(device), m_name(name) { - assert(!name.empty()); - assert(device.device()); - - const auto fence_ci = make_info({ - .flags = static_cast(in_signaled_state ? VK_FENCE_CREATE_SIGNALED_BIT : 0), - }); - +Fence::Fence(const Device &device, const std::string &name) : m_device(device), m_name(name) { + if (name.empty()) { + throw std::invalid_argument("Error: Parameter 'name' is empty!"); + } + const auto fence_ci = tools::make_info(); if (const auto result = vkCreateFence(m_device.device(), &fence_ci, nullptr, &m_fence); result != VK_SUCCESS) { - throw tools::VulkanException("Error: vkCreateFence failed!", result, m_name); + throw VulkanException("Error: vkCreateFence failed!", result, m_name); } m_device.set_debug_name(m_fence, m_name); } @@ -33,10 +30,6 @@ Fence::~Fence() { vkDestroyFence(m_device.device(), m_fence, nullptr); } -void Fence::block(std::uint64_t timeout_limit) const { - vkWaitForFences(m_device.device(), 1, &m_fence, VK_TRUE, timeout_limit); -} - void Fence::reset() const { vkResetFences(m_device.device(), 1, &m_fence); } @@ -45,4 +38,11 @@ VkResult Fence::status() const { return vkGetFenceStatus(m_device.device(), m_fence); } +void Fence::wait() const { + if (const auto result = + vkWaitForFences(m_device.device(), 1, &m_fence, VK_TRUE, std::numeric_limits::max()); + result != VK_SUCCESS) { + throw VulkanException("Error: vkWaitForFences failed!", result, m_name); + } +} } // namespace inexor::vulkan_renderer::wrapper::synchronization diff --git a/src/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.cpp b/src/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.cpp new file mode 100644 index 000000000..132ac44d3 --- /dev/null +++ b/src/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.cpp @@ -0,0 +1,48 @@ +#include "inexor/vulkan-renderer/wrapper/synchronization/pipeline_barrier_batch_builder.hpp" + +#include "inexor/vulkan-renderer/wrapper/commands/command_buffer.hpp" + +#include + +#include + +namespace inexor::vulkan_renderer::wrapper::synchronization { + +bool PipelineBarrierBatchBuilder::empty() const { + return m_memory_barriers.empty() && m_buffer_barriers.empty() && m_image_barriers.empty(); +} + +void PipelineBarrierBatchBuilder::flush(const wrapper::commands::CommandBuffer &cmd_buf) { + assert(!empty() && + "PipelineBarrierBatchBuilder::flush called with no barriers. Use flush_if_not_empty() if this is expected."); + if (empty()) { + return; + } + + const auto dependency_info = VkDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .memoryBarrierCount = static_cast(m_memory_barriers.size()), + .pMemoryBarriers = m_memory_barriers.empty() ? nullptr : m_memory_barriers.data(), + .bufferMemoryBarrierCount = static_cast(m_buffer_barriers.size()), + .pBufferMemoryBarriers = m_buffer_barriers.empty() ? nullptr : m_buffer_barriers.data(), + .imageMemoryBarrierCount = static_cast(m_image_barriers.size()), + .pImageMemoryBarriers = m_image_barriers.empty() ? nullptr : m_image_barriers.data(), + }; + + vkCmdPipelineBarrier2(cmd_buf.command_buffer(), &dependency_info); + reset(); +} + +void PipelineBarrierBatchBuilder::flush_if_not_empty(const wrapper::commands::CommandBuffer &cmd_buf) { + if (!empty()) { + flush(cmd_buf); + } +} + +void PipelineBarrierBatchBuilder::reset() { + m_memory_barriers.clear(); + m_buffer_barriers.clear(); + m_image_barriers.clear(); +} + +} // namespace inexor::vulkan_renderer::wrapper::synchronization diff --git a/src/vulkan-renderer/wrapper/synchronization/semaphore.cpp b/src/vulkan-renderer/wrapper/synchronization/semaphore.cpp index 972ddcaee..330e9ce82 100644 --- a/src/vulkan-renderer/wrapper/synchronization/semaphore.cpp +++ b/src/vulkan-renderer/wrapper/synchronization/semaphore.cpp @@ -1,8 +1,8 @@ #include "inexor/vulkan-renderer/wrapper/synchronization/semaphore.hpp" #include "inexor/vulkan-renderer/tools/exception.hpp" +#include "inexor/vulkan-renderer/tools/make_info.hpp" #include "inexor/vulkan-renderer/wrapper/device.hpp" -#include "inexor/vulkan-renderer/wrapper/make_info.hpp" #include #include @@ -10,19 +10,19 @@ namespace inexor::vulkan_renderer::wrapper::synchronization { Semaphore::Semaphore(const Device &device, const std::string &name) : m_device(device), m_name(name) { - assert(!name.empty()); - - const auto semaphore_ci = make_info(); - + if (name.empty()) { + throw std::invalid_argument("Error: Parameter 'name' is empty!"); + } + const auto semaphore_ci = tools::make_info(); if (const auto result = vkCreateSemaphore(m_device.device(), &semaphore_ci, nullptr, &m_semaphore); result != VK_SUCCESS) { - throw tools::VulkanException("Error: vkCreateSemaphore failed!", result, m_name); + throw VulkanException("Error: vkCreateSemaphore failed!", result, m_name); } m_device.set_debug_name(m_semaphore, m_name); } Semaphore::Semaphore(Semaphore &&other) noexcept : m_device(other.m_device) { - m_semaphore = std::exchange(other.m_semaphore, VK_NULL_HANDLE); + m_semaphore = std::exchange(other.m_semaphore, nullptr); m_name = std::move(other.m_name); } diff --git a/src/vulkan-renderer/wrapper/windows/surface.cpp b/src/vulkan-renderer/wrapper/windows/surface.cpp index 989a85860..4867df8d9 100644 --- a/src/vulkan-renderer/wrapper/windows/surface.cpp +++ b/src/vulkan-renderer/wrapper/windows/surface.cpp @@ -13,11 +13,13 @@ namespace inexor::vulkan_renderer::wrapper::windows { using tools::VulkanException; WindowSurface::WindowSurface(const VkInstance instance, GLFWwindow *window) : m_instance(instance) { - assert(instance); - assert(window); - + if (instance == VK_NULL_HANDLE) { + throw std::invalid_argument("Error: Parameter 'instance' is an invalid pointer!"); + } + if (window == nullptr) { + throw std::invalid_argument("Error: Parameter 'window' is an invalid pointer!"); + } spdlog::trace("Creating window surface"); - if (const auto result = glfwCreateWindowSurface(instance, window, nullptr, &m_surface); result != VK_SUCCESS) { throw VulkanException("Error: glfwCreateWindowSurface failed!", result); } diff --git a/src/vulkan-renderer/wrapper/windows/window.cpp b/src/vulkan-renderer/wrapper/windows/window.cpp index a6f16cb3a..dbba76cbf 100644 --- a/src/vulkan-renderer/wrapper/windows/window.cpp +++ b/src/vulkan-renderer/wrapper/windows/window.cpp @@ -5,14 +5,16 @@ #include #include +#include namespace inexor::vulkan_renderer::wrapper::windows { Window::Window(const std::string &title, const std::uint32_t width, const std::uint32_t height, const bool visible, - const bool resizable, const Mode mode) + const bool resizable, const WindowMode mode) : m_width(width), m_height(height), m_mode(mode) { - assert(!title.empty()); - + if (title.empty()) { + throw std::invalid_argument("Error: Parameter 'title' is an empty string!"); + } if (glfwInit() != GLFW_TRUE) { throw std::runtime_error("Error: glfwInit failed for window " + title + " !"); } @@ -24,9 +26,9 @@ Window::Window(const std::string &title, const std::uint32_t width, const std::u spdlog::trace("Creating window"); GLFWmonitor *monitor = nullptr; - if (m_mode != Mode::WINDOWED) { + if (m_mode != WindowMode::WINDOWED) { monitor = glfwGetPrimaryMonitor(); - if (m_mode == Mode::WINDOWED_FULLSCREEN) { + if (m_mode == WindowMode::WINDOWED_FULLSCREEN) { const auto *video_mode = glfwGetVideoMode(monitor); m_width = video_mode->width; m_height = video_mode->height; @@ -93,6 +95,8 @@ void Window::wait_for_focus() { do { glfwWaitEvents(); glfwGetFramebufferSize(m_window, ¤t_width, ¤t_height); + // Wait for 10 milliseconds instead of pulling aggressively + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (current_width == 0 || current_height == 0); m_width = current_width;