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