diff options
| author | hachem <im@hachem.wtf> | 2026-08-23 17:21:37 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-23 17:21:37 +0200 |
| commit | 3fd33ecff7472e4d6fc9e6b3905f56982e497ef2 (patch) | |
| tree | 66e5812f1a18b926e88a0dd549fe14e7c7327165 /src | |
| parent | e3aa33fc9a99d8b817bda42a567a4c347e18ab32 (diff) | |
[feat]: complete RHI api
Diffstat (limited to 'src')
52 files changed, 2388 insertions, 7991 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp index ae8a5d9..6239c36 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1,36 +1,41 @@ #include "application.h" -#include "rendering/renderer.h" +#include "rendering/render_api.h" // RendererAPI #include "settings_manager.h" #include "hdri_manager.h" -#include "states/simulation_state.h" -#include "states/config_state.h" -#include "states/world_builder_state.h" +#include "platform/opengl/opengl_device.h" +#include "platform/vulkan/vulkan_device.h" #include <GLFW/glfw3.h> #include <imgui.h> -#include <ImGuizmo.h> -#include "platform/vulkan/vulkan_renderer.h" -#include "platform/opengl/opengl_renderer.h" - -#ifdef __APPLE__ -#include "platform/metal/metal_context.h" -#include "platform/vulkan/vulkan_context.h" -#endif +#include <glm/gtc/matrix_transform.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). + static double g_scroll_accum = 0.0; + static GLFWscrollfun g_prev_scroll = nullptr; + static void donut_scroll_callback(GLFWwindow* w, double x, double y) + { + if (g_prev_scroll) g_prev_scroll(w, x, y); + g_scroll_accum += y; + } + Application::Application(const std::string& name, int width, int height) : m_running(true), m_minimized(false) { s_instance = this; - // Select the render API before the window is created: the window is - // built differently for Vulkan (GLFW_NO_API) than for OpenGL. + // Select the render API before the window is created: the window is built + // differently for Vulkan (GLFW_NO_API) than for OpenGL. Logger::init(); SettingsManager::initialize(); { @@ -40,13 +45,10 @@ namespace Donut } if (RendererAPI::get_api() == RendererAPI::API::Vulkan) - vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor + RHI::vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor m_window = create_scope<Window>(name, width, height); - m_window->set_event_callback([this](Event& event) - { - on_event(event); - }); + m_window->set_event_callback([this](Event& event) { on_event(event); }); on_init(); } @@ -57,27 +59,225 @@ namespace Donut s_instance = nullptr; } + auto Application::on_init() -> void + { + int w = 0, h = 0; + glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); + + if (RendererAPI::get_api() == RendererAPI::API::Vulkan) + m_device = RHI::create_vulkan_device(); + else + m_device = RHI::create_opengl_device(); + + if (!m_device->init(m_window->get_native_window(), w, h)) + { + DONUT_ERROR("RHI device initialization failed"); + 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_device->init_imgui(); + // Load the UI font into the device-created ImGui context (backend-agnostic; + // the backend rebuilds the atlas on the next frame). + ImGuiIO& io = ImGui::GetIO(); + if (!io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f)) + io.Fonts->AddFontDefault(); + + g_prev_scroll = glfwSetScrollCallback((GLFWwindow*)m_window->get_native_window(), donut_scroll_callback); + + m_start_time = glfwGetTime(); + DONUT_INFO("Application ready on {} backend", m_device->device_name()); + } + auto Application::run() -> void { while (m_running) { - if (m_renderer) + glfwPollEvents(); + if (!m_minimized && m_device) + render_frame(); + } + } + + auto Application::render_frame() -> void + { + RHI::CommandList* cmd = m_device->begin_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f)); + if (!cmd) return; // frame skipped (e.g. swapchain recreation) + + m_device->imgui_new_frame(); + build_ui(); + process_input(); + + 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) { - glfwPollEvents(); // before the ImGui frame so input is current - if (!m_minimized) - m_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f), - [this] { build_ui(); }); + pos = m_camera.get_position(); + fwd = m_camera.get_forward_direction(); } else { - if (!m_minimized) - { - on_update(); - on_render(); - } - m_window->on_update(); // polls events + swaps buffers (OpenGL) + 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(); } + + m_device->end_frame(); + } + + auto Application::process_input() -> void + { + GLFWwindow* window = (GLFWwindow*)m_window->get_native_window(); + bool over_ui = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureMouse; + + double now = glfwGetTime(); + float dt = m_last_frame_time > 0.0 ? (float)(now - m_last_frame_time) : 0.0f; + m_last_frame_time = now; + + double mx = 0, my = 0; + 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) + { + 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)); + m_fps_last_x = mx; m_fps_last_y = my; + m_left_was_down = left_down; + + bool over_kb = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureKeyboard; + bool moved = false; + 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; } + } + g_scroll_accum = 0.0; + m_user_moving = moved || (left_down && !over_ui); + } + else + { + Camera& cam = m_scene_mode ? m_scene_camera : m_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) + cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); + m_left_was_down = left_down; + + cam.process_orbital_mouse_move(mx, my); + + double scroll = g_scroll_accum; g_scroll_accum = 0.0; + if (scroll != 0.0 && !over_ui) + cam.process_orbital_scroll(0.0, scroll); + + m_user_moving = cam.is_dragging() || cam.is_panning(); + } + } + + 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. @@ -120,8 +320,7 @@ namespace Donut { static bool styled = false; if (!styled) { apply_donut_style(); styled = true; } - if (!m_renderer) return; - RendererBackend& r = *m_renderer; + 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. @@ -130,9 +329,8 @@ namespace Donut ImGui::SetNextWindowSize(ImVec2(340, 580), ImGuiCond_FirstUseEver); ImGui::Begin("Donut \xc2\xb7 Black Hole"); - // --- header: device + performance ----------------------------------- ImGuiIO& io = ImGui::GetIO(); - ImGui::TextDisabled("%s", r.device_name().empty() ? "GPU" : r.device_name().c_str()); + 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(); @@ -155,29 +353,28 @@ namespace Donut } ImGui::Spacing(); - bool scene = r.is_scene_mode(); ImGui::TextDisabled("VIEW"); - if (ImGui::RadioButton("Black hole", !scene)) r.set_scene_mode(false); + if (ImGui::RadioButton("Black hole", !m_scene_mode)) m_scene_mode = false; ImGui::SameLine(); - if (ImGui::RadioButton("Scene", scene)) r.set_scene_mode(true); + if (ImGui::RadioButton("Scene", m_scene_mode)) m_scene_mode = true; ImGui::Separator(); - if (scene) + if (m_scene_mode) { build_scene_ui(); } else { - BlackHoleParams& bh = r.black_hole_params(); + BlackHoleParams& bh = m_bh_params; if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { - bool ff = r.is_free_fly(); - if (ImGui::RadioButton("Orbital", !ff)) r.set_free_fly(false); + 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)) r.set_free_fly(true); + if (ImGui::RadioButton("Free-fly", ff)) set_free_fly(true); ImGui::SameLine(); - if (ImGui::Button("Reset view")) r.reset_camera(); + 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"); @@ -203,15 +400,14 @@ namespace Donut if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) { auto& hdri = HDRIManager::get(); - std::string current = r.current_hdri(); - std::string preview = hdri.get_hdri_name(current); + 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 == current); + bool selected = (path == m_hdri_path); if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected)) - r.set_hdri(path); + set_hdri(path); if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); @@ -225,14 +421,12 @@ namespace Donut auto Application::build_scene_ui() -> void { - RendererBackend& r = *m_renderer; if (!ImGui::CollapsingHeader("Objects", ImGuiTreeNodeFlags_DefaultOpen)) return; ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); - auto& objs = r.scene_objects(); - static int sel = 0; - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + 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) @@ -242,27 +436,26 @@ namespace Donut 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); - sel = (int)objs.size() - 1; + m_selected_object = (int)objs.size() - 1; } ImGui::SameLine(); if (ImGui::Button("Delete") && !objs.empty()) { - objs.erase(objs.begin() + sel); - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + 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(), sel == i)) sel = i; + if (ImGui::Selectable(label.c_str(), m_selected_object == i)) m_selected_object = i; } ImGui::EndChild(); - r.set_selected_object(sel); - if (sel >= 0 && sel < (int)objs.size()) + if (m_selected_object >= 0 && m_selected_object < (int)objs.size()) { - SceneObject& o = objs[sel]; + 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); @@ -286,115 +479,30 @@ namespace Donut dispatcher.dispatch<WindowResizeEvent>([this, &event](WindowResizeEvent& e) { - if (e.get_width() == 0 || e.get_height() == 0) - m_minimized = true; - else - m_minimized = false; - - if (m_renderer) - { - m_renderer->resize(e.get_width(), e.get_height()); - } - else - { - Renderer::on_window_resize(e.get_width(), e.get_height()); - if (m_engine) m_engine->set_window_dimensions(e.get_width(), e.get_height()); - } + m_minimized = (e.get_width() == 0 || e.get_height() == 0); + if (m_device) m_device->resize(e.get_width(), e.get_height()); event.handled = true; return true; }); - - dispatcher.dispatch<KeyPressedEvent>([this, &event](KeyPressedEvent& e) - { - if (!m_state_manager) - return true; - if (e.get_key_code() == GLFW_KEY_1) - { - m_state_manager->switch_to_state("Config"); - event.handled = true; - return true; - } - else if (e.get_key_code() == GLFW_KEY_2) - { - m_state_manager->switch_to_state("Simulation"); - event.handled = true; - return true; - } - else if (e.get_key_code() == GLFW_KEY_3) - { - m_state_manager->switch_to_state("WorldBuilder"); - event.handled = true; - return true; - } - - return true; - }); - - if (m_state_manager) - m_state_manager->on_event(event); - } - - auto Application::on_init() -> void - { - // (Logger, settings and API selection happen in the constructor.) Both - // backends run the SAME application through the RendererBackend interface, - // so the app behaves identically regardless of the active graphics API. - int w = 0, h = 0; - glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); - - if (RendererAPI::get_api() == RendererAPI::API::Vulkan) - m_renderer = create_scope<VulkanRenderer>(); - else - m_renderer = create_scope<OpenGLRenderer>(); - - if (!m_renderer->init(m_window->get_native_window(), w, h)) - DONUT_ERROR("Renderer initialization failed"); - else - m_renderer->init_ui(); } auto Application::on_shutdown() -> void { - if (m_renderer) + if (m_device) { - m_renderer->shutdown(); - m_renderer.reset(); - } - else - { - if (m_state_manager) - m_state_manager->shutdown(); - Renderer::shutdown(); + 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(); + m_device->shutdown(); + m_device.reset(); } SettingsManager::shutdown(); Logger::shutdown(); } - - auto Application::on_update() -> void - { - float current_frame = (float)glfwGetTime(); - m_delta_time = current_frame - m_last_frame; - m_last_frame = current_frame; - - m_state_manager->update(m_delta_time); - } - - auto Application::on_render() -> void - { - m_state_manager->render(); - m_window->begin_im_gui_frame(); - - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(false); - - setup_docking_layout(); - m_state_manager->on_im_ui_render(); - m_window->end_im_gui_frame(); - } - - auto Application::setup_docking_layout() -> void - { - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::DockSpaceOverViewport(0, viewport, ImGuiDockNodeFlags_PassthruCentralNode); - } }; diff --git a/src/core/application.h b/src/core/application.h index 4c377f0..4b67c84 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -1,16 +1,25 @@ #pragma once -#include "state_manager.h" #include "memory.h" #include "window.h" #include "event.h" #include "log.h" +#include "camera.h" -#include "engine/engine.h" -#include "rendering/renderer_backend.h" +#include "rendering/rhi.h" +#include "rendering/scene_renderer.h" +#include "rendering/black_hole_renderer.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. class Application { public: @@ -21,33 +30,52 @@ namespace Donut auto run() -> void; auto close() -> void; - auto get_window() -> Window& { return *m_window; } - auto get_state_manager() -> StateManager& { return *m_state_manager; } - auto get_engine() -> Engine& { return *m_engine; } - static auto get() -> Application& { return *s_instance; } + auto get_window() -> Window& { return *m_window; } + static auto get() -> Application& { return *s_instance; } private: auto on_init() -> void; auto on_shutdown() -> void; - auto on_update() -> void; - auto on_render() -> void; auto on_event(Event& event) -> void; - auto setup_docking_layout() -> void; - auto build_ui() -> void; // shared ImGui panel, drives the active RendererBackend - auto build_scene_ui() -> void; // scene-view (object list) section of the UI + + 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; private: - Scope<StateManager> m_state_manager; Scope<Window> m_window; - Scope<Engine> m_engine; - Scope<RendererBackend> m_renderer; // active backend (non-null once a backend runs the app) + + // 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; + + // input tracking (mirrors the old per-backend process_input) + 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; - float m_delta_time = 0.0f; - float m_last_frame = 0.0f; - static Application* s_instance; }; } diff --git a/src/core/hdri_manager.h b/src/core/hdri_manager.h index fa05330..d98830f 100644 --- a/src/core/hdri_manager.h +++ b/src/core/hdri_manager.h @@ -39,9 +39,9 @@ namespace Donut std::vector<std::string> m_available_hdri = { - "assets/hdri/HDR_blue_nebulae-1.hdr", - "assets/hdri/HDR_subdued_blue_nebulae.hdr", - "assets/hdri/HDR_subdued_multi_nebulae.hdr", + "assets/hdri/hdr_blue_nebulae_1.hdr", + "assets/hdri/hdr_subdued_blue_nebulae.hdr", + "assets/hdri/hdr_subdued_multi_nebulae.hdr", "assets/hdri/night_sky.hdr" }; }; diff --git a/src/core/settings_manager.cpp b/src/core/settings_manager.cpp index ca4cac5..6909c60 100644 --- a/src/core/settings_manager.cpp +++ b/src/core/settings_manager.cpp @@ -1,7 +1,6 @@ #include "settings_manager.h" #include "log.h" #include "theme_manager.h" -#include "rendering/renderer.h" #include <fstream> #include <filesystem> diff --git a/src/core/state.h b/src/core/state.h deleted file mode 100644 index 623f1c2..0000000 --- a/src/core/state.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "event.h" - -namespace Donut -{ - class State - { - public: - virtual ~State() = default; - - virtual auto on_enter() -> void = 0; - virtual auto on_exit() -> void = 0; - virtual auto on_update(float delta_time) -> void = 0; - virtual auto on_render() -> void = 0; - virtual auto on_im_ui_render() -> void = 0; - virtual auto on_event(Event& event) -> void = 0; - }; -}; diff --git a/src/core/state_manager.cpp b/src/core/state_manager.cpp deleted file mode 100644 index 8018401..0000000 --- a/src/core/state_manager.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "state_manager.h" -#include "log.h" - -namespace Donut -{ - auto StateManager::shutdown() -> void - { - if (m_current_state) - m_current_state->on_exit(); - - destroy_states(); - DONUT_INFO("StateManager shutdown"); - } - - auto StateManager::destroy_states() -> void - { - m_states.clear(); - m_current_state = nullptr; - m_current_state_name = ""; - } - - auto StateManager::register_state(const std::string& state_name, Scope<State> state) -> void - { - if (m_states.find(state_name) != m_states.end()) - DONUT_WARN("State '{}' already exists, overwriting", state_name); - m_states[state_name] = std::move(state); - DONUT_INFO("Registered state: {}", state_name); - } - - auto StateManager::switch_to_state(const std::string& state_name) -> void - { - auto it = m_states.find(state_name); - if (it == m_states.end()) - { - DONUT_ERROR("Attempted to switch to unknown state: {}", state_name); - return; - } - - State* new_state = it->second.get(); - if (new_state == m_current_state) - return; - - if (m_current_state) - m_current_state->on_exit(); - - m_current_state = new_state; - m_current_state_name = state_name; - m_current_state->on_enter(); - DONUT_INFO("Switched to state: {}", state_name); - } - - auto StateManager::get_state(const std::string& state_name) -> State* - { - auto it = m_states.find(state_name); - if (it != m_states.end()) - return it->second.get(); - return nullptr; - } - - auto StateManager::has_state(const std::string& state_name) const -> bool - { - return m_states.find(state_name) != m_states.end(); - } - - auto StateManager::update(float delta_time) -> void - { - if (m_current_state) - m_current_state->on_update(delta_time); - } - - auto StateManager::render() -> void - { - if (m_current_state) - m_current_state->on_render(); - } - - auto StateManager::on_im_ui_render() -> void - { - if (m_current_state) - m_current_state->on_im_ui_render(); - } - - auto StateManager::on_event(Event& event) -> void - { - if (m_current_state) - m_current_state->on_event(event); - } -} diff --git a/src/core/state_manager.h b/src/core/state_manager.h deleted file mode 100644 index 446841e..0000000 --- a/src/core/state_manager.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "memory.h" -#include "state.h" - -#include <string> -#include <unordered_map> - -namespace Donut -{ - class StateManager - { - public: - ~StateManager() = default; - auto shutdown() -> void; - - auto update(float delta_time) -> void; - auto render() -> void; - auto on_im_ui_render() -> void; - auto on_event(Event& event) -> void; - - auto register_state(const std::string& state_name, Scope<State> state) -> void; - auto switch_to_state(const std::string& state_name) -> void; - - auto get_current_state_name() const -> std::string { return m_current_state_name; } - auto get_current_state() const -> State* { return m_current_state; } - - auto get_state(const std::string& state_name) -> State*; - auto has_state(const std::string& state_name) const -> bool; - - private: - auto create_states() -> void; - auto destroy_states() -> void; - - private: - State* m_current_state = nullptr; - std::string m_current_state_name = ""; - - std::unordered_map<std::string, Scope<State>> m_states; - }; -} diff --git a/src/core/window.cpp b/src/core/window.cpp index 047ef0d..dd5882c 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -1,14 +1,8 @@ -#include <glad/glad.h> #include "window.h" -#include "theme_manager.h" -#include "rendering/renderer.h" // RendererAPI::get_api() +#include "rendering/render_api.h" // RendererAPI::get_api() #include <cstdint> -#include <imgui.h> -#include <imgui_impl_glfw.h> -#include <imgui_impl_opengl3.h> - namespace Donut { static bool s_glfw_initialized = false; @@ -94,8 +88,6 @@ namespace Donut { DONUT_INFO("Shutting down window: ", m_title); - shutdown_im_gui(); - glfwDestroyWindow(m_window); s_glfw_window_count--; @@ -133,101 +125,6 @@ namespace Donut GLFW_CURSOR_NORMAL : GLFW_CURSOR_HIDDEN); } - auto Window::init_im_gui() -> void - { - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; -#ifndef __APPLE__ - // Multi-viewport (dragging ImGui panels out as separate OS windows) - // relies on a populated platform-monitor list and is unreliable on - // macOS, where it intermittently asserts (Monitors.Size > 0) and - // aborts. Docking stays enabled; panels just remain inside the window. - io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; -#endif - - setup_im_gui_fonts(); - ThemeManager::set_theme(Theme::Dark); - - ImGuiStyle& style = ImGui::GetStyle(); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - style.WindowRounding = 0.0f; - style.Colors[ImGuiCol_WindowBg].w = 1.0f; - } - - ImGui_ImplGlfw_InitForOpenGL(m_window, true); -#ifdef __APPLE__ - // macOS uses a core-profile context, which rejects the legacy - // "#version 130" GLSL the ImGui backend defaults to. 150 is the - // minimum core-profile GLSL that macOS's OpenGL 4.1 accepts. - ImGui_ImplOpenGL3_Init("#version 150"); -#else - ImGui_ImplOpenGL3_Init("#version 130"); -#endif - - DONUT_INFO("ImGUI initialized successfully"); - } - - auto Window::setup_im_gui_fonts() -> void - { - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->Clear(); - - m_main_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f); - if (!m_main_font) - { - DONUT_WARN("Failed to load Inter font, falling back to default"); - m_main_font = io.Fonts->AddFontDefault(); - } - else - DONUT_INFO("Successfully loaded Inter font"); - - m_large_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Bold.ttf", 20.0f); - if (!m_large_font) - m_large_font = m_main_font; - - m_small_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Light.ttf", 12.0f); - if (!m_small_font) - m_small_font = m_main_font; - - io.FontDefault = m_main_font; - DONUT_INFO("ImGUI fonts loaded successfully"); - } - - auto Window::shutdown_im_gui() -> void - { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - - DONUT_INFO("ImGUI shutdown"); - } - - auto Window::begin_im_gui_frame() -> void - { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - } - - auto Window::end_im_gui_frame() -> void - { - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - - ImGuiIO& io = ImGui::GetIO(); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - GLFWwindow* backup_current_context = glfwGetCurrentContext(); - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - glfwMakeContextCurrent(backup_current_context); - } - } - auto Window::glfw_error_callback(int error, const char* description) -> void { DONUT_ERROR("GLFW Error ({}): {}", error, description ? description : ""); diff --git a/src/core/window.h b/src/core/window.h index 9b7d7b9..b8b59c5 100644 --- a/src/core/window.h +++ b/src/core/window.h @@ -4,7 +4,6 @@ #include "log.h" #include <GLFW/glfw3.h> -#include <imgui.h> #include <string> @@ -38,17 +37,10 @@ namespace Donut auto is_cursor_locked() const -> bool { return m_cursor_locked; } auto is_cursor_visible() const -> bool { return m_cursor_visible; } - auto init_im_gui() -> void; - auto begin_im_gui_frame() -> void; - auto end_im_gui_frame() -> void; - private: auto init() -> void; auto shutdown() -> void; - auto setup_im_gui_fonts() -> void; - auto shutdown_im_gui() -> void; - static auto glfw_error_callback(int error, const char* description) -> void; static auto glfw_window_close_callback(GLFWwindow* window) -> void; static auto glfw_window_size_callback(GLFWwindow* window, int width, int height) -> void; @@ -70,9 +62,5 @@ namespace Donut bool m_is_closed; bool m_cursor_locked = false; bool m_cursor_visible = true; - - ImFont* m_main_font = nullptr; - ImFont* m_small_font = nullptr; - ImFont* m_large_font = nullptr; }; } diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp deleted file mode 100644 index bf5a2b6..0000000 --- a/src/engine/engine.cpp +++ /dev/null @@ -1,625 +0,0 @@ -#include <iostream> -#include <fstream> -#include <sstream> - -#include <GLFW/glfw3.h> -#include <glad/glad.h> - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" - -#include "engine.h" -#include "core/log.h" -#include "core/hdri_manager.h" -#include "rendering/vertex_buffer.h" -#include "rendering/index_buffer.h" - -namespace Donut -{ - Engine::Engine() - : m_sag_a(glm::vec3(0.0f, 0.0f, 0.0f), 8.54e36f) - { - m_width = 1280; - m_height = 720; - - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_radius(1e11); - m_camera.set_orbital_limits(1e9, 1e13); - m_camera.set_orbital_speed(0.01f); - m_camera.set_zoom_speed(1e10f); - - m_objects = - { - { glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), glm::vec4(0, 0, 0, 1), static_cast<float>(m_sag_a.m_mass) } - }; - - // The geodesic ray tracer used to be a compute shader; it is now a - // fullscreen vertex+fragment pass (see dispatch_compute) so it runs on - // macOS OpenGL 4.1, which has no compute shaders. - m_compute_program = Ref<Shader>(Shader::create("assets/shaders/Geodesic.glsl")); - m_shader_program = Ref<Shader>(Shader::create("assets/shaders/TexturedQuad.glsl")); - m_blur_shader = Ref<Shader>(Shader::create("assets/shaders/Blur.glsl")); - - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - { - hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr"); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - DONUT_WARN("Failed to load default HDRI, using fallback"); - } - - m_camera_ubo = UniformBuffer::create(128, 1); - m_disk_ubo = UniformBuffer::create(sizeof(float) * 6, 2); // r1,r2,turbulence,thickness,brightness,temperature - - uint32_t obj_ubo_size = sizeof(int) + 3 * sizeof(float) - + 16 * (sizeof(glm::vec4) + sizeof(glm::vec4)) - + 16 * sizeof(float); - m_objects_ubo = UniformBuffer::create(obj_ubo_size, 3); - - m_simulation_ubo = UniformBuffer::create(sizeof(int) * 2 + sizeof(float) * 2, 4); - - auto result = QuadVAO(); - m_quad_vao = result.first; - m_texture = result.second; - - // GLSL 4.10 forbids explicit binding qualifiers on uniform blocks, so - // associate the geodesic shader's blocks with their UBO binding points - // from the host side instead. - if (m_compute_program) - { - uint32_t prog = m_compute_program->get_renderer_id(); - struct { const char* name; uint32_t point; } blocks[] = - { - { "Camera", 1 }, { "Disk", 2 }, { "Objects", 3 }, { "Simulation", 4 } - }; - for (const auto& b : blocks) - { - uint32_t idx = glGetUniformBlockIndex(prog, b.name); - if (idx != GL_INVALID_INDEX) - glUniformBlockBinding(prog, idx, b.point); - } - } - - glGenFramebuffers(1, &m_geodesic_fbo); - } - - auto Engine::update_window_dimensions() -> void - { - update_compute_dimensions(); - } - - auto Engine::set_window_dimensions(int width, int height) -> void - { - int old_width = m_width; - int old_height = m_height; - int old_compute_height = m_compute_height; - - m_width = width; - m_height = height; - - if (old_width != m_width || - old_height != m_height || - old_compute_height != m_compute_height) - update_compute_dimensions(); - } - - auto Engine::update_performance(float delta_time) -> void - { - if (delta_time > 0.0f) - m_current_fps = 1.0f / delta_time; - } - - auto Engine::update_compute_dimensions() -> void - { - m_texture = Texture2D::create(get_compute_width(), m_compute_height); - } - - auto Engine::draw_full_screen_quad() -> void - { - RenderCommand::set_viewport(0, 0, m_width, m_height); - - m_shader_program->bind(); - m_quad_vao->bind(); - - m_texture->bind(0); - m_shader_program->set_int("u_ScreenTexture", 0); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - } - - auto Engine::draw_blur_pass() -> void - { - RenderCommand::set_viewport(0, 0, m_width, m_height); - - m_blur_shader->bind(); - m_quad_vao->bind(); - - m_texture->bind(0); - m_blur_shader->set_int("u_ScreenTexture", 0); - m_blur_shader->set_float2("u_Resolution", glm::vec2(m_width, m_height)); - m_blur_shader->set_float("u_BlurStrength", m_blur_strength); - m_blur_shader->set_float("u_GlowIntensity", m_glow_intensity); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - } - - auto Engine::draw_geodesic_pass(int cw, int ch) -> void - { - m_quad_vao->bind(); - RenderCommand::disable_depth_test(); - -#ifdef __APPLE__ - // macOS aborts any GPU submission that runs longer than a couple of - // seconds ("GPU Hang"). The geodesic ray-marcher can far exceed that in - // a single fullscreen draw, so render it in scissored tiles and flush - // after each, keeping every submission short enough to survive the - // watchdog. Compute-capable platforms draw it in one pass. - // Largest tile that keeps a tile's worst-case work (tile^2 * step_cap) - // inside the safe watchdog zone measured on this GPU (~25M pixel-steps - // per submission); bigger tiles mean fewer glFinish stalls. - const int tile = 64; - glEnable(GL_SCISSOR_TEST); - for (int y = 0; y < ch; y += tile) - { - int th = std::min(tile, ch - y); - for (int x = 0; x < cw; x += tile) - { - int tw = std::min(tile, cw - x); - glScissor(x, y, tw, th); - RenderCommand::draw_arrays(6); - glFinish(); - } - } - glDisable(GL_SCISSOR_TEST); -#else - RenderCommand::draw_arrays(6); -#endif - - RenderCommand::enable_depth_test(); - } - - auto Engine::dispatch_compute(const Camera& cam) -> void - { - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - - int cw = get_compute_width(); - int ch = m_compute_height; - - // Render the geodesic pass into m_texture through an FBO. This replaces - // the old compute dispatch + image_store path, which relied on OpenGL - // 4.3 compute and 4.2 image load/store that macOS does not provide. - glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture->get_renderer_id(), 0); - glViewport(0, 0, cw, ch); - - m_compute_program->bind(); - upload_camera_ubo(cam); - upload_disk_ubo(); - upload_objects_ubo(m_objects); - upload_simulation_ubo(); - m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(cw), static_cast<float>(ch))); - - if (m_hdri_environment) - { - m_hdri_environment->bind(5); - m_compute_program->set_int("u_HDRIEnvironment", 5); - } - - draw_geodesic_pass(cw, ch); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Engine::upload_camera_ubo(const Camera& cam) -> void - { - struct UBOData - { - glm::vec3 pos; float _pad0; - glm::vec3 right; float _pad1; - glm::vec3 up; float _pad2; - glm::vec3 forward; float _pad3; - float tan_half_fov; - float aspect; - bool moving; - int _pad4; - } data; - - glm::vec3 pos, fwd; - if (cam.get_camera_mode() == CameraMode::FPS) - { - pos = cam.get_position(); - fwd = cam.get_forward_direction(); - } - else - { - pos = cam.get_orbital_position(); - fwd = glm::normalize(cam.get_orbital_target() - pos); - } - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - glm::vec3 up = glm::cross(right, fwd); - - data.pos = pos; - data.right = right; - data.up = up; - data.forward = fwd; - data.tan_half_fov = static_cast<float>(tan(glm::radians(m_bh.fov_degrees * 0.5f))); - data.aspect = static_cast<float>(get_compute_width()) / static_cast<float>(m_compute_height); - data.moving = cam.is_dragging() || cam.is_panning(); - - m_camera_ubo->set_data(&data, sizeof(UBOData)); - m_camera_ubo->bind(1); - } - - auto Engine::upload_objects_ubo(const std::vector<ObjectData>& objs) -> void - { - struct UBOData - { - int num_objects; - float _pad0, _pad1, _pad2; - glm::vec4 pos_radius[16]; - glm::vec4 color[16]; - float mass[16]; - } data; - - size_t count = std::min(objs.size(), size_t(16)); - data.num_objects = static_cast<int>(count); - - for (size_t i = 0; i < count; ++i) - { - data.pos_radius[i] = objs[i].m_pos_radius; - data.color[i] = objs[i].m_color; - data.mass[i] = objs[i].m_mass; - } - - m_objects_ubo->set_data(&data, sizeof(data)); - m_objects_ubo->bind(3); - } - - auto Engine::upload_disk_ubo() -> void - { - // Layout matches the shared Geodesic Disk struct (same as Vulkan). Radii - // are in Schwarzschild radii x the shader's SagA_rs constant (1.269e10). - const float rs = 1.269e10f; - float disk_data[6] = { - std::max(m_bh.disk_inner_rs, 3.0f) * rs, - std::max(m_bh.disk_outer_rs, m_bh.disk_inner_rs + 0.5f) * rs, - std::max(m_bh.turbulence, 0.0f), - rs * 0.1f, // slab half-thickness (fixed) - std::max(m_bh.brightness, 0.0f), - std::max(m_bh.temperature, 1000.0f), - }; - m_disk_ubo->set_data(disk_data, sizeof(disk_data)); - m_disk_ubo->bind(2); - } - - auto Engine::upload_simulation_ubo() -> void - { - struct UBOData - { - int max_steps_moving; - int max_steps_static; - float early_exit_distance; - float time; - } data; - - data.max_steps_moving = m_bh.quality_steps; - data.max_steps_static = m_bh.quality_steps; // resolution/tiling is the lever, not step count - data.early_exit_distance = m_early_exit_distance; - data.time = static_cast<float>(glfwGetTime()) * m_rotation_speed; - -#ifdef __APPLE__ - // macOS has no compute shaders, so the geodesic pass runs as a tiled - // fragment shader under the OS GPU watchdog. Cap the step count so a - // single 64px tile stays under the safe per-submission budget (~25M - // pixel-steps); Windows/Linux keep the full count. - data.max_steps_moving = std::min(data.max_steps_moving, 6000); - data.max_steps_static = std::min(data.max_steps_static, 6000); -#endif - - m_simulation_ubo->set_data(&data, sizeof(data)); - m_simulation_ubo->bind(4); - } - - auto Engine::update_physics(float delta_time) -> void - { - for (auto& obj : m_objects) - { - for (auto& obj2 : m_objects) - { - if (&obj == &obj2) continue; - float dx = obj2.m_pos_radius.x - obj.m_pos_radius.x; - float dy = obj2.m_pos_radius.y - obj.m_pos_radius.y; - float dz = obj2.m_pos_radius.z - obj.m_pos_radius.z; - float distance = sqrt(dx * dx + dy * dy + dz * dz); - if (distance > 0) - { - std::vector<double> direction = {dx / distance, dy / distance, dz / distance}; - double Gforce = (G * obj.m_mass * obj2.m_mass) / (distance * distance); - double acc1 = Gforce / obj.m_mass; - std::vector<double> acc = {direction[0] * acc1, direction[1] * acc1, direction[2] * acc1}; - - if (m_gravity) - { - obj.m_velocity.x += static_cast<float>(acc[0]); - obj.m_velocity.y += static_cast<float>(acc[1]); - obj.m_velocity.z += static_cast<float>(acc[2]); - - obj.m_pos_radius.x += static_cast<float>(obj.m_velocity.x); - obj.m_pos_radius.y += static_cast<float>(obj.m_velocity.y); - obj.m_pos_radius.z += static_cast<float>(obj.m_velocity.z); - } - } - } - } - } - - auto Engine::render_scene() -> void - { - RenderCommand::clear(); - m_shader_program->bind(); - m_quad_vao->bind(); - m_texture->bind(0); - RenderCommand::draw_arrays(6); - } - - auto Engine::create_compute_program(const char* path) -> Ref<Shader> - { - std::ifstream in(path); - if(!in.is_open()) - { - std::cerr << "Failed to open compute shader: " << path << "\n"; - return nullptr; - } - std::stringstream ss; - ss << in.rdbuf(); - std::string src_str = ss.str(); - return Ref<Shader>(Shader::create_compute("ComputeShader", src_str)); - } - - auto Engine::QuadVAO() -> std::pair<Ref<VertexArray>, Ref<Texture2D>> - { - float quad_vertices[] = - { - // Positions // TexCoords - -1.0f, 1.0f, 0.0f, 1.0f, - -1.0f, -1.0f, 0.0f, 0.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - 1.0f, 1.0f, 1.0f, 1.0f - }; - - auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(quad_vertices, static_cast<uint32_t>(sizeof(quad_vertices)))); - VertexBufferLayout layout; - layout.push<float>(2); // Position (x, y) - layout.push<float>(2); // TexCoord (u, v) - vertex_buffer->set_layout(layout); - - auto vertex_array = Ref<VertexArray>(VertexArray::create()); - vertex_array->add_vertex_buffer(vertex_buffer); - auto texture = Texture2D::create(get_compute_width(), m_compute_height); - - return { vertex_array, texture }; - } - - auto Engine::load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void - { - m_objects.clear(); - m_objects.push_back( - { - glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), - glm::vec4(0, 0, 0, 1), - static_cast<float>(m_sag_a.m_mass) - }); - - for (const auto& obj : objects) - { - ObjectData engine_obj; - - float scale_factor = 1e10f; - engine_obj.m_pos_radius = glm::vec4 - ( - obj.m_centre.x * scale_factor, - obj.m_centre.y * scale_factor, - obj.m_centre.z * scale_factor, - obj.m_radius * scale_factor - ); - - engine_obj.m_color = glm::vec4(obj.m_material.m_color, 1.0f); - - float volume = (4.0f / 3.0f) * 3.14159f * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w; - float density = 1e12f; - engine_obj.m_mass = volume * density; - engine_obj.m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); - - m_objects.push_back(engine_obj); - } - - DONUT_INFO("Loaded {} objects from WorldBuilder scene (scaled up by {})", objects.size(), 1e10f); - print_object_info(); - } - - auto Engine::print_object_info() const -> void - { - DONUT_INFO("=== Object Information ==="); - DONUT_INFO("Total objects: {}", m_objects.size()); - - for (size_t i = 0; i < m_objects.size(); ++i) - { - const auto& obj = m_objects[i]; - DONUT_INFO("Object {}: Pos=({}, {}, {}), Radius={}, Mass={}, Color=({}, {}, {})", - i, - obj.m_pos_radius.x, obj.m_pos_radius.y, obj.m_pos_radius.z, - obj.m_pos_radius.w, - obj.m_mass, - obj.m_color.x, obj.m_color.y, obj.m_color.z - ); - } - DONUT_INFO("Camera position: ({}, {}, {})", - m_camera.get_orbital_position().x, - m_camera.get_orbital_position().y, - m_camera.get_orbital_position().z - ); - DONUT_INFO("Camera radius: {}", m_camera.get_orbital_radius()); - DONUT_INFO("========================"); - } - - auto Engine::export_high_res_frame(const std::string& filename, int width, int height) -> void - { - DONUT_INFO("Exporting high-resolution frame: {}x{} to {}", width, height, filename); - - if (width <= 0 || height <= 0) - { - DONUT_ERROR("Invalid dimensions for export: {}x{}", width, height); - return; - } - - if (filename.empty()) - { - DONUT_ERROR("Invalid filename for export"); - return; - } - - int original_width = m_width; - int original_height = m_height; - int original_compute_height = m_compute_height; - - m_width = width; - m_height = height; - m_compute_height = height; - - int compute_height = height; - int compute_width = (width * compute_height) / height; - - if (compute_width <= 0 || compute_height <= 0) - { - DONUT_ERROR("Invalid compute dimensions: {}x{}", compute_width, compute_height); - return; - } - - FramebufferSpecification fb_spec; - fb_spec.Width = width; - fb_spec.Height = height; - fb_spec.attachments = { FramebufferTextureFormat::RGBA8 }; - - auto high_res_framebuffer = Framebuffer::create(fb_spec); - if (!high_res_framebuffer) - { - DONUT_ERROR("Failed to create high-resolution framebuffer"); - return; - } - - auto high_res_texture = Texture2D::create(compute_width, compute_height); - if (!high_res_texture) - { - DONUT_ERROR("Failed to create high-resolution texture"); - return; - } - - // Render the geodesic pass into high_res_texture through the geodesic FBO. - glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, high_res_texture->get_renderer_id(), 0); - glViewport(0, 0, compute_width, compute_height); - - m_compute_program->bind(); - - struct UBOData - { - glm::vec3 pos; float _pad0; - glm::vec3 right; float _pad1; - glm::vec3 up; float _pad2; - glm::vec3 forward; float _pad3; - float tan_half_fov; - float aspect; - bool moving; - int _pad4; - } data; - - glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - m_camera.get_orbital_position()); - glm::vec3 up = glm::vec3(0, 1, 0); - glm::vec3 right = glm::normalize(glm::cross(fwd, up)); - up = glm::cross(right, fwd); - - data.pos = m_camera.get_orbital_position(); - data.right = right; - data.up = up; - data.forward = fwd; - data.tan_half_fov = static_cast<float>(tan(glm::radians(60.0f * 0.5f))); - data.aspect = static_cast<float>(compute_width) / static_cast<float>(compute_height); - data.moving = m_camera.is_dragging() || m_camera.is_panning(); - - m_camera_ubo->set_data(&data, sizeof(UBOData)); - m_camera_ubo->bind(1); - - upload_disk_ubo(); - upload_objects_ubo(m_objects); - upload_simulation_ubo(); - m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(compute_width), static_cast<float>(compute_height))); - - if (m_hdri_environment) - { - m_hdri_environment->bind(5); - m_compute_program->set_int("u_HDRIEnvironment", 5); - } - - draw_geodesic_pass(compute_width, compute_height); - - // Display the rendered frame into the high-res framebuffer for read-back. - high_res_framebuffer->bind(); - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::clear(); - - m_shader_program->bind(); - m_quad_vao->bind(); - - high_res_texture->bind(0); - m_shader_program->set_int("u_ScreenTexture", 0); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - - std::vector<unsigned char> pixels(width * height * 4); - DONUT_INFO("Reading {} pixels from framebuffer...", width * height); - RenderCommand::read_pixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - - DONUT_INFO("Flipping image vertically..."); - std::vector<unsigned char> flipped_pixels(width * height * 4); - for (int y = 0; y < height; ++y) - { - for (int x = 0; x < width; ++x) - { - int src_index = (y * width + x) * 4; - int dst_index = ((height - 1 - y) * width + x) * 4; - flipped_pixels[dst_index + 0] = pixels[src_index + 0]; // R - flipped_pixels[dst_index + 1] = pixels[src_index + 1]; // G - flipped_pixels[dst_index + 2] = pixels[src_index + 2]; // B - flipped_pixels[dst_index + 3] = pixels[src_index + 3]; // A - } - } - - DONUT_INFO("Saving PNG file: {}...", filename); - int result = stbi_write_png(filename.c_str(), width, height, 4, flipped_pixels.data(), width * 4); - - if (result) - DONUT_INFO("Successfully exported high-resolution frame to: {}", filename); - else - DONUT_ERROR("Failed to export high-resolution frame to: {}", filename); - - high_res_framebuffer->unbind(); - - m_width = original_width; - m_height = original_height; - m_compute_height = original_compute_height; - - RenderCommand::set_viewport(0, 0, original_width, original_height); - } - -} diff --git a/src/engine/engine.h b/src/engine/engine.h deleted file mode 100644 index fef8b57..0000000 --- a/src/engine/engine.h +++ /dev/null @@ -1,191 +0,0 @@ -#pragma once - -#include <vector> -#include <numbers> -#include <iostream> -#include <fstream> -#include <sstream> -#include <chrono> -#include <cmath> - -#include "core/camera.h" -#include "object.h" - -#include "rendering/renderer.h" -#include "rendering/renderer_backend.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" -#include "rendering/texture.h" -#include "rendering/uniform_buffer.h" -#include "rendering/texture_manager.h" - -#include <GLFW/glfw3.h> -#include <glm/glm.hpp> -#include <glm/gtc/matrix_transform.hpp> -#include <glm/gtc/type_ptr.hpp> - -namespace Donut -{ - const double c = 299792458.0; - const double G = 6.67430e-11; - - struct BlackHole - { - glm::vec3 m_position; - double m_mass; - double m_radius; - double m_rs; - - BlackHole(glm::vec3 pos, float mass) - : m_position(pos), m_mass(mass) - { - m_rs = 2.0 * G * m_mass / (c * c); - } - - bool intercept(float px, float py, float pz) const - { - double dx = double(px) - double(m_position.x); - double dy = double(py) - double(m_position.y); - double dz = double(pz) - double(m_position.z); - double dist2 = dx * dx + dy * dy + dz * dz; - return dist2 < m_rs * m_rs; - } - }; - - struct ObjectData - { - glm::vec4 m_pos_radius; - glm::vec4 m_color; - float m_mass; - glm::vec3 m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); - }; - - class Engine - { - public: - Engine(); - ~Engine() = default; - - auto draw_full_screen_quad() -> void; - auto draw_blur_pass() -> void; - auto dispatch_compute(const Camera& cam) -> void; - auto upload_camera_ubo(const Camera& cam) -> void; - auto upload_objects_ubo(const std::vector<ObjectData>& objs) -> void; - auto upload_disk_ubo() -> void; - auto upload_simulation_ubo() -> void; - auto render_scene() -> void; - auto update_physics(float delta_time) -> void; - auto update_window_dimensions() -> void; - auto set_window_dimensions(int width, int height) -> void; - - auto get_width() const -> int { return m_width; } - auto get_height() const -> int { return m_height; } - - auto get_objects() -> std::vector<ObjectData>& { return m_objects; } - auto get_sag_a() -> BlackHole& { return m_sag_a; } - auto get_camera() -> Camera& { return m_camera; } - auto get_gravity() -> bool& { return m_gravity; } - - auto update_performance(float delta_time) -> void; - auto set_target_fps(int fps) -> void { m_target_fps = fps; } - auto get_target_fps() const -> int { return m_target_fps; } - auto get_current_fps() const -> float { return m_current_fps; } - void set_compute_height(int height) - { -#ifdef __APPLE__ - // Without compute shaders the geodesic pass runs as a tiled - // fragment shader (see draw_geodesic_pass), so very high working - // resolutions make each frame take many seconds. Cap it on macOS. - if (height > 256) height = 256; -#endif - m_compute_height = height; - } - auto get_compute_height() const -> int { return m_compute_height; } - auto get_compute_width() const -> int { return (m_width * m_compute_height) / m_height; } - auto update_compute_dimensions() -> void; - - auto get_max_steps_moving() const -> int { return m_max_steps_moving; } - auto get_max_steps_static() const -> int { return m_max_steps_static; } - auto get_early_exit_distance() const -> float { return m_early_exit_distance; } - auto set_max_steps_moving(int steps) -> void { m_max_steps_moving = steps; } - auto set_max_steps_static(int steps) -> void { m_max_steps_static = steps; } - auto set_early_exit_distance(float distance) -> void { m_early_exit_distance = distance; } - - auto get_disk_thickness() const -> float { return m_disk_thickness; } - auto set_disk_thickness(float thickness) -> void { m_disk_thickness = thickness; } - - auto get_disk_density() const -> float { return m_disk_density; } - auto set_disk_density(float density) -> void { m_disk_density = density; } - - auto get_rotation_speed() const -> float { return m_rotation_speed; } - auto set_rotation_speed(float speed) -> void { m_rotation_speed = speed; } - - auto get_blur_strength() const -> float { return m_blur_strength; } - auto set_blur_strength(float strength) -> void { m_blur_strength = strength; } - - auto get_glow_intensity() const -> float { return m_glow_intensity; } - auto set_glow_intensity(float intensity) -> void { m_glow_intensity = intensity; } - - auto load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void; - auto export_high_res_frame(const std::string& filename, int width = 4096, int height = 3072) -> void; - auto print_object_info() const -> void; - - auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; } - auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; } - - // Shared live black-hole/disk parameters (drives the geodesic UBOs so the - // GL path renders identically to Vulkan). The OpenGL backend mutates this. - auto black_hole_params() -> BlackHoleParams& { return m_bh; } - private: - auto create_compute_program(const char* path) -> Ref<Shader>; - std::pair<Ref<VertexArray>, Ref<Texture2D>> QuadVAO(); - - // Draws the bound geodesic shader over a cw x ch target. On macOS this - // is split into scissored tiles (with a flush each) so no single GPU - // submission trips the OS watchdog; elsewhere it is one fast draw. - auto draw_geodesic_pass(int cw, int ch) -> void; - private: - Ref<VertexArray> m_quad_vao; - Ref<Texture2D> m_texture; - Ref<CubemapTexture> m_hdri_environment; - Ref<Shader> m_shader_program; - Ref<Shader> m_compute_program; - Ref<Shader> m_blur_shader; - Ref<UniformBuffer> m_camera_ubo; - Ref<UniformBuffer> m_disk_ubo; - Ref<UniformBuffer> m_objects_ubo; - Ref<UniformBuffer> m_simulation_ubo; - - // FBO used to render the geodesic pass into m_texture. The geodesic - // shader is a fragment shader (macOS has no compute), so it draws a - // fullscreen quad into this framebuffer instead of dispatching compute. - uint32_t m_geodesic_fbo = 0; - - int m_width; - int m_height; - float m_width_f = 100.0f*1e10f; - float m_height_f = 75.0f*1e10f; - - int m_target_fps = 60; - float m_current_fps = 60.0f; - float m_last_frame_time = 0.0f; - int m_compute_height = 150; - - std::vector<ObjectData> m_objects; - BlackHole m_sag_a; - Camera m_camera; - bool m_gravity = false; - - int m_max_steps_moving = 60000; - int m_max_steps_static = 30000; - float m_early_exit_distance = 5.0e11f; - - BlackHoleParams m_bh; // shared UI-tunable disk/camera params (GL geodesic driver) - - float m_disk_thickness = 0.1f; - float m_disk_density = 0.1f; - float m_rotation_speed = 1.0f; - float m_blur_strength = 2.0f; - float m_glow_intensity = 0.1f; - }; -}; diff --git a/src/engine/object.h b/src/engine/object.h deleted file mode 100644 index 8101540..0000000 --- a/src/engine/object.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include <glm/glm.hpp> - -namespace Donut -{ - class Ray - { - public: - Ray(glm::vec3 o, glm::vec3 d) - : m_origin(o), - m_direction(glm::normalize(d)) { } - - public: - glm::vec3 m_direction; - glm::vec3 m_origin; - }; - - class Material - { - public: - Material() : m_color(1.0f, 1.0f, 1.0f), m_specular(0.5f), m_emission(0.0f) { } - Material(glm::vec3 c, float s, float e) - : m_color(c), - m_specular(s), - m_emission(e) { } - - public: - glm::vec3 m_color; - float m_specular; - float m_emission; - }; - - class Object - { - public: - Object() : m_centre(0.0f, 0.0f, 0.0f), m_radius(1.0f), m_material() { } - Object(glm::vec3 c, float r, Material m) - : m_centre(c), - m_radius(r), - m_material(m) { } - - auto intersect(Ray& ray, float& t) -> bool - { - glm::vec3 oc = ray.m_origin - m_centre; - float a = glm::dot(ray.m_direction, ray.m_direction); - float b = 2.0f * glm::dot(oc, ray.m_direction); - float c = glm::dot(oc, oc) - m_radius * m_radius; - float discriminant = static_cast<float>(b*b - 4*a*c); - - if (discriminant < 0) - return false; - - float intercept = (-b - sqrt(discriminant)) / (2.0f*a); - if (intercept < 0) - { - intercept = (-b + sqrt(discriminant)) / (2.0f*a); - if (intercept < 0) - return false; - } - - t = intercept; - return true; - } - - auto get_normal(glm::vec3& point) const -> glm::vec3 - { - return glm::normalize(point - m_centre); - } - - public: - glm::vec3 m_centre; - float m_radius; - Material m_material; - }; -}; diff --git a/src/engine/scene.h b/src/engine/scene.h deleted file mode 100644 index 4b41203..0000000 --- a/src/engine/scene.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include <vector> -#include <limits> - -#include "object.h" - -namespace Donut -{ - class Scene - { - public: - Scene() - : m_light_pos(5.0f, 5.0f, 5.0f) { } - - auto trace(Ray& ray) -> glm::vec3 - { - float closest = std::numeric_limits<float>::infinity(); - const Object* hit_obj = nullptr; - - for (auto& obj : objs) - { - float t; - - if (obj.intersect(ray, t)) - if (t < closest) - { - closest = t; - hit_obj = &obj; - } - } - - if (hit_obj) - { - glm::vec3 hit_point = ray.m_origin + ray.m_direction * closest; - glm::vec3 normal = hit_obj->get_normal(hit_point); - glm::vec3 light_dir = glm::normalize(m_light_pos - hit_point); - - float diff = std::max(glm::dot(normal, light_dir), 0.0f); - - Ray shadow_ray(hit_point + normal * 0.001f, light_dir); - bool in_shadow = false; - - for (auto& obj : objs) - { - float t; - - if (obj.intersect(shadow_ray, t)) - { - in_shadow = true; - break; - } - } - - glm::vec3 color = hit_obj->m_material.m_color; - float ambient = 0.1f; - - if (in_shadow) - return color * ambient; - return color * (ambient + diff * 0.9f); - } - - return glm::vec3(0.0f, 0.0f, 0.1f); - } - - public: - std::vector<Object> objs; - glm::vec3 m_light_pos; - }; -}; diff --git a/src/platform/metal/metal_context.h b/src/platform/metal/metal_context.h deleted file mode 100644 index f1e376b..0000000 --- a/src/platform/metal/metal_context.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -// Pure-C++ interface to the Metal backend (no Objective-C leaks into the rest -// of the engine). Implemented in MetalContext.mm. -namespace Donut -{ - // Phase 1 bring-up: creates the default Metal device + command queue and - // logs its capabilities, proving the Metal toolchain and build integration - // work natively on Apple Silicon. This grows into the Metal compute island - // that runs the geodesic ray tracer in real time. - auto metal_probe() -> bool; - - // Compiles + dispatches a trivial compute kernel and validates the read-back, - // proving the Metal compute path the geodesic ray tracer will run on. - auto metal_compute_self_test() -> bool; -} diff --git a/src/platform/metal/metal_context.mm b/src/platform/metal/metal_context.mm deleted file mode 100644 index 71e8551..0000000 --- a/src/platform/metal/metal_context.mm +++ /dev/null @@ -1,124 +0,0 @@ -#import <Metal/Metal.h> -#import <Foundation/Foundation.h> - -#include "metal_context.h" -#include "core/log.h" - -#include <vector> - -namespace Donut -{ - bool metal_probe() - { - @autoreleasepool - { - id<MTLDevice> device = MTLCreateSystemDefaultDevice(); - if (device == nil) - { - DONUT_ERROR("Metal: no default device available"); - return false; - } - - id<MTLCommandQueue> queue = [device newCommandQueue]; - const char* name = [[device name] UTF8String]; - - DONUT_INFO("Metal device: {}", name ? name : "(unknown)"); - DONUT_INFO("Metal: unified memory = {}, max threads/threadgroup = {}", - device.hasUnifiedMemory ? "yes" : "no", - (unsigned long)device.maxThreadsPerThreadgroup.width); - - if (queue == nil) - { - DONUT_WARN("Metal: failed to create command queue"); - return false; - } - - return true; - } - } - - // Compiles a trivial compute kernel, dispatches it over a small texture, and - // reads the result back to confirm the full Metal compute path works: source - // compilation, pipeline state, command encoding, dispatch, and shared-memory - // read-back. This is the mechanism the geodesic ray tracer will run on. - bool metal_compute_self_test() - { - @autoreleasepool - { - id<MTLDevice> device = MTLCreateSystemDefaultDevice(); - id<MTLCommandQueue> queue = [device newCommandQueue]; - if (device == nil || queue == nil) - return false; - - NSString* src = - @"#include <metal_stdlib>\n" - "using namespace metal;\n" - "kernel void selfTest(texture2d<float, access::write> outTex [[texture(0)]],\n" - " uint2 gid [[thread_position_in_grid]])\n" - "{\n" - " uint w = outTex.get_width();\n" - " uint h = outTex.get_height();\n" - " if (gid.x >= w || gid.y >= h) return;\n" - " outTex.write(float4(float(gid.x) / float(w - 1),\n" - " float(gid.y) / float(h - 1), 0.5, 1.0), gid);\n" - "}\n"; - - NSError* err = nil; - id<MTLLibrary> lib = [device newLibraryWithSource:src options:nil error:&err]; - if (lib == nil) - { - DONUT_ERROR("Metal self-test: kernel compile failed: {}", - err ? [[err localizedDescription] UTF8String] : "unknown"); - return false; - } - - id<MTLFunction> fn = [lib newFunctionWithName:@"selfTest"]; - id<MTLComputePipelineState> pipeline = - [device newComputePipelineStateWithFunction:fn error:&err]; - if (pipeline == nil) - { - DONUT_ERROR("Metal self-test: pipeline creation failed"); - return false; - } - - const uint32_t W = 64, H = 64; - MTLTextureDescriptor* desc = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm - width:W - height:H - mipmapped:NO]; - desc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead; - desc.storageMode = MTLStorageModeShared; - id<MTLTexture> tex = [device newTextureWithDescriptor:desc]; - - id<MTLCommandBuffer> cb = [queue commandBuffer]; - id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder]; - [enc setComputePipelineState:pipeline]; - [enc setTexture:tex atIndex:0]; - - MTLSize tg = MTLSizeMake(16, 16, 1); - MTLSize grid = MTLSizeMake(W, H, 1); - [enc dispatchThreads:grid threadsPerThreadgroup:tg]; - [enc endEncoding]; - [cb commit]; - [cb waitUntilCompleted]; - - // Read back the far corner; the kernel writes (~1, ~1, 0.5, 1) there. - std::vector<uint8_t> px(W * H * 4); - [tex getBytes:px.data() - bytesPerRow:W * 4 - fromRegion:MTLRegionMake2D(0, 0, W, H) - mipmapLevel:0]; - - size_t corner = ((size_t)(H - 1) * W + (W - 1)) * 4; - DONUT_INFO("Metal compute self-test: corner pixel RGBA = ({}, {}, {}, {})", - (int)px[corner + 0], (int)px[corner + 1], - (int)px[corner + 2], (int)px[corner + 3]); - - bool ok = px[corner + 0] > 250 && px[corner + 1] > 250 && - px[corner + 3] == 255; - DONUT_INFO("Metal compute self-test: {}", ok ? "PASS" : "FAIL"); - return ok; - } - } -} diff --git a/src/platform/opengl/opengl_device.cpp b/src/platform/opengl/opengl_device.cpp new file mode 100644 index 0000000..3d45d24 --- /dev/null +++ b/src/platform/opengl/opengl_device.cpp @@ -0,0 +1,279 @@ +#include "opengl_device.h" + +#include "core/log.h" +#include "core/hdri_manager.h" +#include "rendering/shader.h" +#include "rendering/texture.h" + +#include <glad/glad.h> +#include <GLFW/glfw3.h> +#include <imgui.h> +#include <imgui_impl_glfw.h> +#include <imgui_impl_opengl3.h> + +namespace Donut::RHI +{ + namespace + { + auto gl_topology(Topology t) -> GLenum { return t == Topology::Lines ? GL_LINES : GL_TRIANGLES; } + auto gl_compare(CompareOp o) -> GLenum { return o == CompareOp::Always ? GL_ALWAYS : o == CompareOp::LessEqual ? GL_LEQUAL : GL_LESS; } + auto gl_filter(Filter f) -> GLint { return f == Filter::Nearest ? GL_NEAREST : GL_LINEAR; } + auto gl_internal(Format f) -> GLint { return f == Format::RGBA16F ? GL_RGBA16F : f == Format::D32 ? GL_DEPTH_COMPONENT32F : GL_RGBA8; } + + // ---- Buffer ------------------------------------------------------- + class GLBuffer : public Buffer + { + public: + GLBuffer(BufferType type, size_t size, const void* data) + { + m_target = type == BufferType::Index ? GL_ELEMENT_ARRAY_BUFFER + : type == BufferType::Uniform ? GL_UNIFORM_BUFFER : GL_ARRAY_BUFFER; + glGenBuffers(1, &m_id); + glBindBuffer(m_target, m_id); + glBufferData(m_target, (GLsizeiptr)size, data, GL_DYNAMIC_DRAW); + } + ~GLBuffer() override { if (m_id) glDeleteBuffers(1, &m_id); } + auto update(const void* data, size_t size) -> void override + { + glBindBuffer(m_target, m_id); + glBufferSubData(m_target, 0, (GLsizeiptr)size, data); + } + GLuint m_id = 0; GLenum m_target = GL_ARRAY_BUFFER; + }; + + // ---- Texture ------------------------------------------------------ + class GLTexture : public Texture + { + public: + GLTexture(int w, int h, Format fmt, Filter filter, const void* data) + { + glGenTextures(1, &m_id); m_own = true; m_target = GL_TEXTURE_2D; + glBindTexture(GL_TEXTURE_2D, m_id); + GLenum ext = fmt == Format::D32 ? GL_DEPTH_COMPONENT : GL_RGBA; + GLenum type = fmt == Format::RGBA8 ? GL_UNSIGNED_BYTE : GL_FLOAT; + glTexImage2D(GL_TEXTURE_2D, 0, gl_internal(fmt), w, h, 0, ext, type, data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter(filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter(filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + explicit GLTexture(Ref<CubemapTexture> cube) + : m_cube(std::move(cube)) + { + m_target = GL_TEXTURE_CUBE_MAP; + m_id = m_cube ? m_cube->get_renderer_id() : 0; + } + ~GLTexture() override { if (m_own && m_id) glDeleteTextures(1, &m_id); } + GLuint m_id = 0; GLenum m_target = GL_TEXTURE_2D; bool m_own = false; + Ref<CubemapTexture> m_cube; // keepalive for cubemaps + }; + + // ---- RenderTarget ------------------------------------------------- + class GLRenderTarget : public RenderTarget + { + public: + GLRenderTarget(int w, int h, Format color, bool depth, Filter filter) + : m_w(w), m_h(h) + { + m_color = create_scope<GLTexture>(w, h, color, filter, nullptr); + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color->m_id, 0); + if (depth) + { + glGenRenderbuffers(1, &m_depth); + glBindRenderbuffer(GL_RENDERBUFFER, m_depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depth); + } + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + DONUT_ERROR("GL RHI: render target incomplete"); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + ~GLRenderTarget() override + { + if (m_depth) glDeleteRenderbuffers(1, &m_depth); + if (m_fbo) glDeleteFramebuffers(1, &m_fbo); + } + auto width() const -> int override { return m_w; } + auto height() const -> int override { return m_h; } + auto color_texture() -> Texture* override { return m_color.get(); } + GLuint m_fbo = 0, m_depth = 0; int m_w, m_h; Scope<GLTexture> m_color; + }; + + // ---- Pipeline ----------------------------------------------------- + class GLPipeline : public Pipeline + { + public: + explicit GLPipeline(const PipelineDesc& d) : m_desc(d) + { + std::string path = "assets/shaders/generated/" + d.shader + ".glsl"; + m_shader = Ref<Shader>(Shader::create(path)); + m_prog = m_shader ? m_shader->get_renderer_id() : 0; + if (!m_prog) { DONUT_ERROR("GL RHI: shader '{}' failed", d.shader); return; } + + glUseProgram(m_prog); + for (const auto& r : d.resources) + { + if (r.kind == ResourceKind::UniformBuffer) + { + GLuint bi = glGetUniformBlockIndex(m_prog, r.name.c_str()); + if (bi != GL_INVALID_INDEX) glUniformBlockBinding(m_prog, bi, r.binding); + } + else // Texture: point the sampler at texture unit == binding + { + GLint loc = glGetUniformLocation(m_prog, r.name.c_str()); + if (loc >= 0) glUniform1i(loc, (GLint)r.binding); + } + } + glUseProgram(0); + } + PipelineDesc m_desc; Ref<Shader> m_shader; GLuint m_prog = 0; + }; + + // ---- CommandList (immediate) -------------------------------------- + class GLCommandList : public CommandList + { + public: + explicit GLCommandList(GLuint vao) : m_vao(vao) {} + + auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override + { + auto* rt = static_cast<GLRenderTarget*>(target); + glBindFramebuffer(GL_FRAMEBUFFER, rt ? rt->m_fbo : 0); + int w = rt ? rt->m_w : m_default_w, h = rt ? rt->m_h : m_default_h; + glViewport(0, 0, w, h); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glClearColor(clear.r, clear.g, clear.b, clear.a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + auto end_render_pass() -> void override {} + + auto bind_pipeline(Pipeline* p) -> void override + { + m_pipe = static_cast<GLPipeline*>(p); + glUseProgram(m_pipe->m_prog); + const auto& d = m_pipe->m_desc; + if (d.depth_test) { glEnable(GL_DEPTH_TEST); glDepthFunc(gl_compare(d.depth_op)); } + else glDisable(GL_DEPTH_TEST); + glDepthMask(d.depth_write ? GL_TRUE : GL_FALSE); + if (d.cull == CullMode::None) glDisable(GL_CULL_FACE); + else { glEnable(GL_CULL_FACE); glCullFace(d.cull == CullMode::Back ? GL_BACK : GL_FRONT); } + if (d.blend == BlendMode::AlphaBlend) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } + else glDisable(GL_BLEND); + } + auto set_viewport(int x, int y, int w, int h, bool /*flip_y*/) -> void override { glViewport(x, y, w, h); } + auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override + { + glBindBufferBase(GL_UNIFORM_BUFFER, binding, static_cast<GLBuffer*>(ubo)->m_id); + } + auto bind_texture(uint32_t binding, Texture* tex) -> void override + { + auto* t = static_cast<GLTexture*>(tex); + glActiveTexture(GL_TEXTURE0 + binding); + glBindTexture(t->m_target, t->m_id); + } + auto bind_vertex_buffer(Buffer* vb) -> void override + { + glBindBuffer(GL_ARRAY_BUFFER, static_cast<GLBuffer*>(vb)->m_id); + const auto& layout = m_pipe->m_desc.vertex_layout; + for (const auto& a : layout.attributes) + { + glEnableVertexAttribArray(a.location); + glVertexAttribPointer(a.location, (GLint)a.components, GL_FLOAT, GL_FALSE, + (GLsizei)layout.stride, (const void*)(uintptr_t)a.offset); + } + } + auto bind_index_buffer(Buffer* ib) -> void override + { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLBuffer*>(ib)->m_id); + } + auto draw(uint32_t count) -> void override + { + glDrawArrays(gl_topology(m_pipe->m_desc.topology), 0, (GLsizei)count); + } + auto draw_indexed(uint32_t count) -> void override + { + glDrawElements(gl_topology(m_pipe->m_desc.topology), (GLsizei)count, GL_UNSIGNED_INT, nullptr); + } + + GLuint m_vao; GLPipeline* m_pipe = nullptr; int m_default_w = 1, m_default_h = 1; + }; + + // ---- Device ------------------------------------------------------- + class GLDevice : public Device + { + public: + auto init(void* window, int width, int height) -> bool override + { + m_window = window; + glfwMakeContextCurrent(static_cast<GLFWwindow*>(window)); + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { DONUT_ERROR("GL RHI: GLAD load failed"); return false; } + m_width = width; m_height = height; + const char* name = (const char*)glGetString(GL_RENDERER); + m_name = name ? name : "OpenGL"; + glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); // GL core needs one bound VAO + m_cmds = create_scope<GLCommandList>(m_vao); + DONUT_INFO("GL RHI device: {} ({}x{})", m_name, width, height); + return true; + } + auto shutdown() -> void override + { + if (m_imgui) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } + if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } + } + auto resize(int w, int h) -> void override { m_width = w; m_height = h; } + auto wait_idle() -> void override { glFinish(); } + + auto create_buffer(BufferType t, size_t size, const void* data) -> Ref<Buffer> override + { return create_ref<GLBuffer>(t, size, data); } + auto create_texture(int w, int h, Format f, Filter fl, const void* data) -> Ref<Texture> override + { return create_ref<GLTexture>(w, h, f, fl, data); } + auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override + { + HDRIManager::get().set_current_hdri(path); + return create_ref<GLTexture>(HDRIManager::get().get_current_hdri()); + } + auto create_render_target(int w, int h, Format color, bool depth, Filter fl, int /*mips*/) -> Ref<RenderTarget> override + { return create_ref<GLRenderTarget>(w, h, color, depth, fl); } + auto create_pipeline(const PipelineDesc& d) -> Ref<Pipeline> override + { return create_ref<GLPipeline>(d); } + + auto begin_frame(const glm::vec4& /*clear*/) -> CommandList* override + { + glfwGetFramebufferSize(static_cast<GLFWwindow*>(m_window), &m_width, &m_height); + m_cmds->m_default_w = m_width; m_cmds->m_default_h = m_height; + glBindVertexArray(m_vao); + return m_cmds.get(); + } + auto end_frame() -> void override { glfwSwapBuffers(static_cast<GLFWwindow*>(m_window)); } + + auto init_imgui() -> void override + { + IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui_ImplGlfw_InitForOpenGL(static_cast<GLFWwindow*>(m_window), true); + ImGui_ImplOpenGL3_Init("#version 410"); + m_imgui = true; + DONUT_INFO("GL RHI: ImGui backend initialized"); + } + auto imgui_new_frame() -> void override + { + ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + } + auto imgui_render(CommandList&) -> void override + { + ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + auto device_name() const -> const std::string& override { return m_name; } + + private: + void* m_window = nullptr; std::string m_name; GLuint m_vao = 0; + int m_width = 0, m_height = 0; bool m_imgui = false; + Scope<GLCommandList> m_cmds; + }; + } + + auto create_opengl_device() -> Scope<Device> { return create_scope<GLDevice>(); } +} diff --git a/src/platform/opengl/opengl_device.h b/src/platform/opengl/opengl_device.h new file mode 100644 index 0000000..4004396 --- /dev/null +++ b/src/platform/opengl/opengl_device.h @@ -0,0 +1,11 @@ +#pragma once + +#include "rendering/rhi.h" + +namespace Donut::RHI +{ + // OpenGL implementation of the RHI Device: emulates the explicit model with + // immediate-mode GL 4.1 (one global VAO, glBindBufferBase for UBOs, FBOs for + // render targets, per-draw attribute setup, bindings assigned by name). + auto create_opengl_device() -> Scope<Device>; +} diff --git a/src/platform/opengl/opengl_renderer.cpp b/src/platform/opengl/opengl_renderer.cpp deleted file mode 100644 index ae12f10..0000000 --- a/src/platform/opengl/opengl_renderer.cpp +++ /dev/null @@ -1,377 +0,0 @@ -#include "opengl_renderer.h" - -#include "engine/engine.h" -#include "core/log.h" -#include "core/camera.h" -#include "core/hdri_manager.h" -#include "rendering/renderer.h" // Renderer::init / RenderCommand - -#include <glad/glad.h> -#include <GLFW/glfw3.h> -#include <imgui.h> -#include <imgui_impl_glfw.h> -#include <imgui_impl_opengl3.h> -#include <glm/glm.hpp> -#include <glm/gtc/matrix_transform.hpp> -#include <cmath> -#include <vector> - -#include "core/hdri_manager.h" -#include "rendering/texture.h" // CubemapTexture::bind - -namespace Donut -{ - // Scroll is accumulated through a chained GLFW callback (keeps ImGui's scroll - // handling intact), consumed once per frame for orbital zoom. - static double g_scroll_accum = 0.0; - static GLFWscrollfun g_prev_scroll = nullptr; - static void gl_scroll_cb(GLFWwindow* w, double x, double y) - { - if (g_prev_scroll) g_prev_scroll(w, x, y); - g_scroll_accum += y; - } - - OpenGLRenderer::OpenGLRenderer() = default; - OpenGLRenderer::~OpenGLRenderer() = default; - - auto OpenGLRenderer::init(void* glfwWindow, int width, int height) -> bool - { - m_window = glfwWindow; - auto* win = static_cast<GLFWwindow*>(glfwWindow); - glfwMakeContextCurrent(win); - - Renderer::init(); // GLAD load + default GL state - RenderCommand::set_face_culling(false); - - const GLubyte* name = glGetString(GL_RENDERER); - m_device_name = name ? reinterpret_cast<const char*>(name) : "OpenGL"; - - m_engine = create_scope<Engine>(); - m_engine->set_window_dimensions(width, height); -#ifdef __APPLE__ - m_engine->set_compute_height(256); // capped for the macOS watchdog (tiled fragment pass) -#else - m_engine->set_compute_height(540); -#endif - m_engine->update_compute_dimensions(); - - // Match the Vulkan framing exactly (camera sits outside the disk). - Camera& cam = m_engine->get_camera(); - cam.set_camera_mode(CameraMode::Orbital); - cam.set_orbital_target(glm::vec3(0.0f)); - cam.set_orbital_radius(4e11); - cam.set_orbital_limits(2.2e11, 1.5e12); - cam.set_orbital_speed(0.01f); - cam.set_zoom_speed(3e10); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - - m_hdri_path = "assets/hdri/HDR_blue_nebulae-1.hdr"; - HDRIManager::get().set_current_hdri(m_hdri_path); - - create_scene_resources(); - - DONUT_INFO("OpenGL renderer ready: {} ({}x{})", m_device_name, width, height); - return true; - } - - auto OpenGLRenderer::init_ui() -> bool - { - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::StyleColorsDark(); - ImGui_ImplGlfw_InitForOpenGL(static_cast<GLFWwindow*>(m_window), true); - ImGui_ImplOpenGL3_Init("#version 410"); - g_prev_scroll = glfwSetScrollCallback(static_cast<GLFWwindow*>(m_window), gl_scroll_cb); - m_ui_ready = true; - DONUT_INFO("OpenGL: ImGui backend initialized"); - return true; - } - - auto OpenGLRenderer::shutdown() -> void - { - if (m_ui_ready) - { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - m_ui_ready = false; - } - m_engine.reset(); - } - - auto OpenGLRenderer::resize(int width, int height) -> void - { - if (m_engine) m_engine->set_window_dimensions(width, height); - } - - auto OpenGLRenderer::process_input() -> void - { - auto* win = static_cast<GLFWwindow*>(m_window); - Camera& cam = m_scene_mode ? m_scene_camera : m_engine->get_camera(); - bool over_ui = m_ui_ready && ImGui::GetIO().WantCaptureMouse; - - double now = glfwGetTime(); - float dt = m_last_frame_time > 0.0 ? static_cast<float>(now - m_last_frame_time) : 0.0f; - m_last_frame_time = now; - - double mx = 0, my = 0; glfwGetCursorPos(win, &mx, &my); - bool left_down = glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; - - if (!m_scene_mode && cam.get_camera_mode() == CameraMode::FPS) - { - if (left_down && !m_left_was_down) { m_last_x = mx; m_last_y = my; } - if (left_down && !over_ui) - cam.on_mouse_move(static_cast<float>(mx - m_last_x), static_cast<float>(m_last_y - my)); - m_last_x = mx; m_last_y = my; m_left_was_down = left_down; - - bool over_kb = m_ui_ready && ImGui::GetIO().WantCaptureKeyboard; - if (!over_kb && dt > 0.0f) - { - float mdt = dt * (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); - if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) cam.move_forward(mdt); - if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) cam.move_backward(mdt); - if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) cam.move_left(mdt); - if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) cam.move_right(mdt); - if (glfwGetKey(win, GLFW_KEY_E) == GLFW_PRESS) cam.move_up(mdt); - if (glfwGetKey(win, GLFW_KEY_Q) == GLFW_PRESS) cam.move_down(mdt); - } - g_scroll_accum = 0.0; // scroll unused in free-fly - } - else - { - 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) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); - m_left_was_down = left_down; - - cam.process_orbital_mouse_move(mx, my); - double scroll = g_scroll_accum; g_scroll_accum = 0.0; - if (scroll != 0.0 && !over_ui) cam.process_orbital_scroll(0.0, scroll); - } - } - - // 36-vertex unit cube for the skybox (positions only; the shader forces depth=1). - static const float SKYBOX_CUBE[] = { - -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, - }; - - auto OpenGLRenderer::create_scene_resources() -> void - { - const float PI = 3.14159265358979f; - - m_grid_shader = Ref<Shader>(Shader::create("assets/shaders/Grid.glsl")); - m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl")); - m_skybox_shader = Ref<Shader>(Shader::create("assets/shaders/Skybox.glsl")); - - 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(PI / 3.0f); - - // Grid: +/-50 lines on the XZ plane (Grid shader scales by u_GridSize/50). - std::vector<glm::vec3> lines; - for (int i = -50; i <= 50; ++i) - { - lines.push_back({ (float)i, 0.0f, -50.0f }); lines.push_back({ (float)i, 0.0f, 50.0f }); - lines.push_back({ -50.0f, 0.0f, (float)i }); lines.push_back({ 50.0f, 0.0f, (float)i }); - } - m_grid_vertex_count = (int)lines.size(); - glGenVertexArrays(1, &m_grid_vao); glGenBuffers(1, &m_grid_vbo); - glBindVertexArray(m_grid_vao); glBindBuffer(GL_ARRAY_BUFFER, m_grid_vbo); - glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(glm::vec3), lines.data(), GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); - - // Sphere: indexed UV sphere (position + normal), unit radius. - const int RINGS = 24, SECT = 48; - std::vector<float> sv; std::vector<unsigned int> si; - for (int r = 0; r <= RINGS; ++r) - { - float th = PI * r / RINGS; - for (int s = 0; s <= SECT; ++s) - { - float ph = 2.0f * PI * s / SECT; - glm::vec3 p(sinf(th) * cosf(ph), cosf(th), sinf(th) * sinf(ph)); - sv.insert(sv.end(), { p.x, p.y, p.z, p.x, p.y, p.z }); // pos, normal (unit sphere) - } - } - for (int r = 0; r < RINGS; ++r) - for (int s = 0; s < SECT; ++s) - { - int a = r * (SECT + 1) + s, b = a + SECT + 1; - si.insert(si.end(), { (unsigned)a, (unsigned)b, (unsigned)(a + 1), - (unsigned)(a + 1), (unsigned)b, (unsigned)(b + 1) }); - } - m_sphere_index_count = (int)si.size(); - glGenVertexArrays(1, &m_sphere_vao); glGenBuffers(1, &m_sphere_vbo); glGenBuffers(1, &m_sphere_ebo); - glBindVertexArray(m_sphere_vao); - glBindBuffer(GL_ARRAY_BUFFER, m_sphere_vbo); - glBufferData(GL_ARRAY_BUFFER, sv.size() * sizeof(float), sv.data(), GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphere_ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, si.size() * sizeof(unsigned), si.data(), GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - - // Skybox cube. - glGenVertexArrays(1, &m_skybox_vao); glGenBuffers(1, &m_skybox_vbo); - glBindVertexArray(m_skybox_vao); glBindBuffer(GL_ARRAY_BUFFER, m_skybox_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(SKYBOX_CUBE), SKYBOX_CUBE, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - - glBindVertexArray(0); - m_scene_ready = true; - } - - auto OpenGLRenderer::render_scene(int w, int h) -> void - { - if (!m_grid_shader || !m_sphere_shader || !m_skybox_shader) return; - glViewport(0, 0, w, h); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - float aspect = h > 0 ? (float)w / (float)h : 1.0f; - m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - glm::mat4 proj = m_scene_camera.get_projection_matrix(); - glm::mat4 view = m_scene_camera.get_view_matrix(); - glm::mat4 vp = proj * view; - glm::vec3 cam = m_scene_camera.get_orbital_position(); - - // Skybox behind everything (depth test on, write off; shader forces depth=1). - glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(GL_FALSE); - m_skybox_shader->bind(); - m_skybox_shader->set_mat4("u_Projection", proj); - m_skybox_shader->set_mat4("u_View", glm::mat4(glm::mat3(view))); // strip translation - auto hdri = HDRIManager::get().get_current_hdri(); - if (hdri) { hdri->bind(0); m_skybox_shader->set_int("u_Skybox", 0); } - glBindVertexArray(m_skybox_vao); glDrawArrays(GL_TRIANGLES, 0, 36); - - // Lit spheres. - glDepthFunc(GL_LESS); glDepthMask(GL_TRUE); - m_sphere_shader->bind(); glBindVertexArray(m_sphere_vao); - for (int i = 0; i < (int)m_scene_objects.size(); ++i) - { - const SceneObject& o = m_scene_objects[i]; - m_sphere_shader->set_mat4("u_ViewProjection", vp); - m_sphere_shader->set_mat4("u_Transform", - glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius))); - m_sphere_shader->set_float3("u_Color", o.color); - m_sphere_shader->set_float("u_Specular", 0.6f); - m_sphere_shader->set_float("u_Emission", 0.0f); - m_sphere_shader->set_float3("u_LightPos", glm::vec3(10.0f, 20.0f, 10.0f)); - m_sphere_shader->set_float3("u_CameraPos", cam); - m_sphere_shader->set_int("u_IsSelected", i == m_selected_object ? 1 : 0); - m_sphere_shader->set_float3("u_OutlineColor", glm::vec3(1.0f, 1.0f, 0.0f)); - m_sphere_shader->set_float("u_OutlineWidth", 0.15f); - glDrawElements(GL_TRIANGLES, m_sphere_index_count, GL_UNSIGNED_INT, 0); - } - - // Transparent grid on top (test, no depth write). - glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - m_grid_shader->bind(); - m_grid_shader->set_mat4("u_ViewProjection", vp); - m_grid_shader->set_mat4("u_Transform", glm::mat4(1.0f)); - m_grid_shader->set_float("u_GridSize", 50.0f); - m_grid_shader->set_float3("u_GridColor", glm::vec3(0.55f, 0.55f, 0.6f)); - m_grid_shader->set_float("u_GridAlpha", 0.75f); - m_grid_shader->set_float3("u_CameraPos", cam); - glBindVertexArray(m_grid_vao); glDrawArrays(GL_LINES, 0, m_grid_vertex_count); - glDisable(GL_BLEND); glDepthMask(GL_TRUE); glBindVertexArray(0); - } - - auto OpenGLRenderer::draw_frame(const glm::vec4& clear_color, - const std::function<void()>& build_ui) -> void - { - auto* win = static_cast<GLFWwindow*>(m_window); - process_input(); - - int w = 0, h = 0; glfwGetFramebufferSize(win, &w, &h); - - if (m_scene_mode) - { - render_scene(w, h); - } - else - { - m_engine->set_window_dimensions(w, h); - // Geodesic pass -> low-res texture (watchdog-tiled on macOS), then - // upscale to the window. Disk/camera come from the shared BlackHoleParams. - m_engine->dispatch_compute(m_engine->get_camera()); - RenderCommand::set_viewport(0, 0, w, h); - RenderCommand::set_clear_color(clear_color); - RenderCommand::clear(); - m_engine->draw_full_screen_quad(); - } - - if (m_ui_ready) - { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - if (build_ui) build_ui(); - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - } - glfwSwapBuffers(win); - } - - auto OpenGLRenderer::set_scene_mode(bool enabled) -> void { m_scene_mode = enabled; } - auto OpenGLRenderer::is_scene_mode() const -> bool { return m_scene_mode; } - - auto OpenGLRenderer::set_free_fly(bool enabled) -> void - { - Camera& cam = m_engine->get_camera(); - if (enabled && cam.get_camera_mode() != CameraMode::FPS) - { - glm::vec3 pos = cam.get_orbital_position(); - glm::vec3 fwd = glm::normalize(cam.get_orbital_target() - pos); - float pitch = glm::degrees(asinf(glm::clamp(fwd.y, -1.0f, 1.0f))); - float yaw = glm::degrees(atan2f(fwd.z, fwd.x)); - cam.set_position(pos); - cam.set_rotation(glm::vec3(pitch, yaw, 0.0f)); - cam.set_movement_speed(2.0e10f); - cam.set_mouse_sensitivity(0.15f); - cam.set_camera_mode(CameraMode::FPS); - } - else if (!enabled) - { - cam.set_camera_mode(CameraMode::Orbital); - } - } - - auto OpenGLRenderer::is_free_fly() const -> bool - { - return m_engine->get_camera().get_camera_mode() == CameraMode::FPS; - } - - auto OpenGLRenderer::reset_camera() -> void - { - Camera& cam = m_engine->get_camera(); - cam.set_camera_mode(CameraMode::Orbital); - cam.set_orbital_radius(4e11); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - } - - auto OpenGLRenderer::set_hdri(const std::string& path) -> void - { - HDRIManager::get().set_current_hdri(path); // the Engine re-reads it each frame - m_hdri_path = path; - } - - auto OpenGLRenderer::current_hdri() const -> const std::string& { return m_hdri_path; } - auto OpenGLRenderer::scene_objects() -> std::vector<SceneObject>& { return m_scene_objects; } - auto OpenGLRenderer::set_selected_object(int index) -> void { m_selected_object = index; } - auto OpenGLRenderer::black_hole_params() -> BlackHoleParams& { return m_engine->black_hole_params(); } - auto OpenGLRenderer::device_name() const -> const std::string& { return m_device_name; } -} diff --git a/src/platform/opengl/opengl_renderer.h b/src/platform/opengl/opengl_renderer.h deleted file mode 100644 index 4258f40..0000000 --- a/src/platform/opengl/opengl_renderer.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "rendering/renderer_backend.h" -#include "core/memory.h" -#include "core/camera.h" -#include "rendering/shader.h" - -// OpenGL implementation of the shared RendererBackend interface. It renders the -// SAME cross-compiled Geodesic shader the Vulkan backend does (driven by the -// same BlackHoleParams + camera), so the application behaves identically on -// either backend. Wraps the Engine, which owns the GL geodesic/present passes -// and the macOS watchdog-safe tiled rendering. -namespace Donut -{ - class Engine; - - class OpenGLRenderer : public RendererBackend - { - public: - OpenGLRenderer(); - ~OpenGLRenderer() override; - - auto init(void* glfwWindow, int width, int height) -> bool override; - auto init_ui() -> bool override; - auto shutdown() -> void override; - auto resize(int width, int height) -> void override; - auto draw_frame(const glm::vec4& clear_color, - const std::function<void()>& build_ui) -> void override; - - auto set_scene_mode(bool enabled) -> void override; - auto is_scene_mode() const -> bool override; - auto set_free_fly(bool enabled) -> void override; - auto is_free_fly() const -> bool override; - auto reset_camera() -> void override; - - auto set_hdri(const std::string& path) -> void override; - auto current_hdri() const -> const std::string& override; - auto scene_objects() -> std::vector<SceneObject>& override; - auto set_selected_object(int index) -> void override; - auto black_hole_params() -> BlackHoleParams& override; - - auto device_name() const -> const std::string& override; - - private: - auto process_input() -> void; - auto create_scene_resources() -> void; // grid + sphere + skybox meshes/shaders - auto render_scene(int width, int height) -> void; - - void* m_window = nullptr; - Scope<Engine> m_engine; - - // World-builder scene view (normal-scale): mirrors the Vulkan scene view - // with the same shared Grid/Sphere/Skybox shaders. - Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; - Ref<Shader> m_grid_shader, m_sphere_shader, m_skybox_shader; - unsigned int m_grid_vao = 0, m_grid_vbo = 0; - unsigned int m_sphere_vao = 0, m_sphere_vbo = 0, m_sphere_ebo = 0; - unsigned int m_skybox_vao = 0, m_skybox_vbo = 0; - int m_grid_vertex_count = 0, m_sphere_index_count = 0; - bool m_scene_ready = false; - std::vector<SceneObject> m_scene_objects{ SceneObject{} }; - int m_selected_object = 0; - std::string m_hdri_path; - std::string m_device_name; - bool m_scene_mode = false; - bool m_ui_ready = false; - - double m_last_x = 0.0, m_last_y = 0.0; - bool m_left_was_down = false; - double m_last_frame_time = 0.0; - }; -} diff --git a/src/platform/vulkan/vulkan_context.cpp b/src/platform/vulkan/vulkan_context.cpp deleted file mode 100644 index ece7700..0000000 --- a/src/platform/vulkan/vulkan_context.cpp +++ /dev/null @@ -1,789 +0,0 @@ -#include "vulkan_context.h" -#include "core/log.h" - -#include <vulkan/vulkan.h> - -#include <glm/glm.hpp> -#include <glm/gtc/type_ptr.hpp> -#include "stb_image_write.h" - -#include <vector> -#include <cstring> -#include <cstdlib> -#include <fstream> - -namespace Donut -{ - // Logs and returns false from the enclosing function on any non-success result. - #define VK_CHECK(expr) \ - do { \ - VkResult _r = (expr); \ - if (_r != VK_SUCCESS) { \ - DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ - return false; \ - } \ - } while (0) - - struct VulkanContext::Impl - { - VkInstance instance = VK_NULL_HANDLE; - VkPhysicalDevice physical = VK_NULL_HANDLE; - VkDevice device = VK_NULL_HANDLE; - VkQueue graphics_queue = VK_NULL_HANDLE; - uint32_t graphics_family = 0; - VkPhysicalDeviceMemoryProperties mem_props{}; - - auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t - { - for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) - if ((type_filter & (1u << i)) && - (mem_props.memoryTypes[i].propertyFlags & flags) == flags) - return i; - return UINT32_MAX; - } - - bool create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, - VkBuffer& buf, VkDeviceMemory& mem) const - { - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; - VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(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(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; - vkBindBufferMemory(device, buf, mem, 0); - return true; - } - }; - - VulkanContext::VulkanContext() { m_impl = new Impl(); } - VulkanContext::~VulkanContext() { shutdown(); delete m_impl; m_impl = nullptr; } - - auto VulkanContext::init() -> bool - { - Impl& v = *m_impl; - -#ifdef __APPLE__ - // The Homebrew Vulkan loader does not auto-discover MoltenVK or the - // validation layers; point it at both unless already configured. - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); -#endif - - // Instance - VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; - app.pApplicationName = "Donut"; - app.apiVersion = VK_API_VERSION_1_2; - - // MoltenVK is a portability driver: without the portability-enumeration - // extension + flag, vkEnumeratePhysicalDevices returns zero devices. - std::vector<const char*> exts = { - VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, - VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, - }; - - // Enable validation layers when they are installed (optional). - 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(); - VK_CHECK(vkCreateInstance(&ici, nullptr, &v.instance)); - - // Physical device - uint32_t device_count = 0; - vkEnumeratePhysicalDevices(v.instance, &device_count, nullptr); - if (device_count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } - std::vector<VkPhysicalDevice> devices(device_count); - vkEnumeratePhysicalDevices(v.instance, &device_count, devices.data()); - v.physical = devices[0]; - - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(v.physical, &props); - vkGetPhysicalDeviceMemoryProperties(v.physical, &v.mem_props); - - // Graphics queue family - uint32_t q_count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, nullptr); - std::vector<VkQueueFamilyProperties> qfams(q_count); - vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, qfams.data()); - bool found = false; - for (uint32_t i = 0; i < q_count; ++i) - if (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { v.graphics_family = i; found = true; break; } - if (!found) { DONUT_ERROR("Vulkan: no graphics queue family"); return false; } - - // Logical device - // MoltenVK requires VK_KHR_portability_subset to be enabled if present. - std::vector<const char*> dev_exts; - uint32_t dev_ext_count = 0; - vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, nullptr); - std::vector<VkExtensionProperties> dev_ext_props(dev_ext_count); - vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, dev_ext_props.data()); - for (const auto& e : dev_ext_props) - if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) - dev_exts.push_back("VK_KHR_portability_subset"); - - float priority = 1.0f; - VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; - qci.queueFamilyIndex = v.graphics_family; - qci.queueCount = 1; - qci.pQueuePriorities = &priority; - - VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; - dci.queueCreateInfoCount = 1; - dci.pQueueCreateInfos = &qci; - dci.enabledExtensionCount = (uint32_t)dev_exts.size(); - dci.ppEnabledExtensionNames = dev_exts.data(); - VK_CHECK(vkCreateDevice(v.physical, &dci, nullptr, &v.device)); - vkGetDeviceQueue(v.device, v.graphics_family, 0, &v.graphics_queue); - - DONUT_INFO("Vulkan device: {} (API {}.{}.{}, validation {})", - props.deviceName, - VK_API_VERSION_MAJOR(props.apiVersion), - VK_API_VERSION_MINOR(props.apiVersion), - VK_API_VERSION_PATCH(props.apiVersion), - layers.empty() ? "off" : "on"); - return true; - } - - auto VulkanContext::self_test_clear() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 64, H = 64; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - - // Offscreen colour image - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; - ici.format = fmt; - ici.extent = { W, 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_TRANSFER_SRC_BIT; - ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - - VkMemoryRequirements im_req{}; - vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; - vci.viewType = VK_IMAGE_VIEW_TYPE_2D; - vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - // Render pass (clear -> store, leave in TRANSFER_SRC for readback) - VkAttachmentDescription color{}; - color.format = 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_TRANSFER_SRC_OPTIMAL; - - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color_ref; - - // Ensure colour writes finish before the read-back copy. - 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_TRANSFER_BIT; - dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - - VkRenderPass render_pass = VK_NULL_HANDLE; - 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; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; - fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Host-visible staging buffer for read-back - VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = (VkDeviceSize)W * H * 4; - bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); - VkMemoryRequirements b_req{}; - vkGetBufferMemoryRequirements(v.device, staging, &b_req); - VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - b_alloc.allocationSize = b_req.size; - b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); - VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); - - // Command buffer: clear via render pass, then copy image -> buffer - VkCommandPool pool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); - - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - VkClearValue clear{}; - clear.color = { { 0.2f, 0.4f, 0.8f, 1.0f } }; // -> RGBA8 (51, 102, 204, 255) - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; - rpbi.renderArea = { { 0, 0 }, { W, H } }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdEndRenderPass(cmd); - - VkBufferImageCopy region{}; - region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - // Read back + verify - void* mapped = nullptr; - VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); - const uint8_t* px = (const uint8_t*)mapped; - DONUT_INFO("Vulkan clear self-test: pixel RGBA = ({}, {}, {}, {})", - (int)px[0], (int)px[1], (int)px[2], (int)px[3]); - bool ok = px[0] > 45 && px[0] < 60 && px[1] > 95 && px[1] < 110 && - px[2] > 195 && px[2] < 210 && px[3] == 255; - vkUnmapMemory(v.device, staging_mem); - DONUT_INFO("Vulkan clear self-test: {}", ok ? "PASS" : "FAIL"); - - // Cleanup - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, pool, nullptr); - vkDestroyBuffer(v.device, staging, nullptr); - vkFreeMemory(v.device, staging_mem, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); - vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); - vkDestroyImage(v.device, image, nullptr); - vkFreeMemory(v.device, image_mem, nullptr); - return ok; - } - - static std::vector<uint32_t> load_spirv(const std::string& path) - { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return {}; - size_t size = (size_t)f.tellg(); - std::vector<uint32_t> data(size / 4); - f.seekg(0); - f.read((char*)data.data(), (std::streamsize)size); - return data; - } - - auto VulkanContext::self_test_triangle() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 64, H = 64; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - - // Offscreen image + view (as in the clear test) - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, 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_TRANSFER_SRC_BIT; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - // Render pass + framebuffer - VkAttachmentDescription color{}; - color.format = 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_TRANSFER_SRC_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_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_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - VkRenderPass render_pass = VK_NULL_HANDLE; - 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; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Shader modules from Slang SPIR-V - auto vspv = load_spirv("assets/shaders/generated/VkPipelineTest.vertexMain.spv"); - auto fspv = load_spirv("assets/shaders/generated/VkPipelineTest.fragmentMain.spv"); - if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: VkPipelineTest SPIR-V not found"); return false; } - VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; - VkShaderModuleCreateInfo smci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - smci.codeSize = vspv.size() * 4; smci.pCode = vspv.data(); - VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &vmod)); - smci.codeSize = fspv.size() * 4; smci.pCode = fspv.data(); - VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &fmod)); - - // Graphics pipeline - 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"; - - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 }; - VkRect2D scissor{ { 0, 0 }, { W, H } }; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; - vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &scissor; - 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; - - VkPipelineLayout layout = VK_NULL_HANDLE; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); - - VkPipeline pipeline = VK_NULL_HANDLE; - 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 = layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); - - // Readback staging buffer - VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = (VkDeviceSize)W * H * 4; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); - VkMemoryRequirements b_req{}; vkGetBufferMemoryRequirements(v.device, staging, &b_req); - VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - b_alloc.allocationSize = b_req.size; - b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); - VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); - - // Record + submit - VkCommandPool pool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - VkClearValue clear{}; clear.color = { { 0.0f, 0.0f, 0.0f, 1.0f } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; - rpbi.renderArea = { { 0, 0 }, { W, H } }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - vkCmdDraw(cmd, 3, 1, 0, 0); - vkCmdEndRenderPass(cmd); - VkBufferImageCopy region{}; - region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - // Verify: centre pixel should be the mid-gradient (not black) - void* mapped = nullptr; - VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); - const uint8_t* px = (const uint8_t*)mapped; - size_t c = ((size_t)(H / 2) * W + (W / 2)) * 4; - DONUT_INFO("Vulkan triangle self-test: centre pixel RGBA = ({}, {}, {}, {})", - (int)px[c + 0], (int)px[c + 1], (int)px[c + 2], (int)px[c + 3]); - bool ok = (px[c + 0] > 40 || px[c + 1] > 40) && px[c + 3] == 255; - vkUnmapMemory(v.device, staging_mem); - DONUT_INFO("Vulkan triangle self-test: {}", ok ? "PASS" : "FAIL"); - - // Cleanup - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, pool, nullptr); - vkDestroyBuffer(v.device, staging, nullptr); - vkFreeMemory(v.device, staging_mem, nullptr); - vkDestroyPipeline(v.device, pipeline, nullptr); - vkDestroyPipelineLayout(v.device, layout, nullptr); - vkDestroyShaderModule(v.device, vmod, nullptr); - vkDestroyShaderModule(v.device, fmod, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); - vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); - vkDestroyImage(v.device, image, nullptr); - vkFreeMemory(v.device, image_mem, nullptr); - return ok; - } - - auto VulkanContext::render_geodesic(const char* png_path) -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 384, H = 216; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - const float SagA_rs = 1.269e10f; - - // Offscreen colour target - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, 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_TRANSFER_SRC_BIT; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - VkAttachmentDescription color{}; - color.format = 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_TRANSFER_SRC_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_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_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - VkRenderPass render_pass = VK_NULL_HANDLE; - 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; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Uniform buffers (host-visible), filled to match the shader's std140 layout - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - VkBuffer cam_buf, disk_buf, obj_buf, sim_buf; - VkDeviceMemory cam_mem, disk_mem, obj_mem, sim_mem; - v.create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); - v.create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); - v.create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); - v.create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); - - struct CamUBO { - glm::vec3 pos; float p0; glm::vec3 right; float p1; - glm::vec3 up; float p2; glm::vec3 fwd; float p3; - float tan_half_fov; float aspect; uint32_t moving; int p4; - } cam{}; - glm::vec3 cam_pos(1e11f, 0.32e11f, 0.0f); - glm::vec3 fwd = glm::normalize(glm::vec3(0.0f) - cam_pos); - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - glm::vec3 up = glm::cross(right, fwd); - cam.pos = cam_pos; cam.right = right; cam.up = up; cam.fwd = fwd; - cam.tan_half_fov = 0.57735f; cam.aspect = (float)W / (float)H; cam.moving = 0; - void* p = nullptr; - vkMapMemory(v.device, cam_mem, 0, 128, 0, &p); memcpy(p, &cam, sizeof(cam)); vkUnmapMemory(v.device, cam_mem); - - float disk[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 }; - vkMapMemory(v.device, disk_mem, 0, 32, 0, &p); memcpy(p, disk, sizeof(disk)); vkUnmapMemory(v.device, disk_mem); - - std::vector<uint8_t> obj_data(800, 0); - int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); - float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); - float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); - vkMapMemory(v.device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(v.device, obj_mem); - - struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim{ 6000, 6000, 5e12f, 0.0f }; - vkMapMemory(v.device, sim_mem, 0, 16, 0, &p); memcpy(p, &sim, sizeof(sim)); vkUnmapMemory(v.device, sim_mem); - - // Dark cubemap (stands in for the HDRI for now) - VkImage cube = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; - VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; - cci.imageType = VK_IMAGE_TYPE_2D; cci.format = fmt; cci.extent = { 1, 1, 1 }; - cci.mipLevels = 1; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; - cci.tiling = VK_IMAGE_TILING_OPTIMAL; - cci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - VK_CHECK(vkCreateImage(v.device, &cci, nullptr, &cube)); - VkMemoryRequirements cube_req{}; vkGetImageMemoryRequirements(v.device, cube, &cube_req); - VkMemoryAllocateInfo cube_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - cube_alloc.allocationSize = cube_req.size; - cube_alloc.memoryTypeIndex = v.find_memory_type(cube_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &cube_alloc, nullptr, &cube_mem)); - VK_CHECK(vkBindImageMemory(v.device, cube, cube_mem, 0)); - VkImageView cube_view = VK_NULL_HANDLE; - VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - cvci.image = cube; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = fmt; - cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - VK_CHECK(vkCreateImageView(v.device, &cvci, nullptr, &cube_view)); - VkSampler sampler = VK_NULL_HANDLE; - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(v.device, &smci, nullptr, &sampler)); - - VkBuffer cube_staging; VkDeviceMemory cube_staging_mem; - v.create_buffer(6 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, cube_staging, cube_staging_mem); - uint8_t cube_pixels[6 * 4]; - for (int i = 0; i < 6; ++i) { cube_pixels[i * 4 + 0] = 6; cube_pixels[i * 4 + 1] = 6; cube_pixels[i * 4 + 2] = 14; cube_pixels[i * 4 + 3] = 255; } - vkMapMemory(v.device, cube_staging_mem, 0, 24, 0, &p); memcpy(p, cube_pixels, 24); vkUnmapMemory(v.device, cube_staging_mem); - - // Descriptor set: 4 UBOs (bindings 0-3) + cubemap sampler (binding 4) - VkDescriptorSetLayoutBinding binds[5]{}; - for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } - binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayout set_layout = VK_NULL_HANDLE; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 5; dslci.pBindings = binds; - VK_CHECK(vkCreateDescriptorSetLayout(v.device, &dslci, nullptr, &set_layout)); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPool pool = VK_NULL_HANDLE; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &pool)); - VkDescriptorSet set = VK_NULL_HANDLE; - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; - VK_CHECK(vkAllocateDescriptorSets(v.device, &dsai, &set)); - - // load geodesic SPIR-V + build the pipeline - auto vspv = load_spirv("assets/shaders/generated/Geodesic.vertexMain.spv"); - auto fspv = load_spirv("assets/shaders/generated/Geodesic.fragmentMain.spv"); - if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: geodesic SPIR-V not found"); return false; } - VkShaderModule vmod, fmod; - VkShaderModuleCreateInfo smci2{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - smci2.codeSize = vspv.size() * 4; smci2.pCode = vspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &vmod)); - smci2.codeSize = fspv.size() * 4; smci2.pCode = fspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &fmod)); - - VkPipelineLayout layout = VK_NULL_HANDLE; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; - VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); - - 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; 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)W, (float)H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { W, H } }; - 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 = VK_NULL_HANDLE; - 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 = layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); - - // Fullscreen quad (position.xy, texcoord.uv) - float quad[] = { - -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, - -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, - }; - VkBuffer vbuf; VkDeviceMemory vbuf_mem; - v.create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, vbuf, vbuf_mem); - vkMapMemory(v.device, vbuf_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(v.device, vbuf_mem); - - // Write the descriptor set - VkDescriptorBufferInfo bi[4] = { - { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; - VkDescriptorImageInfo ii{ sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet writes[5]{}; - for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } - writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = ⅈ - vkUpdateDescriptorSets(v.device, 5, writes, 0, nullptr); - - VkBuffer readback; VkDeviceMemory readback_mem; - v.create_buffer((VkDeviceSize)W * H * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, host_vis, readback, readback_mem); - - // Record + submit - VkCommandPool cpool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &cpool)); - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = cpool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - 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 = cube; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - 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 cube_copy{}; cube_copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 6 }; cube_copy.imageExtent = { 1, 1, 1 }; - vkCmdCopyBufferToImage(cmd, cube_staging, cube, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cube_copy); - 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 } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; rpbi.renderArea = { { 0, 0 }, { W, H } }; - 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, layout, 0, 1, &set, 0, nullptr); - VkDeviceSize voff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &vbuf, &voff); - vkCmdDraw(cmd, 6, 1, 0, 0); - vkCmdEndRenderPass(cmd); - VkBufferImageCopy region{}; region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - vkMapMemory(v.device, readback_mem, 0, (VkDeviceSize)W * H * 4, 0, &p); - stbi_write_png(png_path, W, H, 4, p, W * 4); - vkUnmapMemory(v.device, readback_mem); - DONUT_INFO("Vulkan geodesic render written to {}", png_path); - - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, cpool, nullptr); - vkDestroyBuffer(v.device, readback, nullptr); vkFreeMemory(v.device, readback_mem, nullptr); - vkDestroyBuffer(v.device, vbuf, nullptr); vkFreeMemory(v.device, vbuf_mem, nullptr); - vkDestroyPipeline(v.device, pipeline, nullptr); vkDestroyPipelineLayout(v.device, layout, nullptr); - vkDestroyShaderModule(v.device, vmod, nullptr); vkDestroyShaderModule(v.device, fmod, nullptr); - vkDestroyDescriptorPool(v.device, pool, nullptr); vkDestroyDescriptorSetLayout(v.device, set_layout, nullptr); - vkDestroySampler(v.device, sampler, nullptr); vkDestroyImageView(v.device, cube_view, nullptr); - vkDestroyImage(v.device, cube, nullptr); vkFreeMemory(v.device, cube_mem, nullptr); - vkDestroyBuffer(v.device, cube_staging, nullptr); vkFreeMemory(v.device, cube_staging_mem, nullptr); - vkDestroyBuffer(v.device, cam_buf, nullptr); vkFreeMemory(v.device, cam_mem, nullptr); - vkDestroyBuffer(v.device, disk_buf, nullptr); vkFreeMemory(v.device, disk_mem, nullptr); - vkDestroyBuffer(v.device, obj_buf, nullptr); vkFreeMemory(v.device, obj_mem, nullptr); - vkDestroyBuffer(v.device, sim_buf, nullptr); vkFreeMemory(v.device, sim_mem, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); vkDestroyImage(v.device, image, nullptr); vkFreeMemory(v.device, image_mem, nullptr); - return true; - } - - auto VulkanContext::shutdown() -> void - { - Impl& v = *m_impl; - if (v.device) { vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; } - if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } - } - - auto vulkan_self_test() -> bool - { - VulkanContext ctx; - if (!ctx.init()) - { - DONUT_ERROR("Vulkan: initialization failed"); - return false; - } - bool ok = ctx.self_test_clear(); - ok = ctx.self_test_triangle() && ok; - ctx.shutdown(); - return ok; - } -} diff --git a/src/platform/vulkan/vulkan_context.h b/src/platform/vulkan/vulkan_context.h deleted file mode 100644 index de57c90..0000000 --- a/src/platform/vulkan/vulkan_context.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -// Pure-C++ interface to the Vulkan backend (no vulkan.h leaks into the rest of -// the engine; the implementation lives in VulkanContext.cpp). On macOS Vulkan -// runs through MoltenVK (Vulkan -> Metal). -namespace Donut -{ - class VulkanContext - { - public: - VulkanContext(); - ~VulkanContext(); - - // Creates the instance, picks a physical device, and creates the logical - // device + graphics queue. Returns false (and logs) on failure. - auto init() -> bool; - auto shutdown() -> void; - - // Phase 1 verification: renders a known clear colour into an offscreen - // image and reads it back, confirming instance -> device -> render pass - // -> command buffer -> submit -> read-back all work end to end. - auto self_test_clear() -> bool; - - // Phase 2/3 verification: builds a graphics pipeline from Slang-compiled - // SPIR-V and draws a full-screen gradient triangle into the offscreen - // image, confirming the SPIR-V -> pipeline -> draw path works. - auto self_test_triangle() -> bool; - - // B-3: renders the geodesic (black hole) fragment shader through Vulkan - // into an offscreen image and writes it to pngPath. Exercises UBOs, - // descriptor sets, a cubemap sampler and the geodesic pipeline. - auto render_geodesic(const char* pngPath) -> bool; - - private: - struct Impl; - Impl* m_impl = nullptr; - }; - - // Convenience one-shot: init() + self_test_clear() + shutdown(). Logs results. - auto vulkan_self_test() -> bool; -} diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp new file mode 100644 index 0000000..4311d0c --- /dev/null +++ b/src/platform/vulkan/vulkan_device.cpp @@ -0,0 +1,1254 @@ +#include "vulkan_device.h" + +#include "core/log.h" + +#define GLFW_INCLUDE_VULKAN +#include <GLFW/glfw3.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> + +namespace Donut::RHI +{ + namespace + { + 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); + } + 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; + 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 = m_offscreen_rp; rpbi.framebuffer = rt->m_fb; + rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } }; + rpbi.clearValueCount = 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; + VkRenderPass m_offscreen_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(void* glfwWindow, int width, int height) -> 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, bool with_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; + auto create_swapchain_render_pass() -> bool; + auto create_offscreen_render_pass() -> bool; + 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; + VkRenderPass m_offscreen_rp = VK_NULL_HANDLE; + 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; + } + + // Swapchain pass: colour + depth. Scene geometry uses the depth; the + // black-hole present + ImGui simply don't test it. + auto VulkanDevice::create_swapchain_render_pass() -> bool + { + VkAttachmentDescription atts[2]{}; + atts[0].format = m_swapchain_format; 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 = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + atts[1].format = VK_FORMAT_D32_SFLOAT; 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; subpass.pDepthStencilAttachment = &depth_ref; + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 1; rpci.pDependencies = &dep; + VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_swapchain_rp)); + return true; + } + + // Off-screen colour pass (RGBA8), leaving the image SHADER_READ_ONLY so the + // present pass can sample it. Shared by every render target. + auto VulkanDevice::create_offscreen_render_pass() -> bool + { + VkAttachmentDescription color{}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; 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 deps[2]{}; + 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; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 2; rpci.pDependencies = deps; + VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_offscreen_rp)); + return true; + } + + 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(void* glfwWindow, int width, int height) -> bool + { + m_window = (GLFWwindow*)glfwWindow; m_width = width; m_height = height; + if (!create_instance()) return false; + if (!pick_physical_and_device()) return false; + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + if (!create_swapchain_render_pass()) return false; + if (!create_offscreen_render_pass()) 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, bool /*with_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 fmt = vk_format(color); + 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_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 = fmt; + 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); + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = m_offscreen_rp; fbci.attachmentCount = 1; fbci.pAttachments = &rt->m_view; + 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; + } + + 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; + if (desc.has_depth) gpci.pDepthStencilState = &ds; + gpci.layout = p->m_layout; + gpci.renderPass = desc.has_depth ? m_swapchain_rp : m_offscreen_rp; + 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); + + 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_offscreen_rp = m_offscreen_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]; + + 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); + + 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_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 + { + 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); + if (m_offscreen_rp) vkDestroyRenderPass(m_device, m_offscreen_rp, nullptr); + if (m_swapchain_rp) vkDestroyRenderPass(m_device, m_swapchain_rp, nullptr); + 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>(); } + + auto vulkan_prepare_glfw() -> void + { +#ifdef __APPLE__ + if (!getenv("VK_ICD_FILENAMES")) + setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); + if (!getenv("VK_LAYER_PATH")) + setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); + if (!getenv("DYLD_LIBRARY_PATH")) + setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); +#endif + glfwInitVulkanLoader(vkGetInstanceProcAddr); + } +} diff --git a/src/platform/vulkan/vulkan_device.h b/src/platform/vulkan/vulkan_device.h new file mode 100644 index 0000000..efc2433 --- /dev/null +++ b/src/platform/vulkan/vulkan_device.h @@ -0,0 +1,17 @@ +#pragma once + +#include "rendering/rhi.h" + +namespace Donut::RHI +{ + // Vulkan implementation of the RHI Device (MoltenVK on macOS). Owns the + // instance/device/swapchain and translates the RHI's baked pipelines + + // recorded command lists into native Vulkan; the geometry it draws is the + // shared SceneRenderer / BlackHoleRenderer, identical to every other backend. + auto create_vulkan_device() -> Scope<Device>; + + // Must run BEFORE glfwInit() when Vulkan is the selected API: points GLFW at + // the loader the app links against (its own dlopen fails on macOS/Homebrew) + // and configures the MoltenVK ICD / layer paths. + auto vulkan_prepare_glfw() -> void; +} diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp deleted file mode 100644 index 8b0d0c0..0000000 --- a/src/platform/vulkan/vulkan_renderer.cpp +++ /dev/null @@ -1,2038 +0,0 @@ -#include "vulkan_renderer.h" -#include "core/log.h" -#include "core/camera.h" -#include "core/settings_manager.h" - -#define GLFW_INCLUDE_VULKAN -#include <GLFW/glfw3.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 <algorithm> -#include <cstring> -#include <cstdlib> -#include <fstream> - -namespace Donut -{ - #define VK_CHECK(expr) \ - do { \ - VkResult _r = (expr); \ - if (_r != VK_SUCCESS) { \ - DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ - return false; \ - } \ - } while (0) - - static constexpr int MAX_FRAMES_IN_FLIGHT = 2; - - auto vulkan_prepare_glfw() -> void - { -#ifdef __APPLE__ - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); - if (!getenv("DYLD_LIBRARY_PATH")) - setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); -#endif - // GLFW dlopen's the Vulkan loader by bare name, which fails on - // mac_os/Homebrew; hand it the loader entry point we already link against. - glfwInitVulkanLoader(vkGetInstanceProcAddr); - } - - struct VulkanRenderer::Impl - { - GLFWwindow* window = nullptr; - int width = 0, height = 0; - bool framebuffer_resized = false; - - VkInstance instance = VK_NULL_HANDLE; - VkSurfaceKHR surface = VK_NULL_HANDLE; - VkPhysicalDevice physical = VK_NULL_HANDLE; - VkDevice device = VK_NULL_HANDLE; - uint32_t graphics_family = 0, present_family = 0; - VkQueue graphics_queue = VK_NULL_HANDLE, present_queue = VK_NULL_HANDLE; - - VkSwapchainKHR swapchain = VK_NULL_HANDLE; - VkFormat swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; - VkExtent2D swapchain_extent{}; - std::vector<VkImage> images; - std::vector<VkImageView> image_views; - VkRenderPass render_pass = VK_NULL_HANDLE; - std::vector<VkFramebuffer> framebuffers; - - VkCommandPool command_pool = VK_NULL_HANDLE; - std::vector<VkCommandBuffer> command_buffers; // MAX_FRAMES_IN_FLIGHT - - std::vector<VkSemaphore> image_available; // per frame in flight - std::vector<VkSemaphore> render_finished; // per swapchain image - std::vector<VkFence> in_flight; // per frame in flight - std::vector<VkFence> images_in_flight; // per swapchain image - uint32_t current_frame = 0; - - VkDescriptorPool imgui_pool = VK_NULL_HANDLE; - bool imgui_init = false; - - VkPhysicalDeviceMemoryProperties mem_props{}; - - // The geodesic is rendered every frame into an offscreen image, then - // upscaled onto the swapchain by the present pass. Progressive resolution: - // low-res while the camera moves (keeps dragging responsive and each draw - // well under the Metal GPU watchdog) and high-res once it settles (resolves - // the photon ring cleanly). Both are 16:9 so the camera aspect is shared. - static constexpr uint32_t GEO_LO_W = 480, GEO_LO_H = 270; - static constexpr uint32_t GEO_HI_W = 960, GEO_HI_H = 540; - struct GeoTarget - { - uint32_t w = 0, h = 0; - VkImage image = VK_NULL_HANDLE; - VkDeviceMemory mem = VK_NULL_HANDLE; - VkImageView view = VK_NULL_HANDLE; - VkFramebuffer fb = VK_NULL_HANDLE; - VkDescriptorSet present_set = VK_NULL_HANDLE; // present pass samples this target - }; - GeoTarget geo_lo, geo_hi; - VkRenderPass geo_render_pass = VK_NULL_HANDLE; // shared (resolution-independent) - VkBuffer cam_buf = VK_NULL_HANDLE, disk_buf = VK_NULL_HANDLE, obj_buf = VK_NULL_HANDLE, sim_buf = VK_NULL_HANDLE; - VkDeviceMemory cam_mem = VK_NULL_HANDLE, disk_mem = VK_NULL_HANDLE, obj_mem = VK_NULL_HANDLE, sim_mem = VK_NULL_HANDLE; - void* cam_mapped = nullptr; - void* sim_mapped = nullptr; - void* disk_mapped = nullptr; // disk UBO, refilled from bh_params each frame - BlackHoleParams bh_params; // live UI-tunable disk/camera parameters - std::string gpu_name; // physical device name for the UI - Camera camera{ 60.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 100.0f }; - bool left_was_down = false; - double last_frame_time = 0.0; // for free-fly dt - double fps_last_x = 0.0, fps_last_y = 0.0; // cursor tracking for free-fly look - bool user_moving = false; // camera moved/looked this frame (drives step count) - VkImage cube_image = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; - VkImageView cube_view = VK_NULL_HANDLE; VkSampler cube_sampler = VK_NULL_HANDLE; - std::string hdri_path; // path backing the current cubemap (UI display + no-op switch guard) - VkDescriptorSetLayout geo_set_layout = VK_NULL_HANDLE; - VkDescriptorPool geo_pool = VK_NULL_HANDLE; - VkDescriptorSet geo_set = VK_NULL_HANDLE; - VkPipelineLayout geo_pipeline_layout = VK_NULL_HANDLE; - VkPipeline geo_pipeline = VK_NULL_HANDLE; - VkBuffer quad_vb = VK_NULL_HANDLE; VkDeviceMemory quad_vb_mem = VK_NULL_HANDLE; - double start_time = 0.0; - VkFence geo_in_use = VK_NULL_HANDLE; // previous frame's fence; guards the shared geodesic image - - // Present pass: samples the geodesic image with a full-screen textured - // quad, drawn into the swapchain render pass just before the ImGui UI. - VkSampler present_sampler = VK_NULL_HANDLE; - VkDescriptorSetLayout present_set_layout = VK_NULL_HANDLE; - VkDescriptorPool present_pool = VK_NULL_HANDLE; - VkPipelineLayout present_pipeline_layout = VK_NULL_HANDLE; - VkPipeline present_pipeline = VK_NULL_HANDLE; - - // World-builder scene view (grid now; sphere/skybox next). Normal-scale - // orbital camera; geometry drawn straight into the swapchain render pass. - bool scene_mode = false; - Camera scene_camera{ 45.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 1000.0f }; - VkBuffer grid_vb = VK_NULL_HANDLE; VkDeviceMemory grid_vb_mem = VK_NULL_HANDLE; - uint32_t grid_vertex_count = 0; - VkBuffer grid_ubo = VK_NULL_HANDLE; VkDeviceMemory grid_ubo_mem = VK_NULL_HANDLE; - void* grid_ubo_mapped = nullptr; - VkDescriptorSetLayout grid_set_layout = VK_NULL_HANDLE; - VkDescriptorPool grid_pool = VK_NULL_HANDLE; - VkDescriptorSet grid_set = VK_NULL_HANDLE; - VkPipelineLayout grid_pipeline_layout = VK_NULL_HANDLE; - VkPipeline grid_pipeline = VK_NULL_HANDLE; - - // Scene color+depth pass into the swapchain (the grid is coplanar, but the - // sphere needs real depth). Depth image + framebuffers track the swapchain. - VkImage scene_depth_image = VK_NULL_HANDLE; VkDeviceMemory scene_depth_mem = VK_NULL_HANDLE; - VkImageView scene_depth_view = VK_NULL_HANDLE; - VkRenderPass scene_render_pass = VK_NULL_HANDLE; - std::vector<VkFramebuffer> scene_framebuffers; - - // Lit sphere (Sphere.slang): pos+normal indexed mesh; samples the HDRI - // cubemap for ambient at binding 1 (shares the geodesic cube). - VkBuffer sphere_vb = VK_NULL_HANDLE; VkDeviceMemory sphere_vb_mem = VK_NULL_HANDLE; - VkBuffer sphere_ib = VK_NULL_HANDLE; VkDeviceMemory sphere_ib_mem = VK_NULL_HANDLE; - uint32_t sphere_index_count = 0; - static constexpr uint32_t MAX_SCENE_OBJECTS = 64; - std::vector<SceneObject> scene_objects{ SceneObject{} }; // one default sphere - int selected_object = 0; - VkDeviceSize sphere_ubo_stride = 0; // aligned per-object UBO slot (dynamic offset) - VkBuffer sphere_ubo = VK_NULL_HANDLE; VkDeviceMemory sphere_ubo_mem = VK_NULL_HANDLE; - void* sphere_ubo_mapped = nullptr; - VkDescriptorSetLayout sphere_set_layout = VK_NULL_HANDLE; - VkDescriptorPool sphere_pool = VK_NULL_HANDLE; - VkDescriptorSet sphere_set = VK_NULL_HANDLE; - VkPipelineLayout sphere_pipeline_layout = VK_NULL_HANDLE; - VkPipeline sphere_pipeline = VK_NULL_HANDLE; - - // Skybox (Skybox.slang): a cube sampling the HDRI cubemap at the far plane - // (pos.xyww -> depth 1), drawn first as the scene background. - VkBuffer skybox_vb = VK_NULL_HANDLE; VkDeviceMemory skybox_vb_mem = VK_NULL_HANDLE; - VkBuffer skybox_ubo = VK_NULL_HANDLE; VkDeviceMemory skybox_ubo_mem = VK_NULL_HANDLE; - void* skybox_ubo_mapped = nullptr; - VkDescriptorSetLayout skybox_set_layout = VK_NULL_HANDLE; - VkDescriptorPool skybox_pool = VK_NULL_HANDLE; - VkDescriptorSet skybox_set = VK_NULL_HANDLE; - VkPipelineLayout skybox_pipeline_layout = VK_NULL_HANDLE; - VkPipeline skybox_pipeline = VK_NULL_HANDLE; - - auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t; - auto create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; - static auto load_spirv(const std::string& path) -> 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; - auto create_render_pass() -> bool; - auto create_framebuffers() -> bool; - auto create_command_buffers() -> bool; - auto create_sync_objects() -> bool; - auto create_geodesic_resources() -> bool; - auto create_geo_target(GeoTarget& target, uint32_t w, uint32_t h) -> bool; - auto create_hdri_cubemap(const char* path) -> bool; - auto rebuild_hdri_cubemap(const char* path) -> void; - auto create_present_resources() -> bool; - auto create_scene_resources() -> bool; - auto create_scene_targets() -> bool; - auto destroy_scene_targets() -> void; - auto process_input() -> void; - auto update_geodesic_uniforms() -> void; - auto update_scene_uniforms() -> void; - auto destroy_geodesic_resources() -> void; - auto recreate_swapchain() -> bool; - auto cleanup_swapchain() -> void; - auto record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool; - }; - - auto VulkanRenderer::Impl::create_instance() -> bool - { -#ifdef __APPLE__ - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); - // The Homebrew validation-layer manifest names the dylib without a path; - // let dlopen find it in the Homebrew lib dir. - if (!getenv("DYLD_LIBRARY_PATH")) - setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); -#endif - 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: GLFW reports no surface support"); return false; } - std::vector<const char*> exts; - for (uint32_t i = 0; i < glfwExtCount; ++i) exts.push_back(glfwExts[i]); - 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, &instance); - if (r != VK_SUCCESS && !layers.empty()) - { - // The validation layer failed to load (its dylib isn't on the loader - // search path); it is optional, so retry without it. - DONUT_WARN("Vulkan: validation layer unavailable, continuing without it"); - layers.clear(); - ici.enabledLayerCount = 0; - ici.ppEnabledLayerNames = nullptr; - r = vkCreateInstance(&ici, nullptr, &instance); - } - if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan: vkCreateInstance failed ({})", (int)r); return false; } - - VK_CHECK(glfwCreateWindowSurface(instance, window, nullptr, &surface)); - DONUT_INFO("Vulkan: instance + surface created (validation {})", layers.empty() ? "off" : "on"); - return true; - } - - auto VulkanRenderer::Impl::pick_physical_and_device() -> bool - { - uint32_t count = 0; - vkEnumeratePhysicalDevices(instance, &count, nullptr); - if (count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } - std::vector<VkPhysicalDevice> devices(count); - vkEnumeratePhysicalDevices(instance, &count, devices.data()); - physical = devices[0]; - - uint32_t q_count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, nullptr); - std::vector<VkQueueFamilyProperties> qfams(q_count); - vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, qfams.data()); - bool found_g = false, found_p = false; - for (uint32_t i = 0; i < q_count; ++i) - { - if (!found_g && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { graphics_family = i; found_g = true; } - VkBool32 present = VK_FALSE; - vkGetPhysicalDeviceSurfaceSupportKHR(physical, i, surface, &present); - if (!found_p && present) { present_family = i; found_p = true; } - } - if (!found_g || !found_p) { DONUT_ERROR("Vulkan: no graphics/present queue"); return false; } - - std::vector<const char*> dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - uint32_t dev_ext_count = 0; - vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, nullptr); - std::vector<VkExtensionProperties> dev_ext_props(dev_ext_count); - vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, dev_ext_props.data()); - for (const auto& e : dev_ext_props) - 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] = { graphics_family, present_family }; - for (uint32_t i = 0; i < (graphics_family == 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(); - VK_CHECK(vkCreateDevice(physical, &dci, nullptr, &device)); - vkGetDeviceQueue(device, graphics_family, 0, &graphics_queue); - vkGetDeviceQueue(device, present_family, 0, &present_queue); - - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(physical, &props); - vkGetPhysicalDeviceMemoryProperties(physical, &mem_props); - gpu_name = props.deviceName; - DONUT_INFO("Vulkan device: {}", gpu_name); - return true; - } - - auto VulkanRenderer::Impl::create_swapchain() -> bool - { - VkSurfaceCapabilitiesKHR caps{}; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps); - - uint32_t fmt_count = 0; - vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, nullptr); - std::vector<VkSurfaceFormatKHR> formats(fmt_count); - vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, 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; - swapchain_format = chosen.format; - - if (caps.currentExtent.width != UINT32_MAX) - swapchain_extent = caps.currentExtent; - else - { - swapchain_extent.width = std::clamp((uint32_t)width, caps.minImageExtent.width, caps.maxImageExtent.width); - swapchain_extent.height = std::clamp((uint32_t)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 = surface; - sci.minImageCount = image_count; - sci.imageFormat = chosen.format; - sci.imageColorSpace = chosen.colorSpace; - sci.imageExtent = swapchain_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; // always supported, vsync - sci.clipped = VK_TRUE; - - uint32_t fam_idx[2] = { graphics_family, present_family }; - if (graphics_family != present_family) - { - sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; - sci.queueFamilyIndexCount = 2; - sci.pQueueFamilyIndices = fam_idx; - } - else - sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - - VK_CHECK(vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain)); - uint32_t n = 0; - vkGetSwapchainImagesKHR(device, swapchain, &n, nullptr); - images.resize(n); - vkGetSwapchainImagesKHR(device, swapchain, &n, images.data()); - return true; - } - - auto VulkanRenderer::Impl::create_image_views() -> bool - { - image_views.resize(images.size()); - for (size_t i = 0; i < images.size(); ++i) - { - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = images[i]; - vci.viewType = VK_IMAGE_VIEW_TYPE_2D; - vci.format = swapchain_format; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &vci, nullptr, &image_views[i])); - } - return true; - } - - auto VulkanRenderer::Impl::create_render_pass() -> bool - { - VkAttachmentDescription color{}; - color.format = swapchain_format; - 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_PRESENT_SRC_KHR; - - 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 = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - - 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; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &render_pass)); - return true; - } - - auto VulkanRenderer::Impl::create_framebuffers() -> bool - { - framebuffers.resize(image_views.size()); - for (size_t i = 0; i < image_views.size(); ++i) - { - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; - fbci.attachmentCount = 1; fbci.pAttachments = &image_views[i]; - fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i])); - } - return true; - } - - // Depth image + a color+depth render pass + per-image framebuffers for the - // scene view. Swapchain-sized, so recreated alongside the swapchain. - auto VulkanRenderer::Impl::create_scene_targets() -> bool - { - const VkFormat depth_fmt = VK_FORMAT_D32_SFLOAT; - - VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - dici.imageType = VK_IMAGE_TYPE_2D; dici.format = depth_fmt; - dici.extent = { swapchain_extent.width, swapchain_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; - VK_CHECK(vkCreateImage(device, &dici, nullptr, &scene_depth_image)); - VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(device, scene_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); - VK_CHECK(vkAllocateMemory(device, &dai, nullptr, &scene_depth_mem)); - VK_CHECK(vkBindImageMemory(device, scene_depth_image, scene_depth_mem, 0)); - VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - dvci.image = scene_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = depth_fmt; - dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &dvci, nullptr, &scene_depth_view)); - - VkAttachmentDescription atts[2]{}; - atts[0].format = swapchain_format; 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 = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - atts[1].format = depth_fmt; 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; subpass.pDepthStencilAttachment = &depth_ref; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.srcAccessMask = 0; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &scene_render_pass)); - - scene_framebuffers.resize(image_views.size()); - for (size_t i = 0; i < image_views.size(); ++i) - { - VkImageView att[2] = { image_views[i], scene_depth_view }; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = scene_render_pass; fbci.attachmentCount = 2; fbci.pAttachments = att; - fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &scene_framebuffers[i])); - } - return true; - } - - auto VulkanRenderer::Impl::destroy_scene_targets() -> void - { - for (auto fb : scene_framebuffers) vkDestroyFramebuffer(device, fb, nullptr); - scene_framebuffers.clear(); - if (scene_render_pass) { vkDestroyRenderPass(device, scene_render_pass, nullptr); scene_render_pass = VK_NULL_HANDLE; } - if (scene_depth_view) { vkDestroyImageView(device, scene_depth_view, nullptr); scene_depth_view = VK_NULL_HANDLE; } - if (scene_depth_image) { vkDestroyImage(device, scene_depth_image, nullptr); scene_depth_image = VK_NULL_HANDLE; } - if (scene_depth_mem) { vkFreeMemory(device, scene_depth_mem, nullptr); scene_depth_mem = VK_NULL_HANDLE; } - } - - auto VulkanRenderer::Impl::create_command_buffers() -> bool - { - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - pci.queueFamilyIndex = graphics_family; - VK_CHECK(vkCreateCommandPool(device, &pci, nullptr, &command_pool)); - - command_buffers.resize(MAX_FRAMES_IN_FLIGHT); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; - cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; - VK_CHECK(vkAllocateCommandBuffers(device, &cbai, command_buffers.data())); - return true; - } - - auto VulkanRenderer::Impl::create_sync_objects() -> bool - { - image_available.resize(MAX_FRAMES_IN_FLIGHT); - in_flight.resize(MAX_FRAMES_IN_FLIGHT); - render_finished.resize(images.size()); - images_in_flight.assign(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) - { - VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &image_available[i])); - VK_CHECK(vkCreateFence(device, &fci, nullptr, &in_flight[i])); - } - for (size_t i = 0; i < images.size(); ++i) - VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &render_finished[i])); - return true; - } - - auto VulkanRenderer::Impl::find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t - { - for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) - if ((type_filter & (1u << i)) && (mem_props.memoryTypes[i].propertyFlags & flags) == flags) - return i; - return UINT32_MAX; - } - - auto VulkanRenderer::Impl::create_buffer(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(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; - VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(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(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; - vkBindBufferMemory(device, buf, mem, 0); - return true; - } - - auto VulkanRenderer::Impl::load_spirv(const std::string& path) -> 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 VulkanRenderer::Impl::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool - { - auto spv = load_spirv(path); - if (spv.empty()) { DONUT_ERROR("Vulkan: 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(device, &ci, nullptr, &out) == VK_SUCCESS; - } - - // Creates one geodesic render target (image + memory + view + framebuffer) - // at w x h against the shared geo_render_pass. Used for the low- and high-res - // progressive targets. - auto VulkanRenderer::Impl::create_geo_target(GeoTarget& t, uint32_t w, uint32_t h) -> bool - { - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - t.w = w; t.h = h; - - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { w, 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; - VK_CHECK(vkCreateImage(device, &ici, nullptr, &t.image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(device, t.image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(device, &im_alloc, nullptr, &t.mem)); - VK_CHECK(vkBindImageMemory(device, t.image, t.mem, 0)); - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = t.image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &vci, nullptr, &t.view)); - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = geo_render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &t.view; - fbci.width = w; fbci.height = h; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &t.fb)); - return true; - } - - auto VulkanRenderer::Impl::create_geodesic_resources() -> bool - { - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - const float SagA_rs = 1.269e10f; - - VkAttachmentDescription color{}; - color.format = 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 deps[2]{}; - 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; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 2; rpci.pDependencies = deps; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &geo_render_pass)); - - if (!create_geo_target(geo_lo, GEO_LO_W, GEO_LO_H)) return false; - if (!create_geo_target(geo_hi, GEO_HI_W, GEO_HI_H)) return false; - - create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); - create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); - create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); - create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); - - camera.set_camera_mode(CameraMode::Orbital); - camera.set_orbital_target(glm::vec3(0.0f)); - camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk - camera.set_orbital_limits(2.2e11, 1.5e12); - camera.set_orbital_speed(0.01f); - camera.set_zoom_speed(3e10); - camera.set_azimuth(0.0f); - camera.set_elevation(1.25f); - - void* p = nullptr; - vkMapMemory(device, cam_mem, 0, 128, 0, &cam_mapped); // camera UBO refilled every frame - vkMapMemory(device, disk_mem, 0, 32, 0, &disk_mapped); // disk UBO refilled from bh_params every frame - - std::vector<uint8_t> obj_data(800, 0); - int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); - float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); - float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); - vkMapMemory(device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(device, obj_mem); - - vkMapMemory(device, sim_mem, 0, 16, 0, &sim_mapped); - - if (!create_hdri_cubemap("assets/hdri/HDR_blue_nebulae-1.hdr")) return false; - - VkDescriptorSetLayoutBinding binds[5]{}; - for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } - binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 5; dslci.pBindings = binds; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &geo_set_layout)); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &geo_pool)); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = geo_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &geo_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &geo_set)); - VkDescriptorBufferInfo bi[4] = { { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; - VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet writes[5]{}; - for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = geo_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } - writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = geo_set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); - - float quad[] = { - -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, - -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, - }; - create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, quad_vb, quad_vb_mem); - vkMapMemory(device, quad_vb_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(device, quad_vb_mem); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/Geodesic.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/Geodesic.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &geo_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &geo_pipeline_layout)); - 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - // Dynamic viewport/scissor: the same pipeline renders into either the - // low- or high-res target, sized per frame in record_command_buffer. - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; - VkDynamicState dyn_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo dyn{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dyn.dynamicStateCount = 2; dyn.pDynamicStates = dyn_states; - 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; - 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 = &dyn; - gpci.layout = geo_pipeline_layout; gpci.renderPass = geo_render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &geo_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: geodesic pipeline creation failed ({})", (int)pr); return false; } - - start_time = glfwGetTime(); - update_geodesic_uniforms(); - DONUT_INFO("Vulkan: geodesic resources ready ({}x{} moving / {}x{} settled)", - (int)GEO_LO_W, (int)GEO_LO_H, (int)GEO_HI_W, (int)GEO_HI_H); - return true; - } - - auto VulkanRenderer::Impl::create_hdri_cubemap(const char* path) -> bool - { - hdri_path = path; - 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; // full chain (11 for 1024) - - 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; - // COLOR_ATTACHMENT renders the 6 faces (mip 0); TRANSFER_SRC/DST build the - // mip chain by blitting; SAMPLED for shader reads (incl. mip-LOD sampling). - 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; - VK_CHECK(vkCreateImage(device, &cci, nullptr, &cube_image)); - VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(device, cube_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); - VK_CHECK(vkAllocateMemory(device, &cai, nullptr, &cube_mem)); - VK_CHECK(vkBindImageMemory(device, cube_image, cube_mem, 0)); - VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - cvci.image = cube_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; - cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - VK_CHECK(vkCreateImageView(device, &cvci, nullptr, &cube_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; - VK_CHECK(vkCreateSampler(device, &csm, nullptr, &cube_sampler)); - - int w = 0, h = 0, ch = 0; - float* pixels = stbi_loadf(path, &w, &h, &ch, 4); - if (!pixels) - { - DONUT_WARN("Vulkan: HDRI '{}' could not be loaded; using a dark background", path); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(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 = cube_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 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, 1, 0, 6 }; - vkCmdClearColorImage(cmd, cube_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(graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(graphics_queue); - vkFreeCommandBuffers(device, command_pool, 1, &cmd); - return true; - } - - // Apple GPUs can't linearly filter RGBA32F, so store the equirect as - // RGBA16F (convert the loaded floats to half on the way into staging). - 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; - if (!create_buffer(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem)) { stbi_image_free(pixels); return false; } - void* mp = nullptr; vkMapMemory(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]; memcpy(&dst[i], &hf, sizeof(uint16_t)); } - vkUnmapMemory(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; - VK_CHECK(vkCreateImage(device, &eci, nullptr, &eq_image)); - VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(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); - VK_CHECK(vkAllocateMemory(device, &eai, nullptr, &eq_mem)); - VK_CHECK(vkBindImageMemory(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 }; - VK_CHECK(vkCreateImageView(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; // longitude wraps - esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; // latitude clamps - esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(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 = cube_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; - fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; - VK_CHECK(vkCreateImageView(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; - VK_CHECK(vkCreateRenderPass(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; - VK_CHECK(vkCreateFramebuffer(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; - VK_CHECK(vkCreateDescriptorSetLayout(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; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &pool)); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.fragmentMain.spv", fmod)) return false; - VkPipelineLayout playout; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; - VK_CHECK(vkCreatePipelineLayout(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; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: equirect pipeline failed ({})", (int)pr); return false; } - - 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(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); - vkMapMemory(device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(device, cube_vb_mem); - - glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); - proj[1][1] *= -1.0f; // Vulkan clip space is Y-down vs OpenGL - 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(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]) }; // SPIR-V expects row-major - vkMapMemory(device, ubo_mem[i], 0, 128, 0, &mp); memcpy(mp, mats, 128); vkUnmapMemory(device, ubo_mem[i]); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; - VK_CHECK(vkAllocateDescriptorSets(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(device, 2, ws, 0, nullptr); - } - - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd)); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(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); - } - - // Build the mip chain (all 6 faces) by successive linear down-blits, so - // divergence-based LOD sampling can read a blurred sky for lensed rays. - // Mip 0 (every layer) is SHADER_READ_ONLY from the render passes above. - { - VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - src0.image = cube_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 = cube_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, cube_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - cube_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); - - VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - bs.image = cube_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 = cube_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); - } - - VK_CHECK(vkEndCommandBuffer(cmd)); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE)); - VK_CHECK(vkQueueWaitIdle(graphics_queue)); - - vkFreeCommandBuffers(device, command_pool, 1, &cmd); - for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(device, ubo[i], nullptr); vkFreeMemory(device, ubo_mem[i], nullptr); vkDestroyFramebuffer(device, face_fb[i], nullptr); vkDestroyImageView(device, face_views[i], nullptr); } - vkDestroyBuffer(device, cube_vb, nullptr); vkFreeMemory(device, cube_vb_mem, nullptr); - vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, playout, nullptr); - vkDestroyDescriptorPool(device, pool, nullptr); vkDestroyDescriptorSetLayout(device, set_layout, nullptr); - vkDestroyRenderPass(device, rp, nullptr); - vkDestroySampler(device, eq_sampler, nullptr); vkDestroyImageView(device, eq_view, nullptr); - vkDestroyImage(device, eq_image, nullptr); vkFreeMemory(device, eq_mem, nullptr); - vkDestroyBuffer(device, eq_staging, nullptr); vkFreeMemory(device, eq_staging_mem, nullptr); - DONUT_INFO("Vulkan: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); - return true; - } - - // Runtime HDRI switch: drain the device, tear down the old cubemap, build the - // new one, and repoint the geodesic set's samplerCube (binding 4) at it. - auto VulkanRenderer::Impl::rebuild_hdri_cubemap(const char* path) -> void - { - vkDeviceWaitIdle(device); - - if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr); - if (cube_view) vkDestroyImageView(device, cube_view, nullptr); - if (cube_image) vkDestroyImage(device, cube_image, nullptr); - if (cube_mem) vkFreeMemory(device, cube_mem, nullptr); - cube_sampler = VK_NULL_HANDLE; cube_view = VK_NULL_HANDLE; - cube_image = VK_NULL_HANDLE; cube_mem = VK_NULL_HANDLE; - - create_hdri_cubemap(path); - - VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - write.dstSet = geo_set; write.dstBinding = 4; write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - - // The scene sphere samples the same cube (binding 1) — repoint it too. - if (sphere_set) - { - VkWriteDescriptorSet sw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - sw.dstSet = sphere_set; sw.dstBinding = 1; sw.descriptorCount = 1; - sw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &sw, 0, nullptr); - } - if (skybox_set) - { - VkWriteDescriptorSet kw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - kw.dstSet = skybox_set; kw.dstBinding = 1; kw.descriptorCount = 1; - kw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &kw, 0, nullptr); - } - } - - auto VulkanRenderer::Impl::create_present_resources() -> bool - { - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(device, &smci, nullptr, &present_sampler)); - - VkDescriptorSetLayoutBinding bind{}; bind.binding = 0; bind.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bind.descriptorCount = 1; bind.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 1; dslci.pBindings = &bind; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &present_set_layout)); - // One present set per geodesic target (low-/high-res), each sampling its - // own image; record_command_buffer binds whichever was rendered this frame. - VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 2; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &present_pool)); - for (GeoTarget* t : { &geo_lo, &geo_hi }) - { - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = present_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &present_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &t->present_set)); - VkDescriptorImageInfo ii{ present_sampler, t->view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - write.dstSet = t->present_set; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = ⅈ - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - } - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/TexturedQuad.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/TexturedQuad.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &present_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &present_pipeline_layout)); - 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - 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_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; - 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.pDynamicState = &dsci; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; - gpci.layout = present_pipeline_layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &present_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: present pipeline creation failed ({})", (int)pr); return false; } - DONUT_INFO("Vulkan: present pipeline ready"); - return true; - } - - auto VulkanRenderer::Impl::create_scene_resources() -> bool - { - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - - // Scene camera: normal-scale orbital viewer (matches the OpenGL world-builder). - 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(); - - // Line grid on the XZ plane (+/-50 units, 1-unit cells). Grid.slang scales - // the position by u_GridSize/50, so u_GridSize = 50 keeps it 1:1. - std::vector<glm::vec3> lines; - const int N = 50; - for (int i = -N; i <= N; ++i) - { - lines.push_back({ (float)i, 0.0f, (float)-N }); - lines.push_back({ (float)i, 0.0f, (float) N }); - lines.push_back({ (float)-N, 0.0f, (float)i }); - lines.push_back({ (float) N, 0.0f, (float)i }); - } - grid_vertex_count = (uint32_t)lines.size(); - VkDeviceSize vbsize = lines.size() * sizeof(glm::vec3); - if (!create_buffer(vbsize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, grid_vb, grid_vb_mem)) return false; - void* mp = nullptr; vkMapMemory(device, grid_vb_mem, 0, vbsize, 0, &mp); memcpy(mp, lines.data(), vbsize); vkUnmapMemory(device, grid_vb_mem); - - // $Globals UBO (set 0, binding 0), std140, 176 bytes; refilled each frame. - if (!create_buffer(176, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, grid_ubo, grid_ubo_mem)) return false; - vkMapMemory(device, grid_ubo_mem, 0, 176, 0, &grid_ubo_mapped); - - VkDescriptorSetLayoutBinding b{}; b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 1; dslci.pBindings = &b; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &grid_set_layout)); - VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 1; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &grid_pool)); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = grid_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &grid_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &grid_set)); - VkDescriptorBufferInfo bi{ grid_ubo, 0, VK_WHOLE_SIZE }; - VkWriteDescriptorSet w{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; w.dstSet = grid_set; w.dstBinding = 0; w.descriptorCount = 1; w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &bi; - vkUpdateDescriptorSets(device, 1, &w, 0, nullptr); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/Grid.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/Grid.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &grid_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &grid_pipeline_layout)); - 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, sizeof(glm::vec3), 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_LINE_LIST; - 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_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; - VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; // transparent lines: test but don't write - VkPipelineColorBlendAttachmentState cba{}; - 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; - 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; - 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.pDynamicState = &dsci; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDepthStencilState = &ds; - gpci.layout = grid_pipeline_layout; gpci.renderPass = scene_render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &grid_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: grid pipeline creation failed ({})", (int)pr); return false; } - - // Lit sphere: unit UV-sphere mesh (pos+normal, stride 24), placed/scaled by - // u_Transform; samples the geodesic HDRI cubemap for ambient (binding 1). - { - std::vector<float> sv; std::vector<uint32_t> si; - const int RINGS = 24, SECTORS = 48; - for (int r = 0; r <= RINGS; ++r) - { - float phi = (float)std::numbers::pi * r / RINGS; - for (int s = 0; s <= SECTORS; ++s) - { - float theta = 2.0f * (float)std::numbers::pi * s / SECTORS; - float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta); - sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit) - sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position - } - } - for (int r = 0; r < RINGS; ++r) - for (int s = 0; s < SECTORS; ++s) - { - uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1; - si.push_back(a); si.push_back(b); si.push_back(a + 1); - si.push_back(b); si.push_back(b + 1); si.push_back(a + 1); - } - sphere_index_count = (uint32_t)si.size(); - VkDeviceSize svsz = sv.size() * sizeof(float), sisz = si.size() * sizeof(uint32_t); - if (!create_buffer(svsz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, sphere_vb, sphere_vb_mem)) return false; - if (!create_buffer(sisz, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_vis, sphere_ib, sphere_ib_mem)) return false; - void* sp = nullptr; - vkMapMemory(device, sphere_vb_mem, 0, svsz, 0, &sp); memcpy(sp, sv.data(), svsz); vkUnmapMemory(device, sphere_vb_mem); - vkMapMemory(device, sphere_ib_mem, 0, sisz, 0, &sp); memcpy(sp, si.data(), sisz); vkUnmapMemory(device, sphere_ib_mem); - - VkPhysicalDeviceProperties pdp{}; vkGetPhysicalDeviceProperties(physical, &pdp); - VkDeviceSize min_align = pdp.limits.minUniformBufferOffsetAlignment; - sphere_ubo_stride = ((208 + min_align - 1) / min_align) * min_align; // per-object slot - VkDeviceSize sphere_ubo_size = sphere_ubo_stride * MAX_SCENE_OBJECTS; - if (!create_buffer(sphere_ubo_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sphere_ubo, sphere_ubo_mem)) return false; - vkMapMemory(device, sphere_ubo_mem, 0, sphere_ubo_size, 0, &sphere_ubo_mapped); - - VkDescriptorSetLayoutBinding sb[2]{}; - sb[0].binding = 0; sb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sb[0].descriptorCount = 1; sb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - sb[1].binding = 1; sb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sb[1].descriptorCount = 1; sb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo sdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; sdslci.bindingCount = 2; sdslci.pBindings = sb; - VK_CHECK(vkCreateDescriptorSetLayout(device, &sdslci, nullptr, &sphere_set_layout)); - VkDescriptorPoolSize sps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo sdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; sdpci.maxSets = 1; sdpci.poolSizeCount = 2; sdpci.pPoolSizes = sps; - VK_CHECK(vkCreateDescriptorPool(device, &sdpci, nullptr, &sphere_pool)); - VkDescriptorSetAllocateInfo sdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; sdsai.descriptorPool = sphere_pool; sdsai.descriptorSetCount = 1; sdsai.pSetLayouts = &sphere_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &sdsai, &sphere_set)); - VkDescriptorBufferInfo sbi{ sphere_ubo, 0, 208 }; // per-draw size; the dynamic offset selects the object slot - VkDescriptorImageInfo sii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet sw[2]{}; - sw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[0].dstSet = sphere_set; sw[0].dstBinding = 0; sw[0].descriptorCount = 1; sw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sw[0].pBufferInfo = &sbi; - sw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[1].dstSet = sphere_set; sw[1].dstBinding = 1; sw[1].descriptorCount = 1; sw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw[1].pImageInfo = &sii; - vkUpdateDescriptorSets(device, 2, sw, 0, nullptr); - - VkShaderModule svmod, sfmod; - if (!create_shader_module("assets/shaders/generated/Sphere.vertexMain.spv", svmod)) return false; - if (!create_shader_module("assets/shaders/generated/Sphere.fragmentMain.spv", sfmod)) return false; - VkPipelineLayoutCreateInfo splci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; splci.setLayoutCount = 1; splci.pSetLayouts = &sphere_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &splci, nullptr, &sphere_pipeline_layout)); - VkPipelineShaderStageCreateInfo sstages[2]{}; - sstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; sstages[0].module = svmod; sstages[0].pName = "main"; - sstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; sstages[1].module = sfmod; sstages[1].pName = "main"; - VkVertexInputBindingDescription svib{ 0, 6 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription svia[2] = { { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * (uint32_t)sizeof(float) } }; - VkPipelineVertexInputStateCreateInfo svin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - svin.vertexBindingDescriptionCount = 1; svin.pVertexBindingDescriptions = &svib; - svin.vertexAttributeDescriptionCount = 2; svin.pVertexAttributeDescriptions = svia; - VkPipelineInputAssemblyStateCreateInfo sia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; sia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo svps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; svps.viewportCount = 1; svps.scissorCount = 1; - VkDynamicState sdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo sdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; sdsci.dynamicStateCount = 2; sdsci.pDynamicStates = sdyn; - VkPipelineRasterizationStateCreateInfo srs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; srs.polygonMode = VK_POLYGON_MODE_FILL; srs.cullMode = VK_CULL_MODE_NONE; srs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; srs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo sms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; sms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo sds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - sds.depthTestEnable = VK_TRUE; sds.depthWriteEnable = VK_TRUE; sds.depthCompareOp = VK_COMPARE_OP_LESS; - VkPipelineColorBlendAttachmentState scba{}; scba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo scb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; scb.attachmentCount = 1; scb.pAttachments = &scba; - VkGraphicsPipelineCreateInfo sgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - sgpci.stageCount = 2; sgpci.pStages = sstages; - sgpci.pVertexInputState = &svin; sgpci.pInputAssemblyState = &sia; sgpci.pViewportState = &svps; - sgpci.pDynamicState = &sdsci; - sgpci.pRasterizationState = &srs; sgpci.pMultisampleState = &sms; sgpci.pColorBlendState = &scb; sgpci.pDepthStencilState = &sds; - sgpci.layout = sphere_pipeline_layout; sgpci.renderPass = scene_render_pass; sgpci.subpass = 0; - VkResult spr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &sgpci, nullptr, &sphere_pipeline); - vkDestroyShaderModule(device, svmod, nullptr); vkDestroyShaderModule(device, sfmod, nullptr); - if (spr != VK_SUCCESS) { DONUT_ERROR("Vulkan: sphere pipeline creation failed ({})", (int)spr); return false; } - } - - // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex - // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry. - { - const float cube[] = { - -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 - }; - if (!create_buffer(sizeof(cube), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, skybox_vb, skybox_vb_mem)) return false; - void* kp = nullptr; vkMapMemory(device, skybox_vb_mem, 0, sizeof(cube), 0, &kp); memcpy(kp, cube, sizeof(cube)); vkUnmapMemory(device, skybox_vb_mem); - - // $Globals UBO (128 B: u_Projection@0, u_View@64); u_Skybox at binding 1. - if (!create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, skybox_ubo, skybox_ubo_mem)) return false; - vkMapMemory(device, skybox_ubo_mem, 0, 128, 0, &skybox_ubo_mapped); - - VkDescriptorSetLayoutBinding kb[2]{}; - kb[0].binding = 0; kb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kb[0].descriptorCount = 1; kb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - kb[1].binding = 1; kb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kb[1].descriptorCount = 1; kb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo kdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; kdslci.bindingCount = 2; kdslci.pBindings = kb; - VK_CHECK(vkCreateDescriptorSetLayout(device, &kdslci, nullptr, &skybox_set_layout)); - VkDescriptorPoolSize kps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo kdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; kdpci.maxSets = 1; kdpci.poolSizeCount = 2; kdpci.pPoolSizes = kps; - VK_CHECK(vkCreateDescriptorPool(device, &kdpci, nullptr, &skybox_pool)); - VkDescriptorSetAllocateInfo kdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; kdsai.descriptorPool = skybox_pool; kdsai.descriptorSetCount = 1; kdsai.pSetLayouts = &skybox_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &kdsai, &skybox_set)); - VkDescriptorBufferInfo kbi{ skybox_ubo, 0, VK_WHOLE_SIZE }; - VkDescriptorImageInfo kii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet kw[2]{}; - kw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[0].dstSet = skybox_set; kw[0].dstBinding = 0; kw[0].descriptorCount = 1; kw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kw[0].pBufferInfo = &kbi; - kw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[1].dstSet = skybox_set; kw[1].dstBinding = 1; kw[1].descriptorCount = 1; kw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw[1].pImageInfo = &kii; - vkUpdateDescriptorSets(device, 2, kw, 0, nullptr); - - VkShaderModule kvmod, kfmod; - if (!create_shader_module("assets/shaders/generated/Skybox.vertexMain.spv", kvmod)) return false; - if (!create_shader_module("assets/shaders/generated/Skybox.fragmentMain.spv", kfmod)) return false; - VkPipelineLayoutCreateInfo kplci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; kplci.setLayoutCount = 1; kplci.pSetLayouts = &skybox_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &kplci, nullptr, &skybox_pipeline_layout)); - VkPipelineShaderStageCreateInfo kstages[2]{}; - kstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; kstages[0].module = kvmod; kstages[0].pName = "main"; - kstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; kstages[1].module = kfmod; kstages[1].pName = "main"; - VkVertexInputBindingDescription kvib{ 0, 3 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription kvia{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; - VkPipelineVertexInputStateCreateInfo kvin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - kvin.vertexBindingDescriptionCount = 1; kvin.pVertexBindingDescriptions = &kvib; - kvin.vertexAttributeDescriptionCount = 1; kvin.pVertexAttributeDescriptions = &kvia; - VkPipelineInputAssemblyStateCreateInfo kia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; kia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo kvps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; kvps.viewportCount = 1; kvps.scissorCount = 1; - VkDynamicState kdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo kdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; kdsci.dynamicStateCount = 2; kdsci.pDynamicStates = kdyn; - VkPipelineRasterizationStateCreateInfo krs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; krs.polygonMode = VK_POLYGON_MODE_FILL; krs.cullMode = VK_CULL_MODE_NONE; krs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; krs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo kms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; kms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo kds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - kds.depthTestEnable = VK_FALSE; kds.depthWriteEnable = VK_FALSE; // background: neither tests nor writes depth - VkPipelineColorBlendAttachmentState kcba{}; kcba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo kcb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; kcb.attachmentCount = 1; kcb.pAttachments = &kcba; - VkGraphicsPipelineCreateInfo kgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - kgpci.stageCount = 2; kgpci.pStages = kstages; - kgpci.pVertexInputState = &kvin; kgpci.pInputAssemblyState = &kia; kgpci.pViewportState = &kvps; - kgpci.pDynamicState = &kdsci; - kgpci.pRasterizationState = &krs; kgpci.pMultisampleState = &kms; kgpci.pColorBlendState = &kcb; kgpci.pDepthStencilState = &kds; - kgpci.layout = skybox_pipeline_layout; kgpci.renderPass = scene_render_pass; kgpci.subpass = 0; - VkResult kpr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &kgpci, nullptr, &skybox_pipeline); - vkDestroyShaderModule(device, kvmod, nullptr); vkDestroyShaderModule(device, kfmod, nullptr); - if (kpr != VK_SUCCESS) { DONUT_ERROR("Vulkan: skybox pipeline creation failed ({})", (int)kpr); return false; } - } - - DONUT_INFO("Vulkan: scene resources ready ({} grid verts, {} sphere indices, skybox)", grid_vertex_count, sphere_index_count); - return true; - } - - auto VulkanRenderer::Impl::update_scene_uniforms() -> void - { - struct GridUBO { - glm::mat4 view_projection; // 0 (SPIR-V RowMajor -> upload transposed) - glm::mat4 transform; // 64 - float grid_size; float p0[3]; // 128 - glm::vec3 grid_color; // 144 - float grid_alpha; // 156 - glm::vec3 camera_pos; // 160 - float p1; // 172 - } g{}; - static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch"); - - uint32_t h = swapchain_extent.height ? swapchain_extent.height : 1; - float aspect = (float)swapchain_extent.width / (float)h; - scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - glm::mat4 vp = scene_camera.get_projection_matrix() * scene_camera.get_view_matrix(); - - // Slang's mul(M,v) + the SPIR-V RowMajor decoration means glm's column-major - // matrices upload directly here (no transpose) to read as the intended M. - g.view_projection = vp; - g.transform = glm::mat4(1.0f); - g.grid_size = 50.0f; - g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f); - g.grid_alpha = 0.75f; - g.camera_pos = scene_camera.get_orbital_position(); - if (grid_ubo_mapped) memcpy(grid_ubo_mapped, &g, sizeof(g)); - - struct SphereUBO { - glm::mat4 view_projection; // 0 - glm::mat4 transform; // 64 - glm::vec3 color; float specular; // 128, 140 - float emission; float p0[3]; // 144 - glm::vec3 light_pos; float p1; // 160, 172 - glm::vec3 camera_pos; int is_selected; // 176, 188 - glm::vec3 outline_color; float outline_width; // 192, 204 - }; - static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch"); - glm::vec3 cam_pos = scene_camera.get_orbital_position(); - uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS); - for (uint32_t i = 0; i < obj_count; ++i) - { - const SceneObject& o = scene_objects[i]; - SphereUBO s{}; - s.view_projection = vp; // same no-transpose rule as the grid - s.transform = glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius)); - s.color = o.color; - s.specular = 0.6f; - s.emission = 0.0f; - s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f); - s.camera_pos = cam_pos; - s.is_selected = ((int)i == selected_object) ? 1 : 0; - s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f); - s.outline_width = 0.15f; - if (sphere_ubo_mapped) memcpy((char*)sphere_ubo_mapped + (VkDeviceSize)i * sphere_ubo_stride, &s, sizeof(s)); - } - - struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; } sky{}; - static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch"); - sky.projection = scene_camera.get_projection_matrix(); // no transpose (mul(M,v)) - sky.view = glm::mat4(glm::mat3(scene_camera.get_view_matrix())); // strip translation so the sky stays centered - if (skybox_ubo_mapped) memcpy(skybox_ubo_mapped, &sky, sizeof(sky)); - } - - auto VulkanRenderer::Impl::update_geodesic_uniforms() -> void - { - struct CamUBO { - glm::vec3 pos; float p0; glm::vec3 right; float p1; - glm::vec3 up; float p2; glm::vec3 fwd; float p3; - float tan_half_fov; float aspect; uint32_t moving; int p4; - } cam_data{}; - glm::vec3 pos, fwd; - if (camera.get_camera_mode() == CameraMode::FPS) - { - pos = camera.get_position(); - fwd = camera.get_forward_direction(); - } - else - { - pos = camera.get_orbital_position(); - fwd = glm::normalize(camera.get_orbital_target() - pos); - } - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - cam_data.pos = pos; cam_data.right = right; cam_data.up = glm::cross(right, fwd); cam_data.fwd = fwd; - cam_data.tan_half_fov = (float)tan(glm::radians(bh_params.fov_degrees * 0.5f)); - cam_data.aspect = (float)GEO_HI_W / (float)GEO_HI_H; // 16:9, same for both targets - cam_data.moving = user_moving ? 1u : 0u; - if (cam_mapped) memcpy(cam_mapped, &cam_data, sizeof(cam_data)); - - // Integration budget drives how deep the geodesics are traced: too few - // steps and grazing rays exhaust the loop mid-lensing, so the shader - // paints them with the HDRI (Geodesic.slang) and the hole looks sliced - // off "up to a certain depth" -- and near face-on it vanishes entirely. - // Measured: at ~8000 steps the disk disappears at steep poses, ~15000 is - // needed for it to resolve, INDEPENDENT of resolution. So we DON'T drop - // steps while moving (that would make the hole flicker out mid-drag); - // responsiveness comes from the lower-res target instead (see record). - // Sourced from settings.toml (max_steps_static), capped GPU-safe. - constexpr int kStepCeil = 15000; - struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim; - sim.steps_static = std::clamp(bh_params.quality_steps, 1000, kStepCeil); - sim.steps_moving = sim.steps_static; // same budget both frames; resolution is the lever - sim.early_exit = SettingsManager::get_early_exit_distance(); - sim.time = (float)(glfwGetTime() - start_time); - if (sim_mapped) memcpy(sim_mapped, &sim, sizeof(sim)); - - // Disk UBO, refilled live from the UI-tunable parameters. Layout matches - // the shader Disk struct: r1, r2, turbulence, slab thickness, brightness, - // temperature. Radii are in Schwarzschild radii (x SagA_rs). - const float SagA_rs = 1.269e10f; - float disk_data[8] = { - std::max(bh_params.disk_inner_rs, 3.0f) * SagA_rs, - std::max(bh_params.disk_outer_rs, bh_params.disk_inner_rs + 0.5f) * SagA_rs, - std::max(bh_params.turbulence, 0.0f), - SagA_rs * 0.1f, // slab half-thickness (fixed) - std::max(bh_params.brightness, 0.0f), - std::max(bh_params.temperature, 1000.0f), - 0.0f, 0.0f, - }; - if (disk_mapped) memcpy(disk_mapped, disk_data, sizeof(disk_data)); - } - - static double g_ScrollAccum = 0.0; - static GLFWscrollfun g_PrevScroll = nullptr; - static void donut_vk_scroll_callback(GLFWwindow* w, double x, double y) - { - if (g_PrevScroll) g_PrevScroll(w, x, y); // keep ImGui's scroll handling intact - g_ScrollAccum += y; - } - - auto VulkanRenderer::Impl::process_input() -> void - { - bool over_ui = imgui_init && ImGui::GetIO().WantCaptureMouse; - - double now = glfwGetTime(); - float dt = last_frame_time > 0.0 ? (float)(now - last_frame_time) : 0.0f; - last_frame_time = now; - - double mx = 0, my = 0; - glfwGetCursorPos(window, &mx, &my); - bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; - - if (!scene_mode && camera.get_camera_mode() == CameraMode::FPS) - { - // Left-drag looks around (screen-up looks up); WASD/QE move. - if (left_down && !left_was_down) { fps_last_x = mx; fps_last_y = my; } - if (left_down && !over_ui) - camera.on_mouse_move(float(mx - fps_last_x), float(fps_last_y - my)); - fps_last_x = mx; fps_last_y = my; - left_was_down = left_down; - - bool over_kb = imgui_init && ImGui::GetIO().WantCaptureKeyboard; - bool moved = false; - 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) { camera.move_forward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { camera.move_backward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { camera.move_left(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { camera.move_right(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { camera.move_up(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { camera.move_down(mdt); moved = true; } - } - g_ScrollAccum = 0.0; // scroll unused in free-fly - user_moving = moved || (left_down && !over_ui); - } - else - { - Camera& cam = scene_mode ? scene_camera : camera; - if (left_down && !left_was_down && !over_ui) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); - else if (!left_down && left_was_down) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); - left_was_down = left_down; - - cam.process_orbital_mouse_move(mx, my); // orbits only while dragging - - double scroll = g_ScrollAccum; g_ScrollAccum = 0.0; - if (scroll != 0.0 && !over_ui) - cam.process_orbital_scroll(0.0, scroll); - - user_moving = cam.is_dragging() || cam.is_panning(); - } - } - - auto VulkanRenderer::Impl::destroy_geodesic_resources() -> void - { - if (grid_pipeline) vkDestroyPipeline(device, grid_pipeline, nullptr); - if (grid_pipeline_layout) vkDestroyPipelineLayout(device, grid_pipeline_layout, nullptr); - if (grid_pool) vkDestroyDescriptorPool(device, grid_pool, nullptr); - if (grid_set_layout) vkDestroyDescriptorSetLayout(device, grid_set_layout, nullptr); - if (grid_ubo_mapped) { vkUnmapMemory(device, grid_ubo_mem); grid_ubo_mapped = nullptr; } - if (grid_ubo) vkDestroyBuffer(device, grid_ubo, nullptr); - if (grid_ubo_mem) vkFreeMemory(device, grid_ubo_mem, nullptr); - if (grid_vb) vkDestroyBuffer(device, grid_vb, nullptr); - if (grid_vb_mem) vkFreeMemory(device, grid_vb_mem, nullptr); - - if (sphere_pipeline) vkDestroyPipeline(device, sphere_pipeline, nullptr); - if (sphere_pipeline_layout) vkDestroyPipelineLayout(device, sphere_pipeline_layout, nullptr); - if (sphere_pool) vkDestroyDescriptorPool(device, sphere_pool, nullptr); - if (sphere_set_layout) vkDestroyDescriptorSetLayout(device, sphere_set_layout, nullptr); - if (sphere_ubo_mapped) { vkUnmapMemory(device, sphere_ubo_mem); sphere_ubo_mapped = nullptr; } - if (sphere_ubo) vkDestroyBuffer(device, sphere_ubo, nullptr); - if (sphere_ubo_mem) vkFreeMemory(device, sphere_ubo_mem, nullptr); - if (sphere_ib) vkDestroyBuffer(device, sphere_ib, nullptr); - if (sphere_ib_mem) vkFreeMemory(device, sphere_ib_mem, nullptr); - if (sphere_vb) vkDestroyBuffer(device, sphere_vb, nullptr); - if (sphere_vb_mem) vkFreeMemory(device, sphere_vb_mem, nullptr); - - if (skybox_pipeline) vkDestroyPipeline(device, skybox_pipeline, nullptr); - if (skybox_pipeline_layout) vkDestroyPipelineLayout(device, skybox_pipeline_layout, nullptr); - if (skybox_pool) vkDestroyDescriptorPool(device, skybox_pool, nullptr); - if (skybox_set_layout) vkDestroyDescriptorSetLayout(device, skybox_set_layout, nullptr); - if (skybox_ubo_mapped) { vkUnmapMemory(device, skybox_ubo_mem); skybox_ubo_mapped = nullptr; } - if (skybox_ubo) vkDestroyBuffer(device, skybox_ubo, nullptr); - if (skybox_ubo_mem) vkFreeMemory(device, skybox_ubo_mem, nullptr); - if (skybox_vb) vkDestroyBuffer(device, skybox_vb, nullptr); - if (skybox_vb_mem) vkFreeMemory(device, skybox_vb_mem, nullptr); - - if (present_pipeline) vkDestroyPipeline(device, present_pipeline, nullptr); - if (present_pipeline_layout) vkDestroyPipelineLayout(device, present_pipeline_layout, nullptr); - if (present_pool) vkDestroyDescriptorPool(device, present_pool, nullptr); - if (present_set_layout) vkDestroyDescriptorSetLayout(device, present_set_layout, nullptr); - if (present_sampler) vkDestroySampler(device, present_sampler, nullptr); - - if (geo_pipeline) vkDestroyPipeline(device, geo_pipeline, nullptr); - if (geo_pipeline_layout) vkDestroyPipelineLayout(device, geo_pipeline_layout, nullptr); - if (geo_pool) vkDestroyDescriptorPool(device, geo_pool, nullptr); - if (geo_set_layout) vkDestroyDescriptorSetLayout(device, geo_set_layout, nullptr); - if (quad_vb) vkDestroyBuffer(device, quad_vb, nullptr); - if (quad_vb_mem) vkFreeMemory(device, quad_vb_mem, nullptr); - if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr); - if (cube_view) vkDestroyImageView(device, cube_view, nullptr); - if (cube_image) vkDestroyImage(device, cube_image, nullptr); - if (cube_mem) vkFreeMemory(device, cube_mem, nullptr); - if (cam_mapped) { vkUnmapMemory(device, cam_mem); cam_mapped = nullptr; } - if (sim_mapped) { vkUnmapMemory(device, sim_mem); sim_mapped = nullptr; } - if (disk_mapped) { vkUnmapMemory(device, disk_mem); disk_mapped = nullptr; } - VkBuffer ubos[4] = { cam_buf, disk_buf, obj_buf, sim_buf }; - VkDeviceMemory umem[4] = { cam_mem, disk_mem, obj_mem, sim_mem }; - for (int i = 0; i < 4; ++i) { if (ubos[i]) vkDestroyBuffer(device, ubos[i], nullptr); if (umem[i]) vkFreeMemory(device, umem[i], nullptr); } - for (GeoTarget* t : { &geo_lo, &geo_hi }) - { - if (t->fb) vkDestroyFramebuffer(device, t->fb, nullptr); - if (t->view) vkDestroyImageView(device, t->view, nullptr); - if (t->image) vkDestroyImage(device, t->image, nullptr); - if (t->mem) vkFreeMemory(device, t->mem, nullptr); - } - if (geo_render_pass) vkDestroyRenderPass(device, geo_render_pass, nullptr); - } - - auto VulkanRenderer::Impl::record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool - { - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - if (scene_mode) - { - // Scene view: opaque sphere (writes depth) then the transparent grid on - // top, into a color+depth swapchain pass, then ImGui. - 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 }; - rpbi.renderPass = scene_render_pass; rpbi.framebuffer = scene_framebuffers[image_index]; - rpbi.renderArea = { { 0, 0 }, swapchain_extent }; - rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - - // Negative-height viewport flips Y so the scene reads like the GL path. - VkViewport gvp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 }; - VkRect2D gsc{ { 0, 0 }, swapchain_extent }; - vkCmdSetViewport(cmd, 0, 1, &gvp); - vkCmdSetScissor(cmd, 0, 1, &gsc); - - // Skybox background first (forces depth 1; no depth test/write). - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline_layout, 0, 1, &skybox_set, 0, nullptr); - VkDeviceSize skoff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &skybox_vb, &skoff); - vkCmdDraw(cmd, 36, 1, 0, 0); - - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline); - VkDeviceSize soff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &sphere_vb, &soff); - vkCmdBindIndexBuffer(cmd, sphere_ib, 0, VK_INDEX_TYPE_UINT32); - uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS); - for (uint32_t i = 0; i < obj_count; ++i) - { - uint32_t dyn = (uint32_t)((VkDeviceSize)i * sphere_ubo_stride); // select this object's UBO slot - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline_layout, 0, 1, &sphere_set, 1, &dyn); - vkCmdDrawIndexed(cmd, sphere_index_count, 1, 0, 0, 0); - } - - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline_layout, 0, 1, &grid_set, 0, nullptr); - VkDeviceSize goff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &grid_vb, &goff); - vkCmdDraw(cmd, grid_vertex_count, 1, 0, 0); - - if (draw_data) - ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); - vkCmdEndRenderPass(cmd); - VK_CHECK(vkEndCommandBuffer(cmd)); - return true; - } - - // Geodesic offscreen pass. Progressive resolution: low-res while the - // camera moves (4x fewer pixels -> responsive), high-res once it settles - // (resolves the photon ring). Step count is the same for both (the disk - // vanishes below ~15000 steps at steep poses), so resolution is the lever. - GeoTarget& gt = user_moving ? geo_lo : geo_hi; - VkClearValue geo_clear{}; geo_clear.color = { { 0, 0, 0, 1 } }; - VkRenderPassBeginInfo grp{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - grp.renderPass = geo_render_pass; grp.framebuffer = gt.fb; - grp.renderArea = { { 0, 0 }, { gt.w, gt.h } }; - grp.clearValueCount = 1; grp.pClearValues = &geo_clear; - vkCmdBeginRenderPass(cmd, &grp, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline); - VkViewport gvp{ 0, 0, (float)gt.w, (float)gt.h, 0, 1 }; VkRect2D gsc{ { 0, 0 }, { gt.w, gt.h } }; - vkCmdSetViewport(cmd, 0, 1, &gvp); vkCmdSetScissor(cmd, 0, 1, &gsc); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline_layout, 0, 1, &geo_set, 0, nullptr); - VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); - vkCmdDraw(cmd, 6, 1, 0, 0); - vkCmdEndRenderPass(cmd); - - // Swapchain pass: upscale the geodesic image, then the ImGui UI on top - VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; - rpbi.framebuffer = framebuffers[image_index]; - rpbi.renderArea = { { 0, 0 }, swapchain_extent }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &cv; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline); - // Negative-height viewport flips the geodesic image vertically so the scene - // reads the same as the OpenGL path (Vulkan's clip space is Y-down). Only - // this draw is affected; ImGui sets its own viewport. - VkViewport vp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 }; - VkRect2D scissor{ { 0, 0 }, swapchain_extent }; - vkCmdSetViewport(cmd, 0, 1, &vp); - vkCmdSetScissor(cmd, 0, 1, &scissor); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline_layout, 0, 1, >.present_set, 0, nullptr); - vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); - vkCmdDraw(cmd, 6, 1, 0, 0); - if (draw_data) - ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); - vkCmdEndRenderPass(cmd); - - VK_CHECK(vkEndCommandBuffer(cmd)); - return true; - } - - auto VulkanRenderer::Impl::cleanup_swapchain() -> void - { - destroy_scene_targets(); - for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr); - framebuffers.clear(); - for (auto iv : image_views) vkDestroyImageView(device, iv, nullptr); - image_views.clear(); - if (render_pass) { vkDestroyRenderPass(device, render_pass, nullptr); render_pass = VK_NULL_HANDLE; } - if (swapchain) { vkDestroySwapchainKHR(device, swapchain, nullptr); swapchain = VK_NULL_HANDLE; } - } - - auto VulkanRenderer::Impl::recreate_swapchain() -> bool - { - // Wait until the window has a non-zero size (e.g. after un-minimizing). - int w = 0, h = 0; - glfwGetFramebufferSize(window, &w, &h); - while (w == 0 || h == 0) - { - glfwGetFramebufferSize(window, &w, &h); - glfwWaitEvents(); - } - width = w; height = h; - vkDeviceWaitIdle(device); - - cleanup_swapchain(); - // render_finished are tied to image count; recreate below via sync if it changed. - if (!create_swapchain()) return false; - if (!create_image_views()) return false; - if (!create_render_pass()) return false; - if (!create_framebuffers())return false; - if (!create_scene_targets())return false; - images_in_flight.assign(images.size(), VK_NULL_HANDLE); - return true; - } - - VulkanRenderer::VulkanRenderer() { m_impl = new Impl(); } - VulkanRenderer::~VulkanRenderer() { shutdown(); delete m_impl; m_impl = nullptr; } - - auto VulkanRenderer::init(void* glfwWindow, int width, int height) -> bool - { - Impl& v = *m_impl; - v.window = (GLFWwindow*)glfwWindow; - v.width = width; v.height = height; - - if (!v.create_instance()) return false; - if (!v.pick_physical_and_device()) return false; - if (!v.create_swapchain()) return false; - if (!v.create_image_views()) return false; - if (!v.create_render_pass()) return false; - if (!v.create_framebuffers()) return false; - if (!v.create_scene_targets()) return false; - if (!v.create_command_buffers()) return false; - if (!v.create_sync_objects()) return false; - if (!v.create_geodesic_resources()) return false; - if (!v.create_present_resources()) return false; - if (!v.create_scene_resources()) return false; - - DONUT_INFO("Vulkan renderer ready: {} swapchain images, {}x{}", - (int)v.images.size(), v.swapchain_extent.width, v.swapchain_extent.height); - return true; - } - - auto VulkanRenderer::init_ui() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - 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; - VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &v.imgui_pool)); - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::StyleColorsDark(); - - ImGui_ImplGlfw_InitForVulkan(v.window, true); - g_PrevScroll = glfwSetScrollCallback(v.window, donut_vk_scroll_callback); // chain ImGui + camera zoom - ImGui_ImplVulkan_InitInfo info{}; - info.ApiVersion = VK_API_VERSION_1_2; - info.Instance = v.instance; - info.PhysicalDevice = v.physical; - info.Device = v.device; - info.QueueFamily = v.graphics_family; - info.Queue = v.graphics_queue; - info.DescriptorPool = v.imgui_pool; - info.RenderPass = v.render_pass; - info.MinImageCount = 2; - info.ImageCount = (uint32_t)v.images.size(); - info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; - if (!ImGui_ImplVulkan_Init(&info)) - { - DONUT_ERROR("Vulkan: ImGui_ImplVulkan_Init failed"); - return false; - } - - v.imgui_init = true; - DONUT_INFO("Vulkan: ImGui backend initialized"); - return true; - } - - auto VulkanRenderer::resize(int width, int height) -> void - { - m_impl->framebuffer_resized = true; - m_impl->width = width; m_impl->height = height; - } - - auto VulkanRenderer::set_hdri(const std::string& path) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE || v.geo_set == VK_NULL_HANDLE) return; - if (v.hdri_path == path) return; - v.rebuild_hdri_cubemap(path.c_str()); - } - - auto VulkanRenderer::current_hdri() const -> const std::string& - { - return m_impl->hdri_path; - } - - auto VulkanRenderer::set_free_fly(bool enabled) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return; - const bool is_fps = v.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 = v.camera.get_orbital_position(); - glm::vec3 fwd = glm::normalize(v.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)); - v.camera.set_camera_mode(CameraMode::FPS); - v.camera.set_movement_speed(2.0e10f); // scene spans ~1e11 units - v.camera.set_mouse_sensitivity(0.15f); - v.camera.set_position(pos); - v.camera.set_rotation(glm::vec3(pitch, yaw, 0.0f)); - } - else - { - v.camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact - } - } - - auto VulkanRenderer::is_free_fly() const -> bool - { - return m_impl->camera.get_camera_mode() == CameraMode::FPS; - } - - auto VulkanRenderer::set_scene_mode(bool enabled) -> void - { - if (m_impl) m_impl->scene_mode = enabled; - } - - auto VulkanRenderer::is_scene_mode() const -> bool - { - return m_impl && m_impl->scene_mode; - } - - auto VulkanRenderer::scene_objects() -> std::vector<SceneObject>& - { - return m_impl->scene_objects; - } - - auto VulkanRenderer::set_selected_object(int index) -> void - { - if (m_impl) m_impl->selected_object = index; - } - - auto VulkanRenderer::black_hole_params() -> BlackHoleParams& - { - return m_impl->bh_params; - } - - auto VulkanRenderer::reset_camera() -> void - { - if (!m_impl) return; - Camera& cam = m_impl->camera; - cam.set_camera_mode(CameraMode::Orbital); // also flips is_free_fly() back - cam.set_orbital_radius(4e11); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - } - - auto VulkanRenderer::device_name() const -> const std::string& - { - return m_impl->gpu_name; - } - - auto VulkanRenderer::draw_frame(const glm::vec4& clear_color, const std::function<void()>& build_ui) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return; - - ImDrawData* draw_data = nullptr; - if (v.imgui_init) - { - ImGui_ImplVulkan_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - if (build_ui) build_ui(); - ImGui::Render(); - draw_data = ImGui::GetDrawData(); - } - - vkWaitForFences(v.device, 1, &v.in_flight[v.current_frame], VK_TRUE, UINT64_MAX); - - uint32_t image_index = 0; - VkResult r = vkAcquireNextImageKHR(v.device, v.swapchain, UINT64_MAX, - v.image_available[v.current_frame], VK_NULL_HANDLE, &image_index); - if (r == VK_ERROR_OUT_OF_DATE_KHR) { v.recreate_swapchain(); return; } - if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan: acquire failed ({})", (int)r); return; } - - if (v.images_in_flight[image_index] != VK_NULL_HANDLE) - vkWaitForFences(v.device, 1, &v.images_in_flight[image_index], VK_TRUE, UINT64_MAX); - v.images_in_flight[image_index] = v.in_flight[v.current_frame]; - - // The geodesic offscreen image is shared across frames in flight; wait for - // the previous frame to finish reading it before overwriting it this frame. - if (v.geo_in_use != VK_NULL_HANDLE) - vkWaitForFences(v.device, 1, &v.geo_in_use, VK_TRUE, UINT64_MAX); - v.process_input(); - if (v.scene_mode) v.update_scene_uniforms(); else v.update_geodesic_uniforms(); - - vkResetCommandBuffer(v.command_buffers[v.current_frame], 0); - if (!v.record_command_buffer(v.command_buffers[v.current_frame], image_index, clear_color, draw_data)) return; - - VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.waitSemaphoreCount = 1; - submit.pWaitSemaphores = &v.image_available[v.current_frame]; - submit.pWaitDstStageMask = &wait_stage; - submit.commandBufferCount = 1; - submit.pCommandBuffers = &v.command_buffers[v.current_frame]; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &v.render_finished[image_index]; - - vkResetFences(v.device, 1, &v.in_flight[v.current_frame]); - if (vkQueueSubmit(v.graphics_queue, 1, &submit, v.in_flight[v.current_frame]) != VK_SUCCESS) - { DONUT_ERROR("Vulkan: queue submit failed"); return; } - v.geo_in_use = v.in_flight[v.current_frame]; - - VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; - present.waitSemaphoreCount = 1; - present.pWaitSemaphores = &v.render_finished[image_index]; - present.swapchainCount = 1; - present.pSwapchains = &v.swapchain; - present.pImageIndices = &image_index; - r = vkQueuePresentKHR(v.present_queue, &present); - if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || v.framebuffer_resized) - { - v.framebuffer_resized = false; - v.recreate_swapchain(); - } - - v.current_frame = (v.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; - } - - auto VulkanRenderer::shutdown() -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) { if (v.instance && v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } return; } - - vkDeviceWaitIdle(v.device); - v.destroy_geodesic_resources(); - if (v.imgui_init) - { - ImGui_ImplVulkan_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - v.imgui_init = false; - } - if (v.imgui_pool) { vkDestroyDescriptorPool(v.device, v.imgui_pool, nullptr); v.imgui_pool = VK_NULL_HANDLE; } - for (auto s : v.render_finished) vkDestroySemaphore(v.device, s, nullptr); - for (auto s : v.image_available) vkDestroySemaphore(v.device, s, nullptr); - for (auto f : v.in_flight) vkDestroyFence(v.device, f, nullptr); - v.render_finished.clear(); v.image_available.clear(); v.in_flight.clear(); - if (v.command_pool) { vkDestroyCommandPool(v.device, v.command_pool, nullptr); v.command_pool = VK_NULL_HANDLE; } - v.cleanup_swapchain(); - vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; - if (v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } - if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } - } -} diff --git a/src/platform/vulkan/vulkan_renderer.h b/src/platform/vulkan/vulkan_renderer.h deleted file mode 100644 index d583f07..0000000 --- a/src/platform/vulkan/vulkan_renderer.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "rendering/renderer_backend.h" - -// Live-window Vulkan backend: owns the instance, surface, device, swapchain, -// render pass, framebuffers and per-frame synchronization, and drives the -// acquire -> record -> submit -> present loop. Implements the shared -// RendererBackend interface (SceneObject / BlackHoleParams live there). Pure-C++ -// header (no vulkan.h / glfw leak); the GLFW window is passed as an opaque handle. -namespace Donut -{ - // Must be called BEFORE glfwInit() when the Vulkan API is selected: points - // GLFW at the loader the app links against (GLFW's own dlopen fails on - // macOS/Homebrew) and configures the MoltenVK ICD / layer paths. - auto vulkan_prepare_glfw() -> void; - - class VulkanRenderer : public RendererBackend - { - public: - VulkanRenderer(); - ~VulkanRenderer() override; - - // glfwWindow must be a GLFW window created with GLFW_NO_API. - auto init(void* glfwWindow, int width, int height) -> bool override; - auto init_ui() -> bool override; - auto shutdown() -> void override; - auto resize(int width, int height) -> void override; - - auto draw_frame(const glm::vec4& clear_color, - const std::function<void()>& build_ui) -> void override; - - auto set_scene_mode(bool enabled) -> void override; - auto is_scene_mode() const -> bool override; - auto set_free_fly(bool enabled) -> void override; // seeds the fly pose from the orbital framing - auto is_free_fly() const -> bool override; - auto reset_camera() -> void override; - - auto set_hdri(const std::string& path) -> void override; // rebuilds the cubemap (device-idle) - auto current_hdri() const -> const std::string& override; - auto scene_objects() -> std::vector<SceneObject>& override; - auto set_selected_object(int index) -> void override; - auto black_hole_params() -> BlackHoleParams& override; - - auto device_name() const -> const std::string& override; - - private: - struct Impl; - Impl* m_impl = nullptr; - }; -} diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp new file mode 100644 index 0000000..46b002c --- /dev/null +++ b/src/rendering/black_hole_renderer.cpp @@ -0,0 +1,144 @@ +#include "black_hole_renderer.h" + +#include "core/log.h" +#include "core/settings_manager.h" + +#include <algorithm> +#include <cstdint> +#include <cstring> + +namespace Donut +{ + using namespace RHI; + + namespace + { + constexpr float SagA_rs = 1.269e10f; // Schwarzschild radius of the modelled hole + + struct CamUBO { + glm::vec3 pos; float p0; glm::vec3 right; float p1; + glm::vec3 up; float p2; glm::vec3 fwd; float p3; + float tan_half_fov; float aspect; uint32_t moving; int p4; + }; + struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; }; + } + + auto BlackHoleRenderer::init(RHI::Device& device) -> bool + { + m_device = &device; + + m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, false, Filter::Linear); + m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, false, Filter::Linear); + + // Fullscreen quad shared by the geodesic and present passes: pos.xy + uv. + const float quad[] = { + -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, + -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, + }; + m_quad_vb = device.create_buffer(BufferType::Vertex, sizeof(quad), quad); + + m_cam_ubo = device.create_buffer(BufferType::Uniform, 128); + m_disk_ubo = device.create_buffer(BufferType::Uniform, 32); + m_obj_ubo = device.create_buffer(BufferType::Uniform, 800); + m_sim_ubo = device.create_buffer(BufferType::Uniform, 16); + + // The single "object" is the black hole itself (position 0, radius = r_s). + std::vector<uint8_t> obj(800, 0); + int num_objects = 1; std::memcpy(obj.data() + 0, &num_objects, 4); + float pos_radius[4] = { 0, 0, 0, SagA_rs }; std::memcpy(obj.data() + 16, pos_radius, 16); + float color[4] = { 0, 0, 0, 1 }; std::memcpy(obj.data() + 272, color, 16); + m_obj_ubo->update(obj.data(), obj.size()); + + { + PipelineDesc d; + d.shader = "geodesic"; + d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } }; + d.resources = { + { ResourceKind::UniformBuffer, 0, "Camera" }, + { ResourceKind::UniformBuffer, 1, "Disk" }, + { ResourceKind::UniformBuffer, 2, "Objects" }, + { ResourceKind::UniformBuffer, 3, "Simulation" }, + { ResourceKind::Texture, 4, "u_HDRIEnvironment" }, + }; + d.topology = Topology::Triangles; + d.has_depth = false; // color-only off-screen target + m_geo_pipeline = device.create_pipeline(d); + } + { + PipelineDesc d; + d.shader = "textured_quad"; + d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } }; + d.resources = { { ResourceKind::Texture, 0, "u_ScreenTexture" } }; + d.topology = Topology::Triangles; + d.has_depth = true; // swapchain target carries depth (unused here) + m_present_pipeline = device.create_pipeline(d); + } + + DONUT_INFO("BlackHoleRenderer ready ({}x{} moving / {}x{} settled)", + GEO_LO_W, GEO_LO_H, GEO_HI_W, GEO_HI_H); + return true; + } + + auto BlackHoleRenderer::render_geodesic(RHI::CommandList& cmd, const GeodesicView& view, + const BlackHoleParams& params, RHI::Texture* cubemap) -> void + { + CamUBO cam{}; + cam.pos = view.position; cam.right = view.right; cam.up = view.up; cam.fwd = view.forward; + cam.tan_half_fov = view.tan_half_fov; + cam.aspect = view.aspect; + cam.moving = view.moving ? 1u : 0u; + m_cam_ubo->update(&cam, sizeof(cam)); + + // Same integration budget whether moving or settled (the disk vanishes at + // steep poses below ~15000 steps); responsiveness comes from the lower-res + // target instead. Sourced from the UI, clamped GPU-safe. + constexpr int kStepCeil = 15000; + SimUBO sim{}; + sim.steps_static = std::clamp(params.quality_steps, 1000, kStepCeil); + sim.steps_moving = sim.steps_static; + sim.early_exit = SettingsManager::get_early_exit_distance(); + sim.time = view.time; + m_sim_ubo->update(&sim, sizeof(sim)); + + // Disk struct: r_in, r_out, turbulence, slab half-thickness, brightness, + // temperature (radii in Schwarzschild radii). + float disk[8] = { + std::max(params.disk_inner_rs, 3.0f) * SagA_rs, + std::max(params.disk_outer_rs, params.disk_inner_rs + 0.5f) * SagA_rs, + std::max(params.turbulence, 0.0f), + SagA_rs * 0.1f, + std::max(params.brightness, 0.0f), + std::max(params.temperature, 1000.0f), + 0.0f, 0.0f, + }; + m_disk_ubo->update(disk, sizeof(disk)); + + // Progressive resolution: small target while moving, large once settled. + RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get(); + m_last_target = target; + + cmd.begin_render_pass(target, glm::vec4(0, 0, 0, 1)); + cmd.set_viewport(0, 0, target->width(), target->height(), false); + cmd.bind_pipeline(m_geo_pipeline.get()); + cmd.bind_uniform(0, m_cam_ubo.get()); + cmd.bind_uniform(1, m_disk_ubo.get()); + cmd.bind_uniform(2, m_obj_ubo.get()); + cmd.bind_uniform(3, m_sim_ubo.get()); + cmd.bind_texture(4, cubemap); + cmd.bind_vertex_buffer(m_quad_vb.get()); + cmd.draw(6); + cmd.end_render_pass(); + } + + auto BlackHoleRenderer::blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void + { + if (!m_last_target) return; + // flip_y matches the shared top-left orientation (Vulkan flips via a + // negative-height viewport; GL is a no-op). + cmd.set_viewport(0, 0, fb_width, fb_height, true); + cmd.bind_pipeline(m_present_pipeline.get()); + cmd.bind_texture(0, m_last_target->color_texture()); + cmd.bind_vertex_buffer(m_quad_vb.get()); + cmd.draw(6); + } +} diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h new file mode 100644 index 0000000..b9c158b --- /dev/null +++ b/src/rendering/black_hole_renderer.h @@ -0,0 +1,55 @@ +#pragma once + +#include "rhi.h" +#include "renderer_backend.h" // BlackHoleParams +#include <glm/glm.hpp> + +namespace Donut +{ + // The black-hole view, written ONCE against the RHI. Each frame it ray-traces + // the geodesic shader into an off-screen target (progressive resolution: a + // small target while the camera moves, a larger one once it settles) and then + // upscales that target onto the swapchain. The Application owns the camera and + // the HDRI cubemap and passes them in. + struct GeodesicView + { + glm::vec3 position, right, up, forward; + float tan_half_fov = 1.0f; + float aspect = 16.0f / 9.0f; + bool moving = false; + float time = 0.0f; + }; + + class BlackHoleRenderer + { + public: + static constexpr int GEO_LO_W = 480, GEO_LO_H = 270; + static constexpr int GEO_HI_W = 960, GEO_HI_H = 540; + + auto init(RHI::Device& device) -> bool; + + // Off-screen geodesic pass (opens/closes its own render pass). Call before + // the swapchain pass; `cubemap` is the shared HDRI environment. + auto render_geodesic(RHI::CommandList& cmd, const GeodesicView& view, + const BlackHoleParams& params, RHI::Texture* cubemap) -> void; + + // Upscales the geodesic target onto the swapchain (call inside the open + // swapchain render pass, before ImGui). + auto blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void; + + private: + RHI::Device* m_device = nullptr; + + Ref<RHI::Pipeline> m_geo_pipeline; + Ref<RHI::RenderTarget> m_geo_lo; + Ref<RHI::RenderTarget> m_geo_hi; + Ref<RHI::Buffer> m_quad_vb; + Ref<RHI::Buffer> m_cam_ubo; + Ref<RHI::Buffer> m_disk_ubo; + Ref<RHI::Buffer> m_obj_ubo; + Ref<RHI::Buffer> m_sim_ubo; + + Ref<RHI::Pipeline> m_present_pipeline; + RHI::RenderTarget* m_last_target = nullptr; // target chosen this frame + }; +} diff --git a/src/rendering/framebuffer.cpp b/src/rendering/framebuffer.cpp deleted file mode 100644 index 3da4b48..0000000 --- a/src/rendering/framebuffer.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "framebuffer.h" -#include "core/log.h" - -#include <glad/glad.h> - -namespace Donut -{ - namespace Utils - { - static GLenum TextureTarget(bool multisampled) - { - return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; - } - - static void bind_texture(bool multisampled, uint32_t id) - { - glBindTexture(TextureTarget(multisampled), id); - } - - static void CreateTextures(bool multisampled, uint32_t* out_id, uint32_t count) - { - // glCreateTextures is 4.5 DSA; macOS caps at 4.1. Callers bind each - // texture (with the correct target) before use. - glGenTextures(count, out_id); - } - - static void AttachColorTexture(uint32_t id, int samples, GLenum internal_format, GLenum format, uint32_t width, uint32_t height, int index) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internal_format, width, height, GL_FALSE); - } - else - { - // Integer color formats require an integer pixel type even when - // data is null, or macOS's strict core profile rejects the call. - GLenum type = (format == GL_RED_INTEGER) ? GL_INT : GL_UNSIGNED_BYTE; - glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, type, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0); - } - - static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachment_type, uint32_t width, uint32_t height) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE); - } - else - { - // glTexStorage2D is 4.2; use mutable storage for macOS (4.1). - GLenum depth_format = (format == GL_DEPTH24_STENCIL8) ? GL_DEPTH_STENCIL : GL_DEPTH_COMPONENT; - GLenum depth_type = (format == GL_DEPTH24_STENCIL8) ? GL_UNSIGNED_INT_24_8 : GL_FLOAT; - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, depth_format, depth_type, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_type, TextureTarget(multisampled), id, 0); - } - - static bool IsDepthFormat(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: return true; - } - return false; - } - - static GLenum DonutFBTextureFormatToGL(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::RGBA8: return GL_RGBA8; - case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER; - } - - return 0; - } - } - - Framebuffer::Framebuffer(const FramebufferSpecification& spec) - : m_specification(spec) - { - for (auto spec : m_specification.attachments.attachments) - { - if (!Utils::IsDepthFormat(spec.texture_format)) - m_color_attachment_specifications.emplace_back(spec); - else - m_depth_attachment_specification = spec; - } - - invalidate(); - } - - Framebuffer::~Framebuffer() - { - glDeleteFramebuffers(1, &m_renderer_id); - glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data()); - glDeleteTextures(1, &m_depth_attachment); - } - - auto Framebuffer::invalidate() -> void - { - if (m_renderer_id) - { - glDeleteFramebuffers(1, &m_renderer_id); - glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data()); - glDeleteTextures(1, &m_depth_attachment); - - m_color_attachments.clear(); - m_depth_attachment = 0; - } - - glGenFramebuffers(1, &m_renderer_id); // glCreateFramebuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - - bool multisample = m_specification.Samples > 1; - - if (m_color_attachment_specifications.size()) - { - m_color_attachments.resize(m_color_attachment_specifications.size()); - Utils::CreateTextures(multisample, m_color_attachments.data(), static_cast<uint32_t>(m_color_attachments.size())); - - for (size_t i = 0; i < m_color_attachments.size(); i++) - { - Utils::bind_texture(multisample, m_color_attachments[i]); - switch (m_color_attachment_specifications[i].texture_format) - { - case FramebufferTextureFormat::RGBA8: - Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_RGBA8, GL_RGBA, m_specification.Width, m_specification.Height, static_cast<int>(i)); - break; - case FramebufferTextureFormat::RED_INTEGER: - Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_R32I, GL_RED_INTEGER, m_specification.Width, m_specification.Height, static_cast<int>(i)); - break; - } - } - } - - if (m_depth_attachment_specification.texture_format != FramebufferTextureFormat::None) - { - Utils::CreateTextures(multisample, &m_depth_attachment, 1); - Utils::bind_texture(multisample, m_depth_attachment); - switch (m_depth_attachment_specification.texture_format) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: - Utils::AttachDepthTexture(m_depth_attachment, m_specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_specification.Width, m_specification.Height); - break; - } - } - - if (m_color_attachments.size() > 1) - { - GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; - glDrawBuffers(static_cast<GLsizei>(m_color_attachments.size()), buffers); - } - else if (m_color_attachments.empty()) - glDrawBuffer(GL_NONE); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - { - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - switch (status) - { - case GL_FRAMEBUFFER_UNDEFINED: - DONUT_ERROR("Framebuffer is undefined"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - DONUT_ERROR("Framebuffer has incomplete attachment"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - DONUT_ERROR("Framebuffer is missing attachment"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: - DONUT_ERROR("Framebuffer has incomplete draw buffer"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: - DONUT_ERROR("Framebuffer has incomplete read buffer"); - break; - case GL_FRAMEBUFFER_UNSUPPORTED: - DONUT_ERROR("Framebuffer format is unsupported"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: - DONUT_ERROR("Framebuffer has incomplete multisample"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: - DONUT_ERROR("Framebuffer has incomplete layer targets"); - break; - default: - DONUT_ERROR("Framebuffer is incomplete (unknown error: {})", status); - break; - } - } - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Framebuffer::bind() -> void - { - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - glViewport(0, 0, m_specification.Width, m_specification.Height); - } - - auto Framebuffer::unbind() -> void - { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Framebuffer::resize(uint32_t width, uint32_t height) -> void - { - m_specification.Width = width; - m_specification.Height = height; - - invalidate(); - } - - auto Framebuffer::read_pixel(uint32_t attachment_index, int x, int y) -> int - { - glReadBuffer(GL_COLOR_ATTACHMENT0 + attachment_index); - int pixel_data; - glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixel_data); - return pixel_data; - } - - auto Framebuffer::clear_attachment(uint32_t attachment_index, int value) -> void - { - // glClearTexImage is 4.4 and unavailable on macOS. clear the integer - // attachment by binding this framebuffer and clearing its draw buffer. - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - glClearBufferiv(GL_COLOR, static_cast<GLint>(attachment_index), &value); - } - - auto Framebuffer::create(const FramebufferSpecification& spec) -> Ref<Framebuffer> - { - return create_ref<Framebuffer>(spec); - } -}; diff --git a/src/rendering/framebuffer.h b/src/rendering/framebuffer.h deleted file mode 100644 index cf1fbef..0000000 --- a/src/rendering/framebuffer.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "texture.h" - -#include <vector> - -namespace Donut -{ - enum class FramebufferTextureFormat - { - None = 0, - - RGBA8, - RED_INTEGER, - - DEPTH24STENCIL8, - - Depth = DEPTH24STENCIL8 - }; - - struct FramebufferTextureSpecification - { - FramebufferTextureSpecification() = default; - FramebufferTextureSpecification(FramebufferTextureFormat format) - : texture_format(format) {} - - FramebufferTextureFormat texture_format = FramebufferTextureFormat::None; - }; - - struct FramebufferAttachmentSpecification - { - FramebufferAttachmentSpecification() = default; - FramebufferAttachmentSpecification(std::initializer_list<FramebufferTextureSpecification> attachments) - : attachments(attachments) {} - - std::vector<FramebufferTextureSpecification> attachments; - }; - - struct FramebufferSpecification - { - uint32_t Width = 0, Height = 0; - FramebufferAttachmentSpecification attachments; - uint32_t Samples = 1; - - bool SwapChainTarget = false; - }; - - class Framebuffer - { - public: - Framebuffer(const FramebufferSpecification& spec); - ~Framebuffer(); - - auto bind() -> void; - auto unbind() -> void; - - auto resize(uint32_t width, uint32_t height) -> void; - auto read_pixel(uint32_t attachment_index, int x, int y) -> int; - - auto clear_attachment(uint32_t attachment_index, int value) -> void; - auto get_color_attachment_renderer_id(uint32_t index = 0) const -> uint32_t { return m_color_attachments[index]; } - - auto get_specification() const -> const FramebufferSpecification& { return m_specification; } - - static auto create(const FramebufferSpecification& spec) -> Ref<Framebuffer>; - - private: - auto invalidate() -> void; - - uint32_t m_renderer_id = 0; - FramebufferSpecification m_specification; - - std::vector<FramebufferTextureSpecification> m_color_attachment_specifications; - FramebufferTextureSpecification m_depth_attachment_specification = FramebufferTextureFormat::None; - - std::vector<uint32_t> m_color_attachments; - uint32_t m_depth_attachment = 0; - }; -}; diff --git a/src/rendering/index_buffer.cpp b/src/rendering/index_buffer.cpp deleted file mode 100644 index 2c933e0..0000000 --- a/src/rendering/index_buffer.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "index_buffer.h" - -#include <glad/glad.h> - -namespace Donut -{ - IndexBuffer::IndexBuffer(const uint32_t* indices, uint32_t count) - : m_count(count) - { - glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW); - } - - IndexBuffer::~IndexBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto IndexBuffer::bind() const -> void - { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id); - } - - auto IndexBuffer::unbind() const -> void - { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - } - - auto IndexBuffer::create(const uint32_t* indices, uint32_t count) -> IndexBuffer* - { - return new IndexBuffer(indices, count); - } -}; diff --git a/src/rendering/index_buffer.h b/src/rendering/index_buffer.h deleted file mode 100644 index 0b1b2b6..0000000 --- a/src/rendering/index_buffer.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include <cstdint> - -namespace Donut -{ - class IndexBuffer - { - public: - IndexBuffer(const uint32_t* indices, uint32_t count); - ~IndexBuffer(); - - auto bind() const -> void; - auto unbind() const -> void; - auto get_count() const -> uint32_t { return m_count; } - - static auto create(const uint32_t* indices, uint32_t count) -> IndexBuffer*; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_count = 0; - }; -}; diff --git a/src/rendering/render_api.h b/src/rendering/render_api.h new file mode 100644 index 0000000..62742c9 --- /dev/null +++ b/src/rendering/render_api.h @@ -0,0 +1,25 @@ +#pragma once + +namespace Donut +{ + // Which graphics backend the app runs on, selected once at startup (from + // settings) before the window is created. The rendering itself goes through + // the RHI (rendering/rhi.h) and the shared renderers; this is only the + // backend selector the window + application branch on. + class RendererAPI + { + public: + enum class API + { + None = 0, + OpenGL = 1, + Vulkan = 2, + }; + + static auto get_api() -> API { return s_api; } + static auto set_api(API api) -> void { s_api = api; } + + private: + inline static API s_api = API::None; + }; +} diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp deleted file mode 100644 index 1c82447..0000000 --- a/src/rendering/renderer.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "renderer.h" - -#include "core/log.h" - -#include <glad/glad.h> -#include <GLFW/glfw3.h> -#include <glm/gtc/matrix_transform.hpp> - -namespace Donut -{ - RendererAPI::API RendererAPI::s_api = RendererAPI::API::OpenGL; - - auto RendererAPI::create() -> Scope<RendererAPI> - { - return create_scope<RendererAPI>(); - } - - auto RendererAPI::init() -> void - { - if (!glfwGetCurrentContext()) - { - DONUT_ERROR("No OpenGL context is current! Cannot initialize GLAD."); - return; - } - - if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) - { - DONUT_ERROR("Failed to initialize GLAD!"); - return; - } - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - } - - auto RendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void - { - glViewport(x, y, width, height); - } - - auto RendererAPI::set_clear_color(const glm::vec4& color) -> void - { - glClearColor(color.r, color.g, color.b, color.a); - } - - auto RendererAPI::clear() -> void - { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - - auto RendererAPI::enable_depth_test() -> void - { - glEnable(GL_DEPTH_TEST); - } - - auto RendererAPI::disable_depth_test() -> void - { - glDisable(GL_DEPTH_TEST); - } - - auto RendererAPI::set_face_culling(bool enabled) -> void - { - if (enabled) - { - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - } - else - glDisable(GL_CULL_FACE); - } - - auto RendererAPI::enable_blending() -> void - { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } - - auto RendererAPI::disable_blending() -> void - { - glDisable(GL_BLEND); - } - - auto RendererAPI::draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void - { - uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count(); - glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr); - glBindTexture(GL_TEXTURE_2D, 0); - } - - auto RendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void - { - glDrawArrays(GL_TRIANGLES, first, vertex_count); - } - - auto RendererAPI::draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void - { - uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count(); - glDrawElements(GL_LINES, count, GL_UNSIGNED_INT, nullptr); - } - - auto RendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void - { - glActiveTexture(GL_TEXTURE0 + slot); - glBindTexture(GL_TEXTURE_2D, texture_id); - } - - auto RendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void - { - // Image load/store is OpenGL 4.2; the pointer is null on macOS (4.1). - if (glBindImageTexture == nullptr) - return; - glBindImageTexture(slot, texture_id, 0, GL_FALSE, 0, - read_only ? GL_READ_ONLY : GL_WRITE_ONLY, GL_RGBA8); - } - - auto RendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void - { - glReadPixels(x, y, width, height, format, type, pixels); - } - - auto Renderer::init() -> void - { - RenderCommand::init(); - RenderCommand::enable_depth_test(); - } - - auto Renderer::shutdown() -> void - { - } - - auto Renderer::on_window_resize(uint32_t width, uint32_t height) -> void - { - RenderCommand::set_viewport(0, 0, width, height); - } - - auto Renderer::submit(const Ref<Shader>& shader, - const Ref<VertexArray>& vertex_array, - const glm::mat4& transform, - const glm::mat4& view_projection) -> void - { - shader->bind(); - shader->set_mat4("u_ViewProjection", view_projection); - shader->set_mat4("u_Transform", transform); - - vertex_array->bind(); - RenderCommand::draw_indexed(vertex_array); - } - - Scope<RendererAPI> RenderCommand::s_renderer_api = RendererAPI::create(); - - auto Renderer::set_clear_color(const glm::vec4& color) -> void - { - RenderCommand::set_clear_color(color); - } - - auto Renderer::clear() -> void - { - RenderCommand::clear(); - } -}; diff --git a/src/rendering/renderer.h b/src/rendering/renderer.h deleted file mode 100644 index d7855a4..0000000 --- a/src/rendering/renderer.h +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "vertex_array.h" -#include "shader.h" -#include "framebuffer.h" - -#include <glm/glm.hpp> - -namespace Donut -{ - // Single concrete backend: the OpenGL renderer. (Vulkan runs through its own - // dedicated VulkanRenderer, not this immediate-mode command layer.) - class RendererAPI - { - public: - enum class API - { - None = 0, - OpenGL = 1, - Vulkan = 2, - }; - - public: - RendererAPI() = default; - ~RendererAPI() = default; - - auto init() -> void; - auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void; - auto set_clear_color(const glm::vec4& color) -> void; - auto clear() -> void; - auto enable_depth_test() -> void; - auto disable_depth_test() -> void; - auto set_face_culling(bool enabled) -> void; - auto enable_blending() -> void; - auto disable_blending() -> void; - - auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void; - auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void; - auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void; - auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void; - auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void; - auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void; - - inline static auto get_api() -> API { return s_api; } - inline static auto set_api(API api) -> void { s_api = api; } - static auto create() -> Scope<RendererAPI>; - - private: - static API s_api; - }; - - class RenderCommand - { - public: - inline static auto init() -> void - { - s_renderer_api->init(); - } - - inline static auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void - { - s_renderer_api->set_viewport(x, y, width, height); - } - - inline static auto set_clear_color(const glm::vec4& color) -> void - { - s_renderer_api->set_clear_color(color); - } - - inline static auto clear() -> void - { - s_renderer_api->clear(); - } - - inline static auto enable_depth_test() -> void - { - s_renderer_api->enable_depth_test(); - } - - inline static auto disable_depth_test() -> void - { - s_renderer_api->disable_depth_test(); - } - - inline static auto set_face_culling(bool enabled) -> void - { - s_renderer_api->set_face_culling(enabled); - } - - inline static auto enable_blending() -> void - { - s_renderer_api->enable_blending(); - } - - inline static auto disable_blending() -> void - { - s_renderer_api->disable_blending(); - } - - inline static auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void - { - s_renderer_api->draw_indexed(vertex_array, index_count); - } - - inline static auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void - { - s_renderer_api->draw_arrays(vertex_count, first); - } - - inline static auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void - { - s_renderer_api->draw_lines(vertex_array, index_count); - } - - inline static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void - { - s_renderer_api->bind_texture(texture_id, slot); - } - - inline static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void - { - s_renderer_api->bind_image_texture(texture_id, slot, read_only); - } - - inline static auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void - { - s_renderer_api->read_pixels(x, y, width, height, format, type, pixels); - } - - private: - static Scope<RendererAPI> s_renderer_api; - }; - - class Renderer - { - public: - static auto init() -> void; - static auto shutdown() -> void; - - static auto on_window_resize(uint32_t width, uint32_t height) -> void; - - static auto submit(const Ref<Shader>& shader, - const Ref<VertexArray>& vertex_array, - const glm::mat4& transform, - const glm::mat4& view_projection) -> void; - - static auto set_clear_color(const glm::vec4& color) -> void; - static auto clear() -> void; - - inline static auto get_api() -> RendererAPI::API { return RendererAPI::get_api(); } - }; -}; diff --git a/src/rendering/renderer_backend.h b/src/rendering/renderer_backend.h index 6a4f1af..d34983f 100644 --- a/src/rendering/renderer_backend.h +++ b/src/rendering/renderer_backend.h @@ -1,14 +1,15 @@ #pragma once #include <glm/glm.hpp> -#include <functional> -#include <string> -#include <vector> namespace Donut { - // A placeable/editable sphere in the world-builder scene view. Backend- - // agnostic: the app owns the list, every backend renders it the same way. + // Backend-agnostic scene/black-hole data shared by the Application and the + // portable renderers (SceneRenderer, BlackHoleRenderer). The rendering itself + // is written once on the RHI (rendering/rhi.h); these are just the inputs. + + // A placeable/editable sphere in the world-builder scene view. The app owns + // the list; SceneRenderer draws it the same way on every backend. struct SceneObject { glm::vec3 position = { 0.0f, 2.0f, 0.0f }; @@ -28,46 +29,4 @@ namespace Donut int quality_steps = 15000; // geodesic integration steps float fov_degrees = 60.0f; // camera field of view }; - - // The swappable high-level renderer. Each graphics backend (OpenGL, Vulkan, - // and later Metal / D3D12) implements this one interface; the Application, - // states and UI drive it identically, so the app behaves the same no matter - // which backend is active. Backends own their own device/swapchain/pipelines - // and translate these high-level calls into their native API. - class RendererBackend - { - public: - virtual ~RendererBackend() = default; - - // glfwWindow is an opaque GLFW handle (created NO_API for Vulkan/Metal, - // with a GL context for OpenGL). Returns false on failure. - virtual auto init(void* glfwWindow, int width, int height) -> bool = 0; - - // Creates the ImGui context + this backend's ImGui platform/render impl. - virtual auto init_ui() -> bool = 0; - virtual auto shutdown() -> void = 0; - virtual auto resize(int width, int height) -> void = 0; - - // Renders one frame: draws the current view (black hole or scene) from - // the shared state, then invokes build_ui to draw the ImGui panel on top. - virtual auto draw_frame(const glm::vec4& clear_color, - const std::function<void()>& build_ui) -> void = 0; - - // --- shared view / camera state ------------------------------------ - virtual auto set_scene_mode(bool enabled) -> void = 0; // black hole vs scene view - virtual auto is_scene_mode() const -> bool = 0; - virtual auto set_free_fly(bool enabled) -> void = 0; // free-fly vs orbital camera - virtual auto is_free_fly() const -> bool = 0; - virtual auto reset_camera() -> void = 0; - - // --- shared content ------------------------------------------------ - virtual auto set_hdri(const std::string& path) -> void = 0; - virtual auto current_hdri() const -> const std::string& = 0; - virtual auto scene_objects() -> std::vector<SceneObject>& = 0; - virtual auto set_selected_object(int index) -> void = 0; - virtual auto black_hole_params() -> BlackHoleParams& = 0; - - // --- introspection for the UI -------------------------------------- - virtual auto device_name() const -> const std::string& = 0; - }; } diff --git a/src/rendering/rhi/rhi.h b/src/rendering/rhi.h index f64252c..65f10b5 100644 --- a/src/rendering/rhi/rhi.h +++ b/src/rendering/rhi.h @@ -14,7 +14,7 @@ #include <vector> #include <functional> -namespace Donut::rhi +namespace Donut::RHI { enum class Format { @@ -35,9 +35,11 @@ namespace Donut::rhi struct VertexLayout { uint32_t stride = 0; std::vector<VertexAttribute> attributes; }; // A shader resource slot the pipeline exposes. `binding` is the set-0 binding - // index shared by the GLSL/SPIR-V (the app binds resources to these before a draw). + // index used by Vulkan; `name` is the GLSL block/sampler identifier used by the + // OpenGL backend (GL 4.1 has no binding qualifier, so it binds by name). The app + // declares both when building a pipeline (it knows its own shader). enum class ResourceKind { UniformBuffer, Texture }; - struct ResourceSlot { ResourceKind kind; uint32_t binding; }; + struct ResourceSlot { ResourceKind kind; uint32_t binding; std::string name; }; // --- opaque GPU resources (backends subclass) -------------------------- class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; }; @@ -113,13 +115,12 @@ namespace Donut::rhi Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref<RenderTarget> = 0; virtual auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> = 0; - // Frame loop: begin_frame returns the swapchain command list (or nullptr if - // the frame is skipped, e.g. minimised); record into it, then end_frame - // submits + presents. record_offscreen runs a self-contained pass whose - // command list targets a RenderTarget (used before the swapchain frame). + // Frame loop: begin_frame returns the frame's command list (or nullptr if + // the frame is skipped, e.g. minimised); record one or more render passes + // into it — off-screen passes into RenderTargets first, then the swapchain + // pass (begin_render_pass(nullptr, ...)) — then end_frame submits + presents. virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0; virtual auto end_frame() -> void = 0; - virtual auto record_offscreen(const std::function<void(CommandList&)>& record) -> void = 0; // ImGui lives above the RHI but its platform/render backend is per-device. virtual auto init_imgui() -> void = 0; diff --git a/src/rendering/scene_renderer.cpp b/src/rendering/scene_renderer.cpp new file mode 100644 index 0000000..e276074 --- /dev/null +++ b/src/rendering/scene_renderer.cpp @@ -0,0 +1,217 @@ +#include "scene_renderer.h" + +#include "core/log.h" + +#include <glm/gtc/matrix_transform.hpp> +#include <algorithm> +#include <cmath> +#include <numbers> +#include <vector> + +namespace Donut +{ + using namespace RHI; + + namespace + { + // std140 layouts, matching the Slang ConstantBuffer structs exactly. glm's + // column-major matrices upload directly (the shaders decorate the members + // row_major and use mul(M,v), so the same bytes read correctly on every + // backend, with no transpose). + struct GridUBO { + glm::mat4 view_projection; // 0 + glm::mat4 transform; // 64 + float grid_size; float p0[3]; // 128 + glm::vec3 grid_color; // 144 + float grid_alpha; // 156 + glm::vec3 camera_pos; // 160 + float p1; // 172 + }; + static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch"); + + struct SphereUBO { + glm::mat4 view_projection; // 0 + glm::mat4 transform; // 64 + glm::vec3 color; float specular; // 128, 140 + float emission; float p0[3]; // 144 + glm::vec3 light_pos; float p1; // 160, 172 + glm::vec3 camera_pos; int is_selected; // 176, 188 + glm::vec3 outline_color; float outline_width; // 192, 204 + }; + static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch"); + + struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; }; + static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch"); + } + + auto SceneRenderer::init(RHI::Device& device) -> bool + { + m_device = &device; + + // Reference grid: line list on the XZ plane (+/-50, 1-unit cells). Grid.slang + // scales by u_GridSize/50, so u_GridSize = 50 keeps it 1:1. + { + std::vector<glm::vec3> lines; + const int N = 50; + for (int i = -N; i <= N; ++i) + { + lines.push_back({ (float)i, 0.0f, (float)-N }); + lines.push_back({ (float)i, 0.0f, (float) N }); + lines.push_back({ (float)-N, 0.0f, (float)i }); + lines.push_back({ (float) N, 0.0f, (float)i }); + } + m_grid_vertex_count = (int)lines.size(); + m_grid_vb = device.create_buffer(BufferType::Vertex, lines.size() * sizeof(glm::vec3), lines.data()); + m_grid_ubo = device.create_buffer(BufferType::Uniform, sizeof(GridUBO)); + + PipelineDesc d; + d.shader = "grid"; + d.vertex_layout = { sizeof(glm::vec3), { { 0, 3, 0 } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "GridU" } }; + d.topology = Topology::Lines; + d.blend = BlendMode::AlphaBlend; + d.depth_test = true; d.depth_write = false; d.depth_op = CompareOp::LessEqual; + d.has_depth = true; + m_grid_pipeline = device.create_pipeline(d); + } + + // Lit sphere: unit UV-sphere (pos + normal), placed/scaled per object. + { + std::vector<float> sv; std::vector<uint32_t> si; + const int RINGS = 24, SECTORS = 48; + for (int r = 0; r <= RINGS; ++r) + { + float phi = (float)std::numbers::pi * r / RINGS; + for (int s = 0; s <= SECTORS; ++s) + { + float theta = 2.0f * (float)std::numbers::pi * s / SECTORS; + float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta); + sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit) + sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position + } + } + for (int r = 0; r < RINGS; ++r) + for (int s = 0; s < SECTORS; ++s) + { + uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1; + si.push_back(a); si.push_back(b); si.push_back(a + 1); + si.push_back(b); si.push_back(b + 1); si.push_back(a + 1); + } + m_sphere_index_count = (int)si.size(); + m_sphere_vb = device.create_buffer(BufferType::Vertex, sv.size() * sizeof(float), sv.data()); + m_sphere_ib = device.create_buffer(BufferType::Index, si.size() * sizeof(uint32_t), si.data()); + + PipelineDesc d; + d.shader = "sphere"; + d.vertex_layout = { 6 * sizeof(float), { { 0, 3, 0 }, { 1, 3, 3 * sizeof(float) } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "SphereU" }, + { ResourceKind::Texture, 1, "u_HDRIEnvironment" } }; + d.topology = Topology::Triangles; + d.depth_test = true; d.depth_write = true; d.depth_op = CompareOp::Less; + d.has_depth = true; + m_sphere_pipeline = device.create_pipeline(d); + } + + // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex + // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry. + { + const float cube[] = { + -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 + }; + m_skybox_vb = device.create_buffer(BufferType::Vertex, sizeof(cube), cube); + m_skybox_ubo = device.create_buffer(BufferType::Uniform, sizeof(SkyboxUBO)); + + PipelineDesc d; + d.shader = "skybox"; + d.vertex_layout = { 3 * sizeof(float), { { 0, 3, 0 } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "SkyboxU" }, + { ResourceKind::Texture, 1, "u_Skybox" } }; + d.topology = Topology::Triangles; + d.depth_test = false; d.depth_write = false; + d.has_depth = true; + m_skybox_pipeline = device.create_pipeline(d); + } + + DONUT_INFO("SceneRenderer ready ({} grid verts, {} sphere indices, skybox)", + m_grid_vertex_count, m_sphere_index_count); + return true; + } + + auto SceneRenderer::render(RHI::CommandList& cmd, const CameraView& cam, + const std::vector<SceneObject>& objects, int selected, + RHI::Texture* cubemap) -> void + { + // Full-window viewport; flip_y lets the backend match the shared top-left + // orientation (Vulkan flips via a negative-height viewport, GL is a no-op). + cmd.set_viewport(0, 0, cam.fb_width, cam.fb_height, true); + + glm::mat4 vp = cam.projection * cam.view; + + // Skybox background first (depth 1, no depth test/write). + { + SkyboxUBO sky{}; + sky.projection = cam.projection; + sky.view = glm::mat4(glm::mat3(cam.view)); // strip translation + m_skybox_ubo->update(&sky, sizeof(sky)); + cmd.bind_pipeline(m_skybox_pipeline.get()); + cmd.bind_uniform(0, m_skybox_ubo.get()); + cmd.bind_texture(1, cubemap); + cmd.bind_vertex_buffer(m_skybox_vb.get()); + cmd.draw(36); + } + + // Opaque spheres (write depth). One UBO per object slot avoids aliasing the + // per-draw uniforms across the deferred command stream. + { + int count = std::min((int)objects.size(), MAX_OBJECTS); + while ((int)m_sphere_ubos.size() < count) + m_sphere_ubos.push_back(m_device->create_buffer(BufferType::Uniform, sizeof(SphereUBO))); + + cmd.bind_pipeline(m_sphere_pipeline.get()); + for (int i = 0; i < count; ++i) + { + const SceneObject& o = objects[i]; + SphereUBO s{}; + s.view_projection = vp; + s.transform = glm::translate(glm::mat4(1.0f), o.position) + * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius)); + s.color = o.color; + s.specular = 0.6f; + s.emission = 0.0f; + s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f); + s.camera_pos = cam.position; + s.is_selected = (i == selected) ? 1 : 0; + s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f); + s.outline_width = 0.15f; + m_sphere_ubos[i]->update(&s, sizeof(s)); + + cmd.bind_uniform(0, m_sphere_ubos[i].get()); + cmd.bind_texture(1, cubemap); + cmd.bind_vertex_buffer(m_sphere_vb.get()); + cmd.bind_index_buffer(m_sphere_ib.get()); + cmd.draw_indexed(m_sphere_index_count); + } + } + + // Transparent grid on top (tests depth, doesn't write). + { + GridUBO g{}; + g.view_projection = vp; + g.transform = glm::mat4(1.0f); + g.grid_size = 50.0f; + g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f); + g.grid_alpha = 0.75f; + g.camera_pos = cam.position; + m_grid_ubo->update(&g, sizeof(g)); + cmd.bind_pipeline(m_grid_pipeline.get()); + cmd.bind_uniform(0, m_grid_ubo.get()); + cmd.bind_vertex_buffer(m_grid_vb.get()); + cmd.draw(m_grid_vertex_count); + } + } +} diff --git a/src/rendering/scene_renderer.h b/src/rendering/scene_renderer.h new file mode 100644 index 0000000..eafd56e --- /dev/null +++ b/src/rendering/scene_renderer.h @@ -0,0 +1,56 @@ +#pragma once + +#include "rhi.h" +#include "renderer_backend.h" // SceneObject +#include <glm/glm.hpp> +#include <vector> + +namespace Donut +{ + // The world-builder scene view (skybox + lit spheres + reference grid), + // written ONCE against the RHI so it renders identically on every backend. + // The Application owns the camera, the object list and the HDRI cubemap and + // passes them in each frame; this class owns only the GPU resources (meshes, + // pipelines, per-object uniform buffers) and records the draws. + struct CameraView + { + glm::mat4 view; + glm::mat4 projection; + glm::vec3 position; + int fb_width = 1; + int fb_height = 1; + }; + + class SceneRenderer + { + public: + // Builds the grid/sphere/skybox meshes and pipelines on the given device. + auto init(RHI::Device& device) -> bool; + + // Records the scene into the currently-open swapchain render pass. `cubemap` + // is the shared HDRI environment (sphere ambient + skybox background). + auto render(RHI::CommandList& cmd, const CameraView& cam, + const std::vector<SceneObject>& objects, int selected, + RHI::Texture* cubemap) -> void; + + private: + static constexpr int MAX_OBJECTS = 64; + + RHI::Device* m_device = nullptr; + + Ref<RHI::Pipeline> m_grid_pipeline; + Ref<RHI::Buffer> m_grid_vb; + Ref<RHI::Buffer> m_grid_ubo; + int m_grid_vertex_count = 0; + + Ref<RHI::Pipeline> m_sphere_pipeline; + Ref<RHI::Buffer> m_sphere_vb; + Ref<RHI::Buffer> m_sphere_ib; + int m_sphere_index_count = 0; + std::vector<Ref<RHI::Buffer>> m_sphere_ubos; // one per object slot + + Ref<RHI::Pipeline> m_skybox_pipeline; + Ref<RHI::Buffer> m_skybox_vb; + Ref<RHI::Buffer> m_skybox_ubo; + }; +} diff --git a/src/rendering/texture_manager.cpp b/src/rendering/texture_manager.cpp deleted file mode 100644 index 3afba0d..0000000 --- a/src/rendering/texture_manager.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "texture_manager.h" -#include "renderer.h" - -#include <glad/glad.h> - -namespace Donut -{ - auto TextureManager::create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D> - { - return create_ref<Texture2D>(width, height); - } - - auto TextureManager::bind_texture(uint32_t texture_id, uint32_t slot) -> void - { - RenderCommand::bind_texture(texture_id, slot); - } - - auto TextureManager::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void - { - RenderCommand::bind_image_texture(texture_id, slot, read_only); - } - - auto TextureManager::set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void - { - glBindTexture(GL_TEXTURE_2D, texture_id); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data); - } - - auto TextureManager::resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void - { - glBindTexture(GL_TEXTURE_2D, texture_id); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - } -}; diff --git a/src/rendering/texture_manager.h b/src/rendering/texture_manager.h deleted file mode 100644 index 02c32b1..0000000 --- a/src/rendering/texture_manager.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "rendering/texture.h" -#include <cstdint> - -namespace Donut -{ - class TextureManager - { - public: - static auto create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D>; - - static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void; - static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void; - static auto set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void; - static auto resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void; - }; -}; diff --git a/src/rendering/uniform_buffer.cpp b/src/rendering/uniform_buffer.cpp deleted file mode 100644 index c4118b8..0000000 --- a/src/rendering/uniform_buffer.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "uniform_buffer.h" - -#include <glad/glad.h> - -namespace Donut -{ - UniformBuffer::UniformBuffer(uint32_t size, uint32_t binding) - : m_size(size), m_binding(binding) - { - glGenBuffers(1, &m_renderer_id); - glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id); - glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW); - glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id); - } - - UniformBuffer::~UniformBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto UniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void - { - glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id); - glBufferSubData(GL_UNIFORM_BUFFER, offset, size, data); - } - - auto UniformBuffer::bind(uint32_t binding) -> void - { - glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id); - } - - auto UniformBuffer::create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer> - { - return create_ref<UniformBuffer>(size, binding); - } -}; diff --git a/src/rendering/uniform_buffer.h b/src/rendering/uniform_buffer.h deleted file mode 100644 index b5617b9..0000000 --- a/src/rendering/uniform_buffer.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include <cstdint> - -namespace Donut -{ - class UniformBuffer - { - public: - UniformBuffer(uint32_t size, uint32_t binding); - ~UniformBuffer(); - - auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void; - auto bind(uint32_t binding) -> void; - - static auto create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer>; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_size = 0; - uint32_t m_binding = 0; - }; -}; diff --git a/src/rendering/vertex_array.cpp b/src/rendering/vertex_array.cpp deleted file mode 100644 index cddcf35..0000000 --- a/src/rendering/vertex_array.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include <glad/glad.h> - -#include "vertex_array.h" - -namespace Donut -{ - VertexArray::VertexArray() - { - // glCreateVertexArrays is 4.5 DSA; macOS caps at 4.1. glGenVertexArrays - // reserves the name and the VAO is created on first bind (done below). - glGenVertexArrays(1, &m_renderer_id); - } - - VertexArray::~VertexArray() - { - glDeleteVertexArrays(1, &m_renderer_id); - } - - auto VertexArray::bind() const -> void - { - glBindVertexArray(m_renderer_id); - } - - auto VertexArray::unbind() const -> void - { - glBindVertexArray(0); - } - - auto VertexArray::add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void - { - glBindVertexArray(m_renderer_id); - vertex_buffer->bind(); - - const auto& layout = vertex_buffer->get_layout(); - for (const auto& element : layout.get_elements()) - { - glEnableVertexAttribArray(m_vertex_buffer_index); - glVertexAttribPointer(m_vertex_buffer_index, - element.count, - element.type, - element.normalized ? GL_TRUE : GL_FALSE, - layout.get_stride(), - reinterpret_cast<const void*>(static_cast<uintptr_t>(element.offset))); - m_vertex_buffer_index++; - } - - m_vertex_buffers.push_back(vertex_buffer); - } - - auto VertexArray::set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void - { - glBindVertexArray(m_renderer_id); - index_buffer->bind(); - m_index_buffer = index_buffer; - } - - auto VertexArray::create() -> VertexArray* - { - return new VertexArray(); - } -}; diff --git a/src/rendering/vertex_array.h b/src/rendering/vertex_array.h deleted file mode 100644 index 1f06758..0000000 --- a/src/rendering/vertex_array.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "core/memory.h" - -#include "vertex_buffer.h" -#include "index_buffer.h" - -#include <vector> - -namespace Donut -{ - class VertexArray - { - public: - VertexArray(); - ~VertexArray(); - - auto bind() const -> void; - auto unbind() const -> void; - - auto add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void; - auto set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void; - - auto get_vertex_buffers() const -> const std::vector<Ref<VertexBuffer>>& { return m_vertex_buffers; } - auto get_index_buffer() const -> const Ref<IndexBuffer>& { return m_index_buffer; } - - static auto create() -> VertexArray*; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_vertex_buffer_index = 0; - std::vector<Ref<VertexBuffer>> m_vertex_buffers; - Ref<IndexBuffer> m_index_buffer; - }; -}; diff --git a/src/rendering/vertex_buffer.cpp b/src/rendering/vertex_buffer.cpp deleted file mode 100644 index 1bc471c..0000000 --- a/src/rendering/vertex_buffer.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "vertex_buffer.h" - -#include <glad/glad.h> - -namespace Donut -{ - auto VertexBufferElement::get_size_of_type(uint32_t type) -> uint32_t - { - switch (type) - { - case 0x1406: return 4; // GL_FLOAT - case 0x1405: return 4; // GL_UNSIGNED_INT - case 0x1401: return 1; // GL_UNSIGNED_BYTE - default: return 0; - } - } - - VertexBuffer::VertexBuffer(const void* data, uint32_t size) - { - glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW); - } - - VertexBuffer::~VertexBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto VertexBuffer::bind() const -> void - { - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - } - - auto VertexBuffer::unbind() const -> void - { - glBindBuffer(GL_ARRAY_BUFFER, 0); - } - - auto VertexBuffer::set_data(const void* data, uint32_t size) -> void - { - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - glBufferSubData(GL_ARRAY_BUFFER, 0, size, data); - } - - auto VertexBuffer::create(const void* data, uint32_t size) -> VertexBuffer* - { - return new VertexBuffer(data, size); - } -}; diff --git a/src/rendering/vertex_buffer.h b/src/rendering/vertex_buffer.h deleted file mode 100644 index 4e61700..0000000 --- a/src/rendering/vertex_buffer.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include <cstdint> -#include <vector> - -namespace Donut -{ - struct VertexBufferElement - { - uint32_t type; - uint32_t count; - uint8_t normalized; - uint32_t offset; - - static auto get_size_of_type(uint32_t type) -> uint32_t; - }; - - class VertexBufferLayout - { - public: - VertexBufferLayout() = default; - ~VertexBufferLayout() = default; - - template<typename T> - auto push(uint32_t count) -> void - { - static_assert(false); - } - - template<> - void push<float>(uint32_t count) - { - m_elements.push_back({ 0x1406, count, 0, m_stride }); // GL_FLOAT - m_stride += count * VertexBufferElement::get_size_of_type(0x1406); - } - - template<> - void push<uint32_t>(uint32_t count) - { - m_elements.push_back({ 0x1405, count, 0, m_stride }); // GL_UNSIGNED_INT - m_stride += count * VertexBufferElement::get_size_of_type(0x1405); - } - - template<> - void push<uint8_t>(uint32_t count) - { - m_elements.push_back({ 0x1401, count, 1, m_stride }); // GL_UNSIGNED_BYTE - m_stride += count * VertexBufferElement::get_size_of_type(0x1401); - } - - inline auto get_elements() const -> const std::vector<VertexBufferElement>& { return m_elements; } - inline auto get_stride() const -> uint32_t { return m_stride; } - - private: - std::vector<VertexBufferElement> m_elements; - uint32_t m_stride = 0; - }; - - class VertexBuffer - { - public: - VertexBuffer(const void* data, uint32_t size); - ~VertexBuffer(); - - auto bind() const -> void; - auto unbind() const -> void; - auto set_data(const void* data, uint32_t size) -> void; - - auto get_layout() const -> const VertexBufferLayout& { return m_layout; } - auto set_layout(const VertexBufferLayout& layout) -> void { m_layout = layout; } - - static auto create(const void* data, uint32_t size) -> VertexBuffer*; - - private: - uint32_t m_renderer_id = 0; - VertexBufferLayout m_layout; - }; -}; diff --git a/src/states/config_state.cpp b/src/states/config_state.cpp deleted file mode 100644 index d5fca87..0000000 --- a/src/states/config_state.cpp +++ /dev/null @@ -1,307 +0,0 @@ -#include "config_state.h" -#include "rendering/renderer.h" -#include "core/application.h" -#include "core/theme_manager.h" -#include "core/settings_manager.h" - -#include <imgui.h> -#include <imgui_internal.h> -#include <glad/glad.h> - -namespace Donut -{ - auto ConfigState::on_enter() -> void - { - DONUT_INFO("Entering Config State"); - - const auto& settings = SettingsManager::get_settings_const(); - - m_selected_api = (settings.graphics.render_api == "Vulkan") ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL; - m_selected_theme = (settings.graphics.selected_theme == "Light") ? 1 : - (settings.graphics.selected_theme == "Blue") ? 2 : 0; - m_target_fps = settings.simulation.target_fps; - m_compute_height = settings.simulation.compute_height; - m_max_steps_moving = settings.simulation.max_steps_moving; - m_max_steps_static = settings.simulation.max_steps_static; - m_early_exit_distance = settings.simulation.early_exit_distance; - m_gravity_enabled = settings.simulation.gravity_enabled; - m_v_sync_enabled = settings.graphics.v_sync_enabled; - m_show_fps = settings.graphics.show_fps; - m_show_performance_metrics = settings.graphics.show_performance_metrics; - m_show_debug_info = settings.graphics.show_debug_info; - m_enable_anti_aliasing = settings.graphics.enable_anti_aliasing; - } - - auto ConfigState::on_exit() -> void - { - DONUT_INFO("Exiting Config State"); - } - - auto ConfigState::on_update(float delta_time) -> void - { - if (m_show_restart_message) - { - m_restart_message_timer += delta_time; - if (m_restart_message_timer > 3.0f) - { - m_show_restart_message = false; - m_restart_message_timer = 0.0f; - } - } - } - - auto ConfigState::on_render() -> void - { - Renderer::set_clear_color({ 0.1f, 0.1f, 0.1f, 1.0f }); - Renderer::clear(); - } - - auto ConfigState::on_event(Event& event) -> void - { - } - - auto ConfigState::on_im_ui_render() -> void - { - ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), - ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); - - ImGui::Begin("Donut Configuration", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Donut Configuration"); - ImGui::PopFont(); - ImGui::Separator(); - - ImGui::Columns(2, "ConfigColumns", true); - - float available_height = ImGui::GetWindowHeight() - 140; - - ImGui::BeginChild("GraphicsSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Graphics Settings"); - ImGui::Separator(); - - ImGui::Text("Render API:"); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(requires restart)"); - - const char* api_names[] = { "OpenGL", "Vulkan" }; - static int current_api = (int)m_selected_api - 1; - - if (ImGui::BeginCombo("##RenderAPI", api_names[current_api])) - { - for (int i = 0; i < IM_ARRAYSIZE(api_names); i++) - { - const bool is_selected = (current_api == i); - if (ImGui::Selectable(api_names[i], is_selected)) - { - current_api = i; - m_selected_api = (RendererAPI::API)(i + 1); - m_show_restart_message = true; - m_restart_message_timer = 0.0f; - } - - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - - ImGui::EndCombo(); - } - - ImGui::Text("Current API: "); - ImGui::SameLine(); - const char* current_api_name = (Renderer::get_api() == RendererAPI::API::OpenGL) ? "OpenGL" : "Vulkan"; - ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), current_api_name); - - ImGui::Spacing(); - - ImGui::Checkbox("Enable VSync", &m_v_sync_enabled); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(recommended)"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Display"); - ImGui::Separator(); - - ImGui::Text("Window Size: 1280x720"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Fullscreen: Not implemented yet"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "System Info"); - ImGui::Separator(); - - ImGui::Text("OpenGL Version: %s", glGetString(GL_VERSION)); - ImGui::Text("GPU: %s", glGetString(GL_RENDERER)); - ImGui::Text("Vendor: %s", glGetString(GL_VENDOR)); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance"); - ImGui::Separator(); - - ImGui::SliderInt("Target FPS", &m_target_fps, 30, 120, "%d FPS"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Target frame rate for the simulation"); - - ImGui::Checkbox("Show FPS Counter", &m_show_fps); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Display current FPS in the simulation"); - - ImGui::Checkbox("Show Performance Metrics", &m_show_performance_metrics); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Show detailed performance information"); - - ImGui::Checkbox("Show Debug Info", &m_show_debug_info); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Display debug information and statistics"); - - ImGui::Checkbox("Enable Anti-Aliasing", &m_enable_anti_aliasing); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Enable anti-aliasing for smoother rendering"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Theme"); - ImGui::Separator(); - - const char* theme_names[] = { "Dark", "Light", "Blue" }; - if (ImGui::Combo("UI Theme", &m_selected_theme, theme_names, IM_ARRAYSIZE(theme_names))) - ThemeManager::set_theme(static_cast<Theme>(m_selected_theme)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Choose the application theme"); - - ImGui::EndChild(); - - ImGui::NextColumn(); - - ImGui::BeginChild("SimulationSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Settings"); - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality"); - ImGui::Separator(); - - ImGui::SliderInt("Compute Height", &m_compute_height, 64, 2048, "%d px"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Resolution of the compute shader. Higher values give better quality but lower performance."); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Higher = better quality, lower performance"); - - ImGui::SliderInt("Max Steps (Moving)", &m_max_steps_moving, 1000, 60000, "%d"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Maximum ray marching steps when camera is moving"); - - ImGui::SliderInt("Max Steps (Static)", &m_max_steps_static, 1000, 30000, "%d"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Maximum ray marching steps when camera is stationary"); - - ImGui::SliderFloat("Early Exit Distance", &m_early_exit_distance, 1e11f, 1e13f, "%.2e"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Distance at which ray marching stops to improve performance"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Distance at which ray marching stops"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics"); - ImGui::Separator(); - - ImGui::Checkbox("Enable Gravity", &m_gravity_enabled); - - ImGui::EndChild(); - - ImGui::Columns(1); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - float button_width = (ImGui::GetWindowWidth() - 120) / 5.0f; - - if (ImGui::Button("World Builder", ImVec2(button_width, 35))) - { - apply_settings(); - Application::get().get_state_manager().switch_to_state("WorldBuilder"); - } - - ImGui::SameLine(); - if (ImGui::Button("Reset to Defaults", ImVec2(button_width, 35))) - reset_to_defaults(); - - ImGui::SameLine(); - if (ImGui::Button("Apply Settings", ImVec2(button_width, 35))) - apply_settings(); - - ImGui::SameLine(); - if (ImGui::Button("Save Settings", ImVec2(button_width, 35))) - { - apply_settings(); - DONUT_INFO("Settings saved manually"); - } - - ImGui::SameLine(); - if (ImGui::Button("Exit", ImVec2(button_width, 35))) - Application::get().close(); - - if (m_show_restart_message) - { - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); - ImGui::TextWrapped("Warning: Render API changed! Please restart the application for changes to take effect."); - ImGui::PopStyleColor(); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Donut Engine v1.0.0 | Black Hole Simulation"); - - ImGui::End(); - } - - auto ConfigState::apply_settings() -> void - { - SimulationSettings sim_settings; - sim_settings.target_fps = m_target_fps; - sim_settings.compute_height = m_compute_height; - sim_settings.max_steps_moving = m_max_steps_moving; - sim_settings.max_steps_static = m_max_steps_static; - sim_settings.early_exit_distance = m_early_exit_distance; - sim_settings.gravity_enabled = m_gravity_enabled; - - GraphicsSettings gfx_settings; - gfx_settings.render_api = (m_selected_api == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL"; - gfx_settings.v_sync_enabled = m_v_sync_enabled; - gfx_settings.show_fps = m_show_fps; - gfx_settings.show_performance_metrics = m_show_performance_metrics; - gfx_settings.show_debug_info = m_show_debug_info; - gfx_settings.enable_anti_aliasing = m_enable_anti_aliasing; - gfx_settings.selected_theme = (m_selected_theme == 1) ? "Light" : - (m_selected_theme == 2) ? "Blue" : "Dark"; - - SettingsManager::set_simulation_settings(sim_settings); - SettingsManager::set_graphics_settings(gfx_settings); - - RendererAPI::set_api(m_selected_api); - DONUT_INFO("Settings applied and saved"); - } - - auto ConfigState::reset_to_defaults() -> void - { - m_selected_api = RendererAPI::API::OpenGL; - m_target_fps = 60; - m_compute_height = 512; - m_max_steps_moving = 30000; - m_max_steps_static = 15000; - m_early_exit_distance = 5e12f; - m_gravity_enabled = true; - m_v_sync_enabled = true; - m_show_fps = true; - m_show_performance_metrics = true; - m_show_debug_info = false; - m_enable_anti_aliasing = true; - m_selected_theme = 0; - - apply_settings(); - DONUT_INFO("Settings reset to defaults and saved"); - } -}; diff --git a/src/states/config_state.h b/src/states/config_state.h deleted file mode 100644 index e64889b..0000000 --- a/src/states/config_state.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" -#include "rendering/renderer.h" - -namespace Donut -{ - class ConfigState - : public State - { - public: - ~ConfigState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - - private: - auto apply_settings() -> void; - auto reset_to_defaults() -> void; - - private: - RendererAPI::API m_selected_api = RendererAPI::API::OpenGL; - bool m_show_restart_message = false; - float m_restart_message_timer = 0.0f; - - int m_target_fps = 60; - int m_compute_height = 512; - int m_max_steps_moving = 30000; - int m_max_steps_static = 15000; - float m_early_exit_distance = 5e12f; - bool m_gravity_enabled = true; - - bool m_v_sync_enabled = true; - bool m_show_fps = true; - bool m_show_performance_metrics = true; - bool m_show_debug_info = false; - bool m_enable_anti_aliasing = true; - - int m_selected_theme = 0; // 0=Dark, 1=Light, 2=Blue - }; -};
\ No newline at end of file diff --git a/src/states/simulation_state.cpp b/src/states/simulation_state.cpp deleted file mode 100644 index 06955ff..0000000 --- a/src/states/simulation_state.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include "simulation_state.h" -#include "rendering/renderer.h" -#include "core/application.h" -#include "core/hdri_manager.h" -#include "core/window.h" -#include "core/event.h" -#include "core/settings_manager.h" - -#include <imgui.h> -#include <GLFW/glfw3.h> -#include <chrono> -#include <iomanip> -#include <sstream> -#include <vector> - -namespace Donut -{ - auto SimulationState::on_enter() -> void - { - DONUT_INFO("Entering Simulation State"); - - const auto& settings = SettingsManager::get_settings_const(); - auto& engine = Application::get().get_engine(); - - engine.set_target_fps(settings.simulation.target_fps); - engine.set_compute_height(settings.simulation.compute_height); - engine.set_max_steps_moving(settings.simulation.max_steps_moving); - engine.set_max_steps_static(settings.simulation.max_steps_static); - engine.set_early_exit_distance(settings.simulation.early_exit_distance); - engine.get_gravity() = settings.simulation.gravity_enabled; - - engine.set_disk_thickness(settings.simulation.disk_thickness); - engine.set_disk_density(settings.simulation.disk_density); - engine.set_rotation_speed(settings.simulation.rotation_speed); - engine.set_blur_strength(settings.simulation.blur_strength); - engine.set_glow_intensity(settings.simulation.glow_intensity); - - engine.update_compute_dimensions(); - m_initialized = true; - } - - auto SimulationState::on_exit() -> void - { - DONUT_INFO("Exiting Simulation State"); - } - - auto SimulationState::on_update(float delta_time) -> void - { - auto& engine = Application::get().get_engine(); - engine.update_performance(delta_time); - engine.update_window_dimensions(); - engine.update_physics(delta_time); - - if (engine.get_camera().is_dragging()) - { - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - engine.get_camera().process_orbital_mouse_move(xpos, ypos); - } - } - - auto SimulationState::on_render() -> void - { - auto& engine = Application::get().get_engine(); - RenderCommand::set_clear_color(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - RenderCommand::clear(); - RenderCommand::set_viewport(0, 0, static_cast<uint32_t>(engine.get_width()), static_cast<uint32_t>(engine.get_height())); - - engine.dispatch_compute(engine.get_camera()); - engine.draw_blur_pass(); - } - - auto SimulationState::on_event(Event& event) -> void - { - auto& engine = Application::get().get_engine(); - - if (event.get_event_type() == EventType::MouseButtonPressed) - { - MouseButtonPressedEvent& e = (MouseButtonPressedEvent&)event; - int button = e.get_mouse_button(); - - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - Camera& camera = engine.get_camera(); - double last_x = camera.get_last_x(); - double last_y = camera.get_last_y(); - glfwGetCursorPos(window, &last_x, &last_y); - - camera.process_orbital_mouse_button(button, GLFW_PRESS, 0); - } - else if (event.get_event_type() == EventType::MouseButtonReleased) - { - MouseButtonReleasedEvent& e = (MouseButtonReleasedEvent&)event; - int button = e.get_mouse_button(); - engine.get_camera().process_orbital_mouse_button(button, GLFW_RELEASE, 0); - } - - if (event.get_event_type() == EventType::MouseScrolled) - { - MouseScrolledEvent& e = (MouseScrolledEvent&)event; - engine.get_camera().process_orbital_scroll(e.get_x_offset(), e.get_y_offset()); - } - - if (event.get_event_type() == EventType::KeyPressed) - { - KeyPressedEvent& e = (KeyPressedEvent&)event; - if (e.get_key_code() == GLFW_KEY_G) - { - engine.get_gravity() = !engine.get_gravity(); - DONUT_INFO("Gravity turned {}", engine.get_gravity() ? "ON" : "OFF"); - } - } - } - - auto SimulationState::on_im_ui_render() -> void - { - auto& engine = Application::get().get_engine(); - - ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 420, 20), ImGuiCond_FirstUseEver); - - ImGui::Begin("Simulation Controls", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Black Hole Simulation"); - ImGui::SameLine(); - if (ImGui::Button("Back to Config")) - Application::get().get_state_manager().switch_to_state("Config"); - ImGui::SameLine(); - if (ImGui::Button("Save Settings")) - { - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.target_fps = engine.get_target_fps(); - settings.compute_height = engine.get_compute_height(); - settings.max_steps_moving = engine.get_max_steps_moving(); - settings.max_steps_static = engine.get_max_steps_static(); - settings.early_exit_distance = engine.get_early_exit_distance(); - settings.gravity_enabled = engine.get_gravity(); - SettingsManager::set_simulation_settings(settings); - DONUT_INFO("Simulation settings saved manually"); - } - - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance"); - ImGui::Separator(); - - ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); - ImGui::Text("Frame Time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate); - ImGui::Text("Engine FPS: %.1f", engine.get_current_fps()); - - int target_fps = engine.get_target_fps(); - if (ImGui::SliderInt("Target FPS", &target_fps, 30, 120)) - { - engine.set_target_fps(target_fps); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.target_fps = target_fps; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Info"); - ImGui::Separator(); - - ImGui::Text("Resolution: %dx%d", engine.get_width(), engine.get_height()); - ImGui::Text("Compute Resolution: %dx%d", engine.get_compute_width(), engine.get_compute_height()); - ImGui::Text("Objects: %zu", engine.get_objects().size()); - - if (ImGui::Button("Print Object Info")) - engine.print_object_info(); - - int compute_height = engine.get_compute_height(); - if (ImGui::SliderInt("Compute Height", &compute_height, 64, 2048)) - { - engine.set_compute_height(compute_height); - engine.update_compute_dimensions(); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.compute_height = compute_height; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Compute Width: %d (auto-calculated)", engine.get_compute_width()); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality Settings"); - ImGui::Separator(); - - int max_steps_moving = engine.get_max_steps_moving(); - if (ImGui::SliderInt("Max Steps (Moving)", &max_steps_moving, 1000, 60000)) - { - engine.set_max_steps_moving(max_steps_moving); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.max_steps_moving = max_steps_moving; - SettingsManager::set_simulation_settings(settings); - } - int max_steps_static = engine.get_max_steps_static(); - if (ImGui::SliderInt("Max Steps (Static)", &max_steps_static, 1000, 30000)) - { - engine.set_max_steps_static(max_steps_static); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.max_steps_static = max_steps_static; - SettingsManager::set_simulation_settings(settings); - } - float early_exit_distance = engine.get_early_exit_distance(); - if (ImGui::SliderFloat("Early Exit Distance", &early_exit_distance, 1e11f, 1e13f, "%.2e")) - { - engine.set_early_exit_distance(early_exit_distance); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.early_exit_distance = early_exit_distance; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics"); - ImGui::Separator(); - - bool& gravity = engine.get_gravity(); - if (ImGui::Checkbox("Gravity Enabled", &gravity)) - { - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.gravity_enabled = gravity; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Accretion Disk"); - ImGui::Separator(); - - float disk_thickness = engine.get_disk_thickness(); - if (ImGui::SliderFloat("Cloud Thickness", &disk_thickness, 0.1f, 2.0f, "%.2f")) - { - engine.set_disk_thickness(disk_thickness); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.disk_thickness = disk_thickness; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Thickness relative to Schwarzschild radius"); - - float disk_density = engine.get_disk_density(); - if (ImGui::SliderFloat("Cloud Density", &disk_density, 0.1f, 3.0f, "%.2f")) - { - engine.set_disk_density(disk_density); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.disk_density = disk_density; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Overall density multiplier"); - - float rotation_speed = engine.get_rotation_speed(); - if (ImGui::SliderFloat("Rotation Speed", &rotation_speed, 0.0f, 3.0f, "%.2f")) - { - engine.set_rotation_speed(rotation_speed); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.rotation_speed = rotation_speed; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Rotation speed multiplier (0 = no rotation)"); - - float blur_strength = engine.get_blur_strength(); - if (ImGui::SliderFloat("Blur Strength", &blur_strength, 0.5f, 5.0f, "%.2f")) - { - engine.set_blur_strength(blur_strength); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.blur_strength = blur_strength; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Blur radius for glow effect"); - - float glow_intensity = engine.get_glow_intensity(); - if (ImGui::SliderFloat("Glow Intensity", &glow_intensity, 0.1f, 3.0f, "%.2f")) - { - engine.set_glow_intensity(glow_intensity); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.glow_intensity = glow_intensity; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Intensity of the glow effect"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Environment"); - ImGui::Separator(); - - static int selected_hdri = 0; - ImGui::PushID("SimulationHDRI"); - auto& hdri_manager = HDRIManager::get(); - const auto& available_hdri = hdri_manager.get_available_hdri(); - - static std::vector<std::string> hdri_option_names; - static std::vector<const char*> hdri_options; - - if (hdri_option_names.size() != available_hdri.size()) - { - hdri_option_names.clear(); - hdri_options.clear(); - - for (const auto& path : available_hdri) - { - hdri_option_names.push_back(hdri_manager.get_hdri_name(path)); - hdri_options.push_back(hdri_option_names.back().c_str()); - } - } - - if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size()))) - { - auto& hdri_manager = HDRIManager::get(); - hdri_manager.set_current_hdri(available_hdri[selected_hdri]); - engine.set_hdri_environment(hdri_manager.get_current_hdri()); - } - ImGui::TextDisabled("HDRI provides background and lighting for the simulation"); - ImGui::PopID(); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera"); - ImGui::Separator(); - - ImGui::Text("Position: (%.2e, %.2e, %.2e)", - engine.get_camera().get_orbital_position().x, - engine.get_camera().get_orbital_position().y, - engine.get_camera().get_orbital_position().z); - ImGui::Text("Radius: %.2e", engine.get_camera().get_orbital_radius()); - ImGui::Text("Azimuth: %.2f", engine.get_camera().get_azimuth()); - ImGui::Text("Elevation: %.2f", engine.get_camera().get_elevation()); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Export"); - ImGui::Separator(); - - if (ImGui::Button("Export Frame (1080p)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "frame_1080p_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 1920, 1080); - } - - if (ImGui::Button("Export High-Res Frame (4K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_4k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 4096, 3072); - } - - if (ImGui::Button("Export High-Res Frame (8K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_8k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 8192, 6144); - } - - if (ImGui::Button("Export Ultra High-Res Frame (16K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_16k_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".png"; - engine.export_high_res_frame(ss.str(), 16384, 12288); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Controls"); - ImGui::Separator(); - - ImGui::Text("Left Mouse: Orbit camera"); - ImGui::Text("Scroll: Zoom in/out"); - ImGui::Text("G: Toggle gravity"); - ImGui::Text("Right Mouse: Enable gravity (hold)"); - - ImGui::End(); - } -}; diff --git a/src/states/simulation_state.h b/src/states/simulation_state.h deleted file mode 100644 index 625d19e..0000000 --- a/src/states/simulation_state.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" -#include "engine/engine.h" -#include <vector> - -namespace Donut -{ - class SimulationState - : public State - { - public: - ~SimulationState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - - private: - bool m_initialized = false; - }; -}; diff --git a/src/states/world_builder_state.cpp b/src/states/world_builder_state.cpp deleted file mode 100644 index 09283c3..0000000 --- a/src/states/world_builder_state.cpp +++ /dev/null @@ -1,1113 +0,0 @@ -#include "world_builder_state.h" - -#include "core/application.h" -#include "core/window.h" -#include "core/hdri_manager.h" - -#include "rendering/renderer.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" -#include "rendering/vertex_buffer.h" -#include "rendering/index_buffer.h" -#include "rendering/texture.h" - -#include <imgui.h> -#include <ImGuizmo.h> -#include <GLFW/glfw3.h> - -#include <glm/gtc/matrix_transform.hpp> -#include <glm/gtc/type_ptr.hpp> - -#include <nlohmann/json.hpp> - -#include <numbers> -#include <fstream> -#include <sstream> -#include <limits> -#include <vector> - -namespace Donut -{ - auto WorldBuilderState::on_enter() -> void - { - DONUT_INFO("Entering World Builder State"); - - ImGuizmo::Enable(true); - - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); - m_camera.set_orbital_radius(15.0f); - m_camera.set_orbital_limits(2.0f, 200.0f); - m_camera.set_orbital_speed(0.01f); - m_camera.set_zoom_speed(2.0f); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(static_cast<float>(std::numbers::pi) / 3.0f); - m_camera.update_orbital(); - - m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl")); - m_skybox_shader = Ref<Shader>(Shader::create("assets/shaders/Skybox.glsl")); - m_grid_shader = Ref<Shader>(Shader::create("assets/shaders/Grid.glsl")); - - if (!m_sphere_shader) - DONUT_ERROR("Failed to create sphere shader"); - if (!m_skybox_shader) - DONUT_ERROR("Failed to create skybox shader"); - if (!m_grid_shader) - DONUT_ERROR("Failed to create grid shader"); - - initialize_sphere_geometry(); - initialize_skybox_geometry(); - initialize_grid_geometry(); - - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - { - hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr"); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - DONUT_WARN("Failed to load default HDRI for WorldBuilder, using fallback"); - } - - Material black_hole_material(glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, 0.0f); - m_black_hole = Object(glm::vec3(0.0f, 0.0f, 0.0f), 2.0f, black_hole_material); - m_black_hole_initialized = true; - - // Set default grid size - m_grid_size = 10.0f; - - m_initialized = true; - } - - auto WorldBuilderState::on_exit() -> void - { - DONUT_INFO("Exiting World Builder State"); - } - - auto WorldBuilderState::on_update(float delta_time) -> void - { - if (m_camera_dragging && - !ImGuizmo::IsUsing()) - { - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - glm::vec2 current_mouse_pos(xpos, ypos); - glm::vec2 delta = current_mouse_pos - m_last_mouse_pos; - - float sensitivity = 0.005f; - float azimuth_delta = delta.x * sensitivity; - float elevation_delta = -delta.y * sensitivity; - - float new_azimuth = m_camera.get_azimuth() + azimuth_delta; - float new_elevation = m_camera.get_elevation() + elevation_delta; - - new_elevation = glm::clamp(new_elevation, 0.01f, static_cast<float>(std::numbers::pi) - 0.01f); - - m_camera.set_azimuth(new_azimuth); - m_camera.set_elevation(new_elevation); - m_camera.update_orbital(); - - m_last_mouse_pos = current_mouse_pos; - } - } - - auto WorldBuilderState::on_render() -> void - { - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - - RenderCommand::set_clear_color(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f)); - RenderCommand::clear(); - - if (m_hdri_environment) - render_skybox(); - - if (m_show_grid) - render_grid(); - - render_scene(); - } - - auto WorldBuilderState::on_event(Event& event) -> void - { - EventDispatcher dispatcher(event); - - dispatcher.dispatch<MouseButtonPressedEvent>([this](MouseButtonPressedEvent& e) - { - if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) - { - if (ImGuizmo::IsUsing() || - ImGuizmo::IsOver()) - return false; - - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - float ndc_x = (2.0f * static_cast<float>(xpos)) / static_cast<float>(width) - 1.0f; - float ndc_y = 1.0f - (2.0f * static_cast<float>(ypos)) / static_cast<float>(height); - - glm::vec4 rayStart_NDC(ndc_x, ndc_y, -1.0f, 1.0f); - glm::vec4 rayEnd_NDC(ndc_x, ndc_y, 0.0f, 1.0f); - - glm::mat4 inv_vp = glm::inverse(m_camera.get_projection_matrix() * m_camera.get_view_matrix()); - glm::vec4 rayStart_World = inv_vp * rayStart_NDC; - glm::vec4 rayEnd_World = inv_vp * rayEnd_NDC; - - rayStart_World /= rayStart_World.w; - rayEnd_World /= rayEnd_World.w; - - glm::vec3 ray_dir = glm::normalize(glm::vec3(rayEnd_World - rayStart_World)); - glm::vec3 ray_origin = glm::vec3(rayStart_World); - - float closest_distance = std::numeric_limits<float>::max(); - int closest_object_index = -1; - - if (m_black_hole_initialized) - { - glm::vec3 oc = ray_origin - m_black_hole.m_centre; - float a = glm::dot(ray_dir, ray_dir); - float b = 2.0f * glm::dot(oc, ray_dir); - float c = glm::dot(oc, oc) - m_black_hole.m_radius * m_black_hole.m_radius; - float discriminant = b * b - 4 * a * c; - - if (discriminant > 0) - { - float t1 = (-b - sqrt(discriminant)) / (2.0f * a); - float t2 = (-b + sqrt(discriminant)) / (2.0f * a); - - if (t1 > 0 && t1 < closest_distance) - { - closest_distance = t1; - closest_object_index = -2; - } - else if (t2 > 0 && t2 < closest_distance) - { - closest_distance = t2; - closest_object_index = -2; - } - } - } - - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - const Object& obj = m_scene.objs[i]; - - glm::vec3 oc = ray_origin - obj.m_centre; - float a = glm::dot(ray_dir, ray_dir); - float b = 2.0f * glm::dot(oc, ray_dir); - float c = glm::dot(oc, oc) - obj.m_radius * obj.m_radius; - float discriminant = b * b - 4 * a * c; - - if (discriminant > 0) - { - float t1 = (-b - sqrt(discriminant)) / (2.0f * a); - float t2 = (-b + sqrt(discriminant)) / (2.0f * a); - - if (t1 > 0 && t1 < closest_distance) - { - closest_distance = t1; - closest_object_index = static_cast<int>(i); - } - else if (t2 > 0 && t2 < closest_distance) - { - closest_distance = t2; - closest_object_index = static_cast<int>(i); - } - } - } - - if (closest_object_index >= 0) - { - m_selected_object_index = closest_object_index; - DONUT_INFO("Selected object {}", closest_object_index); - return true; - } - else if (closest_object_index == -2) - { - m_selected_object_index = -1; - DONUT_INFO("Black hole clicked (not selectable)"); - return true; - } - else - { - m_selected_object_index = -1; - m_camera_dragging = true; - m_last_mouse_pos = glm::vec2(xpos, ypos); - } - - return true; - } - return false; - }); - - dispatcher.dispatch<MouseButtonReleasedEvent>([this](MouseButtonReleasedEvent& e) - { - if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) - { - if (!ImGuizmo::IsUsing()) - m_camera_dragging = false; - return true; - } - - return false; - }); - - dispatcher.dispatch<WindowResizeEvent>([this](WindowResizeEvent& e) - { - int new_width = e.get_width(); - int new_height = e.get_height(); - if (new_width > 0 && new_height > 0) - { - float aspect = static_cast<float>(new_width) / static_cast<float>(new_height); - m_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - } - return false; - }); - - dispatcher.dispatch<MouseScrolledEvent>([this](MouseScrolledEvent& e) - { - float zoom_speed = 0.1f; - float zoom_delta = e.get_y_offset() * zoom_speed; - - double current_radius = m_camera.get_orbital_radius(); - double new_radius = current_radius - zoom_delta * current_radius * 0.1f; - - double min_radius = 2.0f; - double max_radius = 200.0f; - new_radius = glm::clamp(new_radius, min_radius, max_radius); - - m_camera.set_orbital_radius(new_radius); - m_camera.update_orbital(); - - return true; - }); - - dispatcher.dispatch<KeyPressedEvent>([this](KeyPressedEvent& e) - { - if (m_selected_object_index >= 0 && - m_selected_object_index < static_cast<int>(m_scene.objs.size())) - { - switch (e.get_key_code()) - { - case GLFW_KEY_M: - m_gizmo_operation = ImGuizmo::TRANSLATE; - return true; - case GLFW_KEY_R: - m_gizmo_operation = ImGuizmo::ROTATE; - return true; - case GLFW_KEY_S: - m_gizmo_operation = ImGuizmo::SCALE; - return true; - } - } - return false; - }); - } - - auto WorldBuilderState::on_im_ui_render() -> void - { - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - ImGuiViewport* vp = ImGui::GetMainViewport(); - ImGuizmo::SetRect(vp->Pos.x, vp->Pos.y, vp->Size.x, vp->Size.y); - - ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); - - ImGui::Begin("World Builder", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "World Builder"); - ImGui::PopFont(); - ImGui::Separator(); - - if (ImGui::CollapsingHeader("Scene Info")) - { - ImGui::Text("Objects in scene: %zu/16 (+ 1 black hole)", m_scene.objs.size()); - - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera Info:"); - - glm::vec3 camera_pos = m_camera.get_orbital_position(); - glm::vec3 target = m_camera.get_orbital_target(); - - ImGui::Text("Position: (%.2f, %.2f, %.2f)", camera_pos.x, camera_pos.y, camera_pos.z); - ImGui::Text("Target: (%.2f, %.2f, %.2f)", target.x, target.y, target.z); - ImGui::Text("Distance: %.2f", m_camera.get_orbital_radius()); - ImGui::Text("Azimuth: %.1f°", glm::degrees(m_camera.get_azimuth())); - ImGui::Text("Elevation: %.1f°", glm::degrees(m_camera.get_elevation())); - - ImGui::Separator(); - - if (ImGui::Button("Reset Camera")) - { - m_camera.set_orbital_radius(15.0f); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(static_cast<float>(std::numbers::pi) / 3.0f); - m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); - m_camera.update_orbital(); - } - - ImGui::SameLine(); - if (ImGui::Button("Clear Scene")) - clear_scene(); - - ImGui::SameLine(); - if (ImGui::Button("Focus on Objects")) - { - if (!m_scene.objs.empty()) - { - glm::vec3 center(0.0f); - for (const auto& obj : m_scene.objs) - center += obj.m_centre; - center /= static_cast<float>(m_scene.objs.size()); - m_camera.set_orbital_target(center); - m_camera.update_orbital(); - } - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("create Object")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "New Sphere"); - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Objects: %zu/16", m_scene.objs.size()); - - ImGui::Text("Position:"); - ImGui::DragFloat3("##Position", &m_new_object_position.x, 0.1f); - - ImGui::Text("Radius:"); - ImGui::DragFloat("##Radius", &m_new_object_radius, 0.1f, 0.1f, 10.0f); - - ImGui::Text("Color:"); - ImGui::ColorEdit3("##Color", &m_new_object_color.x); - - ImGui::Text("Specular:"); - ImGui::SliderFloat("##Specular", &m_new_object_specular, 0.0f, 1.0f); - - ImGui::Text("Emission:"); - ImGui::SliderFloat("##Emission", &m_new_object_emission, 0.0f, 1.0f); - - ImGui::Spacing(); - - if (m_scene.objs.size() >= 16) - { - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - ImGui::Button("add Sphere (Limit Reached)", ImVec2(ImGui::GetWindowWidth() - 20, 30)); - ImGui::PopStyleVar(); - } - else - { - if (ImGui::Button("add Sphere", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - add_sphere(); - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Scene Objects")) - { - if (m_black_hole_initialized) - { - ImGui::PushID(-1); - - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "Black Hole (Center)"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Position: (0.00, 0.00, 0.00)"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Radius: %.2f", m_black_hole.m_radius); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Color: Black (with white outline)"); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Cannot be removed or modified"); - - ImGui::PopID(); - ImGui::Separator(); - } - - if (m_scene.objs.empty()) - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No additional objects in scene"); - else - { - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - Object& obj = m_scene.objs[i]; - - ImGui::PushID(static_cast<int>(i)); - - bool is_selected = (m_selected_object_index == static_cast<int>(i)); - if (ImGui::Selectable(("Sphere " + std::to_string(i)).c_str(), is_selected)) - m_selected_object_index = static_cast<int>(i); - - if (is_selected) - { - ImGui::SameLine(); - if (ImGui::Button("Remove")) - { - remove_selected_object(); - } - - ImGui::Text("Position: (%.2f, %.2f, %.2f)", - obj.m_centre.x, obj.m_centre.y, obj.m_centre.z); - ImGui::Text("Radius: %.2f", obj.m_radius); - ImGui::Text("Color: (%.2f, %.2f, %.2f)", - obj.m_material.m_color.x, obj.m_material.m_color.y, obj.m_material.m_color.z); - } - - ImGui::PopID(); - } - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Gizmo Controls")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Gizmo Operation:"); - - if (ImGui::RadioButton("Translate", m_gizmo_operation == ImGuizmo::TRANSLATE)) - m_gizmo_operation = ImGuizmo::TRANSLATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Rotate", m_gizmo_operation == ImGuizmo::ROTATE)) - m_gizmo_operation = ImGuizmo::ROTATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Scale", m_gizmo_operation == ImGuizmo::SCALE)) - m_gizmo_operation = ImGuizmo::SCALE; - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Hotkeys:"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "M = Move, R = Rotate, S = Scale"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Selection Outline")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Outline Settings:"); - - ImGui::Text("Outline Color:"); - ImGui::ColorEdit3("##OutlineColor", &m_outline_color.x); - - ImGui::Text("Outline Width:"); - ImGui::SliderFloat("##OutlineWidth", &m_outline_width, 0.01f, 0.9f, "%.2f"); - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "White rim around sphere silhouette"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Grid Settings")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Grid Settings:"); - - ImGui::Text("Show Grid:"); - ImGui::SameLine(); - ImGui::Checkbox("##ShowGrid", &m_show_grid); - - ImGui::Text("Grid Color:"); - ImGui::ColorEdit3("##GridColor", &m_grid_color.x); - - ImGui::Text("Grid Alpha:"); - ImGui::SliderFloat("##GridAlpha", &m_grid_alpha, 0.0f, 1.0f, "%.2f"); - - ImGui::Text("Grid Size:"); - ImGui::SliderFloat("##GridSize", &m_grid_size, 1.0f, 500.0f, "%.1f"); - - ImGui::Spacing(); - if (ImGui::Button("Regenerate Grid", ImVec2(ImGui::GetWindowWidth() - 20, 25))) - initialize_grid_geometry(); - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Reference grid for spatial orientation"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("HDRI Environment", nullptr)) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Settings:"); - - static int selected_hdri = 0; - ImGui::PushID("WorldBuilderHDRI"); - auto& hdri_manager = HDRIManager::get(); - const auto& available_hdri = hdri_manager.get_available_hdri(); - - static std::vector<std::string> hdri_option_names; - static std::vector<const char*> hdri_options; - - if (hdri_option_names.size() != available_hdri.size()) - { - hdri_option_names.clear(); - hdri_options.clear(); - - for (const auto& path : available_hdri) - { - hdri_option_names.push_back(hdri_manager.get_hdri_name(path)); - hdri_options.push_back(hdri_option_names.back().c_str()); - } - } - - if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size()))) - { - auto& hdri_manager = HDRIManager::get(); - hdri_manager.set_current_hdri(available_hdri[selected_hdri]); - m_hdri_environment = hdri_manager.get_current_hdri(); - } - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "HDRI provides background skybox and lighting for the scene"); - ImGui::PopID(); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - float button_width = (ImGui::GetWindowWidth() - 30) / 2.0f; - - if (ImGui::Button("Save Scene", ImVec2(button_width, 30))) - save_scene(); - - ImGui::SameLine(); - if (ImGui::Button("load Scene", ImVec2(button_width, 30))) - load_scene(); - - ImGui::Spacing(); - - if (ImGui::Button("Start Simulation", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - { - auto& engine = Application::get().get_engine(); - engine.load_objects_from_scene(m_scene.objs); - Application::get().get_state_manager().switch_to_state("Simulation"); - } - - ImGui::Spacing(); - - if (ImGui::Button("Back to Config", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - Application::get().get_state_manager().switch_to_state("Config"); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Camera: Left mouse = rotate, Scroll = zoom"); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Gizmo: M=Move, R=Rotate, S=Scale"); - - ImGui::End(); - - if (m_selected_object_index >= 0 && - m_selected_object_index < static_cast<int>(m_scene.objs.size()) && - m_selected_object_index != -2) - { - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - - Object& selected_obj = m_scene.objs[m_selected_object_index]; - - glm::mat4 object_transform = glm::mat4(1.0f); - object_transform = glm::translate(object_transform, selected_obj.m_centre); - object_transform = glm::scale(object_transform, glm::vec3(selected_obj.m_radius)); - - DONUT_INFO("Object position: ({}, {}, {})", selected_obj.m_centre.x, selected_obj.m_centre.y, selected_obj.m_centre.z); - glm::vec3 camera_pos = m_camera.get_orbital_position(); - DONUT_INFO("Camera position: ({}, {}, {})", camera_pos.x, camera_pos.y, camera_pos.z); - - glm::vec3 view_translation = glm::vec3(view[3][0], view[3][1], view[3][2]); - DONUT_INFO("View translation: ({}, {}, {})", view_translation.x, view_translation.y, view_translation.z); - - ImGuizmo::SetDrawlist(ImGui::GetForegroundDrawList()); - if (ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), - m_gizmo_operation, m_gizmo_mode, - glm::value_ptr(object_transform), - nullptr, nullptr)) - { - glm::vec3 new_position = glm::vec3(object_transform[3][0], object_transform[3][1], object_transform[3][2]); - selected_obj.m_centre = new_position; - - glm::vec3 scale = glm::vec3 - ( - glm::length(glm::vec3(object_transform[0])), - glm::length(glm::vec3(object_transform[1])), - glm::length(glm::vec3(object_transform[2])) - ); - selected_obj.m_radius = (scale.x + scale.y + scale.z) / 3.0f; - - DONUT_INFO("Matrix [3]: ({}, {}, {}, {})", object_transform[3][0], object_transform[3][1], object_transform[3][2], object_transform[3][3]); - DONUT_INFO("New position: ({}, {}, {})", new_position.x, new_position.y, new_position.z); - } - } - } - - auto WorldBuilderState::add_sphere() -> void - { - if (m_scene.objs.size() >= 16) - { - DONUT_WARN("Cannot add more objects. Maximum of 16 objects reached."); - return; - } - - Material material(m_new_object_color, m_new_object_specular, m_new_object_emission); - Object sphere(m_new_object_position, m_new_object_radius, material); - m_scene.objs.push_back(sphere); - - m_selected_object_index = static_cast<int>(m_scene.objs.size() - 1); - - m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); - m_new_object_radius = 1.0f; - m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); - m_new_object_specular = 0.5f; - m_new_object_emission = 0.0f; - - DONUT_INFO("Added sphere to scene and selected it"); - } - - auto WorldBuilderState::remove_selected_object() -> void - { - if (m_selected_object_index >= 0 && m_selected_object_index < static_cast<int>(m_scene.objs.size())) - { - m_scene.objs.erase(m_scene.objs.begin() + m_selected_object_index); - m_selected_object_index = -1; - DONUT_INFO("Removed object from scene"); - } - } - - auto WorldBuilderState::clear_scene() -> void - { - m_scene.objs.clear(); - m_selected_object_index = -1; - DONUT_INFO("Cleared scene (black hole remains at center)"); - } - - auto WorldBuilderState::save_scene() -> void - { - nlohmann::json scene_data; - scene_data["objects"] = nlohmann::json::array(); - - for (const auto& obj : m_scene.objs) - { - nlohmann::json sphere_data; - sphere_data["position"] = - { - obj.m_centre.x, - obj.m_centre.y, - obj.m_centre.z - }; - - sphere_data["color"] = - { - obj.m_material.m_color.x, - obj.m_material.m_color.y, - obj.m_material.m_color.z - }; - - sphere_data["radius"] = obj.m_radius; - sphere_data["specular"] = obj.m_material.m_specular; - sphere_data["emission"] = obj.m_material.m_emission; - scene_data["objects"].push_back(sphere_data); - } - - std::ofstream file("Scene.json"); - if (file.is_open()) - { - file << scene_data.dump(4); - file.close(); - DONUT_INFO("Scene saved to Scene.json (black hole always present at center)"); - } - else - DONUT_ERROR("Failed to save scene"); - } - - auto WorldBuilderState::load_scene() -> void - { - std::ifstream file("Scene.json"); - if (file.is_open()) - { - m_scene.objs.clear(); - m_selected_object_index = -1; - - nlohmann::json scene_data; - file >> scene_data; - - if (scene_data.contains("objects")) - { - for (const auto& sphere_data : scene_data["objects"]) - { - glm::vec3 position; - glm::vec3 color; - float radius; - float specular; - float emission; - - auto pos_array = sphere_data["position"]; - position.x = pos_array[0].get<float>(); - position.y = pos_array[1].get<float>(); - position.z = pos_array[2].get<float>(); - - auto color_array = sphere_data["color"]; - color.x = color_array[0].get<float>(); - color.y = color_array[1].get<float>(); - color.z = color_array[2].get<float>(); - - radius = sphere_data["radius"].get<float>(); - specular = sphere_data["specular"].get<float>(); - emission = sphere_data["emission"].get<float>(); - - Material material(color, specular, emission); - Object sphere(position, radius, material); - m_scene.objs.push_back(sphere); - } - } - - file.close(); - DONUT_INFO("Scene loaded from Scene.json (black hole remains at center)"); - } - else - DONUT_ERROR("Failed to load scene"); - } - - auto WorldBuilderState::initialize_sphere_geometry() -> void - { - std::vector<float> vertices; - std::vector<uint32_t> indices; - - const int segments = 32; - const int rings = 16; - - for (int ring = 0; ring <= rings; ++ring) - { - float phi = static_cast<float>(std::numbers::pi) * ring / rings; - float sin_phi = sin(phi); - float cos_phi = cos(phi); - - for (int segment = 0; segment <= segments; ++segment) - { - float theta = 2.0f * static_cast<float>(std::numbers::pi) * segment / segments; - float sin_theta = sin(theta); - float cos_theta = cos(theta); - - float x = cos_theta * sin_phi; - float y = cos_phi; - float z = sin_theta * sin_phi; - - float nx = x; - float ny = y; - float nz = z; - - vertices.push_back(x); - vertices.push_back(y); - vertices.push_back(z); - vertices.push_back(nx); - vertices.push_back(ny); - vertices.push_back(nz); - } - } - - for (int ring = 0; ring < rings; ++ring) - { - for (int segment = 0; segment < segments; ++segment) - { - uint32_t first = ring * (segments + 1) + segment; - uint32_t second = first + segments + 1; - - indices.push_back(first); - indices.push_back(second); - indices.push_back(first + 1); - - indices.push_back(second); - indices.push_back(second + 1); - indices.push_back(first + 1); - } - } - - auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float)))); - VertexBufferLayout layout; - layout.push<float>(3); - layout.push<float>(3); - vertex_buffer->set_layout(layout); - - auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size()))); - - m_sphere_vao = Ref<VertexArray>(VertexArray::create()); - m_sphere_vao->add_vertex_buffer(vertex_buffer); - m_sphere_vao->set_index_buffer(index_buffer); - } - - auto WorldBuilderState::render_scene() -> void - { - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::enable_depth_test(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - glm::mat4 view_projection = projection * view; - - glm::vec3 light_pos = m_scene.m_light_pos; - glm::vec3 camera_pos = m_camera.get_orbital_position(); - - m_sphere_shader->bind(); - m_sphere_shader->set_mat4("u_ViewProjection", view_projection); - m_sphere_shader->set_float3("u_LightPos", light_pos); - m_sphere_shader->set_float3("u_CameraPos", camera_pos); - - m_sphere_shader->set_float3("u_OutlineColor", m_outline_color); - m_sphere_shader->set_float("u_OutlineWidth", m_outline_width); - - if (m_hdri_environment) - { - m_hdri_environment->bind(1); - m_sphere_shader->set_int("u_HDRIEnvironment", 1); - } - - if (m_black_hole_initialized) - { - glm::mat4 black_hole_transform = glm::translate(glm::mat4(1.0f), m_black_hole.m_centre); - black_hole_transform = glm::scale(black_hole_transform, glm::vec3(m_black_hole.m_radius)); - - m_sphere_shader->set_mat4("u_Transform", black_hole_transform); - m_sphere_shader->set_int("u_IsSelected", 1); - m_sphere_shader->set_float3("u_Color", m_black_hole.m_material.m_color); - m_sphere_shader->set_float("u_Emission", m_black_hole.m_material.m_emission); - m_sphere_shader->set_float("u_Specular", m_black_hole.m_material.m_specular); - m_sphere_shader->set_float("u_OutlineWidth", 0.3f); - - m_sphere_vao->bind(); - RenderCommand::draw_indexed(m_sphere_vao); - - m_sphere_shader->set_float("u_OutlineWidth", m_outline_width); - } - - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - const auto& obj = m_scene.objs[i]; - - glm::mat4 transform = glm::translate(glm::mat4(1.0f), obj.m_centre); - transform = glm::scale(transform, glm::vec3(obj.m_radius)); - - m_sphere_shader->set_mat4("u_Transform", transform); - - bool is_selected = (m_selected_object_index == static_cast<int>(i)); - m_sphere_shader->set_int("u_IsSelected", is_selected ? 1 : 0); - if (is_selected) - { - glm::vec3 highlight_color = obj.m_material.m_color * 1.5f; - highlight_color = glm::clamp(highlight_color, 0.0f, 1.0f); - m_sphere_shader->set_float3("u_Color", highlight_color); - m_sphere_shader->set_float("u_Emission", 0.2f); - } - else - { - m_sphere_shader->set_float3("u_Color", obj.m_material.m_color); - m_sphere_shader->set_float("u_Emission", obj.m_material.m_emission); - } - - m_sphere_shader->set_float("u_Specular", obj.m_material.m_specular); - - m_sphere_vao->bind(); - RenderCommand::draw_indexed(m_sphere_vao); - } - - RenderCommand::disable_depth_test(); - } - - - - auto WorldBuilderState::initialize_skybox_geometry() -> void - { - float skybox_vertices[] = - { - -1.0f, 1.0f, -1.0f, - -1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - - -1.0f, -1.0f, 1.0f, - -1.0f, -1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, 1.0f, - -1.0f, -1.0f, 1.0f, - - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - - -1.0f, -1.0f, 1.0f, - -1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, -1.0f, 1.0f, - -1.0f, -1.0f, 1.0f, - - -1.0f, 1.0f, -1.0f, - 1.0f, 1.0f, -1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - -1.0f, 1.0f, 1.0f, - -1.0f, 1.0f, -1.0f, - - -1.0f, -1.0f, -1.0f, - -1.0f, -1.0f, 1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - -1.0f, -1.0f, 1.0f, - 1.0f, -1.0f, 1.0f - }; - - auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(skybox_vertices, sizeof(skybox_vertices))); - VertexBufferLayout layout; - layout.push<float>(3); - vertex_buffer->set_layout(layout); - - m_skybox_vao = Ref<VertexArray>(VertexArray::create()); - m_skybox_vao->add_vertex_buffer(vertex_buffer); - } - - auto WorldBuilderState::render_skybox() -> void - { - if (!m_skybox_shader || !m_skybox_vao || !m_hdri_environment) - return; - - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::disable_depth_test(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - - view = glm::mat4(glm::mat3(view)); - - m_skybox_shader->bind(); - m_skybox_shader->set_mat4("u_View", view); - m_skybox_shader->set_mat4("u_Projection", projection); - - m_hdri_environment->bind(0); - m_skybox_shader->set_int("u_Skybox", 0); - - m_skybox_vao->bind(); - RenderCommand::draw_arrays(36); - - RenderCommand::enable_depth_test(); - } - - auto WorldBuilderState::initialize_grid_geometry() -> void - { - if (m_grid_vao) - { - m_grid_vao->unbind(); - m_grid_vao.reset(); - } - - const float grid_size = 50.0f; - const int grid_lines = 101; - const float half_size = grid_size * 0.5f; - const float step = grid_size / (grid_lines - 1); - - std::vector<float> vertices; - std::vector<uint32_t> indices; - - for (int i = 0; i < grid_lines; ++i) - { - float z = -half_size + i * step; - - vertices.push_back(-half_size); - vertices.push_back(0.0f); - vertices.push_back(z); - - vertices.push_back(half_size); - vertices.push_back(0.0f); - vertices.push_back(z); - - uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2; - indices.push_back(base_index); - indices.push_back(base_index + 1); - } - - for (int i = 0; i < grid_lines; ++i) - { - float x = -half_size + i * step; - - vertices.push_back(x); - vertices.push_back(0.0f); - vertices.push_back(-half_size); - - vertices.push_back(x); - vertices.push_back(0.0f); - vertices.push_back(half_size); - - uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2; - indices.push_back(base_index); - indices.push_back(base_index + 1); - } - - auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float)))); - VertexBufferLayout layout; - layout.push<float>(3); - vertex_buffer->set_layout(layout); - - auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size()))); - - m_grid_vao = Ref<VertexArray>(VertexArray::create()); - m_grid_vao->add_vertex_buffer(vertex_buffer); - m_grid_vao->set_index_buffer(index_buffer); - - m_grid_vao->unbind(); - } - - auto WorldBuilderState::render_grid() -> void - { - if (!m_grid_shader || !m_grid_vao) - return; - - GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::enable_depth_test(); - RenderCommand::enable_blending(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - glm::mat4 view_projection = projection * view; - glm::mat4 transform = glm::mat4(1.0f); - - glm::vec3 camera_pos = m_camera.get_orbital_position(); - - m_grid_shader->bind(); - m_grid_shader->set_mat4("u_ViewProjection", view_projection); - m_grid_shader->set_mat4("u_Transform", transform); - m_grid_shader->set_float3("u_GridColor", m_grid_color); - m_grid_shader->set_float("u_GridAlpha", m_grid_alpha); - m_grid_shader->set_float("u_GridSize", m_grid_size); - m_grid_shader->set_float3("u_CameraPos", camera_pos); - - m_grid_vao->bind(); - RenderCommand::draw_lines(m_grid_vao); - - // Clean up state to prevent conflicts with scene rendering - m_grid_vao->unbind(); - m_grid_shader->unbind(); - RenderCommand::disable_blending(); - - // Ensure depth test is still enabled for scene rendering - RenderCommand::enable_depth_test(); - } -}; diff --git a/src/states/world_builder_state.h b/src/states/world_builder_state.h deleted file mode 100644 index 8b4678a..0000000 --- a/src/states/world_builder_state.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "core/camera.h" -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" - -#include "engine/scene.h" -#include "engine/object.h" - -#include "rendering/renderer.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" - -#include <imgui.h> -#include <ImGuizmo.h> - -#include <vector> -#include <memory> - -namespace Donut -{ - class WorldBuilderState - : public State - { - public: - ~WorldBuilderState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - private: - auto add_sphere() -> void; - auto remove_selected_object() -> void; - auto clear_scene() -> void; - auto save_scene() -> void; - auto load_scene() -> void; - auto render_scene() -> void; - auto initialize_sphere_geometry() -> void; - auto initialize_grid_geometry() -> void; - auto render_grid() -> void; - private: - Scene m_scene; - Camera m_camera; - bool m_initialized = false; - - Ref<Shader> m_sphere_shader; - Ref<VertexArray> m_sphere_vao; - Ref<CubemapTexture> m_hdri_environment; - - Ref<Shader> m_skybox_shader; - Ref<VertexArray> m_skybox_vao; - - Ref<Shader> m_grid_shader; - Ref<VertexArray> m_grid_vao; - - glm::vec3 m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); - float m_new_object_radius = 1.0f; - glm::vec3 m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); - float m_new_object_specular = 0.5f; - float m_new_object_emission = 0.0f; - - int m_selected_object_index = -1; - bool m_camera_dragging = false; - glm::vec2 m_last_mouse_pos = glm::vec2(0.0f, 0.0f); - - ImGuizmo::OPERATION m_gizmo_operation = ImGuizmo::TRANSLATE; - ImGuizmo::MODE m_gizmo_mode = ImGuizmo::LOCAL; - - bool m_show_object_list = true; - bool m_show_object_creator = true; - bool m_show_scene_info = true; - bool m_show_gizmo_controls = true; - bool m_show_outline_controls = true; - bool m_show_grid = true; - - glm::vec3 m_outline_color = glm::vec3(1.0f, 1.0f, 1.0f); - float m_outline_width = 0.25f; - - glm::vec3 m_grid_color = glm::vec3(0.5f, 0.5f, 0.5f); - float m_grid_alpha = 0.5f; - float m_grid_size = 50.0f; - - Object m_black_hole; - bool m_black_hole_initialized = false; - - auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; } - auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; } - - auto initialize_skybox_geometry() -> void; - auto render_skybox() -> void; - }; -}; |
