diff options
24 files changed, 1937 insertions, 1621 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp index 9cd19ec..1541e34 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2,25 +2,21 @@ #include "rendering/render_api.h" // RendererAPI #include "settings_manager.h" -#include "hdri_manager.h" #include "platform/opengl/opengl_device.h" #include "platform/vulkan/vulkan_device.h" #include <GLFW/glfw3.h> #include <imgui.h> - -#include <glm/gtc/matrix_transform.hpp> +#include <glm/glm.hpp> #include <algorithm> -#include <cmath> -#include <numbers> namespace Donut { Application* Application::s_instance = nullptr; // Scroll is delivered through a GLFW callback; accumulate it here and drain it - // once per frame in process_input (chaining ImGui's own scroll handler). + // once per frame in update_input (chaining ImGui's own scroll handler). static double g_scroll_accum = 0.0; static GLFWscrollfun g_prev_scroll = nullptr; static void donut_scroll_callback(GLFWwindow* w, double x, double y) @@ -30,7 +26,6 @@ namespace Donut } Application::Application(const std::string& name, int width, int height) - : m_running(true), m_minimized(false) { s_instance = this; @@ -76,34 +71,10 @@ namespace Donut return; } - m_hdri_path = "assets/hdri/hdr_blue_nebulae_1.hdr"; - m_cubemap = m_device->create_cubemap_from_hdri(m_hdri_path); - - m_scene_renderer = create_scope<SceneRenderer>(); - m_scene_renderer->init(*m_device); - m_black_hole_renderer = create_scope<BlackHoleRenderer>(); - m_black_hole_renderer->init(*m_device); - - // Black-hole camera: large-scale orbital viewer outside the disk. - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_target(glm::vec3(0.0f)); - m_camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk - m_camera.set_orbital_limits(2.2e11, 1.5e12); - m_camera.set_orbital_speed(0.01f); - m_camera.set_zoom_speed(3e10); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(1.25f); - - // Scene camera: normal-scale orbital world-builder viewer. - m_scene_camera.set_camera_mode(CameraMode::Orbital); - m_scene_camera.set_orbital_target(glm::vec3(0.0f)); - m_scene_camera.set_orbital_radius(15.0); - m_scene_camera.set_orbital_limits(2.0, 200.0); - m_scene_camera.set_orbital_speed(0.01f); - m_scene_camera.set_zoom_speed(2.0); - m_scene_camera.set_azimuth(0.0f); - m_scene_camera.set_elevation((float)std::numbers::pi / 3.0f); - m_scene_camera.update_orbital(); + m_scene = create_scope<Scene>(); + m_render_path = create_scope<RenderPath>(); + m_render_path->init(*m_device, m_scene->hdri_path); + m_ui = create_scope<UILayer>(); m_device->init_imgui(); // Load the UI font into the device-created ImGui context (backend-agnostic; @@ -134,60 +105,19 @@ namespace Donut if (!cmd) return; // frame skipped (e.g. swapchain recreation) m_device->imgui_new_frame(); - build_ui(); - process_input(); + View view = m_ui->draw(UIContext{ *m_scene, m_device->device_name() }); + update_input(view); + m_render_path->sync_hdri(m_scene->hdri_path); - const glm::vec4 clear(0.05f, 0.06f, 0.10f, 1.0f); int w = 0, h = 0; glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); - - if (m_scene_mode) - { - float aspect = (float)w / (float)std::max(h, 1); - m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - CameraView cv; - cv.view = m_scene_camera.get_view_matrix(); - cv.projection = m_scene_camera.get_projection_matrix(); - cv.position = m_scene_camera.get_orbital_position(); - cv.fb_width = w; cv.fb_height = h; - - cmd->begin_render_pass(nullptr, clear); - m_scene_renderer->render(*cmd, cv, m_scene_objects, m_selected_object, m_cubemap.get()); - m_device->imgui_render(*cmd); - cmd->end_render_pass(); - } - else - { - GeodesicView gv; - glm::vec3 pos, fwd; - if (m_camera.get_camera_mode() == CameraMode::FPS) - { - pos = m_camera.get_position(); - fwd = m_camera.get_forward_direction(); - } - else - { - pos = m_camera.get_orbital_position(); - fwd = glm::normalize(m_camera.get_orbital_target() - pos); - } - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - gv.position = pos; gv.right = right; gv.up = glm::cross(right, fwd); gv.forward = fwd; - gv.tan_half_fov = (float)tan(glm::radians(m_bh_params.fov_degrees * 0.5f)); - gv.aspect = (float)BlackHoleRenderer::GEO_HI_W / (float)BlackHoleRenderer::GEO_HI_H; - gv.moving = m_user_moving; - gv.time = (float)(glfwGetTime() - m_start_time); - - m_black_hole_renderer->render_geodesic(*cmd, gv, m_bh_params, m_cubemap.get()); - cmd->begin_render_pass(nullptr, clear); - m_black_hole_renderer->blit(*cmd, w, h); - m_device->imgui_render(*cmd); - cmd->end_render_pass(); - } + float time = (float)(glfwGetTime() - m_start_time); + m_render_path->render(*cmd, *m_scene, view, w, h, m_user_moving, time); m_device->end_frame(); } - auto Application::process_input() -> void + auto Application::update_input(View view) -> void { GLFWwindow* window = (GLFWwindow*)m_window->get_native_window(); bool over_ui = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureMouse; @@ -200,11 +130,23 @@ namespace Donut glfwGetCursorPos(window, &mx, &my); bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; - if (!m_scene_mode && m_camera.get_camera_mode() == CameraMode::FPS) + // Tabs without a live viewport (Setup / Export) have no camera to drive. + if (view == View::None) { + m_left_was_down = left_down; // keep the drag-edge state coherent + g_scroll_accum = 0.0; // discard scroll over inert tabs + m_user_moving = false; + return; + } + + const bool scene_view = view == View::Scene; + + if (!scene_view && m_scene->sim_camera.get_camera_mode() == CameraMode::FPS) + { + Camera& cam = m_scene->sim_camera; if (left_down && !m_left_was_down) { m_fps_last_x = mx; m_fps_last_y = my; } if (left_down && !over_ui) - m_camera.on_mouse_move(float(mx - m_fps_last_x), float(m_fps_last_y - my)); + cam.on_mouse_move(float(mx - m_fps_last_x), float(m_fps_last_y - my)); m_fps_last_x = mx; m_fps_last_y = my; m_left_was_down = left_down; @@ -213,19 +155,19 @@ namespace Donut if (!over_kb && dt > 0.0f) { float mdt = dt * (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); - if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { m_camera.move_forward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { m_camera.move_backward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { m_camera.move_left(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { m_camera.move_right(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { m_camera.move_up(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { m_camera.move_down(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { cam.move_forward(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { cam.move_backward(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { cam.move_left(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { cam.move_right(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { cam.move_up(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { cam.move_down(mdt); moved = true; } } g_scroll_accum = 0.0; m_user_moving = moved || (left_down && !over_ui); } else { - Camera& cam = m_scene_mode ? m_scene_camera : m_camera; + Camera& cam = scene_view ? m_scene->scene_camera : m_scene->sim_camera; if (left_down && !m_left_was_down && !over_ui) cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); else if (!left_down && m_left_was_down) @@ -242,227 +184,6 @@ namespace Donut } } - auto Application::set_free_fly(bool enabled) -> void - { - const bool is_fps = m_camera.get_camera_mode() == CameraMode::FPS; - if (enabled == is_fps) return; - if (enabled) - { - // Seed the fly pose from the current orbital framing so the view is continuous. - glm::vec3 pos = m_camera.get_orbital_position(); - glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - pos); - float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f))); - float yaw = glm::degrees(atan2(fwd.z, fwd.x)); - m_camera.set_camera_mode(CameraMode::FPS); - m_camera.set_movement_speed(2.0e10f); - m_camera.set_mouse_sensitivity(0.15f); - m_camera.set_position(pos); - m_camera.set_rotation(glm::vec3(pitch, yaw, 0.0f)); - } - else - { - m_camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact - } - } - - auto Application::reset_camera() -> void - { - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_radius(4e11); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(1.25f); - } - - auto Application::set_hdri(const std::string& path) -> void - { - if (m_hdri_path == path || !m_device) return; - m_device->wait_idle(); - m_cubemap = m_device->create_cubemap_from_hdri(path); // old cube freed after idle - m_hdri_path = path; - } - - // One-time ImGui theme: rounded, roomy, dark with a warm accretion-disk accent. - static auto apply_donut_style() -> void - { - ImGuiStyle& s = ImGui::GetStyle(); - s.WindowRounding = 7.0f; s.ChildRounding = 5.0f; s.FrameRounding = 4.0f; - s.GrabRounding = 4.0f; s.PopupRounding = 4.0f; s.ScrollbarRounding = 5.0f; s.TabRounding = 4.0f; - s.WindowPadding = ImVec2(12, 12); s.FramePadding = ImVec2(9, 5); - s.ItemSpacing = ImVec2(9, 8); s.ItemInnerSpacing = ImVec2(7, 5); - s.WindowBorderSize = 0.0f; s.FrameBorderSize = 0.0f; s.WindowTitleAlign = ImVec2(0.02f, 0.5f); - - const ImVec4 amber = ImVec4(0.98f, 0.62f, 0.20f, 1.00f); - const ImVec4 amberHi = ImVec4(1.00f, 0.73f, 0.36f, 1.00f); - ImVec4* c = s.Colors; - c[ImGuiCol_WindowBg] = ImVec4(0.07f, 0.08f, 0.10f, 0.97f); - c[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 0.55f); - c[ImGuiCol_PopupBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.98f); - c[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); - c[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); - c[ImGuiCol_Text] = ImVec4(0.90f, 0.91f, 0.93f, 1.00f); - c[ImGuiCol_TextDisabled] = ImVec4(0.48f, 0.50f, 0.54f, 1.00f); - c[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); - c[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.24f, 0.28f, 1.00f); - c[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.28f, 0.33f, 1.00f); - c[ImGuiCol_Header] = ImVec4(0.17f, 0.19f, 0.23f, 1.00f); - c[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.27f, 0.32f, 1.00f); - c[ImGuiCol_HeaderActive] = ImVec4(0.28f, 0.31f, 0.37f, 1.00f); - c[ImGuiCol_Button] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); - c[ImGuiCol_ButtonHovered] = ImVec4(0.27f, 0.30f, 0.35f, 1.00f); - c[ImGuiCol_ButtonActive] = amber; - c[ImGuiCol_SliderGrab] = amber; - c[ImGuiCol_SliderGrabActive]= amberHi; - c[ImGuiCol_CheckMark] = amberHi; - c[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); - c[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); - } - - auto Application::build_ui() -> void - { - static bool styled = false; - if (!styled) { apply_donut_style(); styled = true; } - if (!m_device) return; - - // Full-viewport dock space with a pass-through centre, so the panel can be - // docked to any edge while the render shows through the middle. - ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); - - ImGui::SetNextWindowSize(ImVec2(340, 580), ImGuiCond_FirstUseEver); - ImGui::Begin("Donut \xc2\xb7 Black Hole"); - - ImGuiIO& io = ImGui::GetIO(); - ImGui::TextDisabled("%s", m_device->device_name().empty() ? "GPU" : m_device->device_name().c_str()); - ImGui::Text("%.0f FPS", io.Framerate); - ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f); - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Renderer", ImGuiTreeNodeFlags_DefaultOpen)) - { - auto& s = SettingsManager::get_settings(); - const char* apis[] = { "OpenGL", "Vulkan" }; - int cur = (s.graphics.render_api == "Vulkan") ? 1 : 0; - if (ImGui::Combo("Graphics API", &cur, apis, 2)) - { - s.graphics.render_api = apis[cur]; - SettingsManager::save_settings(); - } - const char* running = (RendererAPI::get_api() == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL"; - if (s.graphics.render_api != running) - ImGui::TextColored(ImVec4(0.98f, 0.62f, 0.20f, 1.0f), "Restart to apply (%s running)", running); - else - ImGui::TextDisabled("Active backend: %s", running); - } - - ImGui::Spacing(); - ImGui::TextDisabled("VIEW"); - if (ImGui::RadioButton("Black hole", !m_scene_mode)) m_scene_mode = false; - ImGui::SameLine(); - if (ImGui::RadioButton("Scene", m_scene_mode)) m_scene_mode = true; - ImGui::Separator(); - - if (m_scene_mode) - { - build_scene_ui(); - } - else - { - BlackHoleParams& bh = m_bh_params; - - if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) - { - bool ff = m_camera.get_camera_mode() == CameraMode::FPS; - if (ImGui::RadioButton("Orbital", !ff)) set_free_fly(false); - ImGui::SameLine(); - if (ImGui::RadioButton("Free-fly", ff)) set_free_fly(true); - ImGui::SameLine(); - if (ImGui::Button("Reset view")) reset_camera(); - ImGui::SliderFloat("FOV", &bh.fov_degrees, 20.0f, 90.0f, "%.0f\xc2\xb0"); - ImGui::TextDisabled(ff ? "WASD move | Q/E down-up | Shift boost | drag look" - : "Drag to orbit | scroll to zoom"); - } - - if (ImGui::CollapsingHeader("Accretion disk", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::SliderFloat("Temperature", &bh.temperature, 2000.0f, 15000.0f, "%.0f K"); - ImGui::SliderFloat("Brightness", &bh.brightness, 0.0f, 3.0f, "%.2f"); - ImGui::SliderFloat("Inner radius", &bh.disk_inner_rs, 3.0f, 12.0f, "%.1f r_s"); - if (bh.disk_outer_rs < bh.disk_inner_rs + 0.5f) bh.disk_outer_rs = bh.disk_inner_rs + 0.5f; - ImGui::SliderFloat("Outer radius", &bh.disk_outer_rs, bh.disk_inner_rs + 0.5f, 25.0f, "%.1f r_s"); - ImGui::SliderFloat("Turbulence", &bh.turbulence, 0.0f, 2.0f, "%.2f"); - ImGui::TextDisabled("Inner edge is the ISCO (3 r_s). Novikov-Thorne profile."); - } - - if (ImGui::CollapsingHeader("Quality")) - { - ImGui::SliderInt("Integration steps", &bh.quality_steps, 2000, 15000); - ImGui::TextDisabled("Deeper lensing at higher cost. Settled frame is 4x supersampled."); - } - - if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) - { - auto& hdri = HDRIManager::get(); - std::string preview = hdri.get_hdri_name(m_hdri_path); - if (ImGui::BeginCombo("Starfield", preview.c_str())) - { - for (const auto& path : hdri.get_available_hdri()) - { - bool selected = (path == m_hdri_path); - if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected)) - set_hdri(path); - if (selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::TextDisabled("Switching rebuilds the cubemap (brief pause)."); - } - } - - ImGui::End(); - } - - auto Application::build_scene_ui() -> void - { - if (!ImGui::CollapsingHeader("Objects", ImGuiTreeNodeFlags_DefaultOpen)) - return; - - ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); - auto& objs = m_scene_objects; - if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1; - - ImGui::Text("Spheres (%d)", (int)objs.size()); - if (ImGui::Button("Add") && objs.size() < 64) - { - SceneObject o; - o.position = glm::vec3(((int)objs.size() % 5) * 5.0f - 10.0f, 2.0f, ((int)objs.size() / 5) * 5.0f); - o.radius = 1.5f; - o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4)); - objs.push_back(o); - m_selected_object = (int)objs.size() - 1; - } - ImGui::SameLine(); - if (ImGui::Button("Delete") && !objs.empty()) - { - objs.erase(objs.begin() + m_selected_object); - if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1; - } - - ImGui::BeginChild("obj_list", ImVec2(0, 90), true); - for (int i = 0; i < (int)objs.size(); ++i) - { - std::string label = "Sphere " + std::to_string(i); - if (ImGui::Selectable(label.c_str(), m_selected_object == i)) m_selected_object = i; - } - ImGui::EndChild(); - - if (m_selected_object >= 0 && m_selected_object < (int)objs.size()) - { - SceneObject& o = objs[m_selected_object]; - ImGui::DragFloat3("Position", &o.position.x, 0.1f); - ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f); - ImGui::ColorEdit3("Color", &o.color.x); - } - } - auto Application::close() -> void { m_running = false; @@ -492,18 +213,14 @@ namespace Donut if (m_device) { m_device->wait_idle(); - m_scene_renderer.reset(); - m_black_hole_renderer.reset(); - m_cubemap.reset(); - // Free HDRIManager's cached GPU textures now, while the render context - // is still alive. On OpenGL these are GL textures owned by a static - // singleton; letting them destruct at program exit would call - // glDeleteTextures after the GL context is gone and crash on close. - HDRIManager::get().clear_cache(); + if (m_render_path) m_render_path->shutdown(); + m_render_path.reset(); + m_ui.reset(); + m_scene.reset(); m_device->shutdown(); m_device.reset(); } SettingsManager::shutdown(); Logger::shutdown(); } -}; +} diff --git a/src/core/application.h b/src/core/application.h index 4b67c84..cbd741d 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -4,22 +4,21 @@ #include "window.h" #include "event.h" #include "log.h" -#include "camera.h" #include "rendering/rhi.h" -#include "rendering/scene_renderer.h" -#include "rendering/black_hole_renderer.h" +#include "rendering/render_path.h" +#include "rendering/view.h" +#include "scene/scene.h" +#include "ui/ui_layer.h" #include <string> -#include <vector> namespace Donut { - // The application owns ONE RHI::Device (OpenGL or Vulkan, chosen at startup) - // and the two portable renderers written on top of it, plus all the shared - // state (cameras, scene objects, disk parameters, HDRI). Because the rendering - // lives in the RHI and the shared renderers, the app behaves identically no - // matter which backend is active. + // Thin shell: owns the window, the RHI device, the Scene (document), the + // RenderPath (device-side rendering) and the UILayer (tabbed workspaces), and + // runs the frame loop that wires them together. All the rendering lives in the + // RHI + shared renderers; all the editing lives in the Scene + workspaces. class Application { public: @@ -38,43 +37,25 @@ namespace Donut auto on_shutdown() -> void; auto on_event(Event& event) -> void; - auto build_ui() -> void; // shared ImGui panel - auto build_scene_ui() -> void; // scene-view (object list) section - - auto process_input() -> void; // orbit / free-fly / zoom, was per-backend - auto set_free_fly(bool enabled) -> void; - auto reset_camera() -> void; - auto set_hdri(const std::string& path) -> void; - auto render_frame() -> void; + auto update_input(View view) -> void; // orbit / free-fly / zoom for the active camera private: - Scope<Window> m_window; - - // The unified renderer: one device + the shared renderers on top of it. - Scope<RHI::Device> m_device; - Scope<SceneRenderer> m_scene_renderer; - Scope<BlackHoleRenderer> m_black_hole_renderer; - Ref<RHI::Texture> m_cubemap; // shared HDRI environment - - Camera m_camera{ 60.0f, 16.0f / 9.0f, 0.1f, 100.0f }; // black-hole view - Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; // world-builder view - - BlackHoleParams m_bh_params; - std::vector<SceneObject> m_scene_objects{ SceneObject{} }; - int m_selected_object = 0; - bool m_scene_mode = false; - std::string m_hdri_path; + Scope<Window> m_window; + Scope<RHI::Device> m_device; + Scope<Scene> m_scene; + Scope<RenderPath> m_render_path; + Scope<UILayer> m_ui; - // input tracking (mirrors the old per-backend process_input) + // input tracking (per-frame transient state) bool m_left_was_down = false; double m_fps_last_x = 0.0, m_fps_last_y = 0.0; double m_last_frame_time = 0.0; double m_start_time = 0.0; bool m_user_moving = false; - bool m_running; - bool m_minimized; + bool m_running = true; + bool m_minimized = false; static Application* s_instance; }; diff --git a/src/core/hdri_manager.h b/src/core/hdri_manager.h index d98830f..6b3df1e 100644 --- a/src/core/hdri_manager.h +++ b/src/core/hdri_manager.h @@ -1,7 +1,7 @@ #pragma once #include "core/log.h" -#include "rendering/texture.h" +#include "platform/opengl/texture.h" #include <string> #include <unordered_map> diff --git a/src/platform/opengl/opengl_device.cpp b/src/platform/opengl/opengl_device.cpp index c4332fa..8516a3c 100644 --- a/src/platform/opengl/opengl_device.cpp +++ b/src/platform/opengl/opengl_device.cpp @@ -2,8 +2,8 @@ #include "core/log.h" #include "core/hdri_manager.h" -#include "rendering/shader.h" -#include "rendering/texture.h" +#include "shader.h" +#include "texture.h" #include <glad/glad.h> #include <GLFW/glfw3.h> @@ -220,6 +220,11 @@ namespace Donut::RHI } auto shutdown() -> void override { + // Free the HDRIManager's cached GL cubemaps while our context is + // still current (they are static-singleton-owned; letting them + // destruct at program exit would glDeleteTextures after the context + // is gone and crash on close). + HDRIManager::get().clear_cache(); if (m_imgui) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } } diff --git a/src/rendering/shader.cpp b/src/platform/opengl/shader.cpp index 80015e6..80015e6 100644 --- a/src/rendering/shader.cpp +++ b/src/platform/opengl/shader.cpp diff --git a/src/rendering/shader.h b/src/platform/opengl/shader.h index 409aca3..409aca3 100644 --- a/src/rendering/shader.h +++ b/src/platform/opengl/shader.h diff --git a/src/rendering/texture.cpp b/src/platform/opengl/texture.cpp index 12e07f1..12e07f1 100644 --- a/src/rendering/texture.cpp +++ b/src/platform/opengl/texture.cpp diff --git a/src/rendering/texture.h b/src/platform/opengl/texture.h index 5f68c78..5f68c78 100644 --- a/src/rendering/texture.h +++ b/src/platform/opengl/texture.h diff --git a/src/platform/vulkan/vulkan_common.h b/src/platform/vulkan/vulkan_common.h new file mode 100644 index 0000000..0faa107 --- /dev/null +++ b/src/platform/vulkan/vulkan_common.h @@ -0,0 +1,343 @@ +#pragma once + +// Shared internals of the Vulkan RHI backend: the enum-mapping helpers, the +// opaque resource classes (Buffer/Texture/RenderTarget/Pipeline/CommandList), and +// the VulkanDevice declaration. The implementation is split across vulkan_device +// (lifecycle + frame loop), vulkan_swapchain (instance/device/swapchain/passes), +// vulkan_resources (buffer/texture/target/pipeline) and vulkan_cubemap. This +// header is private to platform/vulkan/ — nothing outside includes it. + +#include "vulkan_device.h" // rendering/rhi.h (base classes + factory decls) + +#define GLFW_INCLUDE_VULKAN +#include <GLFW/glfw3.h> +#include <glm/glm.hpp> + +#include <vector> +#include <array> +#include <string> +#include <unordered_map> +#include <cstdint> +#include <cstring> +#include <algorithm> + +#include "core/log.h" + +namespace Donut::RHI +{ + constexpr int MAX_FRAMES_IN_FLIGHT = 2; + constexpr uint32_t MAX_BINDINGS = 8; + + #define VKD_CHECK(expr) \ + do { \ + VkResult _r = (expr); \ + if (_r != VK_SUCCESS) { \ + DONUT_ERROR("Vulkan RHI: {} failed ({})", #expr, (int)_r); \ + return false; \ + } \ + } while (0) + + inline auto vk_format(Format f) -> VkFormat + { + switch (f) { + case Format::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT; + case Format::D32: return VK_FORMAT_D32_SFLOAT; + default: return VK_FORMAT_R8G8B8A8_UNORM; + } + } + inline auto vk_attr_format(uint32_t comps) -> VkFormat + { + switch (comps) { + case 1: return VK_FORMAT_R32_SFLOAT; + case 2: return VK_FORMAT_R32G32_SFLOAT; + case 3: return VK_FORMAT_R32G32B32_SFLOAT; + default: return VK_FORMAT_R32G32B32A32_SFLOAT; + } + } + inline auto vk_topology(Topology t) -> VkPrimitiveTopology + { return t == Topology::Lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; } + inline auto vk_compare(CompareOp o) -> VkCompareOp + { return o == CompareOp::Always ? VK_COMPARE_OP_ALWAYS : o == CompareOp::LessEqual ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; } + inline auto vk_filter(Filter f) -> VkFilter { return f == Filter::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } + inline auto vk_cull(CullMode c) -> VkCullModeFlags + { return c == CullMode::None ? VK_CULL_MODE_NONE : c == CullMode::Back ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT; } + + // Buffer: host-visible + coherent, persistently mapped. + class VkBufferR : public Buffer + { + public: + VkBufferR(VkDevice d, VkBuffer b, VkDeviceMemory m, void* mapped, size_t size) + : m_device(d), m_buf(b), m_mem(m), m_mapped(mapped), m_size(size) {} + ~VkBufferR() override + { + if (m_mapped) vkUnmapMemory(m_device, m_mem); + if (m_buf) vkDestroyBuffer(m_device, m_buf, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + } + auto update(const void* data, size_t size) -> void override + { if (m_mapped) std::memcpy(m_mapped, data, std::min(size, m_size)); } + + VkDevice m_device; VkBuffer m_buf; VkDeviceMemory m_mem; void* m_mapped; size_t m_size; + }; + + // Texture: sampled image (2D or cube). Owns its handles unless it is a borrowed + // wrapper around a render-target view. + class VkTextureR : public Texture + { + public: + VkTextureR() = default; + ~VkTextureR() override + { + if (!m_owns) return; + if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); + if (m_view) vkDestroyImageView(m_device, m_view, nullptr); + if (m_image) vkDestroyImage(m_device, m_image, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + } + VkDevice m_device = VK_NULL_HANDLE; + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_mem = VK_NULL_HANDLE; + VkImageView m_view = VK_NULL_HANDLE; + VkSampler m_sampler = VK_NULL_HANDLE; + bool m_owns = true; + }; + + // RenderTarget: off-screen colour image (+ optional depth) + framebuffer. + class VkRenderTargetR : public RenderTarget + { + public: + ~VkRenderTargetR() override + { + if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr); + if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); + if (m_view) vkDestroyImageView(m_device, m_view, nullptr); + if (m_image) vkDestroyImage(m_device, m_image, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + if (m_depth_view) vkDestroyImageView(m_device, m_depth_view, nullptr); + if (m_depth_image) vkDestroyImage(m_device, m_depth_image, nullptr); + if (m_depth_mem) vkFreeMemory(m_device, m_depth_mem, nullptr); + // m_pass is owned by the device's render-pass cache, not by us. + } + auto width() const -> int override { return m_w; } + auto height() const -> int override { return m_h; } + auto color_texture() -> Texture* override { return &m_color; } + + VkDevice m_device = VK_NULL_HANDLE; + int m_w = 0, m_h = 0; + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_mem = VK_NULL_HANDLE; + VkImageView m_view = VK_NULL_HANDLE; + VkSampler m_sampler = VK_NULL_HANDLE; + VkFramebuffer m_fb = VK_NULL_HANDLE; + VkRenderPass m_pass = VK_NULL_HANDLE; // borrowed (device pass cache) + bool m_has_depth = false; + VkImage m_depth_image = VK_NULL_HANDLE; + VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; + VkImageView m_depth_view = VK_NULL_HANDLE; + VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling + }; + + // Pipeline: the graphics pipeline + its (set-0) descriptor layout. + class VkPipelineR : public Pipeline + { + public: + ~VkPipelineR() override + { + if (m_pipeline) vkDestroyPipeline(m_device, m_pipeline, nullptr); + if (m_layout) vkDestroyPipelineLayout(m_device, m_layout, nullptr); + if (m_set_layout) vkDestroyDescriptorSetLayout(m_device, m_set_layout, nullptr); + } + VkDevice m_device = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayout m_layout = VK_NULL_HANDLE; + VkDescriptorSetLayout m_set_layout = VK_NULL_HANDLE; + std::vector<ResourceSlot> m_resources; + }; + + // CommandList: records into the frame's command buffer. Per-draw descriptor + // sets are allocated from a per-frame pool (no push-descriptor extension). + class VkCommandListR : public CommandList + { + public: + auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override + { + VkClearValue cvs[2]{}; + cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } }; + cvs[1].depthStencil = { 1.0f, 0 }; + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + if (target) + { + auto* rt = static_cast<VkRenderTargetR*>(target); + rpbi.renderPass = rt->m_pass; rpbi.framebuffer = rt->m_fb; + rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } }; + rpbi.clearValueCount = rt->m_has_depth ? 2 : 1; rpbi.pClearValues = cvs; + } + else + { + rpbi.renderPass = m_swapchain_rp; rpbi.framebuffer = m_swapchain_fb; + rpbi.renderArea = { { 0, 0 }, m_extent }; + rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; + } + vkCmdBeginRenderPass(m_cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + } + auto end_render_pass() -> void override { vkCmdEndRenderPass(m_cmd); } + + auto bind_pipeline(Pipeline* p) -> void override + { + m_pipe = static_cast<VkPipelineR*>(p); + vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline); + } + auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override + { + VkViewport vp{ (float)x, flip_y ? (float)(y + h) : (float)y, + (float)w, flip_y ? -(float)h : (float)h, 0.0f, 1.0f }; + VkRect2D sc{ { x, y }, { (uint32_t)w, (uint32_t)h } }; + vkCmdSetViewport(m_cmd, 0, 1, &vp); + vkCmdSetScissor(m_cmd, 0, 1, &sc); + } + auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override + { + if (binding >= MAX_BINDINGS) return; + m_buf_info[binding] = { static_cast<VkBufferR*>(ubo)->m_buf, 0, VK_WHOLE_SIZE }; + } + auto bind_texture(uint32_t binding, Texture* texture) -> void override + { + if (binding >= MAX_BINDINGS) return; + auto* t = static_cast<VkTextureR*>(texture); + m_img_info[binding] = { t->m_sampler, t->m_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + } + auto bind_vertex_buffer(Buffer* vb) -> void override + { + VkBuffer b = static_cast<VkBufferR*>(vb)->m_buf; VkDeviceSize off = 0; + vkCmdBindVertexBuffers(m_cmd, 0, 1, &b, &off); + } + auto bind_index_buffer(Buffer* ib) -> void override + { vkCmdBindIndexBuffer(m_cmd, static_cast<VkBufferR*>(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); } + auto draw(uint32_t vertex_count) -> void override + { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } + auto draw_indexed(uint32_t index_count) -> void override + { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } + + // Allocate + write + bind a descriptor set for the current pipeline's + // declared resources, using whatever was bound since bind_pipeline. + auto flush_descriptors() -> void + { + if (!m_pipe || m_pipe->m_resources.empty()) return; + VkDescriptorSetAllocateInfo ai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + ai.descriptorPool = m_frame_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &m_pipe->m_set_layout; + VkDescriptorSet set = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS) + { DONUT_ERROR("Vulkan RHI: descriptor set allocation failed"); return; } + + std::array<VkWriteDescriptorSet, MAX_BINDINGS> writes{}; + uint32_t n = 0; + for (const auto& r : m_pipe->m_resources) + { + VkWriteDescriptorSet& w = writes[n++]; + w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w.dstSet = set; w.dstBinding = r.binding; w.descriptorCount = 1; + if (r.kind == ResourceKind::UniformBuffer) + { w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &m_buf_info[r.binding]; } + else + { w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = &m_img_info[r.binding]; } + } + vkUpdateDescriptorSets(m_device, n, writes.data(), 0, nullptr); + vkCmdBindDescriptorSets(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_layout, 0, 1, &set, 0, nullptr); + } + + // Set by the device at begin_frame: + VkDevice m_device = VK_NULL_HANDLE; + VkCommandBuffer m_cmd = VK_NULL_HANDLE; + VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; + VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE; + VkExtent2D m_extent{}; + VkDescriptorPool m_frame_pool = VK_NULL_HANDLE; + + VkPipelineR* m_pipe = nullptr; + VkDescriptorBufferInfo m_buf_info[MAX_BINDINGS]{}; + VkDescriptorImageInfo m_img_info[MAX_BINDINGS]{}; + }; + + // The backend device. Its methods are defined across vulkan_device.cpp, + // vulkan_swapchain.cpp, vulkan_resources.cpp and vulkan_cubemap.cpp. + class VulkanDevice : public Device + { + public: + auto init(const NativeWindow& window) -> bool override; + auto shutdown() -> void override; + auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; } + auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); } + + auto create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> override; + auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> override; + auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override; + auto create_render_target(int w, int h, Format color, Format depth, Filter filter, int mips) -> Ref<RenderTarget> override; + auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> override; + + auto begin_frame(const glm::vec4& clear) -> CommandList* override; + auto end_frame() -> void override; + + auto init_imgui() -> void override; + auto imgui_new_frame() -> void override; + auto imgui_render(CommandList& cmds) -> void override; + + auto device_name() const -> const std::string& override { return m_gpu_name; } + + // internals (implemented across the vulkan_*.cpp files) + auto find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t; + auto create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; + auto load_spirv(const std::string& path) const -> std::vector<uint32_t>; + auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool; + + auto create_instance() -> bool; + auto pick_physical_and_device() -> bool; + auto create_swapchain() -> bool; + auto create_image_views() -> bool; + // Render passes are format-driven and cached: a pipeline/target's + // attachment signature (colour + optional depth, present vs sampled) + // maps to one pass. `present` = presented swapchain image. + auto get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass; + auto create_depth_and_framebuffers() -> bool; + auto create_command_and_sync() -> bool; + auto recreate_swapchain() -> bool; + auto cleanup_swapchain() -> void; + + GLFWwindow* m_window = nullptr; + int m_width = 0, m_height = 0; + bool m_framebuffer_resized = false; + std::string m_gpu_name; + + VkInstance m_instance = VK_NULL_HANDLE; + VkSurfaceKHR m_surface = VK_NULL_HANDLE; + VkPhysicalDevice m_physical = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + uint32_t m_graphics_family = 0, m_present_family = 0; + VkQueue m_graphics_queue = VK_NULL_HANDLE, m_present_queue = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties m_mem_props{}; + + VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; + VkFormat m_swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; + VkExtent2D m_extent{}; + std::vector<VkImage> m_images; + std::vector<VkImageView> m_image_views; + VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; // also lives in m_pass_cache + std::unordered_map<uint64_t, VkRenderPass> m_pass_cache; // keyed by (color,depth,present) + std::vector<VkFramebuffer> m_framebuffers; + VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE; + + VkCommandPool m_command_pool = VK_NULL_HANDLE; + std::vector<VkCommandBuffer> m_command_buffers; + std::vector<VkSemaphore> m_image_available; + std::vector<VkSemaphore> m_render_finished; + std::vector<VkFence> m_in_flight; + std::vector<VkFence> m_images_in_flight; + VkFence m_geo_in_use = VK_NULL_HANDLE; + uint32_t m_current_frame = 0, m_image_index = 0; + + std::vector<VkDescriptorPool> m_frame_pools; // one per frame in flight + VkDescriptorPool m_imgui_pool = VK_NULL_HANDLE; + bool m_imgui = false; + + VkCommandListR m_cmds; + }; +} diff --git a/src/platform/vulkan/vulkan_cubemap.cpp b/src/platform/vulkan/vulkan_cubemap.cpp new file mode 100644 index 0000000..3ffc496 --- /dev/null +++ b/src/platform/vulkan/vulkan_cubemap.cpp @@ -0,0 +1,288 @@ +#include "vulkan_common.h" + +#include "stb_image.h" +#include <glm/gtc/matrix_transform.hpp> + +namespace Donut::RHI +{ + auto VulkanDevice::create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> + { + auto tex = create_ref<VkTextureR>(); tex->m_device = m_device; + const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + const uint32_t FACE = 1024; + const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; + uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; + + VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 }; + cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; + cci.tiling = VK_IMAGE_TILING_OPTIMAL; + cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT + | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + vkCreateImage(m_device, &cci, nullptr, &tex->m_image); + VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &creq); + VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &cai, nullptr, &tex->m_mem); + vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); + VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + cvci.image = tex->m_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; + cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + vkCreateImageView(m_device, &cvci, nullptr, &tex->m_view); + VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; + csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS; + csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler); + + int w = 0, h = 0, ch = 0; + float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4); + if (!pixels) + { + DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + tb.image = tex->m_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb); + VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f; + VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + vkCmdClearColorImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng); + VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb); + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + return tex; + } + + const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; + size_t texel_count = (size_t)w * h * 4; + VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); + VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; + create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem); + void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp); + uint16_t* dst = (uint16_t*)mp; + for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); } + vkUnmapMemory(m_device, eq_staging_mem); + stbi_image_free(pixels); + + VkImage eq_image; VkDeviceMemory eq_mem; + VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 }; + eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT; + eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &eci, nullptr, &eq_image); + VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(m_device, eq_image, &ereq); + VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &eai, nullptr, &eq_mem); + vkBindImageMemory(m_device, eq_image, eq_mem, 0); + VkImageView eq_view; + VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt; + evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &evci, nullptr, &eq_view); + VkSampler eq_sampler; + VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR; + esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &esm, nullptr, &eq_sampler); + + VkImageView face_views[6]; + for (uint32_t i = 0; i < 6; ++i) + { + VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + fvci.image = tex->m_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; + fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; + vkCreateImageView(m_device, &fvci, nullptr, &face_views[i]); + } + + VkAttachmentDescription color{}; + color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; + color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; + VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + VkRenderPass rp; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; + vkCreateRenderPass(m_device, &rpci, nullptr, &rp); + VkFramebuffer face_fb[6]; + for (uint32_t i = 0; i < 6; ++i) + { + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1; + vkCreateFramebuffer(m_device, &fbci, nullptr, &face_fb[i]); + } + + VkDescriptorSetLayoutBinding binds[2]{}; + binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayout set_layout; + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds; + vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &set_layout); + VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } }; + VkDescriptorPool pool; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; + vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool); + + VkShaderModule vmod, fmod; + create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod); + create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); + VkPipelineLayout playout; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; + vkCreatePipelineLayout(m_device, &plci, nullptr, &playout); + VkPipelineShaderStageCreateInfo stages[2]{}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; + VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } }; + VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ + VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; + VkPipeline pipeline; + VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; + gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.layout = playout; gpci.renderPass = rp; gpci.subpass = 0; + vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); + vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); + + float cube_verts[] = { + -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1, + -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1, + 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1, + -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1, + -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1, + -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1, + }; + VkBuffer cube_vb; VkDeviceMemory cube_vb_mem; + create_buffer_raw(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); + vkMapMemory(m_device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); std::memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(m_device, cube_vb_mem); + + glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); + proj[1][1] *= -1.0f; + glm::mat4 views[6] = { + glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)), + }; + VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6]; + for (uint32_t i = 0; i < 6; ++i) + { + create_buffer_raw(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); + glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; + vkMapMemory(m_device, ubo_mem[i], 0, 128, 0, &mp); std::memcpy(mp, mats, 128); vkUnmapMemory(m_device, ubo_mem[i]); + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; + vkAllocateDescriptorSets(m_device, &dsai, &sets[i]); + VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE }; + VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet ws[2]{}; + ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info; + ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info; + vkUpdateDescriptorSets(m_device, 2, ws, 0, nullptr); + } + + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); + VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; + vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + VkImageMemoryBarrier to_read = to_dst; to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); + + VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; + for (uint32_t i = 0; i < 6; ++i) + { + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = rp; rpbi.framebuffer = face_fb[i]; rpbi.renderArea = { { 0, 0 }, { FACE, FACE } }; rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, playout, 0, 1, &sets[i], 0, nullptr); + VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &cube_vb, &off); + vkCmdDraw(cmd, 36, 1, 0, 0); + vkCmdEndRenderPass(cmd); + } + + { + VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + src0.image = tex->m_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; + src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0); + int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE; + for (uint32_t m = 1; m < CUBE_MIPS; ++m) + { + int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1; + VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + bd.image = tex->m_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; + bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd); + VkImageBlit blit{}; + blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 }; + blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 }; + vkCmdBlitImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); + VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + bs.image = tex->m_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; + bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs); + mipW = nW; mipH = nH; + } + VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + fin.image = tex->m_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin); + } + + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(m_device, ubo[i], nullptr); vkFreeMemory(m_device, ubo_mem[i], nullptr); vkDestroyFramebuffer(m_device, face_fb[i], nullptr); vkDestroyImageView(m_device, face_views[i], nullptr); } + vkDestroyBuffer(m_device, cube_vb, nullptr); vkFreeMemory(m_device, cube_vb_mem, nullptr); + vkDestroyPipeline(m_device, pipeline, nullptr); vkDestroyPipelineLayout(m_device, playout, nullptr); + vkDestroyDescriptorPool(m_device, pool, nullptr); vkDestroyDescriptorSetLayout(m_device, set_layout, nullptr); + vkDestroyRenderPass(m_device, rp, nullptr); + vkDestroySampler(m_device, eq_sampler, nullptr); vkDestroyImageView(m_device, eq_view, nullptr); + vkDestroyImage(m_device, eq_image, nullptr); vkFreeMemory(m_device, eq_mem, nullptr); + vkDestroyBuffer(m_device, eq_staging, nullptr); vkFreeMemory(m_device, eq_staging_mem, nullptr); + DONUT_INFO("Vulkan RHI: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); + return tex; + } + +} diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp index 45e773e..5d80cff 100644 --- a/src/platform/vulkan/vulkan_device.cpp +++ b/src/platform/vulkan/vulkan_device.cpp @@ -1,1290 +1,183 @@ -#include "vulkan_device.h" - -#include "core/log.h" - -#define GLFW_INCLUDE_VULKAN -#include <GLFW/glfw3.h> +#include "vulkan_common.h" #include <imgui.h> #include <imgui_impl_glfw.h> #include <imgui_impl_vulkan.h> -#include <glm/glm.hpp> -#include <glm/gtc/matrix_transform.hpp> -#include "stb_image.h" - -#include <vector> -#include <array> -#include <algorithm> -#include <cstring> -#include <cstdlib> #include <fstream> -#include <unordered_map> +#include <cstdlib> namespace Donut::RHI { - namespace + auto VulkanDevice::find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t { - constexpr int MAX_FRAMES_IN_FLIGHT = 2; - constexpr uint32_t MAX_BINDINGS = 8; - - #define VKD_CHECK(expr) \ - do { \ - VkResult _r = (expr); \ - if (_r != VK_SUCCESS) { \ - DONUT_ERROR("Vulkan RHI: {} failed ({})", #expr, (int)_r); \ - return false; \ - } \ - } while (0) - - auto vk_format(Format f) -> VkFormat - { - switch (f) { - case Format::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT; - case Format::D32: return VK_FORMAT_D32_SFLOAT; - default: return VK_FORMAT_R8G8B8A8_UNORM; - } - } - auto vk_attr_format(uint32_t comps) -> VkFormat - { - switch (comps) { - case 1: return VK_FORMAT_R32_SFLOAT; - case 2: return VK_FORMAT_R32G32_SFLOAT; - case 3: return VK_FORMAT_R32G32B32_SFLOAT; - default: return VK_FORMAT_R32G32B32A32_SFLOAT; - } - } - auto vk_topology(Topology t) -> VkPrimitiveTopology - { return t == Topology::Lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; } - auto vk_compare(CompareOp o) -> VkCompareOp - { return o == CompareOp::Always ? VK_COMPARE_OP_ALWAYS : o == CompareOp::LessEqual ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; } - auto vk_filter(Filter f) -> VkFilter { return f == Filter::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } - auto vk_cull(CullMode c) -> VkCullModeFlags - { return c == CullMode::None ? VK_CULL_MODE_NONE : c == CullMode::Back ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT; } - - class VulkanDevice; - - // ---- Buffer: host-visible + coherent, persistently mapped ----------- - class VkBufferR : public Buffer - { - public: - VkBufferR(VkDevice d, VkBuffer b, VkDeviceMemory m, void* mapped, size_t size) - : m_device(d), m_buf(b), m_mem(m), m_mapped(mapped), m_size(size) {} - ~VkBufferR() override - { - if (m_mapped) vkUnmapMemory(m_device, m_mem); - if (m_buf) vkDestroyBuffer(m_device, m_buf, nullptr); - if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); - } - auto update(const void* data, size_t size) -> void override - { if (m_mapped) std::memcpy(m_mapped, data, std::min(size, m_size)); } - - VkDevice m_device; VkBuffer m_buf; VkDeviceMemory m_mem; void* m_mapped; size_t m_size; - }; - - // ---- Texture: sampled image (2D or cube). Owns its handles unless it is - // a borrowed wrapper around a render-target view. ------------------ - class VkTextureR : public Texture - { - public: - VkTextureR() = default; - ~VkTextureR() override - { - if (!m_owns) return; - if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); - if (m_view) vkDestroyImageView(m_device, m_view, nullptr); - if (m_image) vkDestroyImage(m_device, m_image, nullptr); - if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); - } - VkDevice m_device = VK_NULL_HANDLE; - VkImage m_image = VK_NULL_HANDLE; - VkDeviceMemory m_mem = VK_NULL_HANDLE; - VkImageView m_view = VK_NULL_HANDLE; - VkSampler m_sampler = VK_NULL_HANDLE; - bool m_owns = true; - }; - - // ---- RenderTarget: off-screen colour image + framebuffer ------------ - class VkRenderTargetR : public RenderTarget - { - public: - ~VkRenderTargetR() override - { - if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr); - if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); - if (m_view) vkDestroyImageView(m_device, m_view, nullptr); - if (m_image) vkDestroyImage(m_device, m_image, nullptr); - if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); - if (m_depth_view) vkDestroyImageView(m_device, m_depth_view, nullptr); - if (m_depth_image) vkDestroyImage(m_device, m_depth_image, nullptr); - if (m_depth_mem) vkFreeMemory(m_device, m_depth_mem, nullptr); - // m_pass is owned by the device's render-pass cache, not by us. - } - auto width() const -> int override { return m_w; } - auto height() const -> int override { return m_h; } - auto color_texture() -> Texture* override { return &m_color; } - - VkDevice m_device = VK_NULL_HANDLE; - int m_w = 0, m_h = 0; - VkImage m_image = VK_NULL_HANDLE; - VkDeviceMemory m_mem = VK_NULL_HANDLE; - VkImageView m_view = VK_NULL_HANDLE; - VkSampler m_sampler = VK_NULL_HANDLE; - VkFramebuffer m_fb = VK_NULL_HANDLE; - VkRenderPass m_pass = VK_NULL_HANDLE; // borrowed (device pass cache) - bool m_has_depth = false; - VkImage m_depth_image = VK_NULL_HANDLE; - VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; - VkImageView m_depth_view = VK_NULL_HANDLE; - VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling - }; - - // ---- Pipeline ------------------------------------------------------- - class VkPipelineR : public Pipeline - { - public: - ~VkPipelineR() override - { - if (m_pipeline) vkDestroyPipeline(m_device, m_pipeline, nullptr); - if (m_layout) vkDestroyPipelineLayout(m_device, m_layout, nullptr); - if (m_set_layout) vkDestroyDescriptorSetLayout(m_device, m_set_layout, nullptr); - } - VkDevice m_device = VK_NULL_HANDLE; - VkPipeline m_pipeline = VK_NULL_HANDLE; - VkPipelineLayout m_layout = VK_NULL_HANDLE; - VkDescriptorSetLayout m_set_layout = VK_NULL_HANDLE; - std::vector<ResourceSlot> m_resources; - }; - - // ---- CommandList ---------------------------------------------------- - class VkCommandListR : public CommandList - { - public: - auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override - { - VkClearValue cvs[2]{}; - cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } }; - cvs[1].depthStencil = { 1.0f, 0 }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - if (target) - { - auto* rt = static_cast<VkRenderTargetR*>(target); - rpbi.renderPass = rt->m_pass; rpbi.framebuffer = rt->m_fb; - rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } }; - rpbi.clearValueCount = rt->m_has_depth ? 2 : 1; rpbi.pClearValues = cvs; - } - else - { - rpbi.renderPass = m_swapchain_rp; rpbi.framebuffer = m_swapchain_fb; - rpbi.renderArea = { { 0, 0 }, m_extent }; - rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; - } - vkCmdBeginRenderPass(m_cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - } - auto end_render_pass() -> void override { vkCmdEndRenderPass(m_cmd); } - - auto bind_pipeline(Pipeline* p) -> void override - { - m_pipe = static_cast<VkPipelineR*>(p); - vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline); - } - auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override - { - VkViewport vp{ (float)x, flip_y ? (float)(y + h) : (float)y, - (float)w, flip_y ? -(float)h : (float)h, 0.0f, 1.0f }; - VkRect2D sc{ { x, y }, { (uint32_t)w, (uint32_t)h } }; - vkCmdSetViewport(m_cmd, 0, 1, &vp); - vkCmdSetScissor(m_cmd, 0, 1, &sc); - } - auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override - { - if (binding >= MAX_BINDINGS) return; - m_buf_info[binding] = { static_cast<VkBufferR*>(ubo)->m_buf, 0, VK_WHOLE_SIZE }; - } - auto bind_texture(uint32_t binding, Texture* texture) -> void override - { - if (binding >= MAX_BINDINGS) return; - auto* t = static_cast<VkTextureR*>(texture); - m_img_info[binding] = { t->m_sampler, t->m_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - } - auto bind_vertex_buffer(Buffer* vb) -> void override - { - VkBuffer b = static_cast<VkBufferR*>(vb)->m_buf; VkDeviceSize off = 0; - vkCmdBindVertexBuffers(m_cmd, 0, 1, &b, &off); - } - auto bind_index_buffer(Buffer* ib) -> void override - { vkCmdBindIndexBuffer(m_cmd, static_cast<VkBufferR*>(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); } - auto draw(uint32_t vertex_count) -> void override - { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } - auto draw_indexed(uint32_t index_count) -> void override - { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } - - // Allocate + write + bind a descriptor set for the current pipeline's - // declared resources, using whatever was bound since bind_pipeline. - auto flush_descriptors() -> void - { - if (!m_pipe || m_pipe->m_resources.empty()) return; - VkDescriptorSetAllocateInfo ai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - ai.descriptorPool = m_frame_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &m_pipe->m_set_layout; - VkDescriptorSet set = VK_NULL_HANDLE; - if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS) - { DONUT_ERROR("Vulkan RHI: descriptor set allocation failed"); return; } - - std::array<VkWriteDescriptorSet, MAX_BINDINGS> writes{}; - uint32_t n = 0; - for (const auto& r : m_pipe->m_resources) - { - VkWriteDescriptorSet& w = writes[n++]; - w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w.dstSet = set; w.dstBinding = r.binding; w.descriptorCount = 1; - if (r.kind == ResourceKind::UniformBuffer) - { w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &m_buf_info[r.binding]; } - else - { w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = &m_img_info[r.binding]; } - } - vkUpdateDescriptorSets(m_device, n, writes.data(), 0, nullptr); - vkCmdBindDescriptorSets(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_layout, 0, 1, &set, 0, nullptr); - } - - // Set by the device at begin_frame: - VkDevice m_device = VK_NULL_HANDLE; - VkCommandBuffer m_cmd = VK_NULL_HANDLE; - VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; - VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE; - VkExtent2D m_extent{}; - VkDescriptorPool m_frame_pool = VK_NULL_HANDLE; - - VkPipelineR* m_pipe = nullptr; - VkDescriptorBufferInfo m_buf_info[MAX_BINDINGS]{}; - VkDescriptorImageInfo m_img_info[MAX_BINDINGS]{}; - }; - - // ---- Device --------------------------------------------------------- - class VulkanDevice : public Device - { - public: - auto init(const NativeWindow& window) -> bool override; - auto shutdown() -> void override; - auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; } - auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); } - - auto create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> override; - auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> override; - auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override; - auto create_render_target(int w, int h, Format color, Format depth, Filter filter, int mips) -> Ref<RenderTarget> override; - auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> override; - - auto begin_frame(const glm::vec4& clear) -> CommandList* override; - auto end_frame() -> void override; - - auto init_imgui() -> void override; - auto imgui_new_frame() -> void override; - auto imgui_render(CommandList& cmds) -> void override; - - auto device_name() const -> const std::string& override { return m_gpu_name; } - - // --- internals --- - auto find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t; - auto create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; - auto load_spirv(const std::string& path) const -> std::vector<uint32_t>; - auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool; - - auto create_instance() -> bool; - auto pick_physical_and_device() -> bool; - auto create_swapchain() -> bool; - auto create_image_views() -> bool; - // Render passes are format-driven and cached: a pipeline/target's - // attachment signature (colour + optional depth, present vs sampled) - // maps to one pass. `present` = presented swapchain image. - auto get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass; - auto create_depth_and_framebuffers() -> bool; - auto create_command_and_sync() -> bool; - auto recreate_swapchain() -> bool; - auto cleanup_swapchain() -> void; - - GLFWwindow* m_window = nullptr; - int m_width = 0, m_height = 0; - bool m_framebuffer_resized = false; - std::string m_gpu_name; - - VkInstance m_instance = VK_NULL_HANDLE; - VkSurfaceKHR m_surface = VK_NULL_HANDLE; - VkPhysicalDevice m_physical = VK_NULL_HANDLE; - VkDevice m_device = VK_NULL_HANDLE; - uint32_t m_graphics_family = 0, m_present_family = 0; - VkQueue m_graphics_queue = VK_NULL_HANDLE, m_present_queue = VK_NULL_HANDLE; - VkPhysicalDeviceMemoryProperties m_mem_props{}; - - VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; - VkFormat m_swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; - VkExtent2D m_extent{}; - std::vector<VkImage> m_images; - std::vector<VkImageView> m_image_views; - VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; // also lives in m_pass_cache - std::unordered_map<uint64_t, VkRenderPass> m_pass_cache; // keyed by (color,depth,present) - std::vector<VkFramebuffer> m_framebuffers; - VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE; - - VkCommandPool m_command_pool = VK_NULL_HANDLE; - std::vector<VkCommandBuffer> m_command_buffers; - std::vector<VkSemaphore> m_image_available; - std::vector<VkSemaphore> m_render_finished; - std::vector<VkFence> m_in_flight; - std::vector<VkFence> m_images_in_flight; - VkFence m_geo_in_use = VK_NULL_HANDLE; - uint32_t m_current_frame = 0, m_image_index = 0; - - std::vector<VkDescriptorPool> m_frame_pools; // one per frame in flight - VkDescriptorPool m_imgui_pool = VK_NULL_HANDLE; - bool m_imgui = false; - - VkCommandListR m_cmds; - }; - - // ================================================================== - auto VulkanDevice::find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t - { - for (uint32_t i = 0; i < m_mem_props.memoryTypeCount; ++i) - if ((filter & (1u << i)) && (m_mem_props.memoryTypes[i].propertyFlags & flags) == flags) - return i; - return UINT32_MAX; - } - - auto VulkanDevice::create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, - VkBuffer& buf, VkDeviceMemory& mem) const -> bool - { - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - if (vkCreateBuffer(m_device, &bci, nullptr, &buf) != VK_SUCCESS) return false; - VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(m_device, buf, &req); - VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); - if (vkAllocateMemory(m_device, &ai, nullptr, &mem) != VK_SUCCESS) return false; - vkBindBufferMemory(m_device, buf, mem, 0); - return true; - } - - auto VulkanDevice::load_spirv(const std::string& path) const -> std::vector<uint32_t> - { - std::ifstream file(path, std::ios::ate | std::ios::binary); - if (!file.is_open()) return {}; - size_t size = (size_t)file.tellg(); - std::vector<uint32_t> data(size / 4); - file.seekg(0); file.read(reinterpret_cast<char*>(data.data()), size); - return data; - } - - auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool - { - auto spv = load_spirv(path); - if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; } - VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); - return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS; - } - - auto VulkanDevice::create_instance() -> bool - { - VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; - app.pApplicationName = "Donut"; app.apiVersion = VK_API_VERSION_1_2; - - uint32_t glfwExtCount = 0; - const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); - if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; } - std::vector<const char*> exts(glfwExts, glfwExts + glfwExtCount); - exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); - exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); - - std::vector<const char*> layers; - uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr); - std::vector<VkLayerProperties> avail(layer_count); - vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); - for (const auto& l : avail) - if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) - layers.push_back("VK_LAYER_KHRONOS_validation"); - - VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; - ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; - ici.pApplicationInfo = &app; - ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data(); - ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data(); - VkResult r = vkCreateInstance(&ici, nullptr, &m_instance); - if (r != VK_SUCCESS && !layers.empty()) - { - DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it"); - ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr; - r = vkCreateInstance(&ici, nullptr, &m_instance); - } - if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: vkCreateInstance failed ({})", (int)r); return false; } - VKD_CHECK(glfwCreateWindowSurface(m_instance, m_window, nullptr, &m_surface)); - DONUT_INFO("Vulkan RHI: instance + surface created (validation {})", layers.empty() ? "off" : "on"); - return true; - } - - auto VulkanDevice::pick_physical_and_device() -> bool - { - uint32_t count = 0; vkEnumeratePhysicalDevices(m_instance, &count, nullptr); - if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; } - std::vector<VkPhysicalDevice> devices(count); - vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); - m_physical = devices[0]; - - uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr); - std::vector<VkQueueFamilyProperties> qfams(q); - vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data()); - bool fg = false, fp = false; - for (uint32_t i = 0; i < q; ++i) - { - if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; } - VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present); - if (!fp && present) { m_present_family = i; fp = true; } - } - if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; } - - std::vector<const char*> dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr); - std::vector<VkExtensionProperties> dep(dec); - vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, dep.data()); - for (const auto& e : dep) - if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) - dev_exts.push_back("VK_KHR_portability_subset"); - - float priority = 1.0f; - std::vector<VkDeviceQueueCreateInfo> qcis; - uint32_t families[2] = { m_graphics_family, m_present_family }; - for (uint32_t i = 0; i < (m_graphics_family == m_present_family ? 1u : 2u); ++i) - { - VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; - qci.queueFamilyIndex = families[i]; qci.queueCount = 1; qci.pQueuePriorities = &priority; - qcis.push_back(qci); - } - VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; - dci.queueCreateInfoCount = (uint32_t)qcis.size(); dci.pQueueCreateInfos = qcis.data(); - dci.enabledExtensionCount = (uint32_t)dev_exts.size(); dci.ppEnabledExtensionNames = dev_exts.data(); - VKD_CHECK(vkCreateDevice(m_physical, &dci, nullptr, &m_device)); - vkGetDeviceQueue(m_device, m_graphics_family, 0, &m_graphics_queue); - vkGetDeviceQueue(m_device, m_present_family, 0, &m_present_queue); - - VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(m_physical, &props); - vkGetPhysicalDeviceMemoryProperties(m_physical, &m_mem_props); - m_gpu_name = props.deviceName; - DONUT_INFO("Vulkan RHI device: {}", m_gpu_name); - return true; - } - - auto VulkanDevice::create_swapchain() -> bool - { - VkSurfaceCapabilitiesKHR caps{}; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical, m_surface, &caps); - uint32_t fc = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, nullptr); - std::vector<VkSurfaceFormatKHR> formats(fc); - vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, formats.data()); - VkSurfaceFormatKHR chosen = formats[0]; - for (const auto& f : formats) - if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f; - m_swapchain_format = chosen.format; - - if (caps.currentExtent.width != UINT32_MAX) m_extent = caps.currentExtent; - else { - m_extent.width = std::clamp((uint32_t)m_width, caps.minImageExtent.width, caps.maxImageExtent.width); - m_extent.height = std::clamp((uint32_t)m_height, caps.minImageExtent.height, caps.maxImageExtent.height); - } - uint32_t image_count = caps.minImageCount + 1; - if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) image_count = caps.maxImageCount; - - VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; - sci.surface = m_surface; sci.minImageCount = image_count; - sci.imageFormat = chosen.format; sci.imageColorSpace = chosen.colorSpace; - sci.imageExtent = m_extent; sci.imageArrayLayers = 1; - sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE; - uint32_t fam[2] = { m_graphics_family, m_present_family }; - if (m_graphics_family != m_present_family) - { sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; sci.queueFamilyIndexCount = 2; sci.pQueueFamilyIndices = fam; } - else sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - VKD_CHECK(vkCreateSwapchainKHR(m_device, &sci, nullptr, &m_swapchain)); - uint32_t n = 0; vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, nullptr); - m_images.resize(n); vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, m_images.data()); - return true; - } - - auto VulkanDevice::create_image_views() -> bool - { - m_image_views.resize(m_images.size()); - for (size_t i = 0; i < m_images.size(); ++i) - { - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = m_images[i]; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = m_swapchain_format; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VKD_CHECK(vkCreateImageView(m_device, &vci, nullptr, &m_image_views[i])); - } - return true; - } - - // A render pass for one attachment signature, created once and cached. - // `present` targets (the swapchain) finish PRESENT_SRC and sync on the - // colour-output stage; `sampled` targets (off-screen) finish - // SHADER_READ_ONLY and round-trip through the fragment shader so the next - // pass can sample them. Depth (VK_FORMAT_UNDEFINED = none) is optional. - auto VulkanDevice::get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass - { - uint64_t key = (uint64_t)(uint32_t)color - | ((uint64_t)(uint32_t)depth << 24) - | ((uint64_t)(present ? 1 : 0) << 48); - auto it = m_pass_cache.find(key); - if (it != m_pass_cache.end()) return it->second; - - const bool has_depth = depth != VK_FORMAT_UNDEFINED; - VkAttachmentDescription atts[2]{}; - atts[0].format = color; atts[0].samples = VK_SAMPLE_COUNT_1_BIT; - atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - atts[0].finalLayout = present ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - atts[1].format = depth; atts[1].samples = VK_SAMPLE_COUNT_1_BIT; - atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; - if (has_depth) subpass.pDepthStencilAttachment = &depth_ref; - - VkSubpassDependency deps[2]{}; - uint32_t dep_count; - if (present) - { - deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; - deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - dep_count = 1; - } - else - { - deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; - deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; - deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - dep_count = 2; - } - - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = has_depth ? 2 : 1; rpci.pAttachments = atts; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = dep_count; rpci.pDependencies = deps; - VkRenderPass rp = VK_NULL_HANDLE; - if (vkCreateRenderPass(m_device, &rpci, nullptr, &rp) != VK_SUCCESS) - { DONUT_ERROR("Vulkan RHI: render pass creation failed"); return VK_NULL_HANDLE; } - m_pass_cache[key] = rp; - return rp; - } - - auto VulkanDevice::create_depth_and_framebuffers() -> bool - { - VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - dici.imageType = VK_IMAGE_TYPE_2D; dici.format = VK_FORMAT_D32_SFLOAT; - dici.extent = { m_extent.width, m_extent.height, 1 }; - dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; - dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; - VKD_CHECK(vkCreateImage(m_device, &dici, nullptr, &m_depth_image)); - VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, m_depth_image, &dreq); - VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VKD_CHECK(vkAllocateMemory(m_device, &dai, nullptr, &m_depth_mem)); - VKD_CHECK(vkBindImageMemory(m_device, m_depth_image, m_depth_mem, 0)); - VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - dvci.image = m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = VK_FORMAT_D32_SFLOAT; - dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; - VKD_CHECK(vkCreateImageView(m_device, &dvci, nullptr, &m_depth_view)); - - m_framebuffers.resize(m_image_views.size()); - for (size_t i = 0; i < m_image_views.size(); ++i) - { - VkImageView att[2] = { m_image_views[i], m_depth_view }; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = m_swapchain_rp; fbci.attachmentCount = 2; fbci.pAttachments = att; - fbci.width = m_extent.width; fbci.height = m_extent.height; fbci.layers = 1; - VKD_CHECK(vkCreateFramebuffer(m_device, &fbci, nullptr, &m_framebuffers[i])); - } - return true; - } - - auto VulkanDevice::create_command_and_sync() -> bool - { - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = m_graphics_family; - VKD_CHECK(vkCreateCommandPool(m_device, &pci, nullptr, &m_command_pool)); - m_command_buffers.resize(MAX_FRAMES_IN_FLIGHT); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; - VKD_CHECK(vkAllocateCommandBuffers(m_device, &cbai, m_command_buffers.data())); - - m_image_available.resize(MAX_FRAMES_IN_FLIGHT); - m_in_flight.resize(MAX_FRAMES_IN_FLIGHT); - m_render_finished.resize(m_images.size()); - m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); - VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; - for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) - { VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_image_available[i])); VKD_CHECK(vkCreateFence(m_device, &fci, nullptr, &m_in_flight[i])); } - for (size_t i = 0; i < m_images.size(); ++i) - VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_render_finished[i])); - - m_frame_pools.resize(MAX_FRAMES_IN_FLIGHT); - VkDescriptorPoolSize sizes[2] = { - { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, - { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256 }, - }; - for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) - { - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 256; dpci.poolSizeCount = 2; dpci.pPoolSizes = sizes; - VKD_CHECK(vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_frame_pools[i])); - } - return true; - } - - auto VulkanDevice::init(const NativeWindow& window) -> bool - { - m_window = (GLFWwindow*)window.glfw_handle; m_width = window.width; m_height = window.height; - if (!create_instance()) return false; - if (!pick_physical_and_device()) return false; - if (!create_swapchain()) return false; - if (!create_image_views()) return false; - // The swapchain is a "present" target: its colour format + a D32 depth. - m_swapchain_rp = get_render_pass(m_swapchain_format, VK_FORMAT_D32_SFLOAT, true); - if (!m_swapchain_rp) return false; - if (!create_depth_and_framebuffers())return false; - if (!create_command_and_sync()) return false; - DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height); - return true; - } - - auto VulkanDevice::cleanup_swapchain() -> void - { - for (auto fb : m_framebuffers) vkDestroyFramebuffer(m_device, fb, nullptr); - m_framebuffers.clear(); - if (m_depth_view) { vkDestroyImageView(m_device, m_depth_view, nullptr); m_depth_view = VK_NULL_HANDLE; } - if (m_depth_image) { vkDestroyImage(m_device, m_depth_image, nullptr); m_depth_image = VK_NULL_HANDLE; } - if (m_depth_mem) { vkFreeMemory(m_device, m_depth_mem, nullptr); m_depth_mem = VK_NULL_HANDLE; } - for (auto iv : m_image_views) vkDestroyImageView(m_device, iv, nullptr); - m_image_views.clear(); - if (m_swapchain) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; } - } - - auto VulkanDevice::recreate_swapchain() -> bool - { - int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h); - while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); } - m_width = w; m_height = h; - vkDeviceWaitIdle(m_device); - cleanup_swapchain(); - if (!create_swapchain()) return false; - if (!create_image_views()) return false; - if (!create_depth_and_framebuffers())return false; - m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); - return true; - } - - auto VulkanDevice::create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> - { - VkBufferUsageFlags usage = type == BufferType::Index ? VK_BUFFER_USAGE_INDEX_BUFFER_BIT - : type == BufferType::Uniform ? VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT - : VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; - VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; - create_buffer_raw(size, usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buf, mem); - void* mapped = nullptr; vkMapMemory(m_device, mem, 0, size, 0, &mapped); - if (data && mapped) std::memcpy(mapped, data, size); - return create_ref<VkBufferR>(m_device, buf, mem, mapped, size); - } - - auto VulkanDevice::create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> - { - auto tex = create_ref<VkTextureR>(); tex->m_device = m_device; - VkFormat fmt = vk_format(format); - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; - ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - vkCreateImage(m_device, &ici, nullptr, &tex->m_image); - VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &req); - VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vkAllocateMemory(m_device, &ai, nullptr, &tex->m_mem); - vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); - - size_t bpp = format == Format::RGBA16F ? 8 : 4; - VkDeviceSize sz = (VkDeviceSize)w * h * bpp; - VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; - create_buffer_raw(sz, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging, staging_mem); - void* mp = nullptr; vkMapMemory(m_device, staging_mem, 0, sz, 0, &mp); - if (data) std::memcpy(mp, data, sz); else std::memset(mp, 0, sz); - vkUnmapMemory(m_device, staging_mem); - - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - VkImageMemoryBarrier b{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - b.image = tex->m_image; b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); - VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; - vkCmdCopyBufferToImage(cmd, staging, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); - VkImageMemoryBarrier r = b; r.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; r.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - r.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; r.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &r); - vkEndCommandBuffer(cmd); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); - vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); - vkDestroyBuffer(m_device, staging, nullptr); vkFreeMemory(m_device, staging_mem, nullptr); - - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = tex->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - vkCreateImageView(m_device, &vci, nullptr, &tex->m_view); - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(m_device, &smci, nullptr, &tex->m_sampler); - return tex; - } - - auto VulkanDevice::create_render_target(int w, int h, Format color, Format depth, Filter filter, int /*mips*/) -> Ref<RenderTarget> - { - auto rt = create_ref<VkRenderTargetR>(); - rt->m_device = m_device; rt->m_w = w; rt->m_h = h; - VkFormat cfmt = (color == Format::Swapchain) ? m_swapchain_format : vk_format(color); - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = cfmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; - ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - vkCreateImage(m_device, &ici, nullptr, &rt->m_image); - VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req); - VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem); - vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0); - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = cfmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - vkCreateImageView(m_device, &vci, nullptr, &rt->m_view); - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler); - - // Optional depth attachment (for off-screen passes that need a depth test). - rt->m_has_depth = depth != Format::None; - VkFormat dfmt = VK_FORMAT_UNDEFINED; - if (rt->m_has_depth) - { - dfmt = vk_format(depth); - VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - dici.imageType = VK_IMAGE_TYPE_2D; dici.format = dfmt; dici.extent = { (uint32_t)w, (uint32_t)h, 1 }; - dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; - dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; - vkCreateImage(m_device, &dici, nullptr, &rt->m_depth_image); - VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, rt->m_depth_image, &dreq); - VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vkAllocateMemory(m_device, &dai, nullptr, &rt->m_depth_mem); - vkBindImageMemory(m_device, rt->m_depth_image, rt->m_depth_mem, 0); - VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - dvci.image = rt->m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = dfmt; - dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; - vkCreateImageView(m_device, &dvci, nullptr, &rt->m_depth_view); - } - - rt->m_pass = get_render_pass(cfmt, dfmt, false); // off-screen (sampled) target - VkImageView atts[2] = { rt->m_view, rt->m_depth_view }; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = rt->m_pass; fbci.attachmentCount = rt->m_has_depth ? 2u : 1u; fbci.pAttachments = atts; - fbci.width = w; fbci.height = h; fbci.layers = 1; - vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb); - - rt->m_color.m_device = m_device; rt->m_color.m_view = rt->m_view; rt->m_color.m_sampler = rt->m_sampler; rt->m_color.m_owns = false; - return rt; - } - - // Builds an environment cubemap from an equirectangular HDRI: render the 6 - // faces with the EquirectToCubemap pipeline, then a full mip chain by - // linear down-blits (so divergence-based LOD reads a blurred sky). Returns - // a Texture owning the cube image/view/sampler. - auto VulkanDevice::create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> - { - auto tex = create_ref<VkTextureR>(); tex->m_device = m_device; - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - const uint32_t FACE = 1024; - const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; - uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; - - VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; - cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 }; - cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; - cci.tiling = VK_IMAGE_TILING_OPTIMAL; - cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; - vkCreateImage(m_device, &cci, nullptr, &tex->m_image); - VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &creq); - VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vkAllocateMemory(m_device, &cai, nullptr, &tex->m_mem); - vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); - VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - cvci.image = tex->m_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; - cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - vkCreateImageView(m_device, &cvci, nullptr, &tex->m_view); - VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; - csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS; - csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler); - - int w = 0, h = 0, ch = 0; - float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4); - if (!pixels) - { - DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - tb.image = tex->m_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb); - VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f; - VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - vkCmdClearColorImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng); - VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb); - vkEndCommandBuffer(cmd); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); - vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); - return tex; - } - - const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; - size_t texel_count = (size_t)w * h * 4; - VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); - VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; - create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem); - void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp); - uint16_t* dst = (uint16_t*)mp; - for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); } - vkUnmapMemory(m_device, eq_staging_mem); - stbi_image_free(pixels); - - VkImage eq_image; VkDeviceMemory eq_mem; - VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 }; - eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT; - eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - vkCreateImage(m_device, &eci, nullptr, &eq_image); - VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(m_device, eq_image, &ereq); - VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - vkAllocateMemory(m_device, &eai, nullptr, &eq_mem); - vkBindImageMemory(m_device, eq_image, eq_mem, 0); - VkImageView eq_view; - VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt; - evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - vkCreateImageView(m_device, &evci, nullptr, &eq_view); - VkSampler eq_sampler; - VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR; - esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; - esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(m_device, &esm, nullptr, &eq_sampler); - - VkImageView face_views[6]; - for (uint32_t i = 0; i < 6; ++i) - { - VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - fvci.image = tex->m_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; - fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; - vkCreateImageView(m_device, &fvci, nullptr, &face_views[i]); - } - - VkAttachmentDescription color{}; - color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; - VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - VkRenderPass rp; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; - vkCreateRenderPass(m_device, &rpci, nullptr, &rp); - VkFramebuffer face_fb[6]; - for (uint32_t i = 0; i < 6; ++i) - { - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1; - vkCreateFramebuffer(m_device, &fbci, nullptr, &face_fb[i]); - } - - VkDescriptorSetLayoutBinding binds[2]{}; - binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayout set_layout; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds; - vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &set_layout); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } }; - VkDescriptorPool pool; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool); - - VkShaderModule vmod, fmod; - create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod); - create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); - VkPipelineLayout playout; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; - vkCreatePipelineLayout(m_device, &plci, nullptr, &playout); - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } }; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkPipeline pipeline; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.layout = playout; gpci.renderPass = rp; gpci.subpass = 0; - vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); - vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); - - float cube_verts[] = { - -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1, - -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1, - 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1, - -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1, - -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1, - -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1, - }; - VkBuffer cube_vb; VkDeviceMemory cube_vb_mem; - create_buffer_raw(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); - vkMapMemory(m_device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); std::memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(m_device, cube_vb_mem); - - glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); - proj[1][1] *= -1.0f; - glm::mat4 views[6] = { - glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)), - }; - VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6]; - for (uint32_t i = 0; i < 6; ++i) - { - create_buffer_raw(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); - glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; - vkMapMemory(m_device, ubo_mem[i], 0, 128, 0, &mp); std::memcpy(mp, mats, 128); vkUnmapMemory(m_device, ubo_mem[i]); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; - vkAllocateDescriptorSets(m_device, &dsai, &sets[i]); - VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE }; - VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet ws[2]{}; - ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info; - ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info; - vkUpdateDescriptorSets(m_device, 2, ws, 0, nullptr); - } - - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); - VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; - vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); - VkImageMemoryBarrier to_read = to_dst; to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); - - VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; - for (uint32_t i = 0; i < 6; ++i) - { - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = rp; rpbi.framebuffer = face_fb[i]; rpbi.renderArea = { { 0, 0 }, { FACE, FACE } }; rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, playout, 0, 1, &sets[i], 0, nullptr); - VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &cube_vb, &off); - vkCmdDraw(cmd, 36, 1, 0, 0); - vkCmdEndRenderPass(cmd); - } - - { - VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - src0.image = tex->m_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0); - int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE; - for (uint32_t m = 1; m < CUBE_MIPS; ++m) - { - int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1; - VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - bd.image = tex->m_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; - bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd); - VkImageBlit blit{}; - blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 }; - blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 }; - vkCmdBlitImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); - VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - bs.image = tex->m_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; - bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs); - mipW = nW; mipH = nH; - } - VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - fin.image = tex->m_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin); - } - - vkEndCommandBuffer(cmd); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); - - vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); - for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(m_device, ubo[i], nullptr); vkFreeMemory(m_device, ubo_mem[i], nullptr); vkDestroyFramebuffer(m_device, face_fb[i], nullptr); vkDestroyImageView(m_device, face_views[i], nullptr); } - vkDestroyBuffer(m_device, cube_vb, nullptr); vkFreeMemory(m_device, cube_vb_mem, nullptr); - vkDestroyPipeline(m_device, pipeline, nullptr); vkDestroyPipelineLayout(m_device, playout, nullptr); - vkDestroyDescriptorPool(m_device, pool, nullptr); vkDestroyDescriptorSetLayout(m_device, set_layout, nullptr); - vkDestroyRenderPass(m_device, rp, nullptr); - vkDestroySampler(m_device, eq_sampler, nullptr); vkDestroyImageView(m_device, eq_view, nullptr); - vkDestroyImage(m_device, eq_image, nullptr); vkFreeMemory(m_device, eq_mem, nullptr); - vkDestroyBuffer(m_device, eq_staging, nullptr); vkFreeMemory(m_device, eq_staging_mem, nullptr); - DONUT_INFO("Vulkan RHI: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); - return tex; - } + for (uint32_t i = 0; i < m_mem_props.memoryTypeCount; ++i) + if ((filter & (1u << i)) && (m_mem_props.memoryTypes[i].propertyFlags & flags) == flags) + return i; + return UINT32_MAX; + } - auto VulkanDevice::create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> - { - auto p = create_ref<VkPipelineR>(); p->m_device = m_device; p->m_resources = desc.resources; + auto VulkanDevice::create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, + VkBuffer& buf, VkDeviceMemory& mem) const -> bool + { + VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(m_device, &bci, nullptr, &buf) != VK_SUCCESS) return false; + VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(m_device, buf, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); + if (vkAllocateMemory(m_device, &ai, nullptr, &mem) != VK_SUCCESS) return false; + vkBindBufferMemory(m_device, buf, mem, 0); + return true; + } - std::vector<VkDescriptorSetLayoutBinding> binds; - for (const auto& r : desc.resources) - { - VkDescriptorSetLayoutBinding b{}; - b.binding = r.binding; b.descriptorCount = 1; - b.descriptorType = r.kind == ResourceKind::UniformBuffer ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - binds.push_back(b); - } - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = (uint32_t)binds.size(); dslci.pBindings = binds.data(); - vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &p->m_set_layout); - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &p->m_set_layout; - vkCreatePipelineLayout(m_device, &plci, nullptr, &p->m_layout); + auto VulkanDevice::load_spirv(const std::string& path) const -> std::vector<uint32_t> + { + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file.is_open()) return {}; + size_t size = (size_t)file.tellg(); + std::vector<uint32_t> data(size / 4); + file.seekg(0); file.read(reinterpret_cast<char*>(data.data()), size); + return data; + } - VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; - if (!create_shader_module("assets/shaders/generated/" + desc.shader + ".vertexMain.spv", vmod) || - !create_shader_module("assets/shaders/generated/" + desc.shader + ".fragmentMain.spv", fmod)) - { DONUT_ERROR("Vulkan RHI: shader '{}' modules failed", desc.shader); return p; } - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; + auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool + { + auto spv = load_spirv(path); + if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; } + VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; + ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); + return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS; + } - VkVertexInputBindingDescription vib{ 0, desc.vertex_layout.stride, VK_VERTEX_INPUT_RATE_VERTEX }; - std::vector<VkVertexInputAttributeDescription> vias; - for (const auto& a : desc.vertex_layout.attributes) - vias.push_back({ a.location, 0, vk_attr_format(a.components), a.offset }); - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = desc.vertex_layout.stride ? 1 : 0; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = (uint32_t)vias.size(); vin.pVertexAttributeDescriptions = vias.data(); - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = vk_topology(desc.topology); - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; - VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; - rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = vk_cull(desc.cull); rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - ds.depthTestEnable = desc.depth_test ? VK_TRUE : VK_FALSE; ds.depthWriteEnable = desc.depth_write ? VK_TRUE : VK_FALSE; ds.depthCompareOp = vk_compare(desc.depth_op); - VkPipelineColorBlendAttachmentState cba{}; - cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - if (desc.blend == BlendMode::AlphaBlend) - { - cba.blendEnable = VK_TRUE; - cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD; - cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD; - } - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; + auto VulkanDevice::init(const NativeWindow& window) -> bool + { + m_window = (GLFWwindow*)window.glfw_handle; m_width = window.width; m_height = window.height; + if (!create_instance()) return false; + if (!pick_physical_and_device()) return false; + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + // The swapchain is a "present" target: its colour format + a D32 depth. + m_swapchain_rp = get_render_pass(m_swapchain_format, VK_FORMAT_D32_SFLOAT, true); + if (!m_swapchain_rp) return false; + if (!create_depth_and_framebuffers())return false; + if (!create_command_and_sync()) return false; + DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height); + return true; + } - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci; - // Resolve the target's attachment signature to a (cached) render pass. - // Pipeline<->pass compatibility is by attachment format, so this is the - // same pass the matching swapchain / render target renders into. - VkFormat pcolor = (desc.target.color == Format::Swapchain) ? m_swapchain_format : vk_format(desc.target.color); - VkFormat pdepth = (desc.target.depth == Format::None) ? VK_FORMAT_UNDEFINED : vk_format(desc.target.depth); - bool present = desc.target.color == Format::Swapchain; - if (pdepth != VK_FORMAT_UNDEFINED) gpci.pDepthStencilState = &ds; - gpci.layout = p->m_layout; - gpci.renderPass = get_render_pass(pcolor, pdepth, present); - gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline); - vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); - if (pr != VK_SUCCESS) DONUT_ERROR("Vulkan RHI: pipeline '{}' creation failed ({})", desc.shader, (int)pr); - return p; - } - auto VulkanDevice::begin_frame(const glm::vec4&) -> CommandList* - { - if (m_device == VK_NULL_HANDLE) return nullptr; - vkWaitForFences(m_device, 1, &m_in_flight[m_current_frame], VK_TRUE, UINT64_MAX); - VkResult r = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, m_image_available[m_current_frame], VK_NULL_HANDLE, &m_image_index); - if (r == VK_ERROR_OUT_OF_DATE_KHR) { recreate_swapchain(); return nullptr; } - if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan RHI: acquire failed ({})", (int)r); return nullptr; } - if (m_images_in_flight[m_image_index] != VK_NULL_HANDLE) - vkWaitForFences(m_device, 1, &m_images_in_flight[m_image_index], VK_TRUE, UINT64_MAX); - m_images_in_flight[m_image_index] = m_in_flight[m_current_frame]; - if (m_geo_in_use != VK_NULL_HANDLE) - vkWaitForFences(m_device, 1, &m_geo_in_use, VK_TRUE, UINT64_MAX); + auto VulkanDevice::begin_frame(const glm::vec4&) -> CommandList* + { + if (m_device == VK_NULL_HANDLE) return nullptr; + vkWaitForFences(m_device, 1, &m_in_flight[m_current_frame], VK_TRUE, UINT64_MAX); + VkResult r = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, m_image_available[m_current_frame], VK_NULL_HANDLE, &m_image_index); + if (r == VK_ERROR_OUT_OF_DATE_KHR) { recreate_swapchain(); return nullptr; } + if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan RHI: acquire failed ({})", (int)r); return nullptr; } + if (m_images_in_flight[m_image_index] != VK_NULL_HANDLE) + vkWaitForFences(m_device, 1, &m_images_in_flight[m_image_index], VK_TRUE, UINT64_MAX); + m_images_in_flight[m_image_index] = m_in_flight[m_current_frame]; + if (m_geo_in_use != VK_NULL_HANDLE) + vkWaitForFences(m_device, 1, &m_geo_in_use, VK_TRUE, UINT64_MAX); - vkResetDescriptorPool(m_device, m_frame_pools[m_current_frame], 0); - VkCommandBuffer cmd = m_command_buffers[m_current_frame]; - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - vkBeginCommandBuffer(cmd, &bi); + vkResetDescriptorPool(m_device, m_frame_pools[m_current_frame], 0); + VkCommandBuffer cmd = m_command_buffers[m_current_frame]; + vkResetCommandBuffer(cmd, 0); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + vkBeginCommandBuffer(cmd, &bi); - m_cmds.m_device = m_device; m_cmds.m_cmd = cmd; - m_cmds.m_swapchain_rp = m_swapchain_rp; - m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent; - m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr; - return &m_cmds; - } + m_cmds.m_device = m_device; m_cmds.m_cmd = cmd; + m_cmds.m_swapchain_rp = m_swapchain_rp; + m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent; + m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr; + return &m_cmds; + } - auto VulkanDevice::end_frame() -> void - { - VkCommandBuffer cmd = m_command_buffers[m_current_frame]; - vkEndCommandBuffer(cmd); - VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &m_image_available[m_current_frame]; submit.pWaitDstStageMask = &wait_stage; - submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &m_render_finished[m_image_index]; - vkResetFences(m_device, 1, &m_in_flight[m_current_frame]); - if (vkQueueSubmit(m_graphics_queue, 1, &submit, m_in_flight[m_current_frame]) != VK_SUCCESS) - { DONUT_ERROR("Vulkan RHI: queue submit failed"); return; } - m_geo_in_use = m_in_flight[m_current_frame]; + auto VulkanDevice::end_frame() -> void + { + VkCommandBuffer cmd = m_command_buffers[m_current_frame]; + vkEndCommandBuffer(cmd); + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &m_image_available[m_current_frame]; submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; + submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &m_render_finished[m_image_index]; + vkResetFences(m_device, 1, &m_in_flight[m_current_frame]); + if (vkQueueSubmit(m_graphics_queue, 1, &submit, m_in_flight[m_current_frame]) != VK_SUCCESS) + { DONUT_ERROR("Vulkan RHI: queue submit failed"); return; } + m_geo_in_use = m_in_flight[m_current_frame]; - VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; - present.waitSemaphoreCount = 1; present.pWaitSemaphores = &m_render_finished[m_image_index]; - present.swapchainCount = 1; present.pSwapchains = &m_swapchain; present.pImageIndices = &m_image_index; - VkResult r = vkQueuePresentKHR(m_present_queue, &present); - if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || m_framebuffer_resized) - { m_framebuffer_resized = false; recreate_swapchain(); } - m_current_frame = (m_current_frame + 1) % MAX_FRAMES_IN_FLIGHT; - } + VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + present.waitSemaphoreCount = 1; present.pWaitSemaphores = &m_render_finished[m_image_index]; + present.swapchainCount = 1; present.pSwapchains = &m_swapchain; present.pImageIndices = &m_image_index; + VkResult r = vkQueuePresentKHR(m_present_queue, &present); + if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || m_framebuffer_resized) + { m_framebuffer_resized = false; recreate_swapchain(); } + m_current_frame = (m_current_frame + 1) % MAX_FRAMES_IN_FLIGHT; + } - auto VulkanDevice::init_imgui() -> void - { - VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; dpci.maxSets = 1000; - dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size; - vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_imgui_pool); + auto VulkanDevice::init_imgui() -> void + { + VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; dpci.maxSets = 1000; + dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size; + vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_imgui_pool); - IMGUI_CHECKVERSION(); ImGui::CreateContext(); - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::StyleColorsDark(); - ImGui_ImplGlfw_InitForVulkan(m_window, true); - ImGui_ImplVulkan_InitInfo info{}; - info.ApiVersion = VK_API_VERSION_1_2; info.Instance = m_instance; info.PhysicalDevice = m_physical; - info.Device = m_device; info.QueueFamily = m_graphics_family; info.Queue = m_graphics_queue; - info.DescriptorPool = m_imgui_pool; info.RenderPass = m_swapchain_rp; - info.MinImageCount = 2; info.ImageCount = (uint32_t)m_images.size(); info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; - if (!ImGui_ImplVulkan_Init(&info)) { DONUT_ERROR("Vulkan RHI: ImGui_ImplVulkan_Init failed"); return; } - m_imgui = true; - DONUT_INFO("Vulkan RHI: ImGui backend initialized"); - } + IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui::StyleColorsDark(); + ImGui_ImplGlfw_InitForVulkan(m_window, true); + ImGui_ImplVulkan_InitInfo info{}; + info.ApiVersion = VK_API_VERSION_1_2; info.Instance = m_instance; info.PhysicalDevice = m_physical; + info.Device = m_device; info.QueueFamily = m_graphics_family; info.Queue = m_graphics_queue; + info.DescriptorPool = m_imgui_pool; info.RenderPass = m_swapchain_rp; + info.MinImageCount = 2; info.ImageCount = (uint32_t)m_images.size(); info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + if (!ImGui_ImplVulkan_Init(&info)) { DONUT_ERROR("Vulkan RHI: ImGui_ImplVulkan_Init failed"); return; } + m_imgui = true; + DONUT_INFO("Vulkan RHI: ImGui backend initialized"); + } - auto VulkanDevice::imgui_new_frame() -> void - { - if (!m_imgui) return; - ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - } + auto VulkanDevice::imgui_new_frame() -> void + { + if (!m_imgui) return; + ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + } - auto VulkanDevice::imgui_render(CommandList& cmds) -> void - { - if (!m_imgui) return; - ImGui::Render(); - ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast<VkCommandListR&>(cmds).m_cmd); - } + auto VulkanDevice::imgui_render(CommandList& cmds) -> void + { + if (!m_imgui) return; + ImGui::Render(); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast<VkCommandListR&>(cmds).m_cmd); + } - auto VulkanDevice::shutdown() -> void + auto VulkanDevice::shutdown() -> void + { + if (m_device == VK_NULL_HANDLE) { - if (m_device == VK_NULL_HANDLE) - { - if (m_instance && m_surface) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } - if (m_instance) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } - return; - } - vkDeviceWaitIdle(m_device); - if (m_imgui) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } - if (m_imgui_pool) vkDestroyDescriptorPool(m_device, m_imgui_pool, nullptr); - for (auto p : m_frame_pools) vkDestroyDescriptorPool(m_device, p, nullptr); - m_frame_pools.clear(); - for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr); - for (auto s : m_image_available) vkDestroySemaphore(m_device, s, nullptr); - for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr); - m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear(); - if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr); - for (auto& [key, rp] : m_pass_cache) vkDestroyRenderPass(m_device, rp, nullptr); - m_pass_cache.clear(); m_swapchain_rp = VK_NULL_HANDLE; - cleanup_swapchain(); - vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; - if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr); - if (m_instance) vkDestroyInstance(m_instance, nullptr); - m_surface = VK_NULL_HANDLE; m_instance = VK_NULL_HANDLE; + if (m_instance && m_surface) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } + if (m_instance) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } + return; } + vkDeviceWaitIdle(m_device); + if (m_imgui) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } + if (m_imgui_pool) vkDestroyDescriptorPool(m_device, m_imgui_pool, nullptr); + for (auto p : m_frame_pools) vkDestroyDescriptorPool(m_device, p, nullptr); + m_frame_pools.clear(); + for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr); + for (auto s : m_image_available) vkDestroySemaphore(m_device, s, nullptr); + for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr); + m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear(); + if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr); + for (auto& [key, rp] : m_pass_cache) vkDestroyRenderPass(m_device, rp, nullptr); + m_pass_cache.clear(); m_swapchain_rp = VK_NULL_HANDLE; + cleanup_swapchain(); + vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; + if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr); + if (m_instance) vkDestroyInstance(m_instance, nullptr); + m_surface = VK_NULL_HANDLE; m_instance = VK_NULL_HANDLE; } auto create_vulkan_device() -> Scope<Device> { return create_scope<VulkanDevice>(); } diff --git a/src/platform/vulkan/vulkan_resources.cpp b/src/platform/vulkan/vulkan_resources.cpp new file mode 100644 index 0000000..e40ded3 --- /dev/null +++ b/src/platform/vulkan/vulkan_resources.cpp @@ -0,0 +1,209 @@ +#include "vulkan_common.h" + +namespace Donut::RHI +{ + auto VulkanDevice::create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> + { + VkBufferUsageFlags usage = type == BufferType::Index ? VK_BUFFER_USAGE_INDEX_BUFFER_BIT + : type == BufferType::Uniform ? VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT + : VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; + create_buffer_raw(size, usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buf, mem); + void* mapped = nullptr; vkMapMemory(m_device, mem, 0, size, 0, &mapped); + if (data && mapped) std::memcpy(mapped, data, size); + return create_ref<VkBufferR>(m_device, buf, mem, mapped, size); + } + + auto VulkanDevice::create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> + { + auto tex = create_ref<VkTextureR>(); tex->m_device = m_device; + VkFormat fmt = vk_format(format); + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; + ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &ici, nullptr, &tex->m_image); + VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &ai, nullptr, &tex->m_mem); + vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); + + size_t bpp = format == Format::RGBA16F ? 8 : 4; + VkDeviceSize sz = (VkDeviceSize)w * h * bpp; + VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; + create_buffer_raw(sz, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging, staging_mem); + void* mp = nullptr; vkMapMemory(m_device, staging_mem, 0, sz, 0, &mp); + if (data) std::memcpy(mp, data, sz); else std::memset(mp, 0, sz); + vkUnmapMemory(m_device, staging_mem); + + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier b{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.image = tex->m_image; b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); + VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; + vkCmdCopyBufferToImage(cmd, staging, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + VkImageMemoryBarrier r = b; r.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; r.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + r.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; r.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &r); + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + vkDestroyBuffer(m_device, staging, nullptr); vkFreeMemory(m_device, staging_mem, nullptr); + + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = tex->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &vci, nullptr, &tex->m_view); + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &smci, nullptr, &tex->m_sampler); + return tex; + } + + auto VulkanDevice::create_render_target(int w, int h, Format color, Format depth, Filter filter, int /*mips*/) -> Ref<RenderTarget> + { + auto rt = create_ref<VkRenderTargetR>(); + rt->m_device = m_device; rt->m_w = w; rt->m_h = h; + VkFormat cfmt = (color == Format::Swapchain) ? m_swapchain_format : vk_format(color); + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = cfmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; + ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &ici, nullptr, &rt->m_image); + VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem); + vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0); + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = cfmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &vci, nullptr, &rt->m_view); + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler); + + // Optional depth attachment (for off-screen passes that need a depth test). + rt->m_has_depth = depth != Format::None; + VkFormat dfmt = VK_FORMAT_UNDEFINED; + if (rt->m_has_depth) + { + dfmt = vk_format(depth); + VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + dici.imageType = VK_IMAGE_TYPE_2D; dici.format = dfmt; dici.extent = { (uint32_t)w, (uint32_t)h, 1 }; + dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; + dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + vkCreateImage(m_device, &dici, nullptr, &rt->m_depth_image); + VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, rt->m_depth_image, &dreq); + VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &dai, nullptr, &rt->m_depth_mem); + vkBindImageMemory(m_device, rt->m_depth_image, rt->m_depth_mem, 0); + VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + dvci.image = rt->m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = dfmt; + dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &dvci, nullptr, &rt->m_depth_view); + } + + rt->m_pass = get_render_pass(cfmt, dfmt, false); // off-screen (sampled) target + VkImageView atts[2] = { rt->m_view, rt->m_depth_view }; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = rt->m_pass; fbci.attachmentCount = rt->m_has_depth ? 2u : 1u; fbci.pAttachments = atts; + fbci.width = w; fbci.height = h; fbci.layers = 1; + vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb); + + rt->m_color.m_device = m_device; rt->m_color.m_view = rt->m_view; rt->m_color.m_sampler = rt->m_sampler; rt->m_color.m_owns = false; + return rt; + } + + // Builds an environment cubemap from an equirectangular HDRI: render the 6 + // faces with the EquirectToCubemap pipeline, then a full mip chain by + // linear down-blits (so divergence-based LOD reads a blurred sky). Returns + // a Texture owning the cube image/view/sampler. + + auto VulkanDevice::create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> + { + auto p = create_ref<VkPipelineR>(); p->m_device = m_device; p->m_resources = desc.resources; + + std::vector<VkDescriptorSetLayoutBinding> binds; + for (const auto& r : desc.resources) + { + VkDescriptorSetLayoutBinding b{}; + b.binding = r.binding; b.descriptorCount = 1; + b.descriptorType = r.kind == ResourceKind::UniformBuffer ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + binds.push_back(b); + } + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dslci.bindingCount = (uint32_t)binds.size(); dslci.pBindings = binds.data(); + vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &p->m_set_layout); + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + plci.setLayoutCount = 1; plci.pSetLayouts = &p->m_set_layout; + vkCreatePipelineLayout(m_device, &plci, nullptr, &p->m_layout); + + VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; + if (!create_shader_module("assets/shaders/generated/" + desc.shader + ".vertexMain.spv", vmod) || + !create_shader_module("assets/shaders/generated/" + desc.shader + ".fragmentMain.spv", fmod)) + { DONUT_ERROR("Vulkan RHI: shader '{}' modules failed", desc.shader); return p; } + VkPipelineShaderStageCreateInfo stages[2]{}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; + + VkVertexInputBindingDescription vib{ 0, desc.vertex_layout.stride, VK_VERTEX_INPUT_RATE_VERTEX }; + std::vector<VkVertexInputAttributeDescription> vias; + for (const auto& a : desc.vertex_layout.attributes) + vias.push_back({ a.location, 0, vk_attr_format(a.components), a.offset }); + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = desc.vertex_layout.stride ? 1 : 0; vin.pVertexBindingDescriptions = &vib; + vin.vertexAttributeDescriptionCount = (uint32_t)vias.size(); vin.pVertexAttributeDescriptions = vias.data(); + + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = vk_topology(desc.topology); + VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; + VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; + VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; + rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = vk_cull(desc.cull); rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; + ds.depthTestEnable = desc.depth_test ? VK_TRUE : VK_FALSE; ds.depthWriteEnable = desc.depth_write ? VK_TRUE : VK_FALSE; ds.depthCompareOp = vk_compare(desc.depth_op); + VkPipelineColorBlendAttachmentState cba{}; + cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + if (desc.blend == BlendMode::AlphaBlend) + { + cba.blendEnable = VK_TRUE; + cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD; + cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD; + } + VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; + + VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + gpci.stageCount = 2; gpci.pStages = stages; + gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; + gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci; + // Resolve the target's attachment signature to a (cached) render pass. + // Pipeline<->pass compatibility is by attachment format, so this is the + // same pass the matching swapchain / render target renders into. + VkFormat pcolor = (desc.target.color == Format::Swapchain) ? m_swapchain_format : vk_format(desc.target.color); + VkFormat pdepth = (desc.target.depth == Format::None) ? VK_FORMAT_UNDEFINED : vk_format(desc.target.depth); + bool present = desc.target.color == Format::Swapchain; + if (pdepth != VK_FORMAT_UNDEFINED) gpci.pDepthStencilState = &ds; + gpci.layout = p->m_layout; + gpci.renderPass = get_render_pass(pcolor, pdepth, present); + gpci.subpass = 0; + VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline); + vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); + if (pr != VK_SUCCESS) DONUT_ERROR("Vulkan RHI: pipeline '{}' creation failed ({})", desc.shader, (int)pr); + return p; + } + +} diff --git a/src/platform/vulkan/vulkan_swapchain.cpp b/src/platform/vulkan/vulkan_swapchain.cpp new file mode 100644 index 0000000..fe5bd38 --- /dev/null +++ b/src/platform/vulkan/vulkan_swapchain.cpp @@ -0,0 +1,298 @@ +#include "vulkan_common.h" + +namespace Donut::RHI +{ + auto VulkanDevice::create_instance() -> bool + { + VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; + app.pApplicationName = "Donut"; app.apiVersion = VK_API_VERSION_1_2; + + uint32_t glfwExtCount = 0; + const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); + if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; } + std::vector<const char*> exts(glfwExts, glfwExts + glfwExtCount); + exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + + std::vector<const char*> layers; + uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr); + std::vector<VkLayerProperties> avail(layer_count); + vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); + for (const auto& l : avail) + if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) + layers.push_back("VK_LAYER_KHRONOS_validation"); + + VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; + ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + ici.pApplicationInfo = &app; + ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data(); + ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data(); + VkResult r = vkCreateInstance(&ici, nullptr, &m_instance); + if (r != VK_SUCCESS && !layers.empty()) + { + DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it"); + ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr; + r = vkCreateInstance(&ici, nullptr, &m_instance); + } + if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: vkCreateInstance failed ({})", (int)r); return false; } + VKD_CHECK(glfwCreateWindowSurface(m_instance, m_window, nullptr, &m_surface)); + DONUT_INFO("Vulkan RHI: instance + surface created (validation {})", layers.empty() ? "off" : "on"); + return true; + } + + auto VulkanDevice::pick_physical_and_device() -> bool + { + uint32_t count = 0; vkEnumeratePhysicalDevices(m_instance, &count, nullptr); + if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; } + std::vector<VkPhysicalDevice> devices(count); + vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); + m_physical = devices[0]; + + uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr); + std::vector<VkQueueFamilyProperties> qfams(q); + vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data()); + bool fg = false, fp = false; + for (uint32_t i = 0; i < q; ++i) + { + if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; } + VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present); + if (!fp && present) { m_present_family = i; fp = true; } + } + if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; } + + std::vector<const char*> dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr); + std::vector<VkExtensionProperties> dep(dec); + vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, dep.data()); + for (const auto& e : dep) + if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) + dev_exts.push_back("VK_KHR_portability_subset"); + + float priority = 1.0f; + std::vector<VkDeviceQueueCreateInfo> qcis; + uint32_t families[2] = { m_graphics_family, m_present_family }; + for (uint32_t i = 0; i < (m_graphics_family == m_present_family ? 1u : 2u); ++i) + { + VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; + qci.queueFamilyIndex = families[i]; qci.queueCount = 1; qci.pQueuePriorities = &priority; + qcis.push_back(qci); + } + VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; + dci.queueCreateInfoCount = (uint32_t)qcis.size(); dci.pQueueCreateInfos = qcis.data(); + dci.enabledExtensionCount = (uint32_t)dev_exts.size(); dci.ppEnabledExtensionNames = dev_exts.data(); + VKD_CHECK(vkCreateDevice(m_physical, &dci, nullptr, &m_device)); + vkGetDeviceQueue(m_device, m_graphics_family, 0, &m_graphics_queue); + vkGetDeviceQueue(m_device, m_present_family, 0, &m_present_queue); + + VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(m_physical, &props); + vkGetPhysicalDeviceMemoryProperties(m_physical, &m_mem_props); + m_gpu_name = props.deviceName; + DONUT_INFO("Vulkan RHI device: {}", m_gpu_name); + return true; + } + + auto VulkanDevice::create_swapchain() -> bool + { + VkSurfaceCapabilitiesKHR caps{}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical, m_surface, &caps); + uint32_t fc = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, nullptr); + std::vector<VkSurfaceFormatKHR> formats(fc); + vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, formats.data()); + VkSurfaceFormatKHR chosen = formats[0]; + for (const auto& f : formats) + if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f; + m_swapchain_format = chosen.format; + + if (caps.currentExtent.width != UINT32_MAX) m_extent = caps.currentExtent; + else { + m_extent.width = std::clamp((uint32_t)m_width, caps.minImageExtent.width, caps.maxImageExtent.width); + m_extent.height = std::clamp((uint32_t)m_height, caps.minImageExtent.height, caps.maxImageExtent.height); + } + uint32_t image_count = caps.minImageCount + 1; + if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) image_count = caps.maxImageCount; + + VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; + sci.surface = m_surface; sci.minImageCount = image_count; + sci.imageFormat = chosen.format; sci.imageColorSpace = chosen.colorSpace; + sci.imageExtent = m_extent; sci.imageArrayLayers = 1; + sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE; + uint32_t fam[2] = { m_graphics_family, m_present_family }; + if (m_graphics_family != m_present_family) + { sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; sci.queueFamilyIndexCount = 2; sci.pQueueFamilyIndices = fam; } + else sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + VKD_CHECK(vkCreateSwapchainKHR(m_device, &sci, nullptr, &m_swapchain)); + uint32_t n = 0; vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, nullptr); + m_images.resize(n); vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, m_images.data()); + return true; + } + + auto VulkanDevice::create_image_views() -> bool + { + m_image_views.resize(m_images.size()); + for (size_t i = 0; i < m_images.size(); ++i) + { + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = m_images[i]; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = m_swapchain_format; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VKD_CHECK(vkCreateImageView(m_device, &vci, nullptr, &m_image_views[i])); + } + return true; + } + + // A render pass for one attachment signature, created once and cached. + // `present` targets (the swapchain) finish PRESENT_SRC and sync on the + // colour-output stage; `sampled` targets (off-screen) finish + // SHADER_READ_ONLY and round-trip through the fragment shader so the next + // pass can sample them. Depth (VK_FORMAT_UNDEFINED = none) is optional. + auto VulkanDevice::get_render_pass(VkFormat color, VkFormat depth, bool present) -> VkRenderPass + { + uint64_t key = (uint64_t)(uint32_t)color + | ((uint64_t)(uint32_t)depth << 24) + | ((uint64_t)(present ? 1 : 0) << 48); + auto it = m_pass_cache.find(key); + if (it != m_pass_cache.end()) return it->second; + + const bool has_depth = depth != VK_FORMAT_UNDEFINED; + VkAttachmentDescription atts[2]{}; + atts[0].format = color; atts[0].samples = VK_SAMPLE_COUNT_1_BIT; + atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + atts[0].finalLayout = present ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + atts[1].format = depth; atts[1].samples = VK_SAMPLE_COUNT_1_BIT; + atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; + if (has_depth) subpass.pDepthStencilAttachment = &depth_ref; + + VkSubpassDependency deps[2]{}; + uint32_t dep_count; + if (present) + { + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep_count = 1; + } + else + { + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + dep_count = 2; + } + + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = has_depth ? 2 : 1; rpci.pAttachments = atts; + rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = dep_count; rpci.pDependencies = deps; + VkRenderPass rp = VK_NULL_HANDLE; + if (vkCreateRenderPass(m_device, &rpci, nullptr, &rp) != VK_SUCCESS) + { DONUT_ERROR("Vulkan RHI: render pass creation failed"); return VK_NULL_HANDLE; } + m_pass_cache[key] = rp; + return rp; + } + + auto VulkanDevice::create_depth_and_framebuffers() -> bool + { + VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + dici.imageType = VK_IMAGE_TYPE_2D; dici.format = VK_FORMAT_D32_SFLOAT; + dici.extent = { m_extent.width, m_extent.height, 1 }; + dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; + dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + VKD_CHECK(vkCreateImage(m_device, &dici, nullptr, &m_depth_image)); + VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, m_depth_image, &dreq); + VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VKD_CHECK(vkAllocateMemory(m_device, &dai, nullptr, &m_depth_mem)); + VKD_CHECK(vkBindImageMemory(m_device, m_depth_image, m_depth_mem, 0)); + VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + dvci.image = m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = VK_FORMAT_D32_SFLOAT; + dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; + VKD_CHECK(vkCreateImageView(m_device, &dvci, nullptr, &m_depth_view)); + + m_framebuffers.resize(m_image_views.size()); + for (size_t i = 0; i < m_image_views.size(); ++i) + { + VkImageView att[2] = { m_image_views[i], m_depth_view }; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = m_swapchain_rp; fbci.attachmentCount = 2; fbci.pAttachments = att; + fbci.width = m_extent.width; fbci.height = m_extent.height; fbci.layers = 1; + VKD_CHECK(vkCreateFramebuffer(m_device, &fbci, nullptr, &m_framebuffers[i])); + } + return true; + } + + auto VulkanDevice::create_command_and_sync() -> bool + { + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = m_graphics_family; + VKD_CHECK(vkCreateCommandPool(m_device, &pci, nullptr, &m_command_pool)); + m_command_buffers.resize(MAX_FRAMES_IN_FLIGHT); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; + VKD_CHECK(vkAllocateCommandBuffers(m_device, &cbai, m_command_buffers.data())); + + m_image_available.resize(MAX_FRAMES_IN_FLIGHT); + m_in_flight.resize(MAX_FRAMES_IN_FLIGHT); + m_render_finished.resize(m_images.size()); + m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); + VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; + VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + { VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_image_available[i])); VKD_CHECK(vkCreateFence(m_device, &fci, nullptr, &m_in_flight[i])); } + for (size_t i = 0; i < m_images.size(); ++i) + VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_render_finished[i])); + + m_frame_pools.resize(MAX_FRAMES_IN_FLIGHT); + VkDescriptorPoolSize sizes[2] = { + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256 }, + }; + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + { + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.maxSets = 256; dpci.poolSizeCount = 2; dpci.pPoolSizes = sizes; + VKD_CHECK(vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_frame_pools[i])); + } + return true; + } + + + auto VulkanDevice::cleanup_swapchain() -> void + { + for (auto fb : m_framebuffers) vkDestroyFramebuffer(m_device, fb, nullptr); + m_framebuffers.clear(); + if (m_depth_view) { vkDestroyImageView(m_device, m_depth_view, nullptr); m_depth_view = VK_NULL_HANDLE; } + if (m_depth_image) { vkDestroyImage(m_device, m_depth_image, nullptr); m_depth_image = VK_NULL_HANDLE; } + if (m_depth_mem) { vkFreeMemory(m_device, m_depth_mem, nullptr); m_depth_mem = VK_NULL_HANDLE; } + for (auto iv : m_image_views) vkDestroyImageView(m_device, iv, nullptr); + m_image_views.clear(); + if (m_swapchain) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; } + } + + auto VulkanDevice::recreate_swapchain() -> bool + { + int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h); + while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); } + m_width = w; m_height = h; + vkDeviceWaitIdle(m_device); + cleanup_swapchain(); + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + if (!create_depth_and_framebuffers())return false; + m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); + return true; + } + +} diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h index b9c158b..63c5aa2 100644 --- a/src/rendering/black_hole_renderer.h +++ b/src/rendering/black_hole_renderer.h @@ -1,7 +1,7 @@ #pragma once #include "rhi.h" -#include "renderer_backend.h" // BlackHoleParams +#include "scene/scene_types.h" // BlackHoleParams #include <glm/glm.hpp> namespace Donut diff --git a/src/rendering/render_path.cpp b/src/rendering/render_path.cpp new file mode 100644 index 0000000..172e13c --- /dev/null +++ b/src/rendering/render_path.cpp @@ -0,0 +1,94 @@ +#include "render_path.h" + +#include "scene/scene.h" + +#include <glm/gtc/matrix_transform.hpp> +#include <algorithm> +#include <cmath> + +namespace Donut +{ + auto RenderPath::init(RHI::Device& device, const std::string& hdri_path) -> bool + { + m_device = &device; + m_hdri_path = hdri_path; + m_cubemap = device.create_cubemap_from_hdri(hdri_path); + + m_scene_renderer = create_scope<SceneRenderer>(); + m_scene_renderer->init(device); + m_black_hole_renderer = create_scope<BlackHoleRenderer>(); + m_black_hole_renderer->init(device); + return true; + } + + auto RenderPath::sync_hdri(const std::string& hdri_path) -> void + { + if (hdri_path == m_hdri_path || !m_device) return; + m_device->wait_idle(); + m_cubemap = m_device->create_cubemap_from_hdri(hdri_path); // old cube freed after idle + m_hdri_path = hdri_path; + } + + auto RenderPath::render(RHI::CommandList& cmd, Scene& scene, View view, + int fb_width, int fb_height, bool moving, float time) -> void + { + const glm::vec4 clear(0.05f, 0.06f, 0.10f, 1.0f); + + // The black hole is the only view with an off-screen pass; it renders (and + // runs the expensive geodesic) ONLY on the Simulation tab. Scene and None + // are a single swapchain pass — None draws nothing (empty viewport). + if (view != View::BlackHole) + { + cmd.begin_render_pass(nullptr, clear); + if (view == View::Scene) + { + float aspect = (float)fb_width / (float)std::max(fb_height, 1); + scene.scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); + CameraView cv; + cv.view = scene.scene_camera.get_view_matrix(); + cv.projection = scene.scene_camera.get_projection_matrix(); + cv.position = scene.scene_camera.get_orbital_position(); + cv.fb_width = fb_width; cv.fb_height = fb_height; + m_scene_renderer->render(cmd, cv, scene.objects, scene.selected_object, m_cubemap.get()); + } + m_device->imgui_render(cmd); + cmd.end_render_pass(); + } + else + { + GeodesicView gv; + glm::vec3 pos, fwd; + if (scene.sim_camera.get_camera_mode() == CameraMode::FPS) + { + pos = scene.sim_camera.get_position(); + fwd = scene.sim_camera.get_forward_direction(); + } + else + { + pos = scene.sim_camera.get_orbital_position(); + fwd = glm::normalize(scene.sim_camera.get_orbital_target() - pos); + } + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + gv.position = pos; gv.right = right; gv.up = glm::cross(right, fwd); gv.forward = fwd; + gv.tan_half_fov = (float)tan(glm::radians(scene.black_hole.fov_degrees * 0.5f)); + gv.aspect = (float)BlackHoleRenderer::GEO_HI_W / (float)BlackHoleRenderer::GEO_HI_H; + gv.moving = moving; + gv.time = time; + + m_black_hole_renderer->render_geodesic(cmd, gv, scene.black_hole, m_cubemap.get()); + cmd.begin_render_pass(nullptr, clear); + m_black_hole_renderer->blit(cmd, fb_width, fb_height); + m_device->imgui_render(cmd); + cmd.end_render_pass(); + } + } + + auto RenderPath::shutdown() -> void + { + m_scene_renderer.reset(); + m_black_hole_renderer.reset(); + m_cubemap.reset(); + // Backend-specific HDRI/GPU resource cleanup is the device's job (see e.g. + // the OpenGL device clearing the HDRIManager texture cache on shutdown). + } +} diff --git a/src/rendering/render_path.h b/src/rendering/render_path.h new file mode 100644 index 0000000..0d97395 --- /dev/null +++ b/src/rendering/render_path.h @@ -0,0 +1,40 @@ +#pragma once + +#include "rhi.h" +#include "view.h" +#include "scene_renderer.h" +#include "black_hole_renderer.h" +#include "core/memory.h" + +#include <string> + +namespace Donut +{ + class Scene; + + // Owns the device-side rendering: the two portable renderers, the shared HDRI + // cubemap, and the per-frame composition (off-screen geodesic pass -> present, + // or the scene pass), with ImGui drawn on top inside the swapchain pass. The + // Application hands it a Scene + View each frame; it knows nothing about the UI. + class RenderPath + { + public: + auto init(RHI::Device& device, const std::string& hdri_path) -> bool; + auto shutdown() -> void; + + // Rebuilds the environment cubemap if the path changed (device-idle). + auto sync_hdri(const std::string& hdri_path) -> void; + + // Composes one frame's viewport into the swapchain command list. `moving` + // drives the black hole's progressive resolution; `time` animates the disk. + auto render(RHI::CommandList& cmd, Scene& scene, View view, + int fb_width, int fb_height, bool moving, float time) -> void; + + private: + RHI::Device* m_device = nullptr; + Scope<SceneRenderer> m_scene_renderer; + Scope<BlackHoleRenderer> m_black_hole_renderer; + Ref<RHI::Texture> m_cubemap; + std::string m_hdri_path; + }; +} diff --git a/src/rendering/scene_renderer.h b/src/rendering/scene_renderer.h index eafd56e..c3ee02f 100644 --- a/src/rendering/scene_renderer.h +++ b/src/rendering/scene_renderer.h @@ -1,7 +1,7 @@ #pragma once #include "rhi.h" -#include "renderer_backend.h" // SceneObject +#include "scene/scene_types.h" // SceneObject #include <glm/glm.hpp> #include <vector> diff --git a/src/rendering/view.h b/src/rendering/view.h new file mode 100644 index 0000000..0ac6ae0 --- /dev/null +++ b/src/rendering/view.h @@ -0,0 +1,13 @@ +#pragma once + +namespace Donut +{ + // What the RenderPath draws in the viewport this frame. Each UI workspace + // (tab) declares which one it wants, so switching tabs switches the view. + enum class View + { + None, // empty viewport (tabs that aren't a live view, e.g. Setup / Export) + Scene, // world-builder: grid + placed objects + skybox + BlackHole, // the ray-traced accretion-disk simulation (only the Simulation tab) + }; +} diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp new file mode 100644 index 0000000..db70a8d --- /dev/null +++ b/src/scene/scene.cpp @@ -0,0 +1,65 @@ +#include "scene.h" + +#include <glm/glm.hpp> +#include <glm/gtc/matrix_transform.hpp> +#include <algorithm> +#include <cmath> +#include <numbers> + +namespace Donut +{ + Scene::Scene() + { + // Black-hole camera: large-scale orbital viewer outside the disk. + sim_camera.set_camera_mode(CameraMode::Orbital); + sim_camera.set_orbital_target(glm::vec3(0.0f)); + sim_camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk + sim_camera.set_orbital_limits(2.2e11, 1.5e12); + sim_camera.set_orbital_speed(0.01f); + sim_camera.set_zoom_speed(3e10); + sim_camera.set_azimuth(0.0f); + sim_camera.set_elevation(1.25f); + + // Scene camera: normal-scale orbital world-builder viewer. + scene_camera.set_camera_mode(CameraMode::Orbital); + scene_camera.set_orbital_target(glm::vec3(0.0f)); + scene_camera.set_orbital_radius(15.0); + scene_camera.set_orbital_limits(2.0, 200.0); + scene_camera.set_orbital_speed(0.01f); + scene_camera.set_zoom_speed(2.0); + scene_camera.set_azimuth(0.0f); + scene_camera.set_elevation((float)std::numbers::pi / 3.0f); + scene_camera.update_orbital(); + } + + auto Scene::reset_sim_camera() -> void + { + sim_camera.set_camera_mode(CameraMode::Orbital); + sim_camera.set_orbital_radius(4e11); + sim_camera.set_azimuth(0.0f); + sim_camera.set_elevation(1.25f); + } + + auto Scene::set_free_fly(bool enabled) -> void + { + const bool is_fps = sim_camera.get_camera_mode() == CameraMode::FPS; + if (enabled == is_fps) return; + if (enabled) + { + // Seed the fly pose from the current orbital framing so the view is continuous. + glm::vec3 pos = sim_camera.get_orbital_position(); + glm::vec3 fwd = glm::normalize(sim_camera.get_orbital_target() - pos); + float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f))); + float yaw = glm::degrees(atan2(fwd.z, fwd.x)); + sim_camera.set_camera_mode(CameraMode::FPS); + sim_camera.set_movement_speed(2.0e10f); + sim_camera.set_mouse_sensitivity(0.15f); + sim_camera.set_position(pos); + sim_camera.set_rotation(glm::vec3(pitch, yaw, 0.0f)); + } + else + { + sim_camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact + } + } +} diff --git a/src/scene/scene.h b/src/scene/scene.h new file mode 100644 index 0000000..27d1327 --- /dev/null +++ b/src/scene/scene.h @@ -0,0 +1,39 @@ +#pragma once + +#include "scene_types.h" +#include "core/camera.h" + +#include <string> +#include <vector> + +namespace Donut +{ + // The document: everything the app edits and renders, independent of any + // backend or UI. Workspaces (tabs) read and mutate this; the RenderPath reads + // it to draw. It owns the two viewpoints (cameras) as scene state, plus small + // camera-framing helpers, since a viewpoint is part of the document. + class Scene + { + public: + Scene(); + + // world-builder content + std::vector<SceneObject> objects{ SceneObject{} }; + int selected_object = 0; + + // black hole / accretion disk + BlackHoleParams black_hole; + + // environment + std::string hdri_path = "assets/hdri/hdr_blue_nebulae_1.hdr"; + + // viewpoints + Camera sim_camera{ 60.0f, 16.0f / 9.0f, 0.1f, 100.0f }; // black-hole view + Camera scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; // world-builder view + + // Camera framing helpers (the sim view supports orbital + free-fly). + auto reset_sim_camera() -> void; + auto set_free_fly(bool enabled) -> void; + auto is_free_fly() const -> bool { return sim_camera.get_camera_mode() == CameraMode::FPS; } + }; +} diff --git a/src/rendering/renderer_backend.h b/src/scene/scene_types.h index d34983f..d34983f 100644 --- a/src/rendering/renderer_backend.h +++ b/src/scene/scene_types.h diff --git a/src/ui/ui_layer.cpp b/src/ui/ui_layer.cpp new file mode 100644 index 0000000..6748be2 --- /dev/null +++ b/src/ui/ui_layer.cpp @@ -0,0 +1,275 @@ +#include "ui_layer.h" + +#include "scene/scene.h" +#include "core/settings_manager.h" +#include "core/hdri_manager.h" +#include "rendering/render_api.h" + +#include <imgui.h> +#include <glm/glm.hpp> +#include <string> + +namespace Donut +{ + // One-time ImGui theme: rounded, roomy, dark with a warm accretion-disk accent. + static auto apply_donut_style() -> void + { + ImGuiStyle& s = ImGui::GetStyle(); + s.WindowRounding = 7.0f; s.ChildRounding = 5.0f; s.FrameRounding = 4.0f; + s.GrabRounding = 4.0f; s.PopupRounding = 4.0f; s.ScrollbarRounding = 5.0f; s.TabRounding = 4.0f; + s.WindowPadding = ImVec2(12, 12); s.FramePadding = ImVec2(9, 5); + s.ItemSpacing = ImVec2(9, 8); s.ItemInnerSpacing = ImVec2(7, 5); + s.WindowBorderSize = 0.0f; s.FrameBorderSize = 0.0f; s.WindowTitleAlign = ImVec2(0.02f, 0.5f); + + const ImVec4 amber = ImVec4(0.98f, 0.62f, 0.20f, 1.00f); + const ImVec4 amberHi = ImVec4(1.00f, 0.73f, 0.36f, 1.00f); + ImVec4* c = s.Colors; + c[ImGuiCol_WindowBg] = ImVec4(0.07f, 0.08f, 0.10f, 0.97f); + c[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 0.55f); + c[ImGuiCol_PopupBg] = ImVec4(0.09f, 0.10f, 0.12f, 0.98f); + c[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); + c[ImGuiCol_TitleBgActive] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + c[ImGuiCol_Text] = ImVec4(0.90f, 0.91f, 0.93f, 1.00f); + c[ImGuiCol_TextDisabled] = ImVec4(0.48f, 0.50f, 0.54f, 1.00f); + c[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.17f, 0.20f, 1.00f); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.24f, 0.28f, 1.00f); + c[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.28f, 0.33f, 1.00f); + c[ImGuiCol_Header] = ImVec4(0.17f, 0.19f, 0.23f, 1.00f); + c[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.27f, 0.32f, 1.00f); + c[ImGuiCol_HeaderActive] = ImVec4(0.28f, 0.31f, 0.37f, 1.00f); + c[ImGuiCol_Button] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_ButtonHovered] = ImVec4(0.27f, 0.30f, 0.35f, 1.00f); + c[ImGuiCol_ButtonActive] = amber; + c[ImGuiCol_SliderGrab] = amber; + c[ImGuiCol_SliderGrabActive]= amberHi; + c[ImGuiCol_CheckMark] = amberHi; + c[ImGuiCol_Tab] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + c[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.31f, 0.37f, 1.00f); + c[ImGuiCol_TabActive] = ImVec4(0.22f, 0.24f, 0.28f, 1.00f); + c[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.26f, 1.00f); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.26f, 0.30f, 1.00f); + } + + namespace + { + // Setup: renderer backend + environment + class SetupWorkspace : public Workspace + { + public: + auto name() const -> const char* override { return "Setup"; } + auto view() const -> View override { return View::None; } + auto on_ui(const UIContext& ctx) -> void override + { + if (ImGui::CollapsingHeader("Renderer", ImGuiTreeNodeFlags_DefaultOpen)) + { + auto& s = SettingsManager::get_settings(); + const char* apis[] = { "OpenGL", "Vulkan" }; + int cur = (s.graphics.render_api == "Vulkan") ? 1 : 0; + if (ImGui::Combo("Graphics API", &cur, apis, 2)) + { + s.graphics.render_api = apis[cur]; + SettingsManager::save_settings(); + } + const char* running = (RendererAPI::get_api() == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL"; + if (s.graphics.render_api != running) + ImGui::TextColored(ImVec4(0.98f, 0.62f, 0.20f, 1.0f), "Restart to apply (%s running)", running); + else + ImGui::TextDisabled("Active backend: %s", running); + } + + if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) + { + auto& hdri = HDRIManager::get(); + std::string preview = hdri.get_hdri_name(ctx.scene.hdri_path); + if (ImGui::BeginCombo("Starfield", preview.c_str())) + { + for (const auto& path : hdri.get_available_hdri()) + { + bool selected = (path == ctx.scene.hdri_path); + if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected)) + ctx.scene.hdri_path = path; // RenderPath rebuilds the cubemap next frame + if (selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + ImGui::TextDisabled("Switching rebuilds the cubemap (brief pause)."); + } + } + }; + + // Scene: place and edit objects + class SceneWorkspace : public Workspace + { + public: + auto name() const -> const char* override { return "Scene"; } + auto view() const -> View override { return View::Scene; } + auto on_ui(const UIContext& ctx) -> void override + { + Scene& sc = ctx.scene; + ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); + auto& objs = sc.objects; + if (sc.selected_object >= (int)objs.size()) sc.selected_object = (int)objs.size() - 1; + + ImGui::Text("Spheres (%d)", (int)objs.size()); + if (ImGui::Button("Add") && objs.size() < 64) + { + SceneObject o; + o.position = glm::vec3(((int)objs.size() % 5) * 5.0f - 10.0f, 2.0f, ((int)objs.size() / 5) * 5.0f); + o.radius = 1.5f; + o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4)); + objs.push_back(o); + sc.selected_object = (int)objs.size() - 1; + } + ImGui::SameLine(); + if (ImGui::Button("Delete") && !objs.empty()) + { + objs.erase(objs.begin() + sc.selected_object); + if (sc.selected_object >= (int)objs.size()) sc.selected_object = (int)objs.size() - 1; + } + + ImGui::BeginChild("obj_list", ImVec2(0, 120), true); + for (int i = 0; i < (int)objs.size(); ++i) + { + std::string label = "Sphere " + std::to_string(i); + if (ImGui::Selectable(label.c_str(), sc.selected_object == i)) sc.selected_object = i; + } + ImGui::EndChild(); + + if (sc.selected_object >= 0 && sc.selected_object < (int)objs.size()) + { + SceneObject& o = objs[sc.selected_object]; + ImGui::DragFloat3("Position", &o.position.x, 0.1f); + ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f); + ImGui::ColorEdit3("Color", &o.color.x); + } + } + }; + + // Simulation: camera + accretion disk + quality + class SimulationWorkspace : public Workspace + { + public: + auto name() const -> const char* override { return "Simulation"; } + auto view() const -> View override { return View::BlackHole; } + auto on_ui(const UIContext& ctx) -> void override + { + Scene& sc = ctx.scene; + BlackHoleParams& bh = sc.black_hole; + + if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) + { + bool ff = sc.is_free_fly(); + if (ImGui::RadioButton("Orbital", !ff)) sc.set_free_fly(false); + ImGui::SameLine(); + if (ImGui::RadioButton("Free-fly", ff)) sc.set_free_fly(true); + ImGui::SameLine(); + if (ImGui::Button("Reset view")) sc.reset_sim_camera(); + ImGui::SliderFloat("FOV", &bh.fov_degrees, 20.0f, 90.0f, "%.0f\xc2\xb0"); + ImGui::TextDisabled(ff ? "WASD move | Q/E down-up | Shift boost | drag look" + : "Drag to orbit | scroll to zoom"); + } + + if (ImGui::CollapsingHeader("Accretion disk", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::SliderFloat("Temperature", &bh.temperature, 2000.0f, 15000.0f, "%.0f K"); + ImGui::SliderFloat("Brightness", &bh.brightness, 0.0f, 3.0f, "%.2f"); + ImGui::SliderFloat("Inner radius", &bh.disk_inner_rs, 3.0f, 12.0f, "%.1f r_s"); + if (bh.disk_outer_rs < bh.disk_inner_rs + 0.5f) bh.disk_outer_rs = bh.disk_inner_rs + 0.5f; + ImGui::SliderFloat("Outer radius", &bh.disk_outer_rs, bh.disk_inner_rs + 0.5f, 25.0f, "%.1f r_s"); + ImGui::SliderFloat("Turbulence", &bh.turbulence, 0.0f, 2.0f, "%.2f"); + ImGui::TextDisabled("Inner edge is the ISCO (3 r_s). Novikov-Thorne profile."); + } + + if (ImGui::CollapsingHeader("Quality")) + { + ImGui::SliderInt("Integration steps", &bh.quality_steps, 2000, 15000); + ImGui::TextDisabled("Deeper lensing at higher cost. Settled frame is 4x supersampled."); + } + } + }; + + // Export: pick what to render out (not wired up yet) + class ExportWorkspace : public Workspace + { + public: + auto name() const -> const char* override { return "Export"; } + auto view() const -> View override { return View::None; } + auto on_ui(const UIContext&) -> void override + { + ImGui::TextDisabled("Render observables to disk for analysis."); + ImGui::Spacing(); + if (ImGui::CollapsingHeader("Channels", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("Colour (tonemapped)", &m_color); + ImGui::Checkbox("Redshift (g)", &m_redshift); + ImGui::Checkbox("Emission temperature", &m_temperature); + ImGui::Checkbox("Impact parameter", &m_impact); + } + if (ImGui::CollapsingHeader("Output", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Combo("Resolution", &m_resolution, "960 x 540\0" "1920 x 1080\0" "3840 x 2160\0"); + ImGui::Combo("Format", &m_format, "PNG\0" "EXR (HDR)\0" "CSV (data)\0"); + } + ImGui::Spacing(); + ImGui::BeginDisabled(); + ImGui::Button("Export frame", ImVec2(-1.0f, 0.0f)); + ImGui::EndDisabled(); + ImGui::TextDisabled("Not wired up yet: needs the RHI readback path + G-buffer shader outputs."); + } + private: + bool m_color = true, m_redshift = false, m_temperature = false, m_impact = false; + int m_resolution = 1, m_format = 0; + }; + } + + UILayer::UILayer() + { + m_workspaces.push_back(create_scope<SetupWorkspace>()); + m_workspaces.push_back(create_scope<SceneWorkspace>()); + m_workspaces.push_back(create_scope<SimulationWorkspace>()); + m_workspaces.push_back(create_scope<ExportWorkspace>()); + + // Open on Simulation so the black hole greets the user on launch. + for (int i = 0; i < (int)m_workspaces.size(); ++i) + if (std::string(m_workspaces[i]->name()) == "Simulation") { m_landing = i; m_active = i; } + } + + auto UILayer::draw(const UIContext& ctx) -> View + { + static bool styled = false; + if (!styled) { apply_donut_style(); styled = true; } + + // Full-viewport dock space with a pass-through centre, so the panel docks to + // any edge while the render shows through the middle. + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); + + ImGui::SetNextWindowSize(ImVec2(360, 620), ImGuiCond_FirstUseEver); + ImGui::Begin("Donut"); + + ImGuiIO& io = ImGui::GetIO(); + ImGui::TextDisabled("%s", ctx.device_name.empty() ? "GPU" : ctx.device_name.c_str()); + ImGui::Text("%.0f FPS", io.Framerate); + ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f); + ImGui::Spacing(); + + if (ImGui::BeginTabBar("workspaces")) + { + for (int i = 0; i < (int)m_workspaces.size(); ++i) + { + ImGuiTabItemFlags flags = (!m_landed && i == m_landing) ? ImGuiTabItemFlags_SetSelected : 0; + if (ImGui::BeginTabItem(m_workspaces[i]->name(), nullptr, flags)) + { + m_active = i; + ImGui::Spacing(); + m_workspaces[i]->on_ui(ctx); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + m_landed = true; + } + ImGui::End(); + + return m_workspaces[m_active]->view(); + } +} diff --git a/src/ui/ui_layer.h b/src/ui/ui_layer.h new file mode 100644 index 0000000..67c9d8f --- /dev/null +++ b/src/ui/ui_layer.h @@ -0,0 +1,27 @@ +#pragma once + +#include "workspace.h" +#include "rendering/view.h" +#include "core/memory.h" + +#include <vector> + +namespace Donut +{ + // The Dorico-style tab strip. Owns the workspaces (Setup / Scene / Simulation + // / Export); each frame it draws the dock space, the shared header, the tab + // bar, and delegates the active tab's panels. Returns the active workspace's + // View so the Application knows what the RenderPath should draw. + class UILayer + { + public: + UILayer(); + auto draw(const UIContext& ctx) -> View; + + private: + std::vector<Scope<Workspace>> m_workspaces; + int m_active = 0; + int m_landing = 0; // tab selected on the first frame + bool m_landed = false; + }; +} diff --git a/src/ui/workspace.h b/src/ui/workspace.h new file mode 100644 index 0000000..134d517 --- /dev/null +++ b/src/ui/workspace.h @@ -0,0 +1,29 @@ +#pragma once + +#include "rendering/view.h" +#include <string> + +namespace Donut +{ + class Scene; + + // What a workspace gets each frame to build its panels. `scene` is mutable — + // workspaces edit the document through it. + struct UIContext + { + Scene& scene; + std::string device_name; + }; + + // A Dorico-style workspace: one tab that owns its side panels and declares + // which viewport View it shows. Switching tabs switches both. Concrete + // workspaces (Setup / Scene / Simulation / Export) live in ui_layer.cpp. + class Workspace + { + public: + virtual ~Workspace() = default; + virtual auto name() const -> const char* = 0; + virtual auto view() const -> View = 0; + virtual auto on_ui(const UIContext& ctx) -> void = 0; + }; +} |
