aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-23 17:21:37 +0200
committerhachem <im@hachem.wtf>2026-08-23 17:21:37 +0200
commit3fd33ecff7472e4d6fc9e6b3905f56982e497ef2 (patch)
tree66e5812f1a18b926e88a0dd549fe14e7c7327165 /src
parente3aa33fc9a99d8b817bda42a567a4c347e18ab32 (diff)
[feat]: complete RHI api
Diffstat (limited to 'src')
-rw-r--r--src/core/application.cpp422
-rw-r--r--src/core/application.h64
-rw-r--r--src/core/hdri_manager.h6
-rw-r--r--src/core/settings_manager.cpp1
-rw-r--r--src/core/state.h19
-rw-r--r--src/core/state_manager.cpp88
-rw-r--r--src/core/state_manager.h41
-rw-r--r--src/core/window.cpp105
-rw-r--r--src/core/window.h12
-rw-r--r--src/engine/engine.cpp625
-rw-r--r--src/engine/engine.h191
-rw-r--r--src/engine/object.h76
-rw-r--r--src/engine/scene.h70
-rw-r--r--src/platform/metal/metal_context.h16
-rw-r--r--src/platform/metal/metal_context.mm124
-rw-r--r--src/platform/opengl/opengl_device.cpp279
-rw-r--r--src/platform/opengl/opengl_device.h11
-rw-r--r--src/platform/opengl/opengl_renderer.cpp377
-rw-r--r--src/platform/opengl/opengl_renderer.h72
-rw-r--r--src/platform/vulkan/vulkan_context.cpp789
-rw-r--r--src/platform/vulkan/vulkan_context.h41
-rw-r--r--src/platform/vulkan/vulkan_device.cpp1254
-rw-r--r--src/platform/vulkan/vulkan_device.h17
-rw-r--r--src/platform/vulkan/vulkan_renderer.cpp2038
-rw-r--r--src/platform/vulkan/vulkan_renderer.h50
-rw-r--r--src/rendering/black_hole_renderer.cpp144
-rw-r--r--src/rendering/black_hole_renderer.h55
-rw-r--r--src/rendering/framebuffer.cpp251
-rw-r--r--src/rendering/framebuffer.h80
-rw-r--r--src/rendering/index_buffer.cpp34
-rw-r--r--src/rendering/index_buffer.h23
-rw-r--r--src/rendering/render_api.h25
-rw-r--r--src/rendering/renderer.cpp168
-rw-r--r--src/rendering/renderer.h155
-rw-r--r--src/rendering/renderer_backend.h53
-rw-r--r--src/rendering/rhi.h (renamed from src/rendering/rhi/rhi.h)17
-rw-r--r--src/rendering/scene_renderer.cpp217
-rw-r--r--src/rendering/scene_renderer.h56
-rw-r--r--src/rendering/texture_manager.cpp34
-rw-r--r--src/rendering/texture_manager.h19
-rw-r--r--src/rendering/uniform_buffer.cpp36
-rw-r--r--src/rendering/uniform_buffer.h24
-rw-r--r--src/rendering/vertex_array.cpp61
-rw-r--r--src/rendering/vertex_array.h35
-rw-r--r--src/rendering/vertex_buffer.cpp50
-rw-r--r--src/rendering/vertex_buffer.h78
-rw-r--r--src/states/config_state.cpp307
-rw-r--r--src/states/config_state.h47
-rw-r--r--src/states/simulation_state.cpp386
-rw-r--r--src/states/simulation_state.h27
-rw-r--r--src/states/world_builder_state.cpp1113
-rw-r--r--src/states/world_builder_state.h96
52 files changed, 2388 insertions, 7991 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp
index ae8a5d9..6239c36 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -1,36 +1,41 @@
#include "application.h"
-#include "rendering/renderer.h"
+#include "rendering/render_api.h" // RendererAPI
#include "settings_manager.h"
#include "hdri_manager.h"
-#include "states/simulation_state.h"
-#include "states/config_state.h"
-#include "states/world_builder_state.h"
+#include "platform/opengl/opengl_device.h"
+#include "platform/vulkan/vulkan_device.h"
#include <GLFW/glfw3.h>
#include <imgui.h>
-#include <ImGuizmo.h>
-#include "platform/vulkan/vulkan_renderer.h"
-#include "platform/opengl/opengl_renderer.h"
-
-#ifdef __APPLE__
-#include "platform/metal/metal_context.h"
-#include "platform/vulkan/vulkan_context.h"
-#endif
+#include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
+#include <cmath>
+#include <numbers>
namespace Donut
{
Application* Application::s_instance = nullptr;
+ // Scroll is delivered through a GLFW callback; accumulate it here and drain it
+ // once per frame in process_input (chaining ImGui's own scroll handler).
+ static double g_scroll_accum = 0.0;
+ static GLFWscrollfun g_prev_scroll = nullptr;
+ static void donut_scroll_callback(GLFWwindow* w, double x, double y)
+ {
+ if (g_prev_scroll) g_prev_scroll(w, x, y);
+ g_scroll_accum += y;
+ }
+
Application::Application(const std::string& name, int width, int height)
: m_running(true), m_minimized(false)
{
s_instance = this;
- // Select the render API before the window is created: the window is
- // built differently for Vulkan (GLFW_NO_API) than for OpenGL.
+ // Select the render API before the window is created: the window is built
+ // differently for Vulkan (GLFW_NO_API) than for OpenGL.
Logger::init();
SettingsManager::initialize();
{
@@ -40,13 +45,10 @@ namespace Donut
}
if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
- vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor
+ RHI::vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor
m_window = create_scope<Window>(name, width, height);
- m_window->set_event_callback([this](Event& event)
- {
- on_event(event);
- });
+ m_window->set_event_callback([this](Event& event) { on_event(event); });
on_init();
}
@@ -57,27 +59,225 @@ namespace Donut
s_instance = nullptr;
}
+ auto Application::on_init() -> void
+ {
+ int w = 0, h = 0;
+ glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h);
+
+ if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
+ m_device = RHI::create_vulkan_device();
+ else
+ m_device = RHI::create_opengl_device();
+
+ if (!m_device->init(m_window->get_native_window(), w, h))
+ {
+ DONUT_ERROR("RHI device initialization failed");
+ return;
+ }
+
+ m_hdri_path = "assets/hdri/hdr_blue_nebulae_1.hdr";
+ m_cubemap = m_device->create_cubemap_from_hdri(m_hdri_path);
+
+ m_scene_renderer = create_scope<SceneRenderer>();
+ m_scene_renderer->init(*m_device);
+ m_black_hole_renderer = create_scope<BlackHoleRenderer>();
+ m_black_hole_renderer->init(*m_device);
+
+ // Black-hole camera: large-scale orbital viewer outside the disk.
+ m_camera.set_camera_mode(CameraMode::Orbital);
+ m_camera.set_orbital_target(glm::vec3(0.0f));
+ m_camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk
+ m_camera.set_orbital_limits(2.2e11, 1.5e12);
+ m_camera.set_orbital_speed(0.01f);
+ m_camera.set_zoom_speed(3e10);
+ m_camera.set_azimuth(0.0f);
+ m_camera.set_elevation(1.25f);
+
+ // Scene camera: normal-scale orbital world-builder viewer.
+ m_scene_camera.set_camera_mode(CameraMode::Orbital);
+ m_scene_camera.set_orbital_target(glm::vec3(0.0f));
+ m_scene_camera.set_orbital_radius(15.0);
+ m_scene_camera.set_orbital_limits(2.0, 200.0);
+ m_scene_camera.set_orbital_speed(0.01f);
+ m_scene_camera.set_zoom_speed(2.0);
+ m_scene_camera.set_azimuth(0.0f);
+ m_scene_camera.set_elevation((float)std::numbers::pi / 3.0f);
+ m_scene_camera.update_orbital();
+
+ m_device->init_imgui();
+ // Load the UI font into the device-created ImGui context (backend-agnostic;
+ // the backend rebuilds the atlas on the next frame).
+ ImGuiIO& io = ImGui::GetIO();
+ if (!io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f))
+ io.Fonts->AddFontDefault();
+
+ g_prev_scroll = glfwSetScrollCallback((GLFWwindow*)m_window->get_native_window(), donut_scroll_callback);
+
+ m_start_time = glfwGetTime();
+ DONUT_INFO("Application ready on {} backend", m_device->device_name());
+ }
+
auto Application::run() -> void
{
while (m_running)
{
- if (m_renderer)
+ glfwPollEvents();
+ if (!m_minimized && m_device)
+ render_frame();
+ }
+ }
+
+ auto Application::render_frame() -> void
+ {
+ RHI::CommandList* cmd = m_device->begin_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f));
+ if (!cmd) return; // frame skipped (e.g. swapchain recreation)
+
+ m_device->imgui_new_frame();
+ build_ui();
+ process_input();
+
+ const glm::vec4 clear(0.05f, 0.06f, 0.10f, 1.0f);
+ int w = 0, h = 0;
+ glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h);
+
+ if (m_scene_mode)
+ {
+ float aspect = (float)w / (float)std::max(h, 1);
+ m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f);
+ CameraView cv;
+ cv.view = m_scene_camera.get_view_matrix();
+ cv.projection = m_scene_camera.get_projection_matrix();
+ cv.position = m_scene_camera.get_orbital_position();
+ cv.fb_width = w; cv.fb_height = h;
+
+ cmd->begin_render_pass(nullptr, clear);
+ m_scene_renderer->render(*cmd, cv, m_scene_objects, m_selected_object, m_cubemap.get());
+ m_device->imgui_render(*cmd);
+ cmd->end_render_pass();
+ }
+ else
+ {
+ GeodesicView gv;
+ glm::vec3 pos, fwd;
+ if (m_camera.get_camera_mode() == CameraMode::FPS)
{
- glfwPollEvents(); // before the ImGui frame so input is current
- if (!m_minimized)
- m_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f),
- [this] { build_ui(); });
+ pos = m_camera.get_position();
+ fwd = m_camera.get_forward_direction();
}
else
{
- if (!m_minimized)
- {
- on_update();
- on_render();
- }
- m_window->on_update(); // polls events + swaps buffers (OpenGL)
+ pos = m_camera.get_orbital_position();
+ fwd = glm::normalize(m_camera.get_orbital_target() - pos);
}
+ glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
+ gv.position = pos; gv.right = right; gv.up = glm::cross(right, fwd); gv.forward = fwd;
+ gv.tan_half_fov = (float)tan(glm::radians(m_bh_params.fov_degrees * 0.5f));
+ gv.aspect = (float)BlackHoleRenderer::GEO_HI_W / (float)BlackHoleRenderer::GEO_HI_H;
+ gv.moving = m_user_moving;
+ gv.time = (float)(glfwGetTime() - m_start_time);
+
+ m_black_hole_renderer->render_geodesic(*cmd, gv, m_bh_params, m_cubemap.get());
+ cmd->begin_render_pass(nullptr, clear);
+ m_black_hole_renderer->blit(*cmd, w, h);
+ m_device->imgui_render(*cmd);
+ cmd->end_render_pass();
}
+
+ m_device->end_frame();
+ }
+
+ auto Application::process_input() -> void
+ {
+ GLFWwindow* window = (GLFWwindow*)m_window->get_native_window();
+ bool over_ui = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureMouse;
+
+ double now = glfwGetTime();
+ float dt = m_last_frame_time > 0.0 ? (float)(now - m_last_frame_time) : 0.0f;
+ m_last_frame_time = now;
+
+ double mx = 0, my = 0;
+ glfwGetCursorPos(window, &mx, &my);
+ bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
+
+ if (!m_scene_mode && m_camera.get_camera_mode() == CameraMode::FPS)
+ {
+ if (left_down && !m_left_was_down) { m_fps_last_x = mx; m_fps_last_y = my; }
+ if (left_down && !over_ui)
+ m_camera.on_mouse_move(float(mx - m_fps_last_x), float(m_fps_last_y - my));
+ m_fps_last_x = mx; m_fps_last_y = my;
+ m_left_was_down = left_down;
+
+ bool over_kb = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureKeyboard;
+ bool moved = false;
+ if (!over_kb && dt > 0.0f)
+ {
+ float mdt = dt * (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f);
+ if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { m_camera.move_forward(mdt); moved = true; }
+ if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { m_camera.move_backward(mdt); moved = true; }
+ if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { m_camera.move_left(mdt); moved = true; }
+ if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { m_camera.move_right(mdt); moved = true; }
+ if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { m_camera.move_up(mdt); moved = true; }
+ if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { m_camera.move_down(mdt); moved = true; }
+ }
+ g_scroll_accum = 0.0;
+ m_user_moving = moved || (left_down && !over_ui);
+ }
+ else
+ {
+ Camera& cam = m_scene_mode ? m_scene_camera : m_camera;
+ if (left_down && !m_left_was_down && !over_ui)
+ cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0);
+ else if (!left_down && m_left_was_down)
+ cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0);
+ m_left_was_down = left_down;
+
+ cam.process_orbital_mouse_move(mx, my);
+
+ double scroll = g_scroll_accum; g_scroll_accum = 0.0;
+ if (scroll != 0.0 && !over_ui)
+ cam.process_orbital_scroll(0.0, scroll);
+
+ m_user_moving = cam.is_dragging() || cam.is_panning();
+ }
+ }
+
+ auto Application::set_free_fly(bool enabled) -> void
+ {
+ const bool is_fps = m_camera.get_camera_mode() == CameraMode::FPS;
+ if (enabled == is_fps) return;
+ if (enabled)
+ {
+ // Seed the fly pose from the current orbital framing so the view is continuous.
+ glm::vec3 pos = m_camera.get_orbital_position();
+ glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - pos);
+ float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f)));
+ float yaw = glm::degrees(atan2(fwd.z, fwd.x));
+ m_camera.set_camera_mode(CameraMode::FPS);
+ m_camera.set_movement_speed(2.0e10f);
+ m_camera.set_mouse_sensitivity(0.15f);
+ m_camera.set_position(pos);
+ m_camera.set_rotation(glm::vec3(pitch, yaw, 0.0f));
+ }
+ else
+ {
+ m_camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact
+ }
+ }
+
+ auto Application::reset_camera() -> void
+ {
+ m_camera.set_camera_mode(CameraMode::Orbital);
+ m_camera.set_orbital_radius(4e11);
+ m_camera.set_azimuth(0.0f);
+ m_camera.set_elevation(1.25f);
+ }
+
+ auto Application::set_hdri(const std::string& path) -> void
+ {
+ if (m_hdri_path == path || !m_device) return;
+ m_device->wait_idle();
+ m_cubemap = m_device->create_cubemap_from_hdri(path); // old cube freed after idle
+ m_hdri_path = path;
}
// One-time ImGui theme: rounded, roomy, dark with a warm accretion-disk accent.
@@ -120,8 +320,7 @@ namespace Donut
{
static bool styled = false;
if (!styled) { apply_donut_style(); styled = true; }
- if (!m_renderer) return;
- RendererBackend& r = *m_renderer;
+ if (!m_device) return;
// Full-viewport dock space with a pass-through centre, so the panel can be
// docked to any edge while the render shows through the middle.
@@ -130,9 +329,8 @@ namespace Donut
ImGui::SetNextWindowSize(ImVec2(340, 580), ImGuiCond_FirstUseEver);
ImGui::Begin("Donut \xc2\xb7 Black Hole");
- // --- header: device + performance -----------------------------------
ImGuiIO& io = ImGui::GetIO();
- ImGui::TextDisabled("%s", r.device_name().empty() ? "GPU" : r.device_name().c_str());
+ ImGui::TextDisabled("%s", m_device->device_name().empty() ? "GPU" : m_device->device_name().c_str());
ImGui::Text("%.0f FPS", io.Framerate);
ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f);
ImGui::Spacing();
@@ -155,29 +353,28 @@ namespace Donut
}
ImGui::Spacing();
- bool scene = r.is_scene_mode();
ImGui::TextDisabled("VIEW");
- if (ImGui::RadioButton("Black hole", !scene)) r.set_scene_mode(false);
+ if (ImGui::RadioButton("Black hole", !m_scene_mode)) m_scene_mode = false;
ImGui::SameLine();
- if (ImGui::RadioButton("Scene", scene)) r.set_scene_mode(true);
+ if (ImGui::RadioButton("Scene", m_scene_mode)) m_scene_mode = true;
ImGui::Separator();
- if (scene)
+ if (m_scene_mode)
{
build_scene_ui();
}
else
{
- BlackHoleParams& bh = r.black_hole_params();
+ BlackHoleParams& bh = m_bh_params;
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
{
- bool ff = r.is_free_fly();
- if (ImGui::RadioButton("Orbital", !ff)) r.set_free_fly(false);
+ bool ff = m_camera.get_camera_mode() == CameraMode::FPS;
+ if (ImGui::RadioButton("Orbital", !ff)) set_free_fly(false);
ImGui::SameLine();
- if (ImGui::RadioButton("Free-fly", ff)) r.set_free_fly(true);
+ if (ImGui::RadioButton("Free-fly", ff)) set_free_fly(true);
ImGui::SameLine();
- if (ImGui::Button("Reset view")) r.reset_camera();
+ if (ImGui::Button("Reset view")) reset_camera();
ImGui::SliderFloat("FOV", &bh.fov_degrees, 20.0f, 90.0f, "%.0f\xc2\xb0");
ImGui::TextDisabled(ff ? "WASD move | Q/E down-up | Shift boost | drag look"
: "Drag to orbit | scroll to zoom");
@@ -203,15 +400,14 @@ namespace Donut
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
auto& hdri = HDRIManager::get();
- std::string current = r.current_hdri();
- std::string preview = hdri.get_hdri_name(current);
+ std::string preview = hdri.get_hdri_name(m_hdri_path);
if (ImGui::BeginCombo("Starfield", preview.c_str()))
{
for (const auto& path : hdri.get_available_hdri())
{
- bool selected = (path == current);
+ bool selected = (path == m_hdri_path);
if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected))
- r.set_hdri(path);
+ set_hdri(path);
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
@@ -225,14 +421,12 @@ namespace Donut
auto Application::build_scene_ui() -> void
{
- RendererBackend& r = *m_renderer;
if (!ImGui::CollapsingHeader("Objects", ImGuiTreeNodeFlags_DefaultOpen))
return;
ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind.");
- auto& objs = r.scene_objects();
- static int sel = 0;
- if (sel >= (int)objs.size()) sel = (int)objs.size() - 1;
+ auto& objs = m_scene_objects;
+ if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1;
ImGui::Text("Spheres (%d)", (int)objs.size());
if (ImGui::Button("Add") && objs.size() < 64)
@@ -242,27 +436,26 @@ namespace Donut
o.radius = 1.5f;
o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4));
objs.push_back(o);
- sel = (int)objs.size() - 1;
+ m_selected_object = (int)objs.size() - 1;
}
ImGui::SameLine();
if (ImGui::Button("Delete") && !objs.empty())
{
- objs.erase(objs.begin() + sel);
- if (sel >= (int)objs.size()) sel = (int)objs.size() - 1;
+ objs.erase(objs.begin() + m_selected_object);
+ if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1;
}
ImGui::BeginChild("obj_list", ImVec2(0, 90), true);
for (int i = 0; i < (int)objs.size(); ++i)
{
std::string label = "Sphere " + std::to_string(i);
- if (ImGui::Selectable(label.c_str(), sel == i)) sel = i;
+ if (ImGui::Selectable(label.c_str(), m_selected_object == i)) m_selected_object = i;
}
ImGui::EndChild();
- r.set_selected_object(sel);
- if (sel >= 0 && sel < (int)objs.size())
+ if (m_selected_object >= 0 && m_selected_object < (int)objs.size())
{
- SceneObject& o = objs[sel];
+ SceneObject& o = objs[m_selected_object];
ImGui::DragFloat3("Position", &o.position.x, 0.1f);
ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f);
ImGui::ColorEdit3("Color", &o.color.x);
@@ -286,115 +479,30 @@ namespace Donut
dispatcher.dispatch<WindowResizeEvent>([this, &event](WindowResizeEvent& e)
{
- if (e.get_width() == 0 || e.get_height() == 0)
- m_minimized = true;
- else
- m_minimized = false;
-
- if (m_renderer)
- {
- m_renderer->resize(e.get_width(), e.get_height());
- }
- else
- {
- Renderer::on_window_resize(e.get_width(), e.get_height());
- if (m_engine) m_engine->set_window_dimensions(e.get_width(), e.get_height());
- }
+ m_minimized = (e.get_width() == 0 || e.get_height() == 0);
+ if (m_device) m_device->resize(e.get_width(), e.get_height());
event.handled = true;
return true;
});
-
- dispatcher.dispatch<KeyPressedEvent>([this, &event](KeyPressedEvent& e)
- {
- if (!m_state_manager)
- return true;
- if (e.get_key_code() == GLFW_KEY_1)
- {
- m_state_manager->switch_to_state("Config");
- event.handled = true;
- return true;
- }
- else if (e.get_key_code() == GLFW_KEY_2)
- {
- m_state_manager->switch_to_state("Simulation");
- event.handled = true;
- return true;
- }
- else if (e.get_key_code() == GLFW_KEY_3)
- {
- m_state_manager->switch_to_state("WorldBuilder");
- event.handled = true;
- return true;
- }
-
- return true;
- });
-
- if (m_state_manager)
- m_state_manager->on_event(event);
- }
-
- auto Application::on_init() -> void
- {
- // (Logger, settings and API selection happen in the constructor.) Both
- // backends run the SAME application through the RendererBackend interface,
- // so the app behaves identically regardless of the active graphics API.
- int w = 0, h = 0;
- glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h);
-
- if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
- m_renderer = create_scope<VulkanRenderer>();
- else
- m_renderer = create_scope<OpenGLRenderer>();
-
- if (!m_renderer->init(m_window->get_native_window(), w, h))
- DONUT_ERROR("Renderer initialization failed");
- else
- m_renderer->init_ui();
}
auto Application::on_shutdown() -> void
{
- if (m_renderer)
+ if (m_device)
{
- m_renderer->shutdown();
- m_renderer.reset();
- }
- else
- {
- if (m_state_manager)
- m_state_manager->shutdown();
- Renderer::shutdown();
+ m_device->wait_idle();
+ m_scene_renderer.reset();
+ m_black_hole_renderer.reset();
+ m_cubemap.reset();
+ // Free HDRIManager's cached GPU textures now, while the render context
+ // is still alive. On OpenGL these are GL textures owned by a static
+ // singleton; letting them destruct at program exit would call
+ // glDeleteTextures after the GL context is gone and crash on close.
+ HDRIManager::get().clear_cache();
+ m_device->shutdown();
+ m_device.reset();
}
SettingsManager::shutdown();
Logger::shutdown();
}
-
- auto Application::on_update() -> void
- {
- float current_frame = (float)glfwGetTime();
- m_delta_time = current_frame - m_last_frame;
- m_last_frame = current_frame;
-
- m_state_manager->update(m_delta_time);
- }
-
- auto Application::on_render() -> void
- {
- m_state_manager->render();
- m_window->begin_im_gui_frame();
-
- ImGuizmo::BeginFrame();
- ImGuizmo::SetOrthographic(false);
-
- setup_docking_layout();
- m_state_manager->on_im_ui_render();
- m_window->end_im_gui_frame();
- }
-
- auto Application::setup_docking_layout() -> void
- {
- ImGuiViewport* viewport = ImGui::GetMainViewport();
- ImGui::DockSpaceOverViewport(0, viewport, ImGuiDockNodeFlags_PassthruCentralNode);
- }
};
diff --git a/src/core/application.h b/src/core/application.h
index 4c377f0..4b67c84 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -1,16 +1,25 @@
#pragma once
-#include "state_manager.h"
#include "memory.h"
#include "window.h"
#include "event.h"
#include "log.h"
+#include "camera.h"
-#include "engine/engine.h"
-#include "rendering/renderer_backend.h"
+#include "rendering/rhi.h"
+#include "rendering/scene_renderer.h"
+#include "rendering/black_hole_renderer.h"
+
+#include <string>
+#include <vector>
namespace Donut
{
+ // The application owns ONE RHI::Device (OpenGL or Vulkan, chosen at startup)
+ // and the two portable renderers written on top of it, plus all the shared
+ // state (cameras, scene objects, disk parameters, HDRI). Because the rendering
+ // lives in the RHI and the shared renderers, the app behaves identically no
+ // matter which backend is active.
class Application
{
public:
@@ -21,33 +30,52 @@ namespace Donut
auto run() -> void;
auto close() -> void;
- auto get_window() -> Window& { return *m_window; }
- auto get_state_manager() -> StateManager& { return *m_state_manager; }
- auto get_engine() -> Engine& { return *m_engine; }
- static auto get() -> Application& { return *s_instance; }
+ auto get_window() -> Window& { return *m_window; }
+ static auto get() -> Application& { return *s_instance; }
private:
auto on_init() -> void;
auto on_shutdown() -> void;
- auto on_update() -> void;
- auto on_render() -> void;
auto on_event(Event& event) -> void;
- auto setup_docking_layout() -> void;
- auto build_ui() -> void; // shared ImGui panel, drives the active RendererBackend
- auto build_scene_ui() -> void; // scene-view (object list) section of the UI
+
+ auto build_ui() -> void; // shared ImGui panel
+ auto build_scene_ui() -> void; // scene-view (object list) section
+
+ auto process_input() -> void; // orbit / free-fly / zoom, was per-backend
+ auto set_free_fly(bool enabled) -> void;
+ auto reset_camera() -> void;
+ auto set_hdri(const std::string& path) -> void;
+
+ auto render_frame() -> void;
private:
- Scope<StateManager> m_state_manager;
Scope<Window> m_window;
- Scope<Engine> m_engine;
- Scope<RendererBackend> m_renderer; // active backend (non-null once a backend runs the app)
+
+ // The unified renderer: one device + the shared renderers on top of it.
+ Scope<RHI::Device> m_device;
+ Scope<SceneRenderer> m_scene_renderer;
+ Scope<BlackHoleRenderer> m_black_hole_renderer;
+ Ref<RHI::Texture> m_cubemap; // shared HDRI environment
+
+ Camera m_camera{ 60.0f, 16.0f / 9.0f, 0.1f, 100.0f }; // black-hole view
+ Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; // world-builder view
+
+ BlackHoleParams m_bh_params;
+ std::vector<SceneObject> m_scene_objects{ SceneObject{} };
+ int m_selected_object = 0;
+ bool m_scene_mode = false;
+ std::string m_hdri_path;
+
+ // input tracking (mirrors the old per-backend process_input)
+ bool m_left_was_down = false;
+ double m_fps_last_x = 0.0, m_fps_last_y = 0.0;
+ double m_last_frame_time = 0.0;
+ double m_start_time = 0.0;
+ bool m_user_moving = false;
bool m_running;
bool m_minimized;
- float m_delta_time = 0.0f;
- float m_last_frame = 0.0f;
-
static Application* s_instance;
};
}
diff --git a/src/core/hdri_manager.h b/src/core/hdri_manager.h
index fa05330..d98830f 100644
--- a/src/core/hdri_manager.h
+++ b/src/core/hdri_manager.h
@@ -39,9 +39,9 @@ namespace Donut
std::vector<std::string> m_available_hdri =
{
- "assets/hdri/HDR_blue_nebulae-1.hdr",
- "assets/hdri/HDR_subdued_blue_nebulae.hdr",
- "assets/hdri/HDR_subdued_multi_nebulae.hdr",
+ "assets/hdri/hdr_blue_nebulae_1.hdr",
+ "assets/hdri/hdr_subdued_blue_nebulae.hdr",
+ "assets/hdri/hdr_subdued_multi_nebulae.hdr",
"assets/hdri/night_sky.hdr"
};
};
diff --git a/src/core/settings_manager.cpp b/src/core/settings_manager.cpp
index ca4cac5..6909c60 100644
--- a/src/core/settings_manager.cpp
+++ b/src/core/settings_manager.cpp
@@ -1,7 +1,6 @@
#include "settings_manager.h"
#include "log.h"
#include "theme_manager.h"
-#include "rendering/renderer.h"
#include <fstream>
#include <filesystem>
diff --git a/src/core/state.h b/src/core/state.h
deleted file mode 100644
index 623f1c2..0000000
--- a/src/core/state.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-
-#include "event.h"
-
-namespace Donut
-{
- class State
- {
- public:
- virtual ~State() = default;
-
- virtual auto on_enter() -> void = 0;
- virtual auto on_exit() -> void = 0;
- virtual auto on_update(float delta_time) -> void = 0;
- virtual auto on_render() -> void = 0;
- virtual auto on_im_ui_render() -> void = 0;
- virtual auto on_event(Event& event) -> void = 0;
- };
-};
diff --git a/src/core/state_manager.cpp b/src/core/state_manager.cpp
deleted file mode 100644
index 8018401..0000000
--- a/src/core/state_manager.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-#include "state_manager.h"
-#include "log.h"
-
-namespace Donut
-{
- auto StateManager::shutdown() -> void
- {
- if (m_current_state)
- m_current_state->on_exit();
-
- destroy_states();
- DONUT_INFO("StateManager shutdown");
- }
-
- auto StateManager::destroy_states() -> void
- {
- m_states.clear();
- m_current_state = nullptr;
- m_current_state_name = "";
- }
-
- auto StateManager::register_state(const std::string& state_name, Scope<State> state) -> void
- {
- if (m_states.find(state_name) != m_states.end())
- DONUT_WARN("State '{}' already exists, overwriting", state_name);
- m_states[state_name] = std::move(state);
- DONUT_INFO("Registered state: {}", state_name);
- }
-
- auto StateManager::switch_to_state(const std::string& state_name) -> void
- {
- auto it = m_states.find(state_name);
- if (it == m_states.end())
- {
- DONUT_ERROR("Attempted to switch to unknown state: {}", state_name);
- return;
- }
-
- State* new_state = it->second.get();
- if (new_state == m_current_state)
- return;
-
- if (m_current_state)
- m_current_state->on_exit();
-
- m_current_state = new_state;
- m_current_state_name = state_name;
- m_current_state->on_enter();
- DONUT_INFO("Switched to state: {}", state_name);
- }
-
- auto StateManager::get_state(const std::string& state_name) -> State*
- {
- auto it = m_states.find(state_name);
- if (it != m_states.end())
- return it->second.get();
- return nullptr;
- }
-
- auto StateManager::has_state(const std::string& state_name) const -> bool
- {
- return m_states.find(state_name) != m_states.end();
- }
-
- auto StateManager::update(float delta_time) -> void
- {
- if (m_current_state)
- m_current_state->on_update(delta_time);
- }
-
- auto StateManager::render() -> void
- {
- if (m_current_state)
- m_current_state->on_render();
- }
-
- auto StateManager::on_im_ui_render() -> void
- {
- if (m_current_state)
- m_current_state->on_im_ui_render();
- }
-
- auto StateManager::on_event(Event& event) -> void
- {
- if (m_current_state)
- m_current_state->on_event(event);
- }
-}
diff --git a/src/core/state_manager.h b/src/core/state_manager.h
deleted file mode 100644
index 446841e..0000000
--- a/src/core/state_manager.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma once
-
-#include "memory.h"
-#include "state.h"
-
-#include <string>
-#include <unordered_map>
-
-namespace Donut
-{
- class StateManager
- {
- public:
- ~StateManager() = default;
- auto shutdown() -> void;
-
- auto update(float delta_time) -> void;
- auto render() -> void;
- auto on_im_ui_render() -> void;
- auto on_event(Event& event) -> void;
-
- auto register_state(const std::string& state_name, Scope<State> state) -> void;
- auto switch_to_state(const std::string& state_name) -> void;
-
- auto get_current_state_name() const -> std::string { return m_current_state_name; }
- auto get_current_state() const -> State* { return m_current_state; }
-
- auto get_state(const std::string& state_name) -> State*;
- auto has_state(const std::string& state_name) const -> bool;
-
- private:
- auto create_states() -> void;
- auto destroy_states() -> void;
-
- private:
- State* m_current_state = nullptr;
- std::string m_current_state_name = "";
-
- std::unordered_map<std::string, Scope<State>> m_states;
- };
-}
diff --git a/src/core/window.cpp b/src/core/window.cpp
index 047ef0d..dd5882c 100644
--- a/src/core/window.cpp
+++ b/src/core/window.cpp
@@ -1,14 +1,8 @@
-#include <glad/glad.h>
#include "window.h"
-#include "theme_manager.h"
-#include "rendering/renderer.h" // RendererAPI::get_api()
+#include "rendering/render_api.h" // RendererAPI::get_api()
#include <cstdint>
-#include <imgui.h>
-#include <imgui_impl_glfw.h>
-#include <imgui_impl_opengl3.h>
-
namespace Donut
{
static bool s_glfw_initialized = false;
@@ -94,8 +88,6 @@ namespace Donut
{
DONUT_INFO("Shutting down window: ", m_title);
- shutdown_im_gui();
-
glfwDestroyWindow(m_window);
s_glfw_window_count--;
@@ -133,101 +125,6 @@ namespace Donut
GLFW_CURSOR_NORMAL : GLFW_CURSOR_HIDDEN);
}
- auto Window::init_im_gui() -> void
- {
- IMGUI_CHECKVERSION();
- ImGui::CreateContext();
- ImGuiIO& io = ImGui::GetIO();
- io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
- io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
-#ifndef __APPLE__
- // Multi-viewport (dragging ImGui panels out as separate OS windows)
- // relies on a populated platform-monitor list and is unreliable on
- // macOS, where it intermittently asserts (Monitors.Size > 0) and
- // aborts. Docking stays enabled; panels just remain inside the window.
- io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
-#endif
-
- setup_im_gui_fonts();
- ThemeManager::set_theme(Theme::Dark);
-
- ImGuiStyle& style = ImGui::GetStyle();
- if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
- {
- style.WindowRounding = 0.0f;
- style.Colors[ImGuiCol_WindowBg].w = 1.0f;
- }
-
- ImGui_ImplGlfw_InitForOpenGL(m_window, true);
-#ifdef __APPLE__
- // macOS uses a core-profile context, which rejects the legacy
- // "#version 130" GLSL the ImGui backend defaults to. 150 is the
- // minimum core-profile GLSL that macOS's OpenGL 4.1 accepts.
- ImGui_ImplOpenGL3_Init("#version 150");
-#else
- ImGui_ImplOpenGL3_Init("#version 130");
-#endif
-
- DONUT_INFO("ImGUI initialized successfully");
- }
-
- auto Window::setup_im_gui_fonts() -> void
- {
- ImGuiIO& io = ImGui::GetIO();
- io.Fonts->Clear();
-
- m_main_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f);
- if (!m_main_font)
- {
- DONUT_WARN("Failed to load Inter font, falling back to default");
- m_main_font = io.Fonts->AddFontDefault();
- }
- else
- DONUT_INFO("Successfully loaded Inter font");
-
- m_large_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Bold.ttf", 20.0f);
- if (!m_large_font)
- m_large_font = m_main_font;
-
- m_small_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Light.ttf", 12.0f);
- if (!m_small_font)
- m_small_font = m_main_font;
-
- io.FontDefault = m_main_font;
- DONUT_INFO("ImGUI fonts loaded successfully");
- }
-
- auto Window::shutdown_im_gui() -> void
- {
- ImGui_ImplOpenGL3_Shutdown();
- ImGui_ImplGlfw_Shutdown();
- ImGui::DestroyContext();
-
- DONUT_INFO("ImGUI shutdown");
- }
-
- auto Window::begin_im_gui_frame() -> void
- {
- ImGui_ImplOpenGL3_NewFrame();
- ImGui_ImplGlfw_NewFrame();
- ImGui::NewFrame();
- }
-
- auto Window::end_im_gui_frame() -> void
- {
- ImGui::Render();
- ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
-
- ImGuiIO& io = ImGui::GetIO();
- if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
- {
- GLFWwindow* backup_current_context = glfwGetCurrentContext();
- ImGui::UpdatePlatformWindows();
- ImGui::RenderPlatformWindowsDefault();
- glfwMakeContextCurrent(backup_current_context);
- }
- }
-
auto Window::glfw_error_callback(int error, const char* description) -> void
{
DONUT_ERROR("GLFW Error ({}): {}", error, description ? description : "");
diff --git a/src/core/window.h b/src/core/window.h
index 9b7d7b9..b8b59c5 100644
--- a/src/core/window.h
+++ b/src/core/window.h
@@ -4,7 +4,6 @@
#include "log.h"
#include <GLFW/glfw3.h>
-#include <imgui.h>
#include <string>
@@ -38,17 +37,10 @@ namespace Donut
auto is_cursor_locked() const -> bool { return m_cursor_locked; }
auto is_cursor_visible() const -> bool { return m_cursor_visible; }
- auto init_im_gui() -> void;
- auto begin_im_gui_frame() -> void;
- auto end_im_gui_frame() -> void;
-
private:
auto init() -> void;
auto shutdown() -> void;
- auto setup_im_gui_fonts() -> void;
- auto shutdown_im_gui() -> void;
-
static auto glfw_error_callback(int error, const char* description) -> void;
static auto glfw_window_close_callback(GLFWwindow* window) -> void;
static auto glfw_window_size_callback(GLFWwindow* window, int width, int height) -> void;
@@ -70,9 +62,5 @@ namespace Donut
bool m_is_closed;
bool m_cursor_locked = false;
bool m_cursor_visible = true;
-
- ImFont* m_main_font = nullptr;
- ImFont* m_small_font = nullptr;
- ImFont* m_large_font = nullptr;
};
}
diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp
deleted file mode 100644
index bf5a2b6..0000000
--- a/src/engine/engine.cpp
+++ /dev/null
@@ -1,625 +0,0 @@
-#include <iostream>
-#include <fstream>
-#include <sstream>
-
-#include <GLFW/glfw3.h>
-#include <glad/glad.h>
-
-#define STB_IMAGE_WRITE_IMPLEMENTATION
-#include "stb_image_write.h"
-
-#include "engine.h"
-#include "core/log.h"
-#include "core/hdri_manager.h"
-#include "rendering/vertex_buffer.h"
-#include "rendering/index_buffer.h"
-
-namespace Donut
-{
- Engine::Engine()
- : m_sag_a(glm::vec3(0.0f, 0.0f, 0.0f), 8.54e36f)
- {
- m_width = 1280;
- m_height = 720;
-
- m_camera.set_camera_mode(CameraMode::Orbital);
- m_camera.set_orbital_radius(1e11);
- m_camera.set_orbital_limits(1e9, 1e13);
- m_camera.set_orbital_speed(0.01f);
- m_camera.set_zoom_speed(1e10f);
-
- m_objects =
- {
- { glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), glm::vec4(0, 0, 0, 1), static_cast<float>(m_sag_a.m_mass) }
- };
-
- // The geodesic ray tracer used to be a compute shader; it is now a
- // fullscreen vertex+fragment pass (see dispatch_compute) so it runs on
- // macOS OpenGL 4.1, which has no compute shaders.
- m_compute_program = Ref<Shader>(Shader::create("assets/shaders/Geodesic.glsl"));
- m_shader_program = Ref<Shader>(Shader::create("assets/shaders/TexturedQuad.glsl"));
- m_blur_shader = Ref<Shader>(Shader::create("assets/shaders/Blur.glsl"));
-
- auto& hdri_manager = HDRIManager::get();
- m_hdri_environment = hdri_manager.get_current_hdri();
- if (!m_hdri_environment)
- {
- hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr");
- m_hdri_environment = hdri_manager.get_current_hdri();
- if (!m_hdri_environment)
- DONUT_WARN("Failed to load default HDRI, using fallback");
- }
-
- m_camera_ubo = UniformBuffer::create(128, 1);
- m_disk_ubo = UniformBuffer::create(sizeof(float) * 6, 2); // r1,r2,turbulence,thickness,brightness,temperature
-
- uint32_t obj_ubo_size = sizeof(int) + 3 * sizeof(float)
- + 16 * (sizeof(glm::vec4) + sizeof(glm::vec4))
- + 16 * sizeof(float);
- m_objects_ubo = UniformBuffer::create(obj_ubo_size, 3);
-
- m_simulation_ubo = UniformBuffer::create(sizeof(int) * 2 + sizeof(float) * 2, 4);
-
- auto result = QuadVAO();
- m_quad_vao = result.first;
- m_texture = result.second;
-
- // GLSL 4.10 forbids explicit binding qualifiers on uniform blocks, so
- // associate the geodesic shader's blocks with their UBO binding points
- // from the host side instead.
- if (m_compute_program)
- {
- uint32_t prog = m_compute_program->get_renderer_id();
- struct { const char* name; uint32_t point; } blocks[] =
- {
- { "Camera", 1 }, { "Disk", 2 }, { "Objects", 3 }, { "Simulation", 4 }
- };
- for (const auto& b : blocks)
- {
- uint32_t idx = glGetUniformBlockIndex(prog, b.name);
- if (idx != GL_INVALID_INDEX)
- glUniformBlockBinding(prog, idx, b.point);
- }
- }
-
- glGenFramebuffers(1, &m_geodesic_fbo);
- }
-
- auto Engine::update_window_dimensions() -> void
- {
- update_compute_dimensions();
- }
-
- auto Engine::set_window_dimensions(int width, int height) -> void
- {
- int old_width = m_width;
- int old_height = m_height;
- int old_compute_height = m_compute_height;
-
- m_width = width;
- m_height = height;
-
- if (old_width != m_width ||
- old_height != m_height ||
- old_compute_height != m_compute_height)
- update_compute_dimensions();
- }
-
- auto Engine::update_performance(float delta_time) -> void
- {
- if (delta_time > 0.0f)
- m_current_fps = 1.0f / delta_time;
- }
-
- auto Engine::update_compute_dimensions() -> void
- {
- m_texture = Texture2D::create(get_compute_width(), m_compute_height);
- }
-
- auto Engine::draw_full_screen_quad() -> void
- {
- RenderCommand::set_viewport(0, 0, m_width, m_height);
-
- m_shader_program->bind();
- m_quad_vao->bind();
-
- m_texture->bind(0);
- m_shader_program->set_int("u_ScreenTexture", 0);
-
- RenderCommand::disable_depth_test();
- RenderCommand::draw_arrays(6);
- RenderCommand::enable_depth_test();
- }
-
- auto Engine::draw_blur_pass() -> void
- {
- RenderCommand::set_viewport(0, 0, m_width, m_height);
-
- m_blur_shader->bind();
- m_quad_vao->bind();
-
- m_texture->bind(0);
- m_blur_shader->set_int("u_ScreenTexture", 0);
- m_blur_shader->set_float2("u_Resolution", glm::vec2(m_width, m_height));
- m_blur_shader->set_float("u_BlurStrength", m_blur_strength);
- m_blur_shader->set_float("u_GlowIntensity", m_glow_intensity);
-
- RenderCommand::disable_depth_test();
- RenderCommand::draw_arrays(6);
- RenderCommand::enable_depth_test();
- }
-
- auto Engine::draw_geodesic_pass(int cw, int ch) -> void
- {
- m_quad_vao->bind();
- RenderCommand::disable_depth_test();
-
-#ifdef __APPLE__
- // macOS aborts any GPU submission that runs longer than a couple of
- // seconds ("GPU Hang"). The geodesic ray-marcher can far exceed that in
- // a single fullscreen draw, so render it in scissored tiles and flush
- // after each, keeping every submission short enough to survive the
- // watchdog. Compute-capable platforms draw it in one pass.
- // Largest tile that keeps a tile's worst-case work (tile^2 * step_cap)
- // inside the safe watchdog zone measured on this GPU (~25M pixel-steps
- // per submission); bigger tiles mean fewer glFinish stalls.
- const int tile = 64;
- glEnable(GL_SCISSOR_TEST);
- for (int y = 0; y < ch; y += tile)
- {
- int th = std::min(tile, ch - y);
- for (int x = 0; x < cw; x += tile)
- {
- int tw = std::min(tile, cw - x);
- glScissor(x, y, tw, th);
- RenderCommand::draw_arrays(6);
- glFinish();
- }
- }
- glDisable(GL_SCISSOR_TEST);
-#else
- RenderCommand::draw_arrays(6);
-#endif
-
- RenderCommand::enable_depth_test();
- }
-
- auto Engine::dispatch_compute(const Camera& cam) -> void
- {
- auto& hdri_manager = HDRIManager::get();
- m_hdri_environment = hdri_manager.get_current_hdri();
-
- int cw = get_compute_width();
- int ch = m_compute_height;
-
- // Render the geodesic pass into m_texture through an FBO. This replaces
- // the old compute dispatch + image_store path, which relied on OpenGL
- // 4.3 compute and 4.2 image load/store that macOS does not provide.
- glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture->get_renderer_id(), 0);
- glViewport(0, 0, cw, ch);
-
- m_compute_program->bind();
- upload_camera_ubo(cam);
- upload_disk_ubo();
- upload_objects_ubo(m_objects);
- upload_simulation_ubo();
- m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(cw), static_cast<float>(ch)));
-
- if (m_hdri_environment)
- {
- m_hdri_environment->bind(5);
- m_compute_program->set_int("u_HDRIEnvironment", 5);
- }
-
- draw_geodesic_pass(cw, ch);
-
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
-
- auto Engine::upload_camera_ubo(const Camera& cam) -> void
- {
- struct UBOData
- {
- glm::vec3 pos; float _pad0;
- glm::vec3 right; float _pad1;
- glm::vec3 up; float _pad2;
- glm::vec3 forward; float _pad3;
- float tan_half_fov;
- float aspect;
- bool moving;
- int _pad4;
- } data;
-
- glm::vec3 pos, fwd;
- if (cam.get_camera_mode() == CameraMode::FPS)
- {
- pos = cam.get_position();
- fwd = cam.get_forward_direction();
- }
- else
- {
- pos = cam.get_orbital_position();
- fwd = glm::normalize(cam.get_orbital_target() - pos);
- }
- glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
- glm::vec3 up = glm::cross(right, fwd);
-
- data.pos = pos;
- data.right = right;
- data.up = up;
- data.forward = fwd;
- data.tan_half_fov = static_cast<float>(tan(glm::radians(m_bh.fov_degrees * 0.5f)));
- data.aspect = static_cast<float>(get_compute_width()) / static_cast<float>(m_compute_height);
- data.moving = cam.is_dragging() || cam.is_panning();
-
- m_camera_ubo->set_data(&data, sizeof(UBOData));
- m_camera_ubo->bind(1);
- }
-
- auto Engine::upload_objects_ubo(const std::vector<ObjectData>& objs) -> void
- {
- struct UBOData
- {
- int num_objects;
- float _pad0, _pad1, _pad2;
- glm::vec4 pos_radius[16];
- glm::vec4 color[16];
- float mass[16];
- } data;
-
- size_t count = std::min(objs.size(), size_t(16));
- data.num_objects = static_cast<int>(count);
-
- for (size_t i = 0; i < count; ++i)
- {
- data.pos_radius[i] = objs[i].m_pos_radius;
- data.color[i] = objs[i].m_color;
- data.mass[i] = objs[i].m_mass;
- }
-
- m_objects_ubo->set_data(&data, sizeof(data));
- m_objects_ubo->bind(3);
- }
-
- auto Engine::upload_disk_ubo() -> void
- {
- // Layout matches the shared Geodesic Disk struct (same as Vulkan). Radii
- // are in Schwarzschild radii x the shader's SagA_rs constant (1.269e10).
- const float rs = 1.269e10f;
- float disk_data[6] = {
- std::max(m_bh.disk_inner_rs, 3.0f) * rs,
- std::max(m_bh.disk_outer_rs, m_bh.disk_inner_rs + 0.5f) * rs,
- std::max(m_bh.turbulence, 0.0f),
- rs * 0.1f, // slab half-thickness (fixed)
- std::max(m_bh.brightness, 0.0f),
- std::max(m_bh.temperature, 1000.0f),
- };
- m_disk_ubo->set_data(disk_data, sizeof(disk_data));
- m_disk_ubo->bind(2);
- }
-
- auto Engine::upload_simulation_ubo() -> void
- {
- struct UBOData
- {
- int max_steps_moving;
- int max_steps_static;
- float early_exit_distance;
- float time;
- } data;
-
- data.max_steps_moving = m_bh.quality_steps;
- data.max_steps_static = m_bh.quality_steps; // resolution/tiling is the lever, not step count
- data.early_exit_distance = m_early_exit_distance;
- data.time = static_cast<float>(glfwGetTime()) * m_rotation_speed;
-
-#ifdef __APPLE__
- // macOS has no compute shaders, so the geodesic pass runs as a tiled
- // fragment shader under the OS GPU watchdog. Cap the step count so a
- // single 64px tile stays under the safe per-submission budget (~25M
- // pixel-steps); Windows/Linux keep the full count.
- data.max_steps_moving = std::min(data.max_steps_moving, 6000);
- data.max_steps_static = std::min(data.max_steps_static, 6000);
-#endif
-
- m_simulation_ubo->set_data(&data, sizeof(data));
- m_simulation_ubo->bind(4);
- }
-
- auto Engine::update_physics(float delta_time) -> void
- {
- for (auto& obj : m_objects)
- {
- for (auto& obj2 : m_objects)
- {
- if (&obj == &obj2) continue;
- float dx = obj2.m_pos_radius.x - obj.m_pos_radius.x;
- float dy = obj2.m_pos_radius.y - obj.m_pos_radius.y;
- float dz = obj2.m_pos_radius.z - obj.m_pos_radius.z;
- float distance = sqrt(dx * dx + dy * dy + dz * dz);
- if (distance > 0)
- {
- std::vector<double> direction = {dx / distance, dy / distance, dz / distance};
- double Gforce = (G * obj.m_mass * obj2.m_mass) / (distance * distance);
- double acc1 = Gforce / obj.m_mass;
- std::vector<double> acc = {direction[0] * acc1, direction[1] * acc1, direction[2] * acc1};
-
- if (m_gravity)
- {
- obj.m_velocity.x += static_cast<float>(acc[0]);
- obj.m_velocity.y += static_cast<float>(acc[1]);
- obj.m_velocity.z += static_cast<float>(acc[2]);
-
- obj.m_pos_radius.x += static_cast<float>(obj.m_velocity.x);
- obj.m_pos_radius.y += static_cast<float>(obj.m_velocity.y);
- obj.m_pos_radius.z += static_cast<float>(obj.m_velocity.z);
- }
- }
- }
- }
- }
-
- auto Engine::render_scene() -> void
- {
- RenderCommand::clear();
- m_shader_program->bind();
- m_quad_vao->bind();
- m_texture->bind(0);
- RenderCommand::draw_arrays(6);
- }
-
- auto Engine::create_compute_program(const char* path) -> Ref<Shader>
- {
- std::ifstream in(path);
- if(!in.is_open())
- {
- std::cerr << "Failed to open compute shader: " << path << "\n";
- return nullptr;
- }
- std::stringstream ss;
- ss << in.rdbuf();
- std::string src_str = ss.str();
- return Ref<Shader>(Shader::create_compute("ComputeShader", src_str));
- }
-
- auto Engine::QuadVAO() -> std::pair<Ref<VertexArray>, Ref<Texture2D>>
- {
- float quad_vertices[] =
- {
- // Positions // TexCoords
- -1.0f, 1.0f, 0.0f, 1.0f,
- -1.0f, -1.0f, 0.0f, 0.0f,
- 1.0f, -1.0f, 1.0f, 0.0f,
- -1.0f, 1.0f, 0.0f, 1.0f,
- 1.0f, -1.0f, 1.0f, 0.0f,
- 1.0f, 1.0f, 1.0f, 1.0f
- };
-
- auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(quad_vertices, static_cast<uint32_t>(sizeof(quad_vertices))));
- VertexBufferLayout layout;
- layout.push<float>(2); // Position (x, y)
- layout.push<float>(2); // TexCoord (u, v)
- vertex_buffer->set_layout(layout);
-
- auto vertex_array = Ref<VertexArray>(VertexArray::create());
- vertex_array->add_vertex_buffer(vertex_buffer);
- auto texture = Texture2D::create(get_compute_width(), m_compute_height);
-
- return { vertex_array, texture };
- }
-
- auto Engine::load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void
- {
- m_objects.clear();
- m_objects.push_back(
- {
- glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs),
- glm::vec4(0, 0, 0, 1),
- static_cast<float>(m_sag_a.m_mass)
- });
-
- for (const auto& obj : objects)
- {
- ObjectData engine_obj;
-
- float scale_factor = 1e10f;
- engine_obj.m_pos_radius = glm::vec4
- (
- obj.m_centre.x * scale_factor,
- obj.m_centre.y * scale_factor,
- obj.m_centre.z * scale_factor,
- obj.m_radius * scale_factor
- );
-
- engine_obj.m_color = glm::vec4(obj.m_material.m_color, 1.0f);
-
- float volume = (4.0f / 3.0f) * 3.14159f * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w;
- float density = 1e12f;
- engine_obj.m_mass = volume * density;
- engine_obj.m_velocity = glm::vec3(0.0f, 0.0f, 0.0f);
-
- m_objects.push_back(engine_obj);
- }
-
- DONUT_INFO("Loaded {} objects from WorldBuilder scene (scaled up by {})", objects.size(), 1e10f);
- print_object_info();
- }
-
- auto Engine::print_object_info() const -> void
- {
- DONUT_INFO("=== Object Information ===");
- DONUT_INFO("Total objects: {}", m_objects.size());
-
- for (size_t i = 0; i < m_objects.size(); ++i)
- {
- const auto& obj = m_objects[i];
- DONUT_INFO("Object {}: Pos=({}, {}, {}), Radius={}, Mass={}, Color=({}, {}, {})",
- i,
- obj.m_pos_radius.x, obj.m_pos_radius.y, obj.m_pos_radius.z,
- obj.m_pos_radius.w,
- obj.m_mass,
- obj.m_color.x, obj.m_color.y, obj.m_color.z
- );
- }
- DONUT_INFO("Camera position: ({}, {}, {})",
- m_camera.get_orbital_position().x,
- m_camera.get_orbital_position().y,
- m_camera.get_orbital_position().z
- );
- DONUT_INFO("Camera radius: {}", m_camera.get_orbital_radius());
- DONUT_INFO("========================");
- }
-
- auto Engine::export_high_res_frame(const std::string& filename, int width, int height) -> void
- {
- DONUT_INFO("Exporting high-resolution frame: {}x{} to {}", width, height, filename);
-
- if (width <= 0 || height <= 0)
- {
- DONUT_ERROR("Invalid dimensions for export: {}x{}", width, height);
- return;
- }
-
- if (filename.empty())
- {
- DONUT_ERROR("Invalid filename for export");
- return;
- }
-
- int original_width = m_width;
- int original_height = m_height;
- int original_compute_height = m_compute_height;
-
- m_width = width;
- m_height = height;
- m_compute_height = height;
-
- int compute_height = height;
- int compute_width = (width * compute_height) / height;
-
- if (compute_width <= 0 || compute_height <= 0)
- {
- DONUT_ERROR("Invalid compute dimensions: {}x{}", compute_width, compute_height);
- return;
- }
-
- FramebufferSpecification fb_spec;
- fb_spec.Width = width;
- fb_spec.Height = height;
- fb_spec.attachments = { FramebufferTextureFormat::RGBA8 };
-
- auto high_res_framebuffer = Framebuffer::create(fb_spec);
- if (!high_res_framebuffer)
- {
- DONUT_ERROR("Failed to create high-resolution framebuffer");
- return;
- }
-
- auto high_res_texture = Texture2D::create(compute_width, compute_height);
- if (!high_res_texture)
- {
- DONUT_ERROR("Failed to create high-resolution texture");
- return;
- }
-
- // Render the geodesic pass into high_res_texture through the geodesic FBO.
- glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, high_res_texture->get_renderer_id(), 0);
- glViewport(0, 0, compute_width, compute_height);
-
- m_compute_program->bind();
-
- struct UBOData
- {
- glm::vec3 pos; float _pad0;
- glm::vec3 right; float _pad1;
- glm::vec3 up; float _pad2;
- glm::vec3 forward; float _pad3;
- float tan_half_fov;
- float aspect;
- bool moving;
- int _pad4;
- } data;
-
- glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - m_camera.get_orbital_position());
- glm::vec3 up = glm::vec3(0, 1, 0);
- glm::vec3 right = glm::normalize(glm::cross(fwd, up));
- up = glm::cross(right, fwd);
-
- data.pos = m_camera.get_orbital_position();
- data.right = right;
- data.up = up;
- data.forward = fwd;
- data.tan_half_fov = static_cast<float>(tan(glm::radians(60.0f * 0.5f)));
- data.aspect = static_cast<float>(compute_width) / static_cast<float>(compute_height);
- data.moving = m_camera.is_dragging() || m_camera.is_panning();
-
- m_camera_ubo->set_data(&data, sizeof(UBOData));
- m_camera_ubo->bind(1);
-
- upload_disk_ubo();
- upload_objects_ubo(m_objects);
- upload_simulation_ubo();
- m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast<float>(compute_width), static_cast<float>(compute_height)));
-
- if (m_hdri_environment)
- {
- m_hdri_environment->bind(5);
- m_compute_program->set_int("u_HDRIEnvironment", 5);
- }
-
- draw_geodesic_pass(compute_width, compute_height);
-
- // Display the rendered frame into the high-res framebuffer for read-back.
- high_res_framebuffer->bind();
- RenderCommand::set_viewport(0, 0, width, height);
- RenderCommand::clear();
-
- m_shader_program->bind();
- m_quad_vao->bind();
-
- high_res_texture->bind(0);
- m_shader_program->set_int("u_ScreenTexture", 0);
-
- RenderCommand::disable_depth_test();
- RenderCommand::draw_arrays(6);
- RenderCommand::enable_depth_test();
-
- std::vector<unsigned char> pixels(width * height * 4);
- DONUT_INFO("Reading {} pixels from framebuffer...", width * height);
- RenderCommand::read_pixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
-
- DONUT_INFO("Flipping image vertically...");
- std::vector<unsigned char> flipped_pixels(width * height * 4);
- for (int y = 0; y < height; ++y)
- {
- for (int x = 0; x < width; ++x)
- {
- int src_index = (y * width + x) * 4;
- int dst_index = ((height - 1 - y) * width + x) * 4;
- flipped_pixels[dst_index + 0] = pixels[src_index + 0]; // R
- flipped_pixels[dst_index + 1] = pixels[src_index + 1]; // G
- flipped_pixels[dst_index + 2] = pixels[src_index + 2]; // B
- flipped_pixels[dst_index + 3] = pixels[src_index + 3]; // A
- }
- }
-
- DONUT_INFO("Saving PNG file: {}...", filename);
- int result = stbi_write_png(filename.c_str(), width, height, 4, flipped_pixels.data(), width * 4);
-
- if (result)
- DONUT_INFO("Successfully exported high-resolution frame to: {}", filename);
- else
- DONUT_ERROR("Failed to export high-resolution frame to: {}", filename);
-
- high_res_framebuffer->unbind();
-
- m_width = original_width;
- m_height = original_height;
- m_compute_height = original_compute_height;
-
- RenderCommand::set_viewport(0, 0, original_width, original_height);
- }
-
-}
diff --git a/src/engine/engine.h b/src/engine/engine.h
deleted file mode 100644
index fef8b57..0000000
--- a/src/engine/engine.h
+++ /dev/null
@@ -1,191 +0,0 @@
-#pragma once
-
-#include <vector>
-#include <numbers>
-#include <iostream>
-#include <fstream>
-#include <sstream>
-#include <chrono>
-#include <cmath>
-
-#include "core/camera.h"
-#include "object.h"
-
-#include "rendering/renderer.h"
-#include "rendering/renderer_backend.h"
-#include "rendering/shader.h"
-#include "rendering/vertex_array.h"
-#include "rendering/texture.h"
-#include "rendering/uniform_buffer.h"
-#include "rendering/texture_manager.h"
-
-#include <GLFW/glfw3.h>
-#include <glm/glm.hpp>
-#include <glm/gtc/matrix_transform.hpp>
-#include <glm/gtc/type_ptr.hpp>
-
-namespace Donut
-{
- const double c = 299792458.0;
- const double G = 6.67430e-11;
-
- struct BlackHole
- {
- glm::vec3 m_position;
- double m_mass;
- double m_radius;
- double m_rs;
-
- BlackHole(glm::vec3 pos, float mass)
- : m_position(pos), m_mass(mass)
- {
- m_rs = 2.0 * G * m_mass / (c * c);
- }
-
- bool intercept(float px, float py, float pz) const
- {
- double dx = double(px) - double(m_position.x);
- double dy = double(py) - double(m_position.y);
- double dz = double(pz) - double(m_position.z);
- double dist2 = dx * dx + dy * dy + dz * dz;
- return dist2 < m_rs * m_rs;
- }
- };
-
- struct ObjectData
- {
- glm::vec4 m_pos_radius;
- glm::vec4 m_color;
- float m_mass;
- glm::vec3 m_velocity = glm::vec3(0.0f, 0.0f, 0.0f);
- };
-
- class Engine
- {
- public:
- Engine();
- ~Engine() = default;
-
- auto draw_full_screen_quad() -> void;
- auto draw_blur_pass() -> void;
- auto dispatch_compute(const Camera& cam) -> void;
- auto upload_camera_ubo(const Camera& cam) -> void;
- auto upload_objects_ubo(const std::vector<ObjectData>& objs) -> void;
- auto upload_disk_ubo() -> void;
- auto upload_simulation_ubo() -> void;
- auto render_scene() -> void;
- auto update_physics(float delta_time) -> void;
- auto update_window_dimensions() -> void;
- auto set_window_dimensions(int width, int height) -> void;
-
- auto get_width() const -> int { return m_width; }
- auto get_height() const -> int { return m_height; }
-
- auto get_objects() -> std::vector<ObjectData>& { return m_objects; }
- auto get_sag_a() -> BlackHole& { return m_sag_a; }
- auto get_camera() -> Camera& { return m_camera; }
- auto get_gravity() -> bool& { return m_gravity; }
-
- auto update_performance(float delta_time) -> void;
- auto set_target_fps(int fps) -> void { m_target_fps = fps; }
- auto get_target_fps() const -> int { return m_target_fps; }
- auto get_current_fps() const -> float { return m_current_fps; }
- void set_compute_height(int height)
- {
-#ifdef __APPLE__
- // Without compute shaders the geodesic pass runs as a tiled
- // fragment shader (see draw_geodesic_pass), so very high working
- // resolutions make each frame take many seconds. Cap it on macOS.
- if (height > 256) height = 256;
-#endif
- m_compute_height = height;
- }
- auto get_compute_height() const -> int { return m_compute_height; }
- auto get_compute_width() const -> int { return (m_width * m_compute_height) / m_height; }
- auto update_compute_dimensions() -> void;
-
- auto get_max_steps_moving() const -> int { return m_max_steps_moving; }
- auto get_max_steps_static() const -> int { return m_max_steps_static; }
- auto get_early_exit_distance() const -> float { return m_early_exit_distance; }
- auto set_max_steps_moving(int steps) -> void { m_max_steps_moving = steps; }
- auto set_max_steps_static(int steps) -> void { m_max_steps_static = steps; }
- auto set_early_exit_distance(float distance) -> void { m_early_exit_distance = distance; }
-
- auto get_disk_thickness() const -> float { return m_disk_thickness; }
- auto set_disk_thickness(float thickness) -> void { m_disk_thickness = thickness; }
-
- auto get_disk_density() const -> float { return m_disk_density; }
- auto set_disk_density(float density) -> void { m_disk_density = density; }
-
- auto get_rotation_speed() const -> float { return m_rotation_speed; }
- auto set_rotation_speed(float speed) -> void { m_rotation_speed = speed; }
-
- auto get_blur_strength() const -> float { return m_blur_strength; }
- auto set_blur_strength(float strength) -> void { m_blur_strength = strength; }
-
- auto get_glow_intensity() const -> float { return m_glow_intensity; }
- auto set_glow_intensity(float intensity) -> void { m_glow_intensity = intensity; }
-
- auto load_objects_from_scene(const std::vector<Donut::Object>& objects) -> void;
- auto export_high_res_frame(const std::string& filename, int width = 4096, int height = 3072) -> void;
- auto print_object_info() const -> void;
-
- auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; }
- auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; }
-
- // Shared live black-hole/disk parameters (drives the geodesic UBOs so the
- // GL path renders identically to Vulkan). The OpenGL backend mutates this.
- auto black_hole_params() -> BlackHoleParams& { return m_bh; }
- private:
- auto create_compute_program(const char* path) -> Ref<Shader>;
- std::pair<Ref<VertexArray>, Ref<Texture2D>> QuadVAO();
-
- // Draws the bound geodesic shader over a cw x ch target. On macOS this
- // is split into scissored tiles (with a flush each) so no single GPU
- // submission trips the OS watchdog; elsewhere it is one fast draw.
- auto draw_geodesic_pass(int cw, int ch) -> void;
- private:
- Ref<VertexArray> m_quad_vao;
- Ref<Texture2D> m_texture;
- Ref<CubemapTexture> m_hdri_environment;
- Ref<Shader> m_shader_program;
- Ref<Shader> m_compute_program;
- Ref<Shader> m_blur_shader;
- Ref<UniformBuffer> m_camera_ubo;
- Ref<UniformBuffer> m_disk_ubo;
- Ref<UniformBuffer> m_objects_ubo;
- Ref<UniformBuffer> m_simulation_ubo;
-
- // FBO used to render the geodesic pass into m_texture. The geodesic
- // shader is a fragment shader (macOS has no compute), so it draws a
- // fullscreen quad into this framebuffer instead of dispatching compute.
- uint32_t m_geodesic_fbo = 0;
-
- int m_width;
- int m_height;
- float m_width_f = 100.0f*1e10f;
- float m_height_f = 75.0f*1e10f;
-
- int m_target_fps = 60;
- float m_current_fps = 60.0f;
- float m_last_frame_time = 0.0f;
- int m_compute_height = 150;
-
- std::vector<ObjectData> m_objects;
- BlackHole m_sag_a;
- Camera m_camera;
- bool m_gravity = false;
-
- int m_max_steps_moving = 60000;
- int m_max_steps_static = 30000;
- float m_early_exit_distance = 5.0e11f;
-
- BlackHoleParams m_bh; // shared UI-tunable disk/camera params (GL geodesic driver)
-
- float m_disk_thickness = 0.1f;
- float m_disk_density = 0.1f;
- float m_rotation_speed = 1.0f;
- float m_blur_strength = 2.0f;
- float m_glow_intensity = 0.1f;
- };
-};
diff --git a/src/engine/object.h b/src/engine/object.h
deleted file mode 100644
index 8101540..0000000
--- a/src/engine/object.h
+++ /dev/null
@@ -1,76 +0,0 @@
-#pragma once
-
-#include <glm/glm.hpp>
-
-namespace Donut
-{
- class Ray
- {
- public:
- Ray(glm::vec3 o, glm::vec3 d)
- : m_origin(o),
- m_direction(glm::normalize(d)) { }
-
- public:
- glm::vec3 m_direction;
- glm::vec3 m_origin;
- };
-
- class Material
- {
- public:
- Material() : m_color(1.0f, 1.0f, 1.0f), m_specular(0.5f), m_emission(0.0f) { }
- Material(glm::vec3 c, float s, float e)
- : m_color(c),
- m_specular(s),
- m_emission(e) { }
-
- public:
- glm::vec3 m_color;
- float m_specular;
- float m_emission;
- };
-
- class Object
- {
- public:
- Object() : m_centre(0.0f, 0.0f, 0.0f), m_radius(1.0f), m_material() { }
- Object(glm::vec3 c, float r, Material m)
- : m_centre(c),
- m_radius(r),
- m_material(m) { }
-
- auto intersect(Ray& ray, float& t) -> bool
- {
- glm::vec3 oc = ray.m_origin - m_centre;
- float a = glm::dot(ray.m_direction, ray.m_direction);
- float b = 2.0f * glm::dot(oc, ray.m_direction);
- float c = glm::dot(oc, oc) - m_radius * m_radius;
- float discriminant = static_cast<float>(b*b - 4*a*c);
-
- if (discriminant < 0)
- return false;
-
- float intercept = (-b - sqrt(discriminant)) / (2.0f*a);
- if (intercept < 0)
- {
- intercept = (-b + sqrt(discriminant)) / (2.0f*a);
- if (intercept < 0)
- return false;
- }
-
- t = intercept;
- return true;
- }
-
- auto get_normal(glm::vec3& point) const -> glm::vec3
- {
- return glm::normalize(point - m_centre);
- }
-
- public:
- glm::vec3 m_centre;
- float m_radius;
- Material m_material;
- };
-};
diff --git a/src/engine/scene.h b/src/engine/scene.h
deleted file mode 100644
index 4b41203..0000000
--- a/src/engine/scene.h
+++ /dev/null
@@ -1,70 +0,0 @@
-#pragma once
-
-#include <vector>
-#include <limits>
-
-#include "object.h"
-
-namespace Donut
-{
- class Scene
- {
- public:
- Scene()
- : m_light_pos(5.0f, 5.0f, 5.0f) { }
-
- auto trace(Ray& ray) -> glm::vec3
- {
- float closest = std::numeric_limits<float>::infinity();
- const Object* hit_obj = nullptr;
-
- for (auto& obj : objs)
- {
- float t;
-
- if (obj.intersect(ray, t))
- if (t < closest)
- {
- closest = t;
- hit_obj = &obj;
- }
- }
-
- if (hit_obj)
- {
- glm::vec3 hit_point = ray.m_origin + ray.m_direction * closest;
- glm::vec3 normal = hit_obj->get_normal(hit_point);
- glm::vec3 light_dir = glm::normalize(m_light_pos - hit_point);
-
- float diff = std::max(glm::dot(normal, light_dir), 0.0f);
-
- Ray shadow_ray(hit_point + normal * 0.001f, light_dir);
- bool in_shadow = false;
-
- for (auto& obj : objs)
- {
- float t;
-
- if (obj.intersect(shadow_ray, t))
- {
- in_shadow = true;
- break;
- }
- }
-
- glm::vec3 color = hit_obj->m_material.m_color;
- float ambient = 0.1f;
-
- if (in_shadow)
- return color * ambient;
- return color * (ambient + diff * 0.9f);
- }
-
- return glm::vec3(0.0f, 0.0f, 0.1f);
- }
-
- public:
- std::vector<Object> objs;
- glm::vec3 m_light_pos;
- };
-};
diff --git a/src/platform/metal/metal_context.h b/src/platform/metal/metal_context.h
deleted file mode 100644
index f1e376b..0000000
--- a/src/platform/metal/metal_context.h
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-// Pure-C++ interface to the Metal backend (no Objective-C leaks into the rest
-// of the engine). Implemented in MetalContext.mm.
-namespace Donut
-{
- // Phase 1 bring-up: creates the default Metal device + command queue and
- // logs its capabilities, proving the Metal toolchain and build integration
- // work natively on Apple Silicon. This grows into the Metal compute island
- // that runs the geodesic ray tracer in real time.
- auto metal_probe() -> bool;
-
- // Compiles + dispatches a trivial compute kernel and validates the read-back,
- // proving the Metal compute path the geodesic ray tracer will run on.
- auto metal_compute_self_test() -> bool;
-}
diff --git a/src/platform/metal/metal_context.mm b/src/platform/metal/metal_context.mm
deleted file mode 100644
index 71e8551..0000000
--- a/src/platform/metal/metal_context.mm
+++ /dev/null
@@ -1,124 +0,0 @@
-#import <Metal/Metal.h>
-#import <Foundation/Foundation.h>
-
-#include "metal_context.h"
-#include "core/log.h"
-
-#include <vector>
-
-namespace Donut
-{
- bool metal_probe()
- {
- @autoreleasepool
- {
- id<MTLDevice> device = MTLCreateSystemDefaultDevice();
- if (device == nil)
- {
- DONUT_ERROR("Metal: no default device available");
- return false;
- }
-
- id<MTLCommandQueue> queue = [device newCommandQueue];
- const char* name = [[device name] UTF8String];
-
- DONUT_INFO("Metal device: {}", name ? name : "(unknown)");
- DONUT_INFO("Metal: unified memory = {}, max threads/threadgroup = {}",
- device.hasUnifiedMemory ? "yes" : "no",
- (unsigned long)device.maxThreadsPerThreadgroup.width);
-
- if (queue == nil)
- {
- DONUT_WARN("Metal: failed to create command queue");
- return false;
- }
-
- return true;
- }
- }
-
- // Compiles a trivial compute kernel, dispatches it over a small texture, and
- // reads the result back to confirm the full Metal compute path works: source
- // compilation, pipeline state, command encoding, dispatch, and shared-memory
- // read-back. This is the mechanism the geodesic ray tracer will run on.
- bool metal_compute_self_test()
- {
- @autoreleasepool
- {
- id<MTLDevice> device = MTLCreateSystemDefaultDevice();
- id<MTLCommandQueue> queue = [device newCommandQueue];
- if (device == nil || queue == nil)
- return false;
-
- NSString* src =
- @"#include <metal_stdlib>\n"
- "using namespace metal;\n"
- "kernel void selfTest(texture2d<float, access::write> outTex [[texture(0)]],\n"
- " uint2 gid [[thread_position_in_grid]])\n"
- "{\n"
- " uint w = outTex.get_width();\n"
- " uint h = outTex.get_height();\n"
- " if (gid.x >= w || gid.y >= h) return;\n"
- " outTex.write(float4(float(gid.x) / float(w - 1),\n"
- " float(gid.y) / float(h - 1), 0.5, 1.0), gid);\n"
- "}\n";
-
- NSError* err = nil;
- id<MTLLibrary> lib = [device newLibraryWithSource:src options:nil error:&err];
- if (lib == nil)
- {
- DONUT_ERROR("Metal self-test: kernel compile failed: {}",
- err ? [[err localizedDescription] UTF8String] : "unknown");
- return false;
- }
-
- id<MTLFunction> fn = [lib newFunctionWithName:@"selfTest"];
- id<MTLComputePipelineState> pipeline =
- [device newComputePipelineStateWithFunction:fn error:&err];
- if (pipeline == nil)
- {
- DONUT_ERROR("Metal self-test: pipeline creation failed");
- return false;
- }
-
- const uint32_t W = 64, H = 64;
- MTLTextureDescriptor* desc =
- [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
- width:W
- height:H
- mipmapped:NO];
- desc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead;
- desc.storageMode = MTLStorageModeShared;
- id<MTLTexture> tex = [device newTextureWithDescriptor:desc];
-
- id<MTLCommandBuffer> cb = [queue commandBuffer];
- id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder];
- [enc setComputePipelineState:pipeline];
- [enc setTexture:tex atIndex:0];
-
- MTLSize tg = MTLSizeMake(16, 16, 1);
- MTLSize grid = MTLSizeMake(W, H, 1);
- [enc dispatchThreads:grid threadsPerThreadgroup:tg];
- [enc endEncoding];
- [cb commit];
- [cb waitUntilCompleted];
-
- // Read back the far corner; the kernel writes (~1, ~1, 0.5, 1) there.
- std::vector<uint8_t> px(W * H * 4);
- [tex getBytes:px.data()
- bytesPerRow:W * 4
- fromRegion:MTLRegionMake2D(0, 0, W, H)
- mipmapLevel:0];
-
- size_t corner = ((size_t)(H - 1) * W + (W - 1)) * 4;
- DONUT_INFO("Metal compute self-test: corner pixel RGBA = ({}, {}, {}, {})",
- (int)px[corner + 0], (int)px[corner + 1],
- (int)px[corner + 2], (int)px[corner + 3]);
-
- bool ok = px[corner + 0] > 250 && px[corner + 1] > 250 &&
- px[corner + 3] == 255;
- DONUT_INFO("Metal compute self-test: {}", ok ? "PASS" : "FAIL");
- return ok;
- }
- }
-}
diff --git a/src/platform/opengl/opengl_device.cpp b/src/platform/opengl/opengl_device.cpp
new file mode 100644
index 0000000..3d45d24
--- /dev/null
+++ b/src/platform/opengl/opengl_device.cpp
@@ -0,0 +1,279 @@
+#include "opengl_device.h"
+
+#include "core/log.h"
+#include "core/hdri_manager.h"
+#include "rendering/shader.h"
+#include "rendering/texture.h"
+
+#include <glad/glad.h>
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <imgui_impl_glfw.h>
+#include <imgui_impl_opengl3.h>
+
+namespace Donut::RHI
+{
+ namespace
+ {
+ auto gl_topology(Topology t) -> GLenum { return t == Topology::Lines ? GL_LINES : GL_TRIANGLES; }
+ auto gl_compare(CompareOp o) -> GLenum { return o == CompareOp::Always ? GL_ALWAYS : o == CompareOp::LessEqual ? GL_LEQUAL : GL_LESS; }
+ auto gl_filter(Filter f) -> GLint { return f == Filter::Nearest ? GL_NEAREST : GL_LINEAR; }
+ auto gl_internal(Format f) -> GLint { return f == Format::RGBA16F ? GL_RGBA16F : f == Format::D32 ? GL_DEPTH_COMPONENT32F : GL_RGBA8; }
+
+ // ---- Buffer -------------------------------------------------------
+ class GLBuffer : public Buffer
+ {
+ public:
+ GLBuffer(BufferType type, size_t size, const void* data)
+ {
+ m_target = type == BufferType::Index ? GL_ELEMENT_ARRAY_BUFFER
+ : type == BufferType::Uniform ? GL_UNIFORM_BUFFER : GL_ARRAY_BUFFER;
+ glGenBuffers(1, &m_id);
+ glBindBuffer(m_target, m_id);
+ glBufferData(m_target, (GLsizeiptr)size, data, GL_DYNAMIC_DRAW);
+ }
+ ~GLBuffer() override { if (m_id) glDeleteBuffers(1, &m_id); }
+ auto update(const void* data, size_t size) -> void override
+ {
+ glBindBuffer(m_target, m_id);
+ glBufferSubData(m_target, 0, (GLsizeiptr)size, data);
+ }
+ GLuint m_id = 0; GLenum m_target = GL_ARRAY_BUFFER;
+ };
+
+ // ---- Texture ------------------------------------------------------
+ class GLTexture : public Texture
+ {
+ public:
+ GLTexture(int w, int h, Format fmt, Filter filter, const void* data)
+ {
+ glGenTextures(1, &m_id); m_own = true; m_target = GL_TEXTURE_2D;
+ glBindTexture(GL_TEXTURE_2D, m_id);
+ GLenum ext = fmt == Format::D32 ? GL_DEPTH_COMPONENT : GL_RGBA;
+ GLenum type = fmt == Format::RGBA8 ? GL_UNSIGNED_BYTE : GL_FLOAT;
+ glTexImage2D(GL_TEXTURE_2D, 0, gl_internal(fmt), w, h, 0, ext, type, data);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter(filter));
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter(filter));
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ }
+ explicit GLTexture(Ref<CubemapTexture> cube)
+ : m_cube(std::move(cube))
+ {
+ m_target = GL_TEXTURE_CUBE_MAP;
+ m_id = m_cube ? m_cube->get_renderer_id() : 0;
+ }
+ ~GLTexture() override { if (m_own && m_id) glDeleteTextures(1, &m_id); }
+ GLuint m_id = 0; GLenum m_target = GL_TEXTURE_2D; bool m_own = false;
+ Ref<CubemapTexture> m_cube; // keepalive for cubemaps
+ };
+
+ // ---- RenderTarget -------------------------------------------------
+ class GLRenderTarget : public RenderTarget
+ {
+ public:
+ GLRenderTarget(int w, int h, Format color, bool depth, Filter filter)
+ : m_w(w), m_h(h)
+ {
+ m_color = create_scope<GLTexture>(w, h, color, filter, nullptr);
+ glGenFramebuffers(1, &m_fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color->m_id, 0);
+ if (depth)
+ {
+ glGenRenderbuffers(1, &m_depth);
+ glBindRenderbuffer(GL_RENDERBUFFER, m_depth);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depth);
+ }
+ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+ DONUT_ERROR("GL RHI: render target incomplete");
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ }
+ ~GLRenderTarget() override
+ {
+ if (m_depth) glDeleteRenderbuffers(1, &m_depth);
+ if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
+ }
+ auto width() const -> int override { return m_w; }
+ auto height() const -> int override { return m_h; }
+ auto color_texture() -> Texture* override { return m_color.get(); }
+ GLuint m_fbo = 0, m_depth = 0; int m_w, m_h; Scope<GLTexture> m_color;
+ };
+
+ // ---- Pipeline -----------------------------------------------------
+ class GLPipeline : public Pipeline
+ {
+ public:
+ explicit GLPipeline(const PipelineDesc& d) : m_desc(d)
+ {
+ std::string path = "assets/shaders/generated/" + d.shader + ".glsl";
+ m_shader = Ref<Shader>(Shader::create(path));
+ m_prog = m_shader ? m_shader->get_renderer_id() : 0;
+ if (!m_prog) { DONUT_ERROR("GL RHI: shader '{}' failed", d.shader); return; }
+
+ glUseProgram(m_prog);
+ for (const auto& r : d.resources)
+ {
+ if (r.kind == ResourceKind::UniformBuffer)
+ {
+ GLuint bi = glGetUniformBlockIndex(m_prog, r.name.c_str());
+ if (bi != GL_INVALID_INDEX) glUniformBlockBinding(m_prog, bi, r.binding);
+ }
+ else // Texture: point the sampler at texture unit == binding
+ {
+ GLint loc = glGetUniformLocation(m_prog, r.name.c_str());
+ if (loc >= 0) glUniform1i(loc, (GLint)r.binding);
+ }
+ }
+ glUseProgram(0);
+ }
+ PipelineDesc m_desc; Ref<Shader> m_shader; GLuint m_prog = 0;
+ };
+
+ // ---- CommandList (immediate) --------------------------------------
+ class GLCommandList : public CommandList
+ {
+ public:
+ explicit GLCommandList(GLuint vao) : m_vao(vao) {}
+
+ auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override
+ {
+ auto* rt = static_cast<GLRenderTarget*>(target);
+ glBindFramebuffer(GL_FRAMEBUFFER, rt ? rt->m_fbo : 0);
+ int w = rt ? rt->m_w : m_default_w, h = rt ? rt->m_h : m_default_h;
+ glViewport(0, 0, w, h);
+ glDisable(GL_SCISSOR_TEST);
+ glDepthMask(GL_TRUE);
+ glClearColor(clear.r, clear.g, clear.b, clear.a);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ }
+ auto end_render_pass() -> void override {}
+
+ auto bind_pipeline(Pipeline* p) -> void override
+ {
+ m_pipe = static_cast<GLPipeline*>(p);
+ glUseProgram(m_pipe->m_prog);
+ const auto& d = m_pipe->m_desc;
+ if (d.depth_test) { glEnable(GL_DEPTH_TEST); glDepthFunc(gl_compare(d.depth_op)); }
+ else glDisable(GL_DEPTH_TEST);
+ glDepthMask(d.depth_write ? GL_TRUE : GL_FALSE);
+ if (d.cull == CullMode::None) glDisable(GL_CULL_FACE);
+ else { glEnable(GL_CULL_FACE); glCullFace(d.cull == CullMode::Back ? GL_BACK : GL_FRONT); }
+ if (d.blend == BlendMode::AlphaBlend) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); }
+ else glDisable(GL_BLEND);
+ }
+ auto set_viewport(int x, int y, int w, int h, bool /*flip_y*/) -> void override { glViewport(x, y, w, h); }
+ auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override
+ {
+ glBindBufferBase(GL_UNIFORM_BUFFER, binding, static_cast<GLBuffer*>(ubo)->m_id);
+ }
+ auto bind_texture(uint32_t binding, Texture* tex) -> void override
+ {
+ auto* t = static_cast<GLTexture*>(tex);
+ glActiveTexture(GL_TEXTURE0 + binding);
+ glBindTexture(t->m_target, t->m_id);
+ }
+ auto bind_vertex_buffer(Buffer* vb) -> void override
+ {
+ glBindBuffer(GL_ARRAY_BUFFER, static_cast<GLBuffer*>(vb)->m_id);
+ const auto& layout = m_pipe->m_desc.vertex_layout;
+ for (const auto& a : layout.attributes)
+ {
+ glEnableVertexAttribArray(a.location);
+ glVertexAttribPointer(a.location, (GLint)a.components, GL_FLOAT, GL_FALSE,
+ (GLsizei)layout.stride, (const void*)(uintptr_t)a.offset);
+ }
+ }
+ auto bind_index_buffer(Buffer* ib) -> void override
+ {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLBuffer*>(ib)->m_id);
+ }
+ auto draw(uint32_t count) -> void override
+ {
+ glDrawArrays(gl_topology(m_pipe->m_desc.topology), 0, (GLsizei)count);
+ }
+ auto draw_indexed(uint32_t count) -> void override
+ {
+ glDrawElements(gl_topology(m_pipe->m_desc.topology), (GLsizei)count, GL_UNSIGNED_INT, nullptr);
+ }
+
+ GLuint m_vao; GLPipeline* m_pipe = nullptr; int m_default_w = 1, m_default_h = 1;
+ };
+
+ // ---- Device -------------------------------------------------------
+ class GLDevice : public Device
+ {
+ public:
+ auto init(void* window, int width, int height) -> bool override
+ {
+ m_window = window;
+ glfwMakeContextCurrent(static_cast<GLFWwindow*>(window));
+ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { DONUT_ERROR("GL RHI: GLAD load failed"); return false; }
+ m_width = width; m_height = height;
+ const char* name = (const char*)glGetString(GL_RENDERER);
+ m_name = name ? name : "OpenGL";
+ glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); // GL core needs one bound VAO
+ m_cmds = create_scope<GLCommandList>(m_vao);
+ DONUT_INFO("GL RHI device: {} ({}x{})", m_name, width, height);
+ return true;
+ }
+ auto shutdown() -> void override
+ {
+ if (m_imgui) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; }
+ if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; }
+ }
+ auto resize(int w, int h) -> void override { m_width = w; m_height = h; }
+ auto wait_idle() -> void override { glFinish(); }
+
+ auto create_buffer(BufferType t, size_t size, const void* data) -> Ref<Buffer> override
+ { return create_ref<GLBuffer>(t, size, data); }
+ auto create_texture(int w, int h, Format f, Filter fl, const void* data) -> Ref<Texture> override
+ { return create_ref<GLTexture>(w, h, f, fl, data); }
+ auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override
+ {
+ HDRIManager::get().set_current_hdri(path);
+ return create_ref<GLTexture>(HDRIManager::get().get_current_hdri());
+ }
+ auto create_render_target(int w, int h, Format color, bool depth, Filter fl, int /*mips*/) -> Ref<RenderTarget> override
+ { return create_ref<GLRenderTarget>(w, h, color, depth, fl); }
+ auto create_pipeline(const PipelineDesc& d) -> Ref<Pipeline> override
+ { return create_ref<GLPipeline>(d); }
+
+ auto begin_frame(const glm::vec4& /*clear*/) -> CommandList* override
+ {
+ glfwGetFramebufferSize(static_cast<GLFWwindow*>(m_window), &m_width, &m_height);
+ m_cmds->m_default_w = m_width; m_cmds->m_default_h = m_height;
+ glBindVertexArray(m_vao);
+ return m_cmds.get();
+ }
+ auto end_frame() -> void override { glfwSwapBuffers(static_cast<GLFWwindow*>(m_window)); }
+
+ auto init_imgui() -> void override
+ {
+ IMGUI_CHECKVERSION(); ImGui::CreateContext();
+ ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
+ ImGui_ImplGlfw_InitForOpenGL(static_cast<GLFWwindow*>(m_window), true);
+ ImGui_ImplOpenGL3_Init("#version 410");
+ m_imgui = true;
+ DONUT_INFO("GL RHI: ImGui backend initialized");
+ }
+ auto imgui_new_frame() -> void override
+ {
+ ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame();
+ }
+ auto imgui_render(CommandList&) -> void override
+ {
+ ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+ }
+ auto device_name() const -> const std::string& override { return m_name; }
+
+ private:
+ void* m_window = nullptr; std::string m_name; GLuint m_vao = 0;
+ int m_width = 0, m_height = 0; bool m_imgui = false;
+ Scope<GLCommandList> m_cmds;
+ };
+ }
+
+ auto create_opengl_device() -> Scope<Device> { return create_scope<GLDevice>(); }
+}
diff --git a/src/platform/opengl/opengl_device.h b/src/platform/opengl/opengl_device.h
new file mode 100644
index 0000000..4004396
--- /dev/null
+++ b/src/platform/opengl/opengl_device.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "rendering/rhi.h"
+
+namespace Donut::RHI
+{
+ // OpenGL implementation of the RHI Device: emulates the explicit model with
+ // immediate-mode GL 4.1 (one global VAO, glBindBufferBase for UBOs, FBOs for
+ // render targets, per-draw attribute setup, bindings assigned by name).
+ auto create_opengl_device() -> Scope<Device>;
+}
diff --git a/src/platform/opengl/opengl_renderer.cpp b/src/platform/opengl/opengl_renderer.cpp
deleted file mode 100644
index ae12f10..0000000
--- a/src/platform/opengl/opengl_renderer.cpp
+++ /dev/null
@@ -1,377 +0,0 @@
-#include "opengl_renderer.h"
-
-#include "engine/engine.h"
-#include "core/log.h"
-#include "core/camera.h"
-#include "core/hdri_manager.h"
-#include "rendering/renderer.h" // Renderer::init / RenderCommand
-
-#include <glad/glad.h>
-#include <GLFW/glfw3.h>
-#include <imgui.h>
-#include <imgui_impl_glfw.h>
-#include <imgui_impl_opengl3.h>
-#include <glm/glm.hpp>
-#include <glm/gtc/matrix_transform.hpp>
-#include <cmath>
-#include <vector>
-
-#include "core/hdri_manager.h"
-#include "rendering/texture.h" // CubemapTexture::bind
-
-namespace Donut
-{
- // Scroll is accumulated through a chained GLFW callback (keeps ImGui's scroll
- // handling intact), consumed once per frame for orbital zoom.
- static double g_scroll_accum = 0.0;
- static GLFWscrollfun g_prev_scroll = nullptr;
- static void gl_scroll_cb(GLFWwindow* w, double x, double y)
- {
- if (g_prev_scroll) g_prev_scroll(w, x, y);
- g_scroll_accum += y;
- }
-
- OpenGLRenderer::OpenGLRenderer() = default;
- OpenGLRenderer::~OpenGLRenderer() = default;
-
- auto OpenGLRenderer::init(void* glfwWindow, int width, int height) -> bool
- {
- m_window = glfwWindow;
- auto* win = static_cast<GLFWwindow*>(glfwWindow);
- glfwMakeContextCurrent(win);
-
- Renderer::init(); // GLAD load + default GL state
- RenderCommand::set_face_culling(false);
-
- const GLubyte* name = glGetString(GL_RENDERER);
- m_device_name = name ? reinterpret_cast<const char*>(name) : "OpenGL";
-
- m_engine = create_scope<Engine>();
- m_engine->set_window_dimensions(width, height);
-#ifdef __APPLE__
- m_engine->set_compute_height(256); // capped for the macOS watchdog (tiled fragment pass)
-#else
- m_engine->set_compute_height(540);
-#endif
- m_engine->update_compute_dimensions();
-
- // Match the Vulkan framing exactly (camera sits outside the disk).
- Camera& cam = m_engine->get_camera();
- cam.set_camera_mode(CameraMode::Orbital);
- cam.set_orbital_target(glm::vec3(0.0f));
- cam.set_orbital_radius(4e11);
- cam.set_orbital_limits(2.2e11, 1.5e12);
- cam.set_orbital_speed(0.01f);
- cam.set_zoom_speed(3e10);
- cam.set_azimuth(0.0f);
- cam.set_elevation(1.25f);
-
- m_hdri_path = "assets/hdri/HDR_blue_nebulae-1.hdr";
- HDRIManager::get().set_current_hdri(m_hdri_path);
-
- create_scene_resources();
-
- DONUT_INFO("OpenGL renderer ready: {} ({}x{})", m_device_name, width, height);
- return true;
- }
-
- auto OpenGLRenderer::init_ui() -> bool
- {
- IMGUI_CHECKVERSION();
- ImGui::CreateContext();
- ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
- ImGui::StyleColorsDark();
- ImGui_ImplGlfw_InitForOpenGL(static_cast<GLFWwindow*>(m_window), true);
- ImGui_ImplOpenGL3_Init("#version 410");
- g_prev_scroll = glfwSetScrollCallback(static_cast<GLFWwindow*>(m_window), gl_scroll_cb);
- m_ui_ready = true;
- DONUT_INFO("OpenGL: ImGui backend initialized");
- return true;
- }
-
- auto OpenGLRenderer::shutdown() -> void
- {
- if (m_ui_ready)
- {
- ImGui_ImplOpenGL3_Shutdown();
- ImGui_ImplGlfw_Shutdown();
- ImGui::DestroyContext();
- m_ui_ready = false;
- }
- m_engine.reset();
- }
-
- auto OpenGLRenderer::resize(int width, int height) -> void
- {
- if (m_engine) m_engine->set_window_dimensions(width, height);
- }
-
- auto OpenGLRenderer::process_input() -> void
- {
- auto* win = static_cast<GLFWwindow*>(m_window);
- Camera& cam = m_scene_mode ? m_scene_camera : m_engine->get_camera();
- bool over_ui = m_ui_ready && ImGui::GetIO().WantCaptureMouse;
-
- double now = glfwGetTime();
- float dt = m_last_frame_time > 0.0 ? static_cast<float>(now - m_last_frame_time) : 0.0f;
- m_last_frame_time = now;
-
- double mx = 0, my = 0; glfwGetCursorPos(win, &mx, &my);
- bool left_down = glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
-
- if (!m_scene_mode && cam.get_camera_mode() == CameraMode::FPS)
- {
- if (left_down && !m_left_was_down) { m_last_x = mx; m_last_y = my; }
- if (left_down && !over_ui)
- cam.on_mouse_move(static_cast<float>(mx - m_last_x), static_cast<float>(m_last_y - my));
- m_last_x = mx; m_last_y = my; m_left_was_down = left_down;
-
- bool over_kb = m_ui_ready && ImGui::GetIO().WantCaptureKeyboard;
- if (!over_kb && dt > 0.0f)
- {
- float mdt = dt * (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f);
- if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) cam.move_forward(mdt);
- if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) cam.move_backward(mdt);
- if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) cam.move_left(mdt);
- if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) cam.move_right(mdt);
- if (glfwGetKey(win, GLFW_KEY_E) == GLFW_PRESS) cam.move_up(mdt);
- if (glfwGetKey(win, GLFW_KEY_Q) == GLFW_PRESS) cam.move_down(mdt);
- }
- g_scroll_accum = 0.0; // scroll unused in free-fly
- }
- else
- {
- if (left_down && !m_left_was_down && !over_ui)
- cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0);
- else if (!left_down && m_left_was_down)
- cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0);
- m_left_was_down = left_down;
-
- cam.process_orbital_mouse_move(mx, my);
- double scroll = g_scroll_accum; g_scroll_accum = 0.0;
- if (scroll != 0.0 && !over_ui) cam.process_orbital_scroll(0.0, scroll);
- }
- }
-
- // 36-vertex unit cube for the skybox (positions only; the shader forces depth=1).
- static const float SKYBOX_CUBE[] = {
- -1, 1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1,
- -1,-1, 1, -1,-1,-1, -1, 1,-1, -1, 1,-1, -1, 1, 1, -1,-1, 1,
- 1,-1,-1, 1,-1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1,-1,-1,
- -1,-1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1, -1,-1, 1,
- -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1,
- -1,-1,-1, -1,-1, 1, 1,-1,-1, 1,-1,-1, -1,-1, 1, 1,-1, 1,
- };
-
- auto OpenGLRenderer::create_scene_resources() -> void
- {
- const float PI = 3.14159265358979f;
-
- m_grid_shader = Ref<Shader>(Shader::create("assets/shaders/Grid.glsl"));
- m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl"));
- m_skybox_shader = Ref<Shader>(Shader::create("assets/shaders/Skybox.glsl"));
-
- m_scene_camera.set_camera_mode(CameraMode::Orbital);
- m_scene_camera.set_orbital_target(glm::vec3(0.0f));
- m_scene_camera.set_orbital_radius(15.0);
- m_scene_camera.set_orbital_limits(2.0, 200.0);
- m_scene_camera.set_orbital_speed(0.01f);
- m_scene_camera.set_zoom_speed(2.0);
- m_scene_camera.set_azimuth(0.0f);
- m_scene_camera.set_elevation(PI / 3.0f);
-
- // Grid: +/-50 lines on the XZ plane (Grid shader scales by u_GridSize/50).
- std::vector<glm::vec3> lines;
- for (int i = -50; i <= 50; ++i)
- {
- lines.push_back({ (float)i, 0.0f, -50.0f }); lines.push_back({ (float)i, 0.0f, 50.0f });
- lines.push_back({ -50.0f, 0.0f, (float)i }); lines.push_back({ 50.0f, 0.0f, (float)i });
- }
- m_grid_vertex_count = (int)lines.size();
- glGenVertexArrays(1, &m_grid_vao); glGenBuffers(1, &m_grid_vbo);
- glBindVertexArray(m_grid_vao); glBindBuffer(GL_ARRAY_BUFFER, m_grid_vbo);
- glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(glm::vec3), lines.data(), GL_STATIC_DRAW);
- glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
-
- // Sphere: indexed UV sphere (position + normal), unit radius.
- const int RINGS = 24, SECT = 48;
- std::vector<float> sv; std::vector<unsigned int> si;
- for (int r = 0; r <= RINGS; ++r)
- {
- float th = PI * r / RINGS;
- for (int s = 0; s <= SECT; ++s)
- {
- float ph = 2.0f * PI * s / SECT;
- glm::vec3 p(sinf(th) * cosf(ph), cosf(th), sinf(th) * sinf(ph));
- sv.insert(sv.end(), { p.x, p.y, p.z, p.x, p.y, p.z }); // pos, normal (unit sphere)
- }
- }
- for (int r = 0; r < RINGS; ++r)
- for (int s = 0; s < SECT; ++s)
- {
- int a = r * (SECT + 1) + s, b = a + SECT + 1;
- si.insert(si.end(), { (unsigned)a, (unsigned)b, (unsigned)(a + 1),
- (unsigned)(a + 1), (unsigned)b, (unsigned)(b + 1) });
- }
- m_sphere_index_count = (int)si.size();
- glGenVertexArrays(1, &m_sphere_vao); glGenBuffers(1, &m_sphere_vbo); glGenBuffers(1, &m_sphere_ebo);
- glBindVertexArray(m_sphere_vao);
- glBindBuffer(GL_ARRAY_BUFFER, m_sphere_vbo);
- glBufferData(GL_ARRAY_BUFFER, sv.size() * sizeof(float), sv.data(), GL_STATIC_DRAW);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphere_ebo);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, si.size() * sizeof(unsigned), si.data(), GL_STATIC_DRAW);
- glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
- glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
-
- // Skybox cube.
- glGenVertexArrays(1, &m_skybox_vao); glGenBuffers(1, &m_skybox_vbo);
- glBindVertexArray(m_skybox_vao); glBindBuffer(GL_ARRAY_BUFFER, m_skybox_vbo);
- glBufferData(GL_ARRAY_BUFFER, sizeof(SKYBOX_CUBE), SKYBOX_CUBE, GL_STATIC_DRAW);
- glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
-
- glBindVertexArray(0);
- m_scene_ready = true;
- }
-
- auto OpenGLRenderer::render_scene(int w, int h) -> void
- {
- if (!m_grid_shader || !m_sphere_shader || !m_skybox_shader) return;
- glViewport(0, 0, w, h);
- glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
- float aspect = h > 0 ? (float)w / (float)h : 1.0f;
- m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f);
- glm::mat4 proj = m_scene_camera.get_projection_matrix();
- glm::mat4 view = m_scene_camera.get_view_matrix();
- glm::mat4 vp = proj * view;
- glm::vec3 cam = m_scene_camera.get_orbital_position();
-
- // Skybox behind everything (depth test on, write off; shader forces depth=1).
- glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(GL_FALSE);
- m_skybox_shader->bind();
- m_skybox_shader->set_mat4("u_Projection", proj);
- m_skybox_shader->set_mat4("u_View", glm::mat4(glm::mat3(view))); // strip translation
- auto hdri = HDRIManager::get().get_current_hdri();
- if (hdri) { hdri->bind(0); m_skybox_shader->set_int("u_Skybox", 0); }
- glBindVertexArray(m_skybox_vao); glDrawArrays(GL_TRIANGLES, 0, 36);
-
- // Lit spheres.
- glDepthFunc(GL_LESS); glDepthMask(GL_TRUE);
- m_sphere_shader->bind(); glBindVertexArray(m_sphere_vao);
- for (int i = 0; i < (int)m_scene_objects.size(); ++i)
- {
- const SceneObject& o = m_scene_objects[i];
- m_sphere_shader->set_mat4("u_ViewProjection", vp);
- m_sphere_shader->set_mat4("u_Transform",
- glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius)));
- m_sphere_shader->set_float3("u_Color", o.color);
- m_sphere_shader->set_float("u_Specular", 0.6f);
- m_sphere_shader->set_float("u_Emission", 0.0f);
- m_sphere_shader->set_float3("u_LightPos", glm::vec3(10.0f, 20.0f, 10.0f));
- m_sphere_shader->set_float3("u_CameraPos", cam);
- m_sphere_shader->set_int("u_IsSelected", i == m_selected_object ? 1 : 0);
- m_sphere_shader->set_float3("u_OutlineColor", glm::vec3(1.0f, 1.0f, 0.0f));
- m_sphere_shader->set_float("u_OutlineWidth", 0.15f);
- glDrawElements(GL_TRIANGLES, m_sphere_index_count, GL_UNSIGNED_INT, 0);
- }
-
- // Transparent grid on top (test, no depth write).
- glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- m_grid_shader->bind();
- m_grid_shader->set_mat4("u_ViewProjection", vp);
- m_grid_shader->set_mat4("u_Transform", glm::mat4(1.0f));
- m_grid_shader->set_float("u_GridSize", 50.0f);
- m_grid_shader->set_float3("u_GridColor", glm::vec3(0.55f, 0.55f, 0.6f));
- m_grid_shader->set_float("u_GridAlpha", 0.75f);
- m_grid_shader->set_float3("u_CameraPos", cam);
- glBindVertexArray(m_grid_vao); glDrawArrays(GL_LINES, 0, m_grid_vertex_count);
- glDisable(GL_BLEND); glDepthMask(GL_TRUE); glBindVertexArray(0);
- }
-
- auto OpenGLRenderer::draw_frame(const glm::vec4& clear_color,
- const std::function<void()>& build_ui) -> void
- {
- auto* win = static_cast<GLFWwindow*>(m_window);
- process_input();
-
- int w = 0, h = 0; glfwGetFramebufferSize(win, &w, &h);
-
- if (m_scene_mode)
- {
- render_scene(w, h);
- }
- else
- {
- m_engine->set_window_dimensions(w, h);
- // Geodesic pass -> low-res texture (watchdog-tiled on macOS), then
- // upscale to the window. Disk/camera come from the shared BlackHoleParams.
- m_engine->dispatch_compute(m_engine->get_camera());
- RenderCommand::set_viewport(0, 0, w, h);
- RenderCommand::set_clear_color(clear_color);
- RenderCommand::clear();
- m_engine->draw_full_screen_quad();
- }
-
- if (m_ui_ready)
- {
- ImGui_ImplOpenGL3_NewFrame();
- ImGui_ImplGlfw_NewFrame();
- ImGui::NewFrame();
- if (build_ui) build_ui();
- ImGui::Render();
- ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
- }
- glfwSwapBuffers(win);
- }
-
- auto OpenGLRenderer::set_scene_mode(bool enabled) -> void { m_scene_mode = enabled; }
- auto OpenGLRenderer::is_scene_mode() const -> bool { return m_scene_mode; }
-
- auto OpenGLRenderer::set_free_fly(bool enabled) -> void
- {
- Camera& cam = m_engine->get_camera();
- if (enabled && cam.get_camera_mode() != CameraMode::FPS)
- {
- glm::vec3 pos = cam.get_orbital_position();
- glm::vec3 fwd = glm::normalize(cam.get_orbital_target() - pos);
- float pitch = glm::degrees(asinf(glm::clamp(fwd.y, -1.0f, 1.0f)));
- float yaw = glm::degrees(atan2f(fwd.z, fwd.x));
- cam.set_position(pos);
- cam.set_rotation(glm::vec3(pitch, yaw, 0.0f));
- cam.set_movement_speed(2.0e10f);
- cam.set_mouse_sensitivity(0.15f);
- cam.set_camera_mode(CameraMode::FPS);
- }
- else if (!enabled)
- {
- cam.set_camera_mode(CameraMode::Orbital);
- }
- }
-
- auto OpenGLRenderer::is_free_fly() const -> bool
- {
- return m_engine->get_camera().get_camera_mode() == CameraMode::FPS;
- }
-
- auto OpenGLRenderer::reset_camera() -> void
- {
- Camera& cam = m_engine->get_camera();
- cam.set_camera_mode(CameraMode::Orbital);
- cam.set_orbital_radius(4e11);
- cam.set_azimuth(0.0f);
- cam.set_elevation(1.25f);
- }
-
- auto OpenGLRenderer::set_hdri(const std::string& path) -> void
- {
- HDRIManager::get().set_current_hdri(path); // the Engine re-reads it each frame
- m_hdri_path = path;
- }
-
- auto OpenGLRenderer::current_hdri() const -> const std::string& { return m_hdri_path; }
- auto OpenGLRenderer::scene_objects() -> std::vector<SceneObject>& { return m_scene_objects; }
- auto OpenGLRenderer::set_selected_object(int index) -> void { m_selected_object = index; }
- auto OpenGLRenderer::black_hole_params() -> BlackHoleParams& { return m_engine->black_hole_params(); }
- auto OpenGLRenderer::device_name() const -> const std::string& { return m_device_name; }
-}
diff --git a/src/platform/opengl/opengl_renderer.h b/src/platform/opengl/opengl_renderer.h
deleted file mode 100644
index 4258f40..0000000
--- a/src/platform/opengl/opengl_renderer.h
+++ /dev/null
@@ -1,72 +0,0 @@
-#pragma once
-
-#include "rendering/renderer_backend.h"
-#include "core/memory.h"
-#include "core/camera.h"
-#include "rendering/shader.h"
-
-// OpenGL implementation of the shared RendererBackend interface. It renders the
-// SAME cross-compiled Geodesic shader the Vulkan backend does (driven by the
-// same BlackHoleParams + camera), so the application behaves identically on
-// either backend. Wraps the Engine, which owns the GL geodesic/present passes
-// and the macOS watchdog-safe tiled rendering.
-namespace Donut
-{
- class Engine;
-
- class OpenGLRenderer : public RendererBackend
- {
- public:
- OpenGLRenderer();
- ~OpenGLRenderer() override;
-
- auto init(void* glfwWindow, int width, int height) -> bool override;
- auto init_ui() -> bool override;
- auto shutdown() -> void override;
- auto resize(int width, int height) -> void override;
- auto draw_frame(const glm::vec4& clear_color,
- const std::function<void()>& build_ui) -> void override;
-
- auto set_scene_mode(bool enabled) -> void override;
- auto is_scene_mode() const -> bool override;
- auto set_free_fly(bool enabled) -> void override;
- auto is_free_fly() const -> bool override;
- auto reset_camera() -> void override;
-
- auto set_hdri(const std::string& path) -> void override;
- auto current_hdri() const -> const std::string& override;
- auto scene_objects() -> std::vector<SceneObject>& override;
- auto set_selected_object(int index) -> void override;
- auto black_hole_params() -> BlackHoleParams& override;
-
- auto device_name() const -> const std::string& override;
-
- private:
- auto process_input() -> void;
- auto create_scene_resources() -> void; // grid + sphere + skybox meshes/shaders
- auto render_scene(int width, int height) -> void;
-
- void* m_window = nullptr;
- Scope<Engine> m_engine;
-
- // World-builder scene view (normal-scale): mirrors the Vulkan scene view
- // with the same shared Grid/Sphere/Skybox shaders.
- Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f };
- Ref<Shader> m_grid_shader, m_sphere_shader, m_skybox_shader;
- unsigned int m_grid_vao = 0, m_grid_vbo = 0;
- unsigned int m_sphere_vao = 0, m_sphere_vbo = 0, m_sphere_ebo = 0;
- unsigned int m_skybox_vao = 0, m_skybox_vbo = 0;
- int m_grid_vertex_count = 0, m_sphere_index_count = 0;
- bool m_scene_ready = false;
- std::vector<SceneObject> m_scene_objects{ SceneObject{} };
- int m_selected_object = 0;
- std::string m_hdri_path;
- std::string m_device_name;
- bool m_scene_mode = false;
- bool m_ui_ready = false;
-
- double m_last_x = 0.0, m_last_y = 0.0;
- bool m_left_was_down = false;
- double m_last_frame_time = 0.0;
- };
-}
diff --git a/src/platform/vulkan/vulkan_context.cpp b/src/platform/vulkan/vulkan_context.cpp
deleted file mode 100644
index ece7700..0000000
--- a/src/platform/vulkan/vulkan_context.cpp
+++ /dev/null
@@ -1,789 +0,0 @@
-#include "vulkan_context.h"
-#include "core/log.h"
-
-#include <vulkan/vulkan.h>
-
-#include <glm/glm.hpp>
-#include <glm/gtc/type_ptr.hpp>
-#include "stb_image_write.h"
-
-#include <vector>
-#include <cstring>
-#include <cstdlib>
-#include <fstream>
-
-namespace Donut
-{
- // Logs and returns false from the enclosing function on any non-success result.
- #define VK_CHECK(expr) \
- do { \
- VkResult _r = (expr); \
- if (_r != VK_SUCCESS) { \
- DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \
- return false; \
- } \
- } while (0)
-
- struct VulkanContext::Impl
- {
- VkInstance instance = VK_NULL_HANDLE;
- VkPhysicalDevice physical = VK_NULL_HANDLE;
- VkDevice device = VK_NULL_HANDLE;
- VkQueue graphics_queue = VK_NULL_HANDLE;
- uint32_t graphics_family = 0;
- VkPhysicalDeviceMemoryProperties mem_props{};
-
- auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t
- {
- for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i)
- if ((type_filter & (1u << i)) &&
- (mem_props.memoryTypes[i].propertyFlags & flags) == flags)
- return i;
- return UINT32_MAX;
- }
-
- bool create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props,
- VkBuffer& buf, VkDeviceMemory& mem) const
- {
- VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
- bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
- if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false;
- VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, buf, &req);
- VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- ai.allocationSize = req.size;
- ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props);
- if (vkAllocateMemory(device, &ai, nullptr, &mem) != VK_SUCCESS) return false;
- vkBindBufferMemory(device, buf, mem, 0);
- return true;
- }
- };
-
- VulkanContext::VulkanContext() { m_impl = new Impl(); }
- VulkanContext::~VulkanContext() { shutdown(); delete m_impl; m_impl = nullptr; }
-
- auto VulkanContext::init() -> bool
- {
- Impl& v = *m_impl;
-
-#ifdef __APPLE__
- // The Homebrew Vulkan loader does not auto-discover MoltenVK or the
- // validation layers; point it at both unless already configured.
- if (!getenv("VK_ICD_FILENAMES"))
- setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0);
- if (!getenv("VK_LAYER_PATH"))
- setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0);
-#endif
-
- // Instance
- VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO };
- app.pApplicationName = "Donut";
- app.apiVersion = VK_API_VERSION_1_2;
-
- // MoltenVK is a portability driver: without the portability-enumeration
- // extension + flag, vkEnumeratePhysicalDevices returns zero devices.
- std::vector<const char*> exts = {
- VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
- VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME,
- };
-
- // Enable validation layers when they are installed (optional).
- std::vector<const char*> layers;
- uint32_t layer_count = 0;
- vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
- std::vector<VkLayerProperties> avail(layer_count);
- vkEnumerateInstanceLayerProperties(&layer_count, avail.data());
- for (const auto& l : avail)
- if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0)
- layers.push_back("VK_LAYER_KHRONOS_validation");
-
- VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
- ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
- ici.pApplicationInfo = &app;
- ici.enabledExtensionCount = (uint32_t)exts.size();
- ici.ppEnabledExtensionNames = exts.data();
- ici.enabledLayerCount = (uint32_t)layers.size();
- ici.ppEnabledLayerNames = layers.data();
- VK_CHECK(vkCreateInstance(&ici, nullptr, &v.instance));
-
- // Physical device
- uint32_t device_count = 0;
- vkEnumeratePhysicalDevices(v.instance, &device_count, nullptr);
- if (device_count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; }
- std::vector<VkPhysicalDevice> devices(device_count);
- vkEnumeratePhysicalDevices(v.instance, &device_count, devices.data());
- v.physical = devices[0];
-
- VkPhysicalDeviceProperties props{};
- vkGetPhysicalDeviceProperties(v.physical, &props);
- vkGetPhysicalDeviceMemoryProperties(v.physical, &v.mem_props);
-
- // Graphics queue family
- uint32_t q_count = 0;
- vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, nullptr);
- std::vector<VkQueueFamilyProperties> qfams(q_count);
- vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, qfams.data());
- bool found = false;
- for (uint32_t i = 0; i < q_count; ++i)
- if (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { v.graphics_family = i; found = true; break; }
- if (!found) { DONUT_ERROR("Vulkan: no graphics queue family"); return false; }
-
- // Logical device
- // MoltenVK requires VK_KHR_portability_subset to be enabled if present.
- std::vector<const char*> dev_exts;
- uint32_t dev_ext_count = 0;
- vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, nullptr);
- std::vector<VkExtensionProperties> dev_ext_props(dev_ext_count);
- vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, dev_ext_props.data());
- for (const auto& e : dev_ext_props)
- if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0)
- dev_exts.push_back("VK_KHR_portability_subset");
-
- float priority = 1.0f;
- VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
- qci.queueFamilyIndex = v.graphics_family;
- qci.queueCount = 1;
- qci.pQueuePriorities = &priority;
-
- VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
- dci.queueCreateInfoCount = 1;
- dci.pQueueCreateInfos = &qci;
- dci.enabledExtensionCount = (uint32_t)dev_exts.size();
- dci.ppEnabledExtensionNames = dev_exts.data();
- VK_CHECK(vkCreateDevice(v.physical, &dci, nullptr, &v.device));
- vkGetDeviceQueue(v.device, v.graphics_family, 0, &v.graphics_queue);
-
- DONUT_INFO("Vulkan device: {} (API {}.{}.{}, validation {})",
- props.deviceName,
- VK_API_VERSION_MAJOR(props.apiVersion),
- VK_API_VERSION_MINOR(props.apiVersion),
- VK_API_VERSION_PATCH(props.apiVersion),
- layers.empty() ? "off" : "on");
- return true;
- }
-
- auto VulkanContext::self_test_clear() -> bool
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return false;
-
- const uint32_t W = 64, H = 64;
- const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
-
- // Offscreen colour image
- VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE;
- VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- ici.imageType = VK_IMAGE_TYPE_2D;
- ici.format = fmt;
- ici.extent = { W, H, 1 };
- ici.mipLevels = 1;
- ici.arrayLayers = 1;
- ici.samples = VK_SAMPLE_COUNT_1_BIT;
- ici.tiling = VK_IMAGE_TILING_OPTIMAL;
- ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
- ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
- VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image));
-
- VkMemoryRequirements im_req{};
- vkGetImageMemoryRequirements(v.device, image, &im_req);
- VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- im_alloc.allocationSize = im_req.size;
- im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem));
- VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0));
-
- VkImageView view = VK_NULL_HANDLE;
- VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = image;
- vci.viewType = VK_IMAGE_VIEW_TYPE_2D;
- vci.format = fmt;
- vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view));
-
- // Render pass (clear -> store, leave in TRANSFER_SRC for readback)
- VkAttachmentDescription color{};
- color.format = fmt;
- color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
- color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
- color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
- color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
-
- VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{};
- subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1;
- subpass.pColorAttachments = &color_ref;
-
- // Ensure colour writes finish before the read-back copy.
- VkSubpassDependency dep{};
- dep.srcSubpass = 0;
- dep.dstSubpass = VK_SUBPASS_EXTERNAL;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
- dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
- dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
-
- VkRenderPass render_pass = VK_NULL_HANDLE;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color;
- rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass));
-
- VkFramebuffer fb = VK_NULL_HANDLE;
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = render_pass;
- fbci.attachmentCount = 1; fbci.pAttachments = &view;
- fbci.width = W; fbci.height = H; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb));
-
- // Host-visible staging buffer for read-back
- VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE;
- VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
- bci.size = (VkDeviceSize)W * H * 4;
- bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
- bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
- VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging));
- VkMemoryRequirements b_req{};
- vkGetBufferMemoryRequirements(v.device, staging, &b_req);
- VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- b_alloc.allocationSize = b_req.size;
- b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits,
- VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem));
- VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0));
-
- // Command buffer: clear via render pass, then copy image -> buffer
- VkCommandPool pool = VK_NULL_HANDLE;
- VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
- pci.queueFamilyIndex = v.graphics_family;
- VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool));
-
- VkCommandBuffer cmd = VK_NULL_HANDLE;
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
- cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
- VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd));
-
- VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
- begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
- VK_CHECK(vkBeginCommandBuffer(cmd, &begin));
-
- VkClearValue clear{};
- clear.color = { { 0.2f, 0.4f, 0.8f, 1.0f } }; // -> RGBA8 (51, 102, 204, 255)
- VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = render_pass; rpbi.framebuffer = fb;
- rpbi.renderArea = { { 0, 0 }, { W, H } };
- rpbi.clearValueCount = 1; rpbi.pClearValues = &clear;
- vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdEndRenderPass(cmd);
-
- VkBufferImageCopy region{};
- region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
- region.imageExtent = { W, H, 1 };
- vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, &region);
- VK_CHECK(vkEndCommandBuffer(cmd));
-
- VkFence fence = VK_NULL_HANDLE;
- VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
- VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence));
- VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
- submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd;
- VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence));
- VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX));
-
- // Read back + verify
- void* mapped = nullptr;
- VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped));
- const uint8_t* px = (const uint8_t*)mapped;
- DONUT_INFO("Vulkan clear self-test: pixel RGBA = ({}, {}, {}, {})",
- (int)px[0], (int)px[1], (int)px[2], (int)px[3]);
- bool ok = px[0] > 45 && px[0] < 60 && px[1] > 95 && px[1] < 110 &&
- px[2] > 195 && px[2] < 210 && px[3] == 255;
- vkUnmapMemory(v.device, staging_mem);
- DONUT_INFO("Vulkan clear self-test: {}", ok ? "PASS" : "FAIL");
-
- // Cleanup
- vkDestroyFence(v.device, fence, nullptr);
- vkDestroyCommandPool(v.device, pool, nullptr);
- vkDestroyBuffer(v.device, staging, nullptr);
- vkFreeMemory(v.device, staging_mem, nullptr);
- vkDestroyFramebuffer(v.device, fb, nullptr);
- vkDestroyRenderPass(v.device, render_pass, nullptr);
- vkDestroyImageView(v.device, view, nullptr);
- vkDestroyImage(v.device, image, nullptr);
- vkFreeMemory(v.device, image_mem, nullptr);
- return ok;
- }
-
- static std::vector<uint32_t> load_spirv(const std::string& path)
- {
- std::ifstream f(path, std::ios::binary | std::ios::ate);
- if (!f) return {};
- size_t size = (size_t)f.tellg();
- std::vector<uint32_t> data(size / 4);
- f.seekg(0);
- f.read((char*)data.data(), (std::streamsize)size);
- return data;
- }
-
- auto VulkanContext::self_test_triangle() -> bool
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return false;
-
- const uint32_t W = 64, H = 64;
- const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
-
- // Offscreen image + view (as in the clear test)
- VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE;
- VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, H, 1 };
- ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
- ici.tiling = VK_IMAGE_TILING_OPTIMAL;
- ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
- VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image));
- VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req);
- VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- im_alloc.allocationSize = im_req.size;
- im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem));
- VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0));
- VkImageView view = VK_NULL_HANDLE;
- VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt;
- vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view));
-
- // Render pass + framebuffer
- VkAttachmentDescription color{};
- color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
- VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{};
- subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref;
- VkSubpassDependency dep{};
- dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
- VkRenderPass render_pass = VK_NULL_HANDLE;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color;
- rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass));
- VkFramebuffer fb = VK_NULL_HANDLE;
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view;
- fbci.width = W; fbci.height = H; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb));
-
- // Shader modules from Slang SPIR-V
- auto vspv = load_spirv("assets/shaders/generated/VkPipelineTest.vertexMain.spv");
- auto fspv = load_spirv("assets/shaders/generated/VkPipelineTest.fragmentMain.spv");
- if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: VkPipelineTest SPIR-V not found"); return false; }
- VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE;
- VkShaderModuleCreateInfo smci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
- smci.codeSize = vspv.size() * 4; smci.pCode = vspv.data();
- VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &vmod));
- smci.codeSize = fspv.size() * 4; smci.pCode = fspv.data();
- VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &fmod));
-
- // Graphics pipeline
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
- stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
- stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
-
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO };
- ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 };
- VkRect2D scissor{ { 0, 0 }, { W, H } };
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO };
- vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &scissor;
- VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO };
- rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO };
- ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineColorBlendAttachmentState cba{};
- cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO };
- cb.attachmentCount = 1; cb.pAttachments = &cba;
-
- VkPipelineLayout layout = VK_NULL_HANDLE;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
- VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout));
-
- VkPipeline pipeline = VK_NULL_HANDLE;
- VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- gpci.stageCount = 2; gpci.pStages = stages;
- gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia;
- gpci.pViewportState = &vps; gpci.pRasterizationState = &rs;
- gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb;
- gpci.layout = layout; gpci.renderPass = render_pass; gpci.subpass = 0;
- VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline));
-
- // Readback staging buffer
- VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE;
- VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
- bci.size = (VkDeviceSize)W * H * 4; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
- VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging));
- VkMemoryRequirements b_req{}; vkGetBufferMemoryRequirements(v.device, staging, &b_req);
- VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- b_alloc.allocationSize = b_req.size;
- b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem));
- VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0));
-
- // Record + submit
- VkCommandPool pool = VK_NULL_HANDLE;
- VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
- pci.queueFamilyIndex = v.graphics_family;
- VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool));
- VkCommandBuffer cmd = VK_NULL_HANDLE;
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
- cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
- VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd));
- VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
- begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
- VK_CHECK(vkBeginCommandBuffer(cmd, &begin));
- VkClearValue clear{}; clear.color = { { 0.0f, 0.0f, 0.0f, 1.0f } };
- VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = render_pass; rpbi.framebuffer = fb;
- rpbi.renderArea = { { 0, 0 }, { W, H } };
- rpbi.clearValueCount = 1; rpbi.pClearValues = &clear;
- vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
- vkCmdDraw(cmd, 3, 1, 0, 0);
- vkCmdEndRenderPass(cmd);
- VkBufferImageCopy region{};
- region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
- region.imageExtent = { W, H, 1 };
- vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, &region);
- VK_CHECK(vkEndCommandBuffer(cmd));
-
- VkFence fence = VK_NULL_HANDLE;
- VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
- VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence));
- VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
- submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd;
- VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence));
- VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX));
-
- // Verify: centre pixel should be the mid-gradient (not black)
- void* mapped = nullptr;
- VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped));
- const uint8_t* px = (const uint8_t*)mapped;
- size_t c = ((size_t)(H / 2) * W + (W / 2)) * 4;
- DONUT_INFO("Vulkan triangle self-test: centre pixel RGBA = ({}, {}, {}, {})",
- (int)px[c + 0], (int)px[c + 1], (int)px[c + 2], (int)px[c + 3]);
- bool ok = (px[c + 0] > 40 || px[c + 1] > 40) && px[c + 3] == 255;
- vkUnmapMemory(v.device, staging_mem);
- DONUT_INFO("Vulkan triangle self-test: {}", ok ? "PASS" : "FAIL");
-
- // Cleanup
- vkDestroyFence(v.device, fence, nullptr);
- vkDestroyCommandPool(v.device, pool, nullptr);
- vkDestroyBuffer(v.device, staging, nullptr);
- vkFreeMemory(v.device, staging_mem, nullptr);
- vkDestroyPipeline(v.device, pipeline, nullptr);
- vkDestroyPipelineLayout(v.device, layout, nullptr);
- vkDestroyShaderModule(v.device, vmod, nullptr);
- vkDestroyShaderModule(v.device, fmod, nullptr);
- vkDestroyFramebuffer(v.device, fb, nullptr);
- vkDestroyRenderPass(v.device, render_pass, nullptr);
- vkDestroyImageView(v.device, view, nullptr);
- vkDestroyImage(v.device, image, nullptr);
- vkFreeMemory(v.device, image_mem, nullptr);
- return ok;
- }
-
- auto VulkanContext::render_geodesic(const char* png_path) -> bool
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return false;
-
- const uint32_t W = 384, H = 216;
- const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
- const float SagA_rs = 1.269e10f;
-
- // Offscreen colour target
- VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE;
- VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, H, 1 };
- ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
- ici.tiling = VK_IMAGE_TILING_OPTIMAL;
- ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
- VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image));
- VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req);
- VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- im_alloc.allocationSize = im_req.size;
- im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem));
- VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0));
- VkImageView view = VK_NULL_HANDLE;
- VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt;
- vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view));
-
- VkAttachmentDescription color{};
- color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
- VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref;
- VkSubpassDependency dep{};
- dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
- VkRenderPass render_pass = VK_NULL_HANDLE;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color;
- rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass));
- VkFramebuffer fb = VK_NULL_HANDLE;
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view;
- fbci.width = W; fbci.height = H; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb));
-
- // Uniform buffers (host-visible), filled to match the shader's std140 layout
- const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
- VkBuffer cam_buf, disk_buf, obj_buf, sim_buf;
- VkDeviceMemory cam_mem, disk_mem, obj_mem, sim_mem;
- v.create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem);
- v.create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem);
- v.create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem);
- v.create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem);
-
- struct CamUBO {
- glm::vec3 pos; float p0; glm::vec3 right; float p1;
- glm::vec3 up; float p2; glm::vec3 fwd; float p3;
- float tan_half_fov; float aspect; uint32_t moving; int p4;
- } cam{};
- glm::vec3 cam_pos(1e11f, 0.32e11f, 0.0f);
- glm::vec3 fwd = glm::normalize(glm::vec3(0.0f) - cam_pos);
- glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
- glm::vec3 up = glm::cross(right, fwd);
- cam.pos = cam_pos; cam.right = right; cam.up = up; cam.fwd = fwd;
- cam.tan_half_fov = 0.57735f; cam.aspect = (float)W / (float)H; cam.moving = 0;
- void* p = nullptr;
- vkMapMemory(v.device, cam_mem, 0, 128, 0, &p); memcpy(p, &cam, sizeof(cam)); vkUnmapMemory(v.device, cam_mem);
-
- float disk[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 };
- vkMapMemory(v.device, disk_mem, 0, 32, 0, &p); memcpy(p, disk, sizeof(disk)); vkUnmapMemory(v.device, disk_mem);
-
- std::vector<uint8_t> obj_data(800, 0);
- int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4);
- float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16);
- float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16);
- vkMapMemory(v.device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(v.device, obj_mem);
-
- struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim{ 6000, 6000, 5e12f, 0.0f };
- vkMapMemory(v.device, sim_mem, 0, 16, 0, &p); memcpy(p, &sim, sizeof(sim)); vkUnmapMemory(v.device, sim_mem);
-
- // Dark cubemap (stands in for the HDRI for now)
- VkImage cube = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE;
- VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
- cci.imageType = VK_IMAGE_TYPE_2D; cci.format = fmt; cci.extent = { 1, 1, 1 };
- cci.mipLevels = 1; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT;
- cci.tiling = VK_IMAGE_TILING_OPTIMAL;
- cci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
- VK_CHECK(vkCreateImage(v.device, &cci, nullptr, &cube));
- VkMemoryRequirements cube_req{}; vkGetImageMemoryRequirements(v.device, cube, &cube_req);
- VkMemoryAllocateInfo cube_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- cube_alloc.allocationSize = cube_req.size;
- cube_alloc.memoryTypeIndex = v.find_memory_type(cube_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(v.device, &cube_alloc, nullptr, &cube_mem));
- VK_CHECK(vkBindImageMemory(v.device, cube, cube_mem, 0));
- VkImageView cube_view = VK_NULL_HANDLE;
- VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- cvci.image = cube; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = fmt;
- cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 };
- VK_CHECK(vkCreateImageView(v.device, &cvci, nullptr, &cube_view));
- VkSampler sampler = VK_NULL_HANDLE;
- VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
- smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR;
- smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
- VK_CHECK(vkCreateSampler(v.device, &smci, nullptr, &sampler));
-
- VkBuffer cube_staging; VkDeviceMemory cube_staging_mem;
- v.create_buffer(6 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, cube_staging, cube_staging_mem);
- uint8_t cube_pixels[6 * 4];
- for (int i = 0; i < 6; ++i) { cube_pixels[i * 4 + 0] = 6; cube_pixels[i * 4 + 1] = 6; cube_pixels[i * 4 + 2] = 14; cube_pixels[i * 4 + 3] = 255; }
- vkMapMemory(v.device, cube_staging_mem, 0, 24, 0, &p); memcpy(p, cube_pixels, 24); vkUnmapMemory(v.device, cube_staging_mem);
-
- // Descriptor set: 4 UBOs (bindings 0-3) + cubemap sampler (binding 4)
- VkDescriptorSetLayoutBinding binds[5]{};
- for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; }
- binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayout set_layout = VK_NULL_HANDLE;
- VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
- dslci.bindingCount = 5; dslci.pBindings = binds;
- VK_CHECK(vkCreateDescriptorSetLayout(v.device, &dslci, nullptr, &set_layout));
- VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } };
- VkDescriptorPool pool = VK_NULL_HANDLE;
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
- dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes;
- VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &pool));
- VkDescriptorSet set = VK_NULL_HANDLE;
- VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
- dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout;
- VK_CHECK(vkAllocateDescriptorSets(v.device, &dsai, &set));
-
- // load geodesic SPIR-V + build the pipeline
- auto vspv = load_spirv("assets/shaders/generated/Geodesic.vertexMain.spv");
- auto fspv = load_spirv("assets/shaders/generated/Geodesic.fragmentMain.spv");
- if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: geodesic SPIR-V not found"); return false; }
- VkShaderModule vmod, fmod;
- VkShaderModuleCreateInfo smci2{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
- smci2.codeSize = vspv.size() * 4; smci2.pCode = vspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &vmod));
- smci2.codeSize = fspv.size() * 4; smci2.pCode = fspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &fmod));
-
- VkPipelineLayout layout = VK_NULL_HANDLE;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
- plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout;
- VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout));
-
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
- VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } };
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib;
- vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via;
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { W, H } };
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &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 = VK_NULL_HANDLE;
- VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- gpci.stageCount = 2; gpci.pStages = stages;
- gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
- gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb;
- gpci.layout = layout; gpci.renderPass = render_pass; gpci.subpass = 0;
- VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline));
-
- // Fullscreen quad (position.xy, texcoord.uv)
- float quad[] = {
- -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f,
- -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f,
- };
- VkBuffer vbuf; VkDeviceMemory vbuf_mem;
- v.create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, vbuf, vbuf_mem);
- vkMapMemory(v.device, vbuf_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(v.device, vbuf_mem);
-
- // Write the descriptor set
- VkDescriptorBufferInfo bi[4] = {
- { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } };
- VkDescriptorImageInfo ii{ sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet writes[5]{};
- for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; }
- writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = &ii;
- vkUpdateDescriptorSets(v.device, 5, writes, 0, nullptr);
-
- VkBuffer readback; VkDeviceMemory readback_mem;
- v.create_buffer((VkDeviceSize)W * H * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, host_vis, readback, readback_mem);
-
- // Record + submit
- VkCommandPool cpool = VK_NULL_HANDLE;
- VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; pci.queueFamilyIndex = v.graphics_family;
- VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &cpool));
- VkCommandBuffer cmd = VK_NULL_HANDLE;
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = cpool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
- VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd));
- VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
- VK_CHECK(vkBeginCommandBuffer(cmd, &begin));
-
- VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
- to_dst.image = cube; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 };
- to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst);
- VkBufferImageCopy cube_copy{}; cube_copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 6 }; cube_copy.imageExtent = { 1, 1, 1 };
- vkCmdCopyBufferToImage(cmd, cube_staging, cube, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cube_copy);
- VkImageMemoryBarrier to_read = to_dst;
- to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read);
-
- VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } };
- VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = render_pass; rpbi.framebuffer = fb; rpbi.renderArea = { { 0, 0 }, { W, H } };
- rpbi.clearValueCount = 1; rpbi.pClearValues = &clear;
- vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &set, 0, nullptr);
- VkDeviceSize voff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &vbuf, &voff);
- vkCmdDraw(cmd, 6, 1, 0, 0);
- vkCmdEndRenderPass(cmd);
- VkBufferImageCopy region{}; region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; region.imageExtent = { W, H, 1 };
- vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, &region);
- VK_CHECK(vkEndCommandBuffer(cmd));
-
- VkFence fence = VK_NULL_HANDLE; VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
- VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence));
- VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd;
- VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence));
- VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX));
-
- vkMapMemory(v.device, readback_mem, 0, (VkDeviceSize)W * H * 4, 0, &p);
- stbi_write_png(png_path, W, H, 4, p, W * 4);
- vkUnmapMemory(v.device, readback_mem);
- DONUT_INFO("Vulkan geodesic render written to {}", png_path);
-
- vkDestroyFence(v.device, fence, nullptr);
- vkDestroyCommandPool(v.device, cpool, nullptr);
- vkDestroyBuffer(v.device, readback, nullptr); vkFreeMemory(v.device, readback_mem, nullptr);
- vkDestroyBuffer(v.device, vbuf, nullptr); vkFreeMemory(v.device, vbuf_mem, nullptr);
- vkDestroyPipeline(v.device, pipeline, nullptr); vkDestroyPipelineLayout(v.device, layout, nullptr);
- vkDestroyShaderModule(v.device, vmod, nullptr); vkDestroyShaderModule(v.device, fmod, nullptr);
- vkDestroyDescriptorPool(v.device, pool, nullptr); vkDestroyDescriptorSetLayout(v.device, set_layout, nullptr);
- vkDestroySampler(v.device, sampler, nullptr); vkDestroyImageView(v.device, cube_view, nullptr);
- vkDestroyImage(v.device, cube, nullptr); vkFreeMemory(v.device, cube_mem, nullptr);
- vkDestroyBuffer(v.device, cube_staging, nullptr); vkFreeMemory(v.device, cube_staging_mem, nullptr);
- vkDestroyBuffer(v.device, cam_buf, nullptr); vkFreeMemory(v.device, cam_mem, nullptr);
- vkDestroyBuffer(v.device, disk_buf, nullptr); vkFreeMemory(v.device, disk_mem, nullptr);
- vkDestroyBuffer(v.device, obj_buf, nullptr); vkFreeMemory(v.device, obj_mem, nullptr);
- vkDestroyBuffer(v.device, sim_buf, nullptr); vkFreeMemory(v.device, sim_mem, nullptr);
- vkDestroyFramebuffer(v.device, fb, nullptr); vkDestroyRenderPass(v.device, render_pass, nullptr);
- vkDestroyImageView(v.device, view, nullptr); vkDestroyImage(v.device, image, nullptr); vkFreeMemory(v.device, image_mem, nullptr);
- return true;
- }
-
- auto VulkanContext::shutdown() -> void
- {
- Impl& v = *m_impl;
- if (v.device) { vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; }
- if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; }
- }
-
- auto vulkan_self_test() -> bool
- {
- VulkanContext ctx;
- if (!ctx.init())
- {
- DONUT_ERROR("Vulkan: initialization failed");
- return false;
- }
- bool ok = ctx.self_test_clear();
- ok = ctx.self_test_triangle() && ok;
- ctx.shutdown();
- return ok;
- }
-}
diff --git a/src/platform/vulkan/vulkan_context.h b/src/platform/vulkan/vulkan_context.h
deleted file mode 100644
index de57c90..0000000
--- a/src/platform/vulkan/vulkan_context.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma once
-
-// Pure-C++ interface to the Vulkan backend (no vulkan.h leaks into the rest of
-// the engine; the implementation lives in VulkanContext.cpp). On macOS Vulkan
-// runs through MoltenVK (Vulkan -> Metal).
-namespace Donut
-{
- class VulkanContext
- {
- public:
- VulkanContext();
- ~VulkanContext();
-
- // Creates the instance, picks a physical device, and creates the logical
- // device + graphics queue. Returns false (and logs) on failure.
- auto init() -> bool;
- auto shutdown() -> void;
-
- // Phase 1 verification: renders a known clear colour into an offscreen
- // image and reads it back, confirming instance -> device -> render pass
- // -> command buffer -> submit -> read-back all work end to end.
- auto self_test_clear() -> bool;
-
- // Phase 2/3 verification: builds a graphics pipeline from Slang-compiled
- // SPIR-V and draws a full-screen gradient triangle into the offscreen
- // image, confirming the SPIR-V -> pipeline -> draw path works.
- auto self_test_triangle() -> bool;
-
- // B-3: renders the geodesic (black hole) fragment shader through Vulkan
- // into an offscreen image and writes it to pngPath. Exercises UBOs,
- // descriptor sets, a cubemap sampler and the geodesic pipeline.
- auto render_geodesic(const char* pngPath) -> bool;
-
- private:
- struct Impl;
- Impl* m_impl = nullptr;
- };
-
- // Convenience one-shot: init() + self_test_clear() + shutdown(). Logs results.
- auto vulkan_self_test() -> bool;
-}
diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp
new file mode 100644
index 0000000..4311d0c
--- /dev/null
+++ b/src/platform/vulkan/vulkan_device.cpp
@@ -0,0 +1,1254 @@
+#include "vulkan_device.h"
+
+#include "core/log.h"
+
+#define GLFW_INCLUDE_VULKAN
+#include <GLFW/glfw3.h>
+
+#include <imgui.h>
+#include <imgui_impl_glfw.h>
+#include <imgui_impl_vulkan.h>
+
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include "stb_image.h"
+
+#include <vector>
+#include <array>
+#include <algorithm>
+#include <cstring>
+#include <cstdlib>
+#include <fstream>
+
+namespace Donut::RHI
+{
+ namespace
+ {
+ constexpr int MAX_FRAMES_IN_FLIGHT = 2;
+ constexpr uint32_t MAX_BINDINGS = 8;
+
+ #define VKD_CHECK(expr) \
+ do { \
+ VkResult _r = (expr); \
+ if (_r != VK_SUCCESS) { \
+ DONUT_ERROR("Vulkan RHI: {} failed ({})", #expr, (int)_r); \
+ return false; \
+ } \
+ } while (0)
+
+ auto vk_format(Format f) -> VkFormat
+ {
+ switch (f) {
+ case Format::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT;
+ case Format::D32: return VK_FORMAT_D32_SFLOAT;
+ default: return VK_FORMAT_R8G8B8A8_UNORM;
+ }
+ }
+ auto vk_attr_format(uint32_t comps) -> VkFormat
+ {
+ switch (comps) {
+ case 1: return VK_FORMAT_R32_SFLOAT;
+ case 2: return VK_FORMAT_R32G32_SFLOAT;
+ case 3: return VK_FORMAT_R32G32B32_SFLOAT;
+ default: return VK_FORMAT_R32G32B32A32_SFLOAT;
+ }
+ }
+ auto vk_topology(Topology t) -> VkPrimitiveTopology
+ { return t == Topology::Lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; }
+ auto vk_compare(CompareOp o) -> VkCompareOp
+ { return o == CompareOp::Always ? VK_COMPARE_OP_ALWAYS : o == CompareOp::LessEqual ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; }
+ auto vk_filter(Filter f) -> VkFilter { return f == Filter::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; }
+ auto vk_cull(CullMode c) -> VkCullModeFlags
+ { return c == CullMode::None ? VK_CULL_MODE_NONE : c == CullMode::Back ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT; }
+
+ class VulkanDevice;
+
+ // ---- Buffer: host-visible + coherent, persistently mapped -----------
+ class VkBufferR : public Buffer
+ {
+ public:
+ VkBufferR(VkDevice d, VkBuffer b, VkDeviceMemory m, void* mapped, size_t size)
+ : m_device(d), m_buf(b), m_mem(m), m_mapped(mapped), m_size(size) {}
+ ~VkBufferR() override
+ {
+ if (m_mapped) vkUnmapMemory(m_device, m_mem);
+ if (m_buf) vkDestroyBuffer(m_device, m_buf, nullptr);
+ if (m_mem) vkFreeMemory(m_device, m_mem, nullptr);
+ }
+ auto update(const void* data, size_t size) -> void override
+ { if (m_mapped) std::memcpy(m_mapped, data, std::min(size, m_size)); }
+
+ VkDevice m_device; VkBuffer m_buf; VkDeviceMemory m_mem; void* m_mapped; size_t m_size;
+ };
+
+ // ---- Texture: sampled image (2D or cube). Owns its handles unless it is
+ // a borrowed wrapper around a render-target view. ------------------
+ class VkTextureR : public Texture
+ {
+ public:
+ VkTextureR() = default;
+ ~VkTextureR() override
+ {
+ if (!m_owns) return;
+ if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr);
+ if (m_view) vkDestroyImageView(m_device, m_view, nullptr);
+ if (m_image) vkDestroyImage(m_device, m_image, nullptr);
+ if (m_mem) vkFreeMemory(m_device, m_mem, nullptr);
+ }
+ VkDevice m_device = VK_NULL_HANDLE;
+ VkImage m_image = VK_NULL_HANDLE;
+ VkDeviceMemory m_mem = VK_NULL_HANDLE;
+ VkImageView m_view = VK_NULL_HANDLE;
+ VkSampler m_sampler = VK_NULL_HANDLE;
+ bool m_owns = true;
+ };
+
+ // ---- RenderTarget: off-screen colour image + framebuffer ------------
+ class VkRenderTargetR : public RenderTarget
+ {
+ public:
+ ~VkRenderTargetR() override
+ {
+ if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr);
+ if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr);
+ if (m_view) vkDestroyImageView(m_device, m_view, nullptr);
+ if (m_image) vkDestroyImage(m_device, m_image, nullptr);
+ if (m_mem) vkFreeMemory(m_device, m_mem, nullptr);
+ }
+ auto width() const -> int override { return m_w; }
+ auto height() const -> int override { return m_h; }
+ auto color_texture() -> Texture* override { return &m_color; }
+
+ VkDevice m_device = VK_NULL_HANDLE;
+ int m_w = 0, m_h = 0;
+ VkImage m_image = VK_NULL_HANDLE;
+ VkDeviceMemory m_mem = VK_NULL_HANDLE;
+ VkImageView m_view = VK_NULL_HANDLE;
+ VkSampler m_sampler = VK_NULL_HANDLE;
+ VkFramebuffer m_fb = VK_NULL_HANDLE;
+ VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling
+ };
+
+ // ---- Pipeline -------------------------------------------------------
+ class VkPipelineR : public Pipeline
+ {
+ public:
+ ~VkPipelineR() override
+ {
+ if (m_pipeline) vkDestroyPipeline(m_device, m_pipeline, nullptr);
+ if (m_layout) vkDestroyPipelineLayout(m_device, m_layout, nullptr);
+ if (m_set_layout) vkDestroyDescriptorSetLayout(m_device, m_set_layout, nullptr);
+ }
+ VkDevice m_device = VK_NULL_HANDLE;
+ VkPipeline m_pipeline = VK_NULL_HANDLE;
+ VkPipelineLayout m_layout = VK_NULL_HANDLE;
+ VkDescriptorSetLayout m_set_layout = VK_NULL_HANDLE;
+ std::vector<ResourceSlot> m_resources;
+ };
+
+ // ---- CommandList ----------------------------------------------------
+ class VkCommandListR : public CommandList
+ {
+ public:
+ auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override
+ {
+ VkClearValue cvs[2]{};
+ cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } };
+ cvs[1].depthStencil = { 1.0f, 0 };
+ VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
+ if (target)
+ {
+ auto* rt = static_cast<VkRenderTargetR*>(target);
+ rpbi.renderPass = m_offscreen_rp; rpbi.framebuffer = rt->m_fb;
+ rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } };
+ rpbi.clearValueCount = 1; rpbi.pClearValues = cvs;
+ }
+ else
+ {
+ rpbi.renderPass = m_swapchain_rp; rpbi.framebuffer = m_swapchain_fb;
+ rpbi.renderArea = { { 0, 0 }, m_extent };
+ rpbi.clearValueCount = 2; rpbi.pClearValues = cvs;
+ }
+ vkCmdBeginRenderPass(m_cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
+ }
+ auto end_render_pass() -> void override { vkCmdEndRenderPass(m_cmd); }
+
+ auto bind_pipeline(Pipeline* p) -> void override
+ {
+ m_pipe = static_cast<VkPipelineR*>(p);
+ vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline);
+ }
+ auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override
+ {
+ VkViewport vp{ (float)x, flip_y ? (float)(y + h) : (float)y,
+ (float)w, flip_y ? -(float)h : (float)h, 0.0f, 1.0f };
+ VkRect2D sc{ { x, y }, { (uint32_t)w, (uint32_t)h } };
+ vkCmdSetViewport(m_cmd, 0, 1, &vp);
+ vkCmdSetScissor(m_cmd, 0, 1, &sc);
+ }
+ auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override
+ {
+ if (binding >= MAX_BINDINGS) return;
+ m_buf_info[binding] = { static_cast<VkBufferR*>(ubo)->m_buf, 0, VK_WHOLE_SIZE };
+ }
+ auto bind_texture(uint32_t binding, Texture* texture) -> void override
+ {
+ if (binding >= MAX_BINDINGS) return;
+ auto* t = static_cast<VkTextureR*>(texture);
+ m_img_info[binding] = { t->m_sampler, t->m_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
+ }
+ auto bind_vertex_buffer(Buffer* vb) -> void override
+ {
+ VkBuffer b = static_cast<VkBufferR*>(vb)->m_buf; VkDeviceSize off = 0;
+ vkCmdBindVertexBuffers(m_cmd, 0, 1, &b, &off);
+ }
+ auto bind_index_buffer(Buffer* ib) -> void override
+ { vkCmdBindIndexBuffer(m_cmd, static_cast<VkBufferR*>(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); }
+ auto draw(uint32_t vertex_count) -> void override
+ { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); }
+ auto draw_indexed(uint32_t index_count) -> void override
+ { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); }
+
+ // Allocate + write + bind a descriptor set for the current pipeline's
+ // declared resources, using whatever was bound since bind_pipeline.
+ auto flush_descriptors() -> void
+ {
+ if (!m_pipe || m_pipe->m_resources.empty()) return;
+ VkDescriptorSetAllocateInfo ai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
+ ai.descriptorPool = m_frame_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &m_pipe->m_set_layout;
+ VkDescriptorSet set = VK_NULL_HANDLE;
+ if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS)
+ { DONUT_ERROR("Vulkan RHI: descriptor set allocation failed"); return; }
+
+ std::array<VkWriteDescriptorSet, MAX_BINDINGS> writes{};
+ uint32_t n = 0;
+ for (const auto& r : m_pipe->m_resources)
+ {
+ VkWriteDescriptorSet& w = writes[n++];
+ w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+ w.dstSet = set; w.dstBinding = r.binding; w.descriptorCount = 1;
+ if (r.kind == ResourceKind::UniformBuffer)
+ { w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &m_buf_info[r.binding]; }
+ else
+ { w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = &m_img_info[r.binding]; }
+ }
+ vkUpdateDescriptorSets(m_device, n, writes.data(), 0, nullptr);
+ vkCmdBindDescriptorSets(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_layout, 0, 1, &set, 0, nullptr);
+ }
+
+ // Set by the device at begin_frame:
+ VkDevice m_device = VK_NULL_HANDLE;
+ VkCommandBuffer m_cmd = VK_NULL_HANDLE;
+ VkRenderPass m_swapchain_rp = VK_NULL_HANDLE;
+ VkRenderPass m_offscreen_rp = VK_NULL_HANDLE;
+ VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE;
+ VkExtent2D m_extent{};
+ VkDescriptorPool m_frame_pool = VK_NULL_HANDLE;
+
+ VkPipelineR* m_pipe = nullptr;
+ VkDescriptorBufferInfo m_buf_info[MAX_BINDINGS]{};
+ VkDescriptorImageInfo m_img_info[MAX_BINDINGS]{};
+ };
+
+ // ---- Device ---------------------------------------------------------
+ class VulkanDevice : public Device
+ {
+ public:
+ auto init(void* glfwWindow, int width, int height) -> bool override;
+ auto shutdown() -> void override;
+ auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; }
+ auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); }
+
+ auto create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer> override;
+ auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture> override;
+ auto create_cubemap_from_hdri(const std::string& path) -> Ref<Texture> override;
+ auto create_render_target(int w, int h, Format color, bool with_depth, Filter filter, int mips) -> Ref<RenderTarget> override;
+ auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> override;
+
+ auto begin_frame(const glm::vec4& clear) -> CommandList* override;
+ auto end_frame() -> void override;
+
+ auto init_imgui() -> void override;
+ auto imgui_new_frame() -> void override;
+ auto imgui_render(CommandList& cmds) -> void override;
+
+ auto device_name() const -> const std::string& override { return m_gpu_name; }
+
+ // --- internals ---
+ auto find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t;
+ auto create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool;
+ auto load_spirv(const std::string& path) const -> std::vector<uint32_t>;
+ auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool;
+
+ auto create_instance() -> bool;
+ auto pick_physical_and_device() -> bool;
+ auto create_swapchain() -> bool;
+ auto create_image_views() -> bool;
+ auto create_swapchain_render_pass() -> bool;
+ auto create_offscreen_render_pass() -> bool;
+ auto create_depth_and_framebuffers() -> bool;
+ auto create_command_and_sync() -> bool;
+ auto recreate_swapchain() -> bool;
+ auto cleanup_swapchain() -> void;
+
+ GLFWwindow* m_window = nullptr;
+ int m_width = 0, m_height = 0;
+ bool m_framebuffer_resized = false;
+ std::string m_gpu_name;
+
+ VkInstance m_instance = VK_NULL_HANDLE;
+ VkSurfaceKHR m_surface = VK_NULL_HANDLE;
+ VkPhysicalDevice m_physical = VK_NULL_HANDLE;
+ VkDevice m_device = VK_NULL_HANDLE;
+ uint32_t m_graphics_family = 0, m_present_family = 0;
+ VkQueue m_graphics_queue = VK_NULL_HANDLE, m_present_queue = VK_NULL_HANDLE;
+ VkPhysicalDeviceMemoryProperties m_mem_props{};
+
+ VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
+ VkFormat m_swapchain_format = VK_FORMAT_B8G8R8A8_UNORM;
+ VkExtent2D m_extent{};
+ std::vector<VkImage> m_images;
+ std::vector<VkImageView> m_image_views;
+ VkRenderPass m_swapchain_rp = VK_NULL_HANDLE;
+ VkRenderPass m_offscreen_rp = VK_NULL_HANDLE;
+ std::vector<VkFramebuffer> m_framebuffers;
+ VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE;
+
+ VkCommandPool m_command_pool = VK_NULL_HANDLE;
+ std::vector<VkCommandBuffer> m_command_buffers;
+ std::vector<VkSemaphore> m_image_available;
+ std::vector<VkSemaphore> m_render_finished;
+ std::vector<VkFence> m_in_flight;
+ std::vector<VkFence> m_images_in_flight;
+ VkFence m_geo_in_use = VK_NULL_HANDLE;
+ uint32_t m_current_frame = 0, m_image_index = 0;
+
+ std::vector<VkDescriptorPool> m_frame_pools; // one per frame in flight
+ VkDescriptorPool m_imgui_pool = VK_NULL_HANDLE;
+ bool m_imgui = false;
+
+ VkCommandListR m_cmds;
+ };
+
+ // ==================================================================
+ auto VulkanDevice::find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t
+ {
+ for (uint32_t i = 0; i < m_mem_props.memoryTypeCount; ++i)
+ if ((filter & (1u << i)) && (m_mem_props.memoryTypes[i].propertyFlags & flags) == flags)
+ return i;
+ return UINT32_MAX;
+ }
+
+ auto VulkanDevice::create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props,
+ VkBuffer& buf, VkDeviceMemory& mem) const -> bool
+ {
+ VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
+ bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+ if (vkCreateBuffer(m_device, &bci, nullptr, &buf) != VK_SUCCESS) return false;
+ VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(m_device, buf, &req);
+ VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props);
+ if (vkAllocateMemory(m_device, &ai, nullptr, &mem) != VK_SUCCESS) return false;
+ vkBindBufferMemory(m_device, buf, mem, 0);
+ return true;
+ }
+
+ auto VulkanDevice::load_spirv(const std::string& path) const -> std::vector<uint32_t>
+ {
+ std::ifstream file(path, std::ios::ate | std::ios::binary);
+ if (!file.is_open()) return {};
+ size_t size = (size_t)file.tellg();
+ std::vector<uint32_t> data(size / 4);
+ file.seekg(0); file.read(reinterpret_cast<char*>(data.data()), size);
+ return data;
+ }
+
+ auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool
+ {
+ auto spv = load_spirv(path);
+ if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; }
+ VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
+ ci.codeSize = spv.size() * 4; ci.pCode = spv.data();
+ return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS;
+ }
+
+ auto VulkanDevice::create_instance() -> bool
+ {
+ VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO };
+ app.pApplicationName = "Donut"; app.apiVersion = VK_API_VERSION_1_2;
+
+ uint32_t glfwExtCount = 0;
+ const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount);
+ if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; }
+ std::vector<const char*> exts(glfwExts, glfwExts + glfwExtCount);
+ exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
+ exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
+
+ std::vector<const char*> layers;
+ uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
+ std::vector<VkLayerProperties> avail(layer_count);
+ vkEnumerateInstanceLayerProperties(&layer_count, avail.data());
+ for (const auto& l : avail)
+ if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0)
+ layers.push_back("VK_LAYER_KHRONOS_validation");
+
+ VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
+ ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
+ ici.pApplicationInfo = &app;
+ ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data();
+ ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data();
+ VkResult r = vkCreateInstance(&ici, nullptr, &m_instance);
+ if (r != VK_SUCCESS && !layers.empty())
+ {
+ DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it");
+ ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr;
+ r = vkCreateInstance(&ici, nullptr, &m_instance);
+ }
+ if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: vkCreateInstance failed ({})", (int)r); return false; }
+ VKD_CHECK(glfwCreateWindowSurface(m_instance, m_window, nullptr, &m_surface));
+ DONUT_INFO("Vulkan RHI: instance + surface created (validation {})", layers.empty() ? "off" : "on");
+ return true;
+ }
+
+ auto VulkanDevice::pick_physical_and_device() -> bool
+ {
+ uint32_t count = 0; vkEnumeratePhysicalDevices(m_instance, &count, nullptr);
+ if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; }
+ std::vector<VkPhysicalDevice> devices(count);
+ vkEnumeratePhysicalDevices(m_instance, &count, devices.data());
+ m_physical = devices[0];
+
+ uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr);
+ std::vector<VkQueueFamilyProperties> qfams(q);
+ vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data());
+ bool fg = false, fp = false;
+ for (uint32_t i = 0; i < q; ++i)
+ {
+ if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; }
+ VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present);
+ if (!fp && present) { m_present_family = i; fp = true; }
+ }
+ if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; }
+
+ std::vector<const char*> dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+ uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr);
+ std::vector<VkExtensionProperties> dep(dec);
+ vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, dep.data());
+ for (const auto& e : dep)
+ if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0)
+ dev_exts.push_back("VK_KHR_portability_subset");
+
+ float priority = 1.0f;
+ std::vector<VkDeviceQueueCreateInfo> qcis;
+ uint32_t families[2] = { m_graphics_family, m_present_family };
+ for (uint32_t i = 0; i < (m_graphics_family == m_present_family ? 1u : 2u); ++i)
+ {
+ VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
+ qci.queueFamilyIndex = families[i]; qci.queueCount = 1; qci.pQueuePriorities = &priority;
+ qcis.push_back(qci);
+ }
+ VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
+ dci.queueCreateInfoCount = (uint32_t)qcis.size(); dci.pQueueCreateInfos = qcis.data();
+ dci.enabledExtensionCount = (uint32_t)dev_exts.size(); dci.ppEnabledExtensionNames = dev_exts.data();
+ VKD_CHECK(vkCreateDevice(m_physical, &dci, nullptr, &m_device));
+ vkGetDeviceQueue(m_device, m_graphics_family, 0, &m_graphics_queue);
+ vkGetDeviceQueue(m_device, m_present_family, 0, &m_present_queue);
+
+ VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(m_physical, &props);
+ vkGetPhysicalDeviceMemoryProperties(m_physical, &m_mem_props);
+ m_gpu_name = props.deviceName;
+ DONUT_INFO("Vulkan RHI device: {}", m_gpu_name);
+ return true;
+ }
+
+ auto VulkanDevice::create_swapchain() -> bool
+ {
+ VkSurfaceCapabilitiesKHR caps{};
+ vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical, m_surface, &caps);
+ uint32_t fc = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, nullptr);
+ std::vector<VkSurfaceFormatKHR> formats(fc);
+ vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, formats.data());
+ VkSurfaceFormatKHR chosen = formats[0];
+ for (const auto& f : formats)
+ if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f;
+ m_swapchain_format = chosen.format;
+
+ if (caps.currentExtent.width != UINT32_MAX) m_extent = caps.currentExtent;
+ else {
+ m_extent.width = std::clamp((uint32_t)m_width, caps.minImageExtent.width, caps.maxImageExtent.width);
+ m_extent.height = std::clamp((uint32_t)m_height, caps.minImageExtent.height, caps.maxImageExtent.height);
+ }
+ uint32_t image_count = caps.minImageCount + 1;
+ if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) image_count = caps.maxImageCount;
+
+ VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
+ sci.surface = m_surface; sci.minImageCount = image_count;
+ sci.imageFormat = chosen.format; sci.imageColorSpace = chosen.colorSpace;
+ sci.imageExtent = m_extent; sci.imageArrayLayers = 1;
+ sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+ sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
+ sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE;
+ uint32_t fam[2] = { m_graphics_family, m_present_family };
+ if (m_graphics_family != m_present_family)
+ { sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; sci.queueFamilyIndexCount = 2; sci.pQueueFamilyIndices = fam; }
+ else sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
+ VKD_CHECK(vkCreateSwapchainKHR(m_device, &sci, nullptr, &m_swapchain));
+ uint32_t n = 0; vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, nullptr);
+ m_images.resize(n); vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, m_images.data());
+ return true;
+ }
+
+ auto VulkanDevice::create_image_views() -> bool
+ {
+ m_image_views.resize(m_images.size());
+ for (size_t i = 0; i < m_images.size(); ++i)
+ {
+ VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ vci.image = m_images[i]; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = m_swapchain_format;
+ vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
+ VKD_CHECK(vkCreateImageView(m_device, &vci, nullptr, &m_image_views[i]));
+ }
+ return true;
+ }
+
+ // Swapchain pass: colour + depth. Scene geometry uses the depth; the
+ // black-hole present + ImGui simply don't test it.
+ auto VulkanDevice::create_swapchain_render_pass() -> bool
+ {
+ VkAttachmentDescription atts[2]{};
+ atts[0].format = m_swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+ atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+ atts[1].format = VK_FORMAT_D32_SFLOAT; atts[1].samples = VK_SAMPLE_COUNT_1_BIT;
+ atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+ VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
+ VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
+ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+ subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref;
+ VkSubpassDependency dep{};
+ dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0;
+ dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+ dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+ VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
+ rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
+ rpci.dependencyCount = 1; rpci.pDependencies = &dep;
+ VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_swapchain_rp));
+ return true;
+ }
+
+ // Off-screen colour pass (RGBA8), leaving the image SHADER_READ_ONLY so the
+ // present pass can sample it. Shared by every render target.
+ auto VulkanDevice::create_offscreen_render_pass() -> bool
+ {
+ VkAttachmentDescription color{};
+ color.format = VK_FORMAT_R8G8B8A8_UNORM; color.samples = VK_SAMPLE_COUNT_1_BIT;
+ color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+ color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+ VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
+ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+ subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref;
+ VkSubpassDependency deps[2]{};
+ deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0;
+ deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
+ deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
+ rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
+ rpci.dependencyCount = 2; rpci.pDependencies = deps;
+ VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_offscreen_rp));
+ return true;
+ }
+
+ auto VulkanDevice::create_depth_and_framebuffers() -> bool
+ {
+ VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ dici.imageType = VK_IMAGE_TYPE_2D; dici.format = VK_FORMAT_D32_SFLOAT;
+ dici.extent = { m_extent.width, m_extent.height, 1 };
+ dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT;
+ dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
+ VKD_CHECK(vkCreateImage(m_device, &dici, nullptr, &m_depth_image));
+ VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, m_depth_image, &dreq);
+ VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ VKD_CHECK(vkAllocateMemory(m_device, &dai, nullptr, &m_depth_mem));
+ VKD_CHECK(vkBindImageMemory(m_device, m_depth_image, m_depth_mem, 0));
+ VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ dvci.image = m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = VK_FORMAT_D32_SFLOAT;
+ dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 };
+ VKD_CHECK(vkCreateImageView(m_device, &dvci, nullptr, &m_depth_view));
+
+ m_framebuffers.resize(m_image_views.size());
+ for (size_t i = 0; i < m_image_views.size(); ++i)
+ {
+ VkImageView att[2] = { m_image_views[i], m_depth_view };
+ VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
+ fbci.renderPass = m_swapchain_rp; fbci.attachmentCount = 2; fbci.pAttachments = att;
+ fbci.width = m_extent.width; fbci.height = m_extent.height; fbci.layers = 1;
+ VKD_CHECK(vkCreateFramebuffer(m_device, &fbci, nullptr, &m_framebuffers[i]));
+ }
+ return true;
+ }
+
+ auto VulkanDevice::create_command_and_sync() -> bool
+ {
+ VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
+ pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = m_graphics_family;
+ VKD_CHECK(vkCreateCommandPool(m_device, &pci, nullptr, &m_command_pool));
+ m_command_buffers.resize(MAX_FRAMES_IN_FLIGHT);
+ VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
+ cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT;
+ VKD_CHECK(vkAllocateCommandBuffers(m_device, &cbai, m_command_buffers.data()));
+
+ m_image_available.resize(MAX_FRAMES_IN_FLIGHT);
+ m_in_flight.resize(MAX_FRAMES_IN_FLIGHT);
+ m_render_finished.resize(m_images.size());
+ m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE);
+ VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
+ VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
+ for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i)
+ { VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_image_available[i])); VKD_CHECK(vkCreateFence(m_device, &fci, nullptr, &m_in_flight[i])); }
+ for (size_t i = 0; i < m_images.size(); ++i)
+ VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_render_finished[i]));
+
+ m_frame_pools.resize(MAX_FRAMES_IN_FLIGHT);
+ VkDescriptorPoolSize sizes[2] = {
+ { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 },
+ { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256 },
+ };
+ for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i)
+ {
+ VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
+ dpci.maxSets = 256; dpci.poolSizeCount = 2; dpci.pPoolSizes = sizes;
+ VKD_CHECK(vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_frame_pools[i]));
+ }
+ return true;
+ }
+
+ auto VulkanDevice::init(void* glfwWindow, int width, int height) -> bool
+ {
+ m_window = (GLFWwindow*)glfwWindow; m_width = width; m_height = height;
+ if (!create_instance()) return false;
+ if (!pick_physical_and_device()) return false;
+ if (!create_swapchain()) return false;
+ if (!create_image_views()) return false;
+ if (!create_swapchain_render_pass()) return false;
+ if (!create_offscreen_render_pass()) return false;
+ if (!create_depth_and_framebuffers())return false;
+ if (!create_command_and_sync()) return false;
+ DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height);
+ return true;
+ }
+
+ auto VulkanDevice::cleanup_swapchain() -> void
+ {
+ for (auto fb : m_framebuffers) vkDestroyFramebuffer(m_device, fb, nullptr);
+ m_framebuffers.clear();
+ if (m_depth_view) { vkDestroyImageView(m_device, m_depth_view, nullptr); m_depth_view = VK_NULL_HANDLE; }
+ if (m_depth_image) { vkDestroyImage(m_device, m_depth_image, nullptr); m_depth_image = VK_NULL_HANDLE; }
+ if (m_depth_mem) { vkFreeMemory(m_device, m_depth_mem, nullptr); m_depth_mem = VK_NULL_HANDLE; }
+ for (auto iv : m_image_views) vkDestroyImageView(m_device, iv, nullptr);
+ m_image_views.clear();
+ if (m_swapchain) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; }
+ }
+
+ auto VulkanDevice::recreate_swapchain() -> bool
+ {
+ int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h);
+ while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); }
+ m_width = w; m_height = h;
+ vkDeviceWaitIdle(m_device);
+ cleanup_swapchain();
+ if (!create_swapchain()) return false;
+ if (!create_image_views()) return false;
+ if (!create_depth_and_framebuffers())return false;
+ m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE);
+ return true;
+ }
+
+ auto VulkanDevice::create_buffer(BufferType type, size_t size, const void* data) -> Ref<Buffer>
+ {
+ VkBufferUsageFlags usage = type == BufferType::Index ? VK_BUFFER_USAGE_INDEX_BUFFER_BIT
+ : type == BufferType::Uniform ? VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
+ : VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
+ VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE;
+ create_buffer_raw(size, usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buf, mem);
+ void* mapped = nullptr; vkMapMemory(m_device, mem, 0, size, 0, &mapped);
+ if (data && mapped) std::memcpy(mapped, data, size);
+ return create_ref<VkBufferR>(m_device, buf, mem, mapped, size);
+ }
+
+ auto VulkanDevice::create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref<Texture>
+ {
+ auto tex = create_ref<VkTextureR>(); tex->m_device = m_device;
+ VkFormat fmt = vk_format(format);
+ VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 };
+ ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
+ ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+ vkCreateImage(m_device, &ici, nullptr, &tex->m_image);
+ VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &req);
+ VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(m_device, &ai, nullptr, &tex->m_mem);
+ vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0);
+
+ size_t bpp = format == Format::RGBA16F ? 8 : 4;
+ VkDeviceSize sz = (VkDeviceSize)w * h * bpp;
+ VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE;
+ create_buffer_raw(sz, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging, staging_mem);
+ void* mp = nullptr; vkMapMemory(m_device, staging_mem, 0, sz, 0, &mp);
+ if (data) std::memcpy(mp, data, sz); else std::memset(mp, 0, sz);
+ vkUnmapMemory(m_device, staging_mem);
+
+ VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
+ cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
+ VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd);
+ VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+ vkBeginCommandBuffer(cmd, &bi);
+ VkImageMemoryBarrier b{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
+ b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+ b.image = tex->m_image; b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
+ b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+ vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
+ VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 };
+ vkCmdCopyBufferToImage(cmd, staging, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &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, bool /*with_depth*/, Filter filter, int /*mips*/) -> Ref<RenderTarget>
+ {
+ auto rt = create_ref<VkRenderTargetR>();
+ rt->m_device = m_device; rt->m_w = w; rt->m_h = h;
+ VkFormat fmt = vk_format(color);
+ VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 };
+ ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
+ ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+ vkCreateImage(m_device, &ici, nullptr, &rt->m_image);
+ VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req);
+ VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem);
+ vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0);
+ VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt;
+ vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
+ vkCreateImageView(m_device, &vci, nullptr, &rt->m_view);
+ VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
+ smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter);
+ smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler);
+ VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
+ fbci.renderPass = m_offscreen_rp; fbci.attachmentCount = 1; fbci.pAttachments = &rt->m_view;
+ fbci.width = w; fbci.height = h; fbci.layers = 1;
+ vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb);
+
+ rt->m_color.m_device = m_device; rt->m_color.m_view = rt->m_view; rt->m_color.m_sampler = rt->m_sampler; rt->m_color.m_owns = false;
+ return rt;
+ }
+
+ // Builds an environment cubemap from an equirectangular HDRI: render the 6
+ // faces with the EquirectToCubemap pipeline, then a full mip chain by
+ // linear down-blits (so divergence-based LOD reads a blurred sky). Returns
+ // a Texture owning the cube image/view/sampler.
+ auto VulkanDevice::create_cubemap_from_hdri(const std::string& path) -> Ref<Texture>
+ {
+ auto tex = create_ref<VkTextureR>(); tex->m_device = m_device;
+ const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
+ const uint32_t FACE = 1024;
+ const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT;
+ uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS;
+
+ VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
+ cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 };
+ cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT;
+ cci.tiling = VK_IMAGE_TILING_OPTIMAL;
+ cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT
+ | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+ vkCreateImage(m_device, &cci, nullptr, &tex->m_image);
+ VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &creq);
+ VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(m_device, &cai, nullptr, &tex->m_mem);
+ vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0);
+ VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ cvci.image = tex->m_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt;
+ cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 };
+ vkCreateImageView(m_device, &cvci, nullptr, &tex->m_view);
+ VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
+ csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR;
+ csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS;
+ csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler);
+
+ int w = 0, h = 0, ch = 0;
+ float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4);
+ if (!pixels)
+ {
+ DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path);
+ VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
+ cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
+ VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd);
+ VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+ vkBeginCommandBuffer(cmd, &bi);
+ VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
+ tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+ tb.image = tex->m_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 };
+ tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+ vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb);
+ VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f;
+ VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 };
+ vkCmdClearColorImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng);
+ VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+ rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb);
+ vkEndCommandBuffer(cmd);
+ VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
+ vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue);
+ vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd);
+ return tex;
+ }
+
+ const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT;
+ size_t texel_count = (size_t)w * h * 4;
+ VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t);
+ VkBuffer eq_staging; VkDeviceMemory eq_staging_mem;
+ create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem);
+ void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp);
+ uint16_t* dst = (uint16_t*)mp;
+ for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); }
+ vkUnmapMemory(m_device, eq_staging_mem);
+ stbi_image_free(pixels);
+
+ VkImage eq_image; VkDeviceMemory eq_mem;
+ VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 };
+ eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT;
+ eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+ vkCreateImage(m_device, &eci, nullptr, &eq_image);
+ VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(m_device, eq_image, &ereq);
+ VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
+ eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(m_device, &eai, nullptr, &eq_mem);
+ vkBindImageMemory(m_device, eq_image, eq_mem, 0);
+ VkImageView eq_view;
+ VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt;
+ evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
+ vkCreateImageView(m_device, &evci, nullptr, &eq_view);
+ VkSampler eq_sampler;
+ VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
+ esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR;
+ esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+ esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+ vkCreateSampler(m_device, &esm, nullptr, &eq_sampler);
+
+ VkImageView face_views[6];
+ for (uint32_t i = 0; i < 6; ++i)
+ {
+ VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ fvci.image = tex->m_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt;
+ fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 };
+ vkCreateImageView(m_device, &fvci, nullptr, &face_views[i]);
+ }
+
+ VkAttachmentDescription color{};
+ color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT;
+ color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+ color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+ color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+ VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
+ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref;
+ VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL;
+ dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+ VkRenderPass rp;
+ VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
+ rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep;
+ vkCreateRenderPass(m_device, &rpci, nullptr, &rp);
+ VkFramebuffer face_fb[6];
+ for (uint32_t i = 0; i < 6; ++i)
+ {
+ VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
+ fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1;
+ vkCreateFramebuffer(m_device, &fbci, nullptr, &face_fb[i]);
+ }
+
+ VkDescriptorSetLayoutBinding binds[2]{};
+ binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
+ binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+ VkDescriptorSetLayout set_layout;
+ VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds;
+ vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &set_layout);
+ VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } };
+ VkDescriptorPool pool;
+ VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes;
+ vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool);
+
+ VkShaderModule vmod, fmod;
+ create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod);
+ create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod);
+ VkPipelineLayout playout;
+ VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout;
+ vkCreatePipelineLayout(m_device, &plci, nullptr, &playout);
+ VkPipelineShaderStageCreateInfo stages[2]{};
+ stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
+ stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
+ VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX };
+ VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 };
+ VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
+ vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via;
+ VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+ VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } };
+ VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &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;
+ }
+
+ auto VulkanDevice::create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline>
+ {
+ auto p = create_ref<VkPipelineR>(); p->m_device = m_device; p->m_resources = desc.resources;
+
+ std::vector<VkDescriptorSetLayoutBinding> binds;
+ for (const auto& r : desc.resources)
+ {
+ VkDescriptorSetLayoutBinding b{};
+ b.binding = r.binding; b.descriptorCount = 1;
+ b.descriptorType = r.kind == ResourceKind::UniformBuffer ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+ b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
+ binds.push_back(b);
+ }
+ VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
+ dslci.bindingCount = (uint32_t)binds.size(); dslci.pBindings = binds.data();
+ vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &p->m_set_layout);
+ VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
+ plci.setLayoutCount = 1; plci.pSetLayouts = &p->m_set_layout;
+ vkCreatePipelineLayout(m_device, &plci, nullptr, &p->m_layout);
+
+ VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE;
+ if (!create_shader_module("assets/shaders/generated/" + desc.shader + ".vertexMain.spv", vmod) ||
+ !create_shader_module("assets/shaders/generated/" + desc.shader + ".fragmentMain.spv", fmod))
+ { DONUT_ERROR("Vulkan RHI: shader '{}' modules failed", desc.shader); return p; }
+ VkPipelineShaderStageCreateInfo stages[2]{};
+ stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
+ stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
+
+ VkVertexInputBindingDescription vib{ 0, desc.vertex_layout.stride, VK_VERTEX_INPUT_RATE_VERTEX };
+ std::vector<VkVertexInputAttributeDescription> vias;
+ for (const auto& a : desc.vertex_layout.attributes)
+ vias.push_back({ a.location, 0, vk_attr_format(a.components), a.offset });
+ VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
+ vin.vertexBindingDescriptionCount = desc.vertex_layout.stride ? 1 : 0; vin.pVertexBindingDescriptions = &vib;
+ vin.vertexAttributeDescriptionCount = (uint32_t)vias.size(); vin.pVertexAttributeDescriptions = vias.data();
+
+ VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = vk_topology(desc.topology);
+ VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1;
+ VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
+ VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn;
+ VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO };
+ rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = vk_cull(desc.cull); rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
+ VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+ VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
+ ds.depthTestEnable = desc.depth_test ? VK_TRUE : VK_FALSE; ds.depthWriteEnable = desc.depth_write ? VK_TRUE : VK_FALSE; ds.depthCompareOp = vk_compare(desc.depth_op);
+ VkPipelineColorBlendAttachmentState cba{};
+ cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+ if (desc.blend == BlendMode::AlphaBlend)
+ {
+ cba.blendEnable = VK_TRUE;
+ cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD;
+ cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD;
+ }
+ VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba;
+
+ VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
+ gpci.stageCount = 2; gpci.pStages = stages;
+ gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
+ gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci;
+ if (desc.has_depth) gpci.pDepthStencilState = &ds;
+ gpci.layout = p->m_layout;
+ gpci.renderPass = desc.has_depth ? m_swapchain_rp : m_offscreen_rp;
+ gpci.subpass = 0;
+ VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline);
+ vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr);
+ if (pr != VK_SUCCESS) DONUT_ERROR("Vulkan RHI: pipeline '{}' creation failed ({})", desc.shader, (int)pr);
+ return p;
+ }
+
+ auto VulkanDevice::begin_frame(const glm::vec4&) -> CommandList*
+ {
+ if (m_device == VK_NULL_HANDLE) return nullptr;
+ vkWaitForFences(m_device, 1, &m_in_flight[m_current_frame], VK_TRUE, UINT64_MAX);
+ VkResult r = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, m_image_available[m_current_frame], VK_NULL_HANDLE, &m_image_index);
+ if (r == VK_ERROR_OUT_OF_DATE_KHR) { recreate_swapchain(); return nullptr; }
+ if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan RHI: acquire failed ({})", (int)r); return nullptr; }
+ if (m_images_in_flight[m_image_index] != VK_NULL_HANDLE)
+ vkWaitForFences(m_device, 1, &m_images_in_flight[m_image_index], VK_TRUE, UINT64_MAX);
+ m_images_in_flight[m_image_index] = m_in_flight[m_current_frame];
+ if (m_geo_in_use != VK_NULL_HANDLE)
+ vkWaitForFences(m_device, 1, &m_geo_in_use, VK_TRUE, UINT64_MAX);
+
+ vkResetDescriptorPool(m_device, m_frame_pools[m_current_frame], 0);
+ VkCommandBuffer cmd = m_command_buffers[m_current_frame];
+ vkResetCommandBuffer(cmd, 0);
+ VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
+ vkBeginCommandBuffer(cmd, &bi);
+
+ m_cmds.m_device = m_device; m_cmds.m_cmd = cmd;
+ m_cmds.m_swapchain_rp = m_swapchain_rp; m_cmds.m_offscreen_rp = m_offscreen_rp;
+ m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent;
+ m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr;
+ return &m_cmds;
+ }
+
+ auto VulkanDevice::end_frame() -> void
+ {
+ VkCommandBuffer cmd = m_command_buffers[m_current_frame];
+ vkEndCommandBuffer(cmd);
+ VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+ VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
+ submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &m_image_available[m_current_frame]; submit.pWaitDstStageMask = &wait_stage;
+ submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd;
+ submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &m_render_finished[m_image_index];
+ vkResetFences(m_device, 1, &m_in_flight[m_current_frame]);
+ if (vkQueueSubmit(m_graphics_queue, 1, &submit, m_in_flight[m_current_frame]) != VK_SUCCESS)
+ { DONUT_ERROR("Vulkan RHI: queue submit failed"); return; }
+ m_geo_in_use = m_in_flight[m_current_frame];
+
+ VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
+ present.waitSemaphoreCount = 1; present.pWaitSemaphores = &m_render_finished[m_image_index];
+ present.swapchainCount = 1; present.pSwapchains = &m_swapchain; present.pImageIndices = &m_image_index;
+ VkResult r = vkQueuePresentKHR(m_present_queue, &present);
+ if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || m_framebuffer_resized)
+ { m_framebuffer_resized = false; recreate_swapchain(); }
+ m_current_frame = (m_current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
+ }
+
+ auto VulkanDevice::init_imgui() -> void
+ {
+ VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 };
+ VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
+ dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; dpci.maxSets = 1000;
+ dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size;
+ vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_imgui_pool);
+
+ IMGUI_CHECKVERSION(); ImGui::CreateContext();
+ ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
+ ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable;
+ ImGui::StyleColorsDark();
+ ImGui_ImplGlfw_InitForVulkan(m_window, true);
+ ImGui_ImplVulkan_InitInfo info{};
+ info.ApiVersion = VK_API_VERSION_1_2; info.Instance = m_instance; info.PhysicalDevice = m_physical;
+ info.Device = m_device; info.QueueFamily = m_graphics_family; info.Queue = m_graphics_queue;
+ info.DescriptorPool = m_imgui_pool; info.RenderPass = m_swapchain_rp;
+ info.MinImageCount = 2; info.ImageCount = (uint32_t)m_images.size(); info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
+ if (!ImGui_ImplVulkan_Init(&info)) { DONUT_ERROR("Vulkan RHI: ImGui_ImplVulkan_Init failed"); return; }
+ m_imgui = true;
+ DONUT_INFO("Vulkan RHI: ImGui backend initialized");
+ }
+
+ auto VulkanDevice::imgui_new_frame() -> void
+ {
+ if (!m_imgui) return;
+ ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame();
+ }
+
+ auto VulkanDevice::imgui_render(CommandList& cmds) -> void
+ {
+ if (!m_imgui) return;
+ ImGui::Render();
+ ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast<VkCommandListR&>(cmds).m_cmd);
+ }
+
+ auto VulkanDevice::shutdown() -> void
+ {
+ if (m_device == VK_NULL_HANDLE)
+ {
+ if (m_instance && m_surface) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; }
+ if (m_instance) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; }
+ return;
+ }
+ vkDeviceWaitIdle(m_device);
+ if (m_imgui) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; }
+ if (m_imgui_pool) vkDestroyDescriptorPool(m_device, m_imgui_pool, nullptr);
+ for (auto p : m_frame_pools) vkDestroyDescriptorPool(m_device, p, nullptr);
+ m_frame_pools.clear();
+ for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr);
+ for (auto s : m_image_available) vkDestroySemaphore(m_device, s, nullptr);
+ for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr);
+ m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear();
+ if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr);
+ if (m_offscreen_rp) vkDestroyRenderPass(m_device, m_offscreen_rp, nullptr);
+ if (m_swapchain_rp) vkDestroyRenderPass(m_device, m_swapchain_rp, nullptr);
+ cleanup_swapchain();
+ vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE;
+ if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
+ if (m_instance) vkDestroyInstance(m_instance, nullptr);
+ m_surface = VK_NULL_HANDLE; m_instance = VK_NULL_HANDLE;
+ }
+ }
+
+ auto create_vulkan_device() -> Scope<Device> { return create_scope<VulkanDevice>(); }
+
+ auto vulkan_prepare_glfw() -> void
+ {
+#ifdef __APPLE__
+ if (!getenv("VK_ICD_FILENAMES"))
+ setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0);
+ if (!getenv("VK_LAYER_PATH"))
+ setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0);
+ if (!getenv("DYLD_LIBRARY_PATH"))
+ setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0);
+#endif
+ glfwInitVulkanLoader(vkGetInstanceProcAddr);
+ }
+}
diff --git a/src/platform/vulkan/vulkan_device.h b/src/platform/vulkan/vulkan_device.h
new file mode 100644
index 0000000..efc2433
--- /dev/null
+++ b/src/platform/vulkan/vulkan_device.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "rendering/rhi.h"
+
+namespace Donut::RHI
+{
+ // Vulkan implementation of the RHI Device (MoltenVK on macOS). Owns the
+ // instance/device/swapchain and translates the RHI's baked pipelines +
+ // recorded command lists into native Vulkan; the geometry it draws is the
+ // shared SceneRenderer / BlackHoleRenderer, identical to every other backend.
+ auto create_vulkan_device() -> Scope<Device>;
+
+ // Must run BEFORE glfwInit() when Vulkan is the selected API: points GLFW at
+ // the loader the app links against (its own dlopen fails on macOS/Homebrew)
+ // and configures the MoltenVK ICD / layer paths.
+ auto vulkan_prepare_glfw() -> void;
+}
diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp
deleted file mode 100644
index 8b0d0c0..0000000
--- a/src/platform/vulkan/vulkan_renderer.cpp
+++ /dev/null
@@ -1,2038 +0,0 @@
-#include "vulkan_renderer.h"
-#include "core/log.h"
-#include "core/camera.h"
-#include "core/settings_manager.h"
-
-#define GLFW_INCLUDE_VULKAN
-#include <GLFW/glfw3.h>
-
-#include <imgui.h>
-#include <imgui_impl_glfw.h>
-#include <imgui_impl_vulkan.h>
-
-#include <glm/glm.hpp>
-#include <glm/gtc/matrix_transform.hpp>
-#include "stb_image.h"
-
-#include <vector>
-#include <algorithm>
-#include <cstring>
-#include <cstdlib>
-#include <fstream>
-
-namespace Donut
-{
- #define VK_CHECK(expr) \
- do { \
- VkResult _r = (expr); \
- if (_r != VK_SUCCESS) { \
- DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \
- return false; \
- } \
- } while (0)
-
- static constexpr int MAX_FRAMES_IN_FLIGHT = 2;
-
- auto vulkan_prepare_glfw() -> void
- {
-#ifdef __APPLE__
- if (!getenv("VK_ICD_FILENAMES"))
- setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0);
- if (!getenv("VK_LAYER_PATH"))
- setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0);
- if (!getenv("DYLD_LIBRARY_PATH"))
- setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0);
-#endif
- // GLFW dlopen's the Vulkan loader by bare name, which fails on
- // mac_os/Homebrew; hand it the loader entry point we already link against.
- glfwInitVulkanLoader(vkGetInstanceProcAddr);
- }
-
- struct VulkanRenderer::Impl
- {
- GLFWwindow* window = nullptr;
- int width = 0, height = 0;
- bool framebuffer_resized = false;
-
- VkInstance instance = VK_NULL_HANDLE;
- VkSurfaceKHR surface = VK_NULL_HANDLE;
- VkPhysicalDevice physical = VK_NULL_HANDLE;
- VkDevice device = VK_NULL_HANDLE;
- uint32_t graphics_family = 0, present_family = 0;
- VkQueue graphics_queue = VK_NULL_HANDLE, present_queue = VK_NULL_HANDLE;
-
- VkSwapchainKHR swapchain = VK_NULL_HANDLE;
- VkFormat swapchain_format = VK_FORMAT_B8G8R8A8_UNORM;
- VkExtent2D swapchain_extent{};
- std::vector<VkImage> images;
- std::vector<VkImageView> image_views;
- VkRenderPass render_pass = VK_NULL_HANDLE;
- std::vector<VkFramebuffer> framebuffers;
-
- VkCommandPool command_pool = VK_NULL_HANDLE;
- std::vector<VkCommandBuffer> command_buffers; // MAX_FRAMES_IN_FLIGHT
-
- std::vector<VkSemaphore> image_available; // per frame in flight
- std::vector<VkSemaphore> render_finished; // per swapchain image
- std::vector<VkFence> in_flight; // per frame in flight
- std::vector<VkFence> images_in_flight; // per swapchain image
- uint32_t current_frame = 0;
-
- VkDescriptorPool imgui_pool = VK_NULL_HANDLE;
- bool imgui_init = false;
-
- VkPhysicalDeviceMemoryProperties mem_props{};
-
- // The geodesic is rendered every frame into an offscreen image, then
- // upscaled onto the swapchain by the present pass. Progressive resolution:
- // low-res while the camera moves (keeps dragging responsive and each draw
- // well under the Metal GPU watchdog) and high-res once it settles (resolves
- // the photon ring cleanly). Both are 16:9 so the camera aspect is shared.
- static constexpr uint32_t GEO_LO_W = 480, GEO_LO_H = 270;
- static constexpr uint32_t GEO_HI_W = 960, GEO_HI_H = 540;
- struct GeoTarget
- {
- uint32_t w = 0, h = 0;
- VkImage image = VK_NULL_HANDLE;
- VkDeviceMemory mem = VK_NULL_HANDLE;
- VkImageView view = VK_NULL_HANDLE;
- VkFramebuffer fb = VK_NULL_HANDLE;
- VkDescriptorSet present_set = VK_NULL_HANDLE; // present pass samples this target
- };
- GeoTarget geo_lo, geo_hi;
- VkRenderPass geo_render_pass = VK_NULL_HANDLE; // shared (resolution-independent)
- VkBuffer cam_buf = VK_NULL_HANDLE, disk_buf = VK_NULL_HANDLE, obj_buf = VK_NULL_HANDLE, sim_buf = VK_NULL_HANDLE;
- VkDeviceMemory cam_mem = VK_NULL_HANDLE, disk_mem = VK_NULL_HANDLE, obj_mem = VK_NULL_HANDLE, sim_mem = VK_NULL_HANDLE;
- void* cam_mapped = nullptr;
- void* sim_mapped = nullptr;
- void* disk_mapped = nullptr; // disk UBO, refilled from bh_params each frame
- BlackHoleParams bh_params; // live UI-tunable disk/camera parameters
- std::string gpu_name; // physical device name for the UI
- Camera camera{ 60.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 100.0f };
- bool left_was_down = false;
- double last_frame_time = 0.0; // for free-fly dt
- double fps_last_x = 0.0, fps_last_y = 0.0; // cursor tracking for free-fly look
- bool user_moving = false; // camera moved/looked this frame (drives step count)
- VkImage cube_image = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE;
- VkImageView cube_view = VK_NULL_HANDLE; VkSampler cube_sampler = VK_NULL_HANDLE;
- std::string hdri_path; // path backing the current cubemap (UI display + no-op switch guard)
- VkDescriptorSetLayout geo_set_layout = VK_NULL_HANDLE;
- VkDescriptorPool geo_pool = VK_NULL_HANDLE;
- VkDescriptorSet geo_set = VK_NULL_HANDLE;
- VkPipelineLayout geo_pipeline_layout = VK_NULL_HANDLE;
- VkPipeline geo_pipeline = VK_NULL_HANDLE;
- VkBuffer quad_vb = VK_NULL_HANDLE; VkDeviceMemory quad_vb_mem = VK_NULL_HANDLE;
- double start_time = 0.0;
- VkFence geo_in_use = VK_NULL_HANDLE; // previous frame's fence; guards the shared geodesic image
-
- // Present pass: samples the geodesic image with a full-screen textured
- // quad, drawn into the swapchain render pass just before the ImGui UI.
- VkSampler present_sampler = VK_NULL_HANDLE;
- VkDescriptorSetLayout present_set_layout = VK_NULL_HANDLE;
- VkDescriptorPool present_pool = VK_NULL_HANDLE;
- VkPipelineLayout present_pipeline_layout = VK_NULL_HANDLE;
- VkPipeline present_pipeline = VK_NULL_HANDLE;
-
- // World-builder scene view (grid now; sphere/skybox next). Normal-scale
- // orbital camera; geometry drawn straight into the swapchain render pass.
- bool scene_mode = false;
- Camera scene_camera{ 45.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 1000.0f };
- VkBuffer grid_vb = VK_NULL_HANDLE; VkDeviceMemory grid_vb_mem = VK_NULL_HANDLE;
- uint32_t grid_vertex_count = 0;
- VkBuffer grid_ubo = VK_NULL_HANDLE; VkDeviceMemory grid_ubo_mem = VK_NULL_HANDLE;
- void* grid_ubo_mapped = nullptr;
- VkDescriptorSetLayout grid_set_layout = VK_NULL_HANDLE;
- VkDescriptorPool grid_pool = VK_NULL_HANDLE;
- VkDescriptorSet grid_set = VK_NULL_HANDLE;
- VkPipelineLayout grid_pipeline_layout = VK_NULL_HANDLE;
- VkPipeline grid_pipeline = VK_NULL_HANDLE;
-
- // Scene color+depth pass into the swapchain (the grid is coplanar, but the
- // sphere needs real depth). Depth image + framebuffers track the swapchain.
- VkImage scene_depth_image = VK_NULL_HANDLE; VkDeviceMemory scene_depth_mem = VK_NULL_HANDLE;
- VkImageView scene_depth_view = VK_NULL_HANDLE;
- VkRenderPass scene_render_pass = VK_NULL_HANDLE;
- std::vector<VkFramebuffer> scene_framebuffers;
-
- // Lit sphere (Sphere.slang): pos+normal indexed mesh; samples the HDRI
- // cubemap for ambient at binding 1 (shares the geodesic cube).
- VkBuffer sphere_vb = VK_NULL_HANDLE; VkDeviceMemory sphere_vb_mem = VK_NULL_HANDLE;
- VkBuffer sphere_ib = VK_NULL_HANDLE; VkDeviceMemory sphere_ib_mem = VK_NULL_HANDLE;
- uint32_t sphere_index_count = 0;
- static constexpr uint32_t MAX_SCENE_OBJECTS = 64;
- std::vector<SceneObject> scene_objects{ SceneObject{} }; // one default sphere
- int selected_object = 0;
- VkDeviceSize sphere_ubo_stride = 0; // aligned per-object UBO slot (dynamic offset)
- VkBuffer sphere_ubo = VK_NULL_HANDLE; VkDeviceMemory sphere_ubo_mem = VK_NULL_HANDLE;
- void* sphere_ubo_mapped = nullptr;
- VkDescriptorSetLayout sphere_set_layout = VK_NULL_HANDLE;
- VkDescriptorPool sphere_pool = VK_NULL_HANDLE;
- VkDescriptorSet sphere_set = VK_NULL_HANDLE;
- VkPipelineLayout sphere_pipeline_layout = VK_NULL_HANDLE;
- VkPipeline sphere_pipeline = VK_NULL_HANDLE;
-
- // Skybox (Skybox.slang): a cube sampling the HDRI cubemap at the far plane
- // (pos.xyww -> depth 1), drawn first as the scene background.
- VkBuffer skybox_vb = VK_NULL_HANDLE; VkDeviceMemory skybox_vb_mem = VK_NULL_HANDLE;
- VkBuffer skybox_ubo = VK_NULL_HANDLE; VkDeviceMemory skybox_ubo_mem = VK_NULL_HANDLE;
- void* skybox_ubo_mapped = nullptr;
- VkDescriptorSetLayout skybox_set_layout = VK_NULL_HANDLE;
- VkDescriptorPool skybox_pool = VK_NULL_HANDLE;
- VkDescriptorSet skybox_set = VK_NULL_HANDLE;
- VkPipelineLayout skybox_pipeline_layout = VK_NULL_HANDLE;
- VkPipeline skybox_pipeline = VK_NULL_HANDLE;
-
- auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t;
- auto create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool;
- static auto load_spirv(const std::string& path) -> std::vector<uint32_t>;
- auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool;
-
- auto create_instance() -> bool;
- auto pick_physical_and_device() -> bool;
- auto create_swapchain() -> bool;
- auto create_image_views() -> bool;
- auto create_render_pass() -> bool;
- auto create_framebuffers() -> bool;
- auto create_command_buffers() -> bool;
- auto create_sync_objects() -> bool;
- auto create_geodesic_resources() -> bool;
- auto create_geo_target(GeoTarget& target, uint32_t w, uint32_t h) -> bool;
- auto create_hdri_cubemap(const char* path) -> bool;
- auto rebuild_hdri_cubemap(const char* path) -> void;
- auto create_present_resources() -> bool;
- auto create_scene_resources() -> bool;
- auto create_scene_targets() -> bool;
- auto destroy_scene_targets() -> void;
- auto process_input() -> void;
- auto update_geodesic_uniforms() -> void;
- auto update_scene_uniforms() -> void;
- auto destroy_geodesic_resources() -> void;
- auto recreate_swapchain() -> bool;
- auto cleanup_swapchain() -> void;
- auto record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool;
- };
-
- auto VulkanRenderer::Impl::create_instance() -> bool
- {
-#ifdef __APPLE__
- if (!getenv("VK_ICD_FILENAMES"))
- setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0);
- if (!getenv("VK_LAYER_PATH"))
- setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0);
- // The Homebrew validation-layer manifest names the dylib without a path;
- // let dlopen find it in the Homebrew lib dir.
- if (!getenv("DYLD_LIBRARY_PATH"))
- setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0);
-#endif
- VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO };
- app.pApplicationName = "Donut";
- app.apiVersion = VK_API_VERSION_1_2;
-
- uint32_t glfwExtCount = 0;
- const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount);
- if (!glfwExts) { DONUT_ERROR("Vulkan: GLFW reports no surface support"); return false; }
- std::vector<const char*> exts;
- for (uint32_t i = 0; i < glfwExtCount; ++i) exts.push_back(glfwExts[i]);
- exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
- exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
-
- std::vector<const char*> layers;
- uint32_t layer_count = 0;
- vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
- std::vector<VkLayerProperties> avail(layer_count);
- vkEnumerateInstanceLayerProperties(&layer_count, avail.data());
- for (const auto& l : avail)
- if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0)
- layers.push_back("VK_LAYER_KHRONOS_validation");
-
- VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
- ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
- ici.pApplicationInfo = &app;
- ici.enabledExtensionCount = (uint32_t)exts.size();
- ici.ppEnabledExtensionNames = exts.data();
- ici.enabledLayerCount = (uint32_t)layers.size();
- ici.ppEnabledLayerNames = layers.data();
-
- VkResult r = vkCreateInstance(&ici, nullptr, &instance);
- if (r != VK_SUCCESS && !layers.empty())
- {
- // The validation layer failed to load (its dylib isn't on the loader
- // search path); it is optional, so retry without it.
- DONUT_WARN("Vulkan: validation layer unavailable, continuing without it");
- layers.clear();
- ici.enabledLayerCount = 0;
- ici.ppEnabledLayerNames = nullptr;
- r = vkCreateInstance(&ici, nullptr, &instance);
- }
- if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan: vkCreateInstance failed ({})", (int)r); return false; }
-
- VK_CHECK(glfwCreateWindowSurface(instance, window, nullptr, &surface));
- DONUT_INFO("Vulkan: instance + surface created (validation {})", layers.empty() ? "off" : "on");
- return true;
- }
-
- auto VulkanRenderer::Impl::pick_physical_and_device() -> bool
- {
- uint32_t count = 0;
- vkEnumeratePhysicalDevices(instance, &count, nullptr);
- if (count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; }
- std::vector<VkPhysicalDevice> devices(count);
- vkEnumeratePhysicalDevices(instance, &count, devices.data());
- physical = devices[0];
-
- uint32_t q_count = 0;
- vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, nullptr);
- std::vector<VkQueueFamilyProperties> qfams(q_count);
- vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, qfams.data());
- bool found_g = false, found_p = false;
- for (uint32_t i = 0; i < q_count; ++i)
- {
- if (!found_g && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { graphics_family = i; found_g = true; }
- VkBool32 present = VK_FALSE;
- vkGetPhysicalDeviceSurfaceSupportKHR(physical, i, surface, &present);
- if (!found_p && present) { present_family = i; found_p = true; }
- }
- if (!found_g || !found_p) { DONUT_ERROR("Vulkan: no graphics/present queue"); return false; }
-
- std::vector<const char*> dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
- uint32_t dev_ext_count = 0;
- vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, nullptr);
- std::vector<VkExtensionProperties> dev_ext_props(dev_ext_count);
- vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, dev_ext_props.data());
- for (const auto& e : dev_ext_props)
- if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0)
- dev_exts.push_back("VK_KHR_portability_subset");
-
- float priority = 1.0f;
- std::vector<VkDeviceQueueCreateInfo> qcis;
- uint32_t families[2] = { graphics_family, present_family };
- for (uint32_t i = 0; i < (graphics_family == present_family ? 1u : 2u); ++i)
- {
- VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
- qci.queueFamilyIndex = families[i];
- qci.queueCount = 1;
- qci.pQueuePriorities = &priority;
- qcis.push_back(qci);
- }
- VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
- dci.queueCreateInfoCount = (uint32_t)qcis.size();
- dci.pQueueCreateInfos = qcis.data();
- dci.enabledExtensionCount = (uint32_t)dev_exts.size();
- dci.ppEnabledExtensionNames = dev_exts.data();
- VK_CHECK(vkCreateDevice(physical, &dci, nullptr, &device));
- vkGetDeviceQueue(device, graphics_family, 0, &graphics_queue);
- vkGetDeviceQueue(device, present_family, 0, &present_queue);
-
- VkPhysicalDeviceProperties props{};
- vkGetPhysicalDeviceProperties(physical, &props);
- vkGetPhysicalDeviceMemoryProperties(physical, &mem_props);
- gpu_name = props.deviceName;
- DONUT_INFO("Vulkan device: {}", gpu_name);
- return true;
- }
-
- auto VulkanRenderer::Impl::create_swapchain() -> bool
- {
- VkSurfaceCapabilitiesKHR caps{};
- vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps);
-
- uint32_t fmt_count = 0;
- vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, nullptr);
- std::vector<VkSurfaceFormatKHR> formats(fmt_count);
- vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, formats.data());
- VkSurfaceFormatKHR chosen = formats[0];
- for (const auto& f : formats)
- if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
- chosen = f;
- swapchain_format = chosen.format;
-
- if (caps.currentExtent.width != UINT32_MAX)
- swapchain_extent = caps.currentExtent;
- else
- {
- swapchain_extent.width = std::clamp((uint32_t)width, caps.minImageExtent.width, caps.maxImageExtent.width);
- swapchain_extent.height = std::clamp((uint32_t)height, caps.minImageExtent.height, caps.maxImageExtent.height);
- }
-
- uint32_t image_count = caps.minImageCount + 1;
- if (caps.maxImageCount > 0 && image_count > caps.maxImageCount)
- image_count = caps.maxImageCount;
-
- VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
- sci.surface = surface;
- sci.minImageCount = image_count;
- sci.imageFormat = chosen.format;
- sci.imageColorSpace = chosen.colorSpace;
- sci.imageExtent = swapchain_extent;
- sci.imageArrayLayers = 1;
- sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
- sci.preTransform = caps.currentTransform;
- sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
- sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; // always supported, vsync
- sci.clipped = VK_TRUE;
-
- uint32_t fam_idx[2] = { graphics_family, present_family };
- if (graphics_family != present_family)
- {
- sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
- sci.queueFamilyIndexCount = 2;
- sci.pQueueFamilyIndices = fam_idx;
- }
- else
- sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
-
- VK_CHECK(vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain));
- uint32_t n = 0;
- vkGetSwapchainImagesKHR(device, swapchain, &n, nullptr);
- images.resize(n);
- vkGetSwapchainImagesKHR(device, swapchain, &n, images.data());
- return true;
- }
-
- auto VulkanRenderer::Impl::create_image_views() -> bool
- {
- image_views.resize(images.size());
- for (size_t i = 0; i < images.size(); ++i)
- {
- VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = images[i];
- vci.viewType = VK_IMAGE_VIEW_TYPE_2D;
- vci.format = swapchain_format;
- vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(device, &vci, nullptr, &image_views[i]));
- }
- return true;
- }
-
- auto VulkanRenderer::Impl::create_render_pass() -> bool
- {
- VkAttachmentDescription color{};
- color.format = swapchain_format;
- color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
- color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
- color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
- color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
-
- VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{};
- subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1;
- subpass.pColorAttachments = &ref;
-
- VkSubpassDependency dep{};
- dep.srcSubpass = VK_SUBPASS_EXTERNAL;
- dep.dstSubpass = 0;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
- dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
-
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color;
- rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &render_pass));
- return true;
- }
-
- auto VulkanRenderer::Impl::create_framebuffers() -> bool
- {
- framebuffers.resize(image_views.size());
- for (size_t i = 0; i < image_views.size(); ++i)
- {
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = render_pass;
- fbci.attachmentCount = 1; fbci.pAttachments = &image_views[i];
- fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i]));
- }
- return true;
- }
-
- // Depth image + a color+depth render pass + per-image framebuffers for the
- // scene view. Swapchain-sized, so recreated alongside the swapchain.
- auto VulkanRenderer::Impl::create_scene_targets() -> bool
- {
- const VkFormat depth_fmt = VK_FORMAT_D32_SFLOAT;
-
- VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- dici.imageType = VK_IMAGE_TYPE_2D; dici.format = depth_fmt;
- dici.extent = { swapchain_extent.width, swapchain_extent.height, 1 };
- dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT;
- dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
- VK_CHECK(vkCreateImage(device, &dici, nullptr, &scene_depth_image));
- VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(device, scene_depth_image, &dreq);
- VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(device, &dai, nullptr, &scene_depth_mem));
- VK_CHECK(vkBindImageMemory(device, scene_depth_image, scene_depth_mem, 0));
- VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- dvci.image = scene_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = depth_fmt;
- dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(device, &dvci, nullptr, &scene_depth_view));
-
- VkAttachmentDescription atts[2]{};
- atts[0].format = swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT;
- atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
- atts[1].format = depth_fmt; atts[1].samples = VK_SAMPLE_COUNT_1_BIT;
- atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
- VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref;
- VkSubpassDependency dep{};
- dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
- dep.srcAccessMask = 0;
- dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &scene_render_pass));
-
- scene_framebuffers.resize(image_views.size());
- for (size_t i = 0; i < image_views.size(); ++i)
- {
- VkImageView att[2] = { image_views[i], scene_depth_view };
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = scene_render_pass; fbci.attachmentCount = 2; fbci.pAttachments = att;
- fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &scene_framebuffers[i]));
- }
- return true;
- }
-
- auto VulkanRenderer::Impl::destroy_scene_targets() -> void
- {
- for (auto fb : scene_framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
- scene_framebuffers.clear();
- if (scene_render_pass) { vkDestroyRenderPass(device, scene_render_pass, nullptr); scene_render_pass = VK_NULL_HANDLE; }
- if (scene_depth_view) { vkDestroyImageView(device, scene_depth_view, nullptr); scene_depth_view = VK_NULL_HANDLE; }
- if (scene_depth_image) { vkDestroyImage(device, scene_depth_image, nullptr); scene_depth_image = VK_NULL_HANDLE; }
- if (scene_depth_mem) { vkFreeMemory(device, scene_depth_mem, nullptr); scene_depth_mem = VK_NULL_HANDLE; }
- }
-
- auto VulkanRenderer::Impl::create_command_buffers() -> bool
- {
- VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
- pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
- pci.queueFamilyIndex = graphics_family;
- VK_CHECK(vkCreateCommandPool(device, &pci, nullptr, &command_pool));
-
- command_buffers.resize(MAX_FRAMES_IN_FLIGHT);
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
- cbai.commandPool = command_pool;
- cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
- cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT;
- VK_CHECK(vkAllocateCommandBuffers(device, &cbai, command_buffers.data()));
- return true;
- }
-
- auto VulkanRenderer::Impl::create_sync_objects() -> bool
- {
- image_available.resize(MAX_FRAMES_IN_FLIGHT);
- in_flight.resize(MAX_FRAMES_IN_FLIGHT);
- render_finished.resize(images.size());
- images_in_flight.assign(images.size(), VK_NULL_HANDLE);
-
- VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
- VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
- fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
- for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i)
- {
- VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &image_available[i]));
- VK_CHECK(vkCreateFence(device, &fci, nullptr, &in_flight[i]));
- }
- for (size_t i = 0; i < images.size(); ++i)
- VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &render_finished[i]));
- return true;
- }
-
- auto VulkanRenderer::Impl::find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t
- {
- for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i)
- if ((type_filter & (1u << i)) && (mem_props.memoryTypes[i].propertyFlags & flags) == flags)
- return i;
- return UINT32_MAX;
- }
-
- auto VulkanRenderer::Impl::create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props,
- VkBuffer& buf, VkDeviceMemory& mem) const -> bool
- {
- VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
- bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
- if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false;
- VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, buf, &req);
- VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- ai.allocationSize = req.size;
- ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props);
- if (vkAllocateMemory(device, &ai, nullptr, &mem) != VK_SUCCESS) return false;
- vkBindBufferMemory(device, buf, mem, 0);
- return true;
- }
-
- auto VulkanRenderer::Impl::load_spirv(const std::string& path) -> std::vector<uint32_t>
- {
- std::ifstream file(path, std::ios::ate | std::ios::binary);
- if (!file.is_open()) return {};
- size_t size = (size_t)file.tellg();
- std::vector<uint32_t> data(size / 4);
- file.seekg(0);
- file.read(reinterpret_cast<char*>(data.data()), size);
- return data;
- }
-
- auto VulkanRenderer::Impl::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool
- {
- auto spv = load_spirv(path);
- if (spv.empty()) { DONUT_ERROR("Vulkan: failed to load SPIR-V {}", path); return false; }
- VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
- ci.codeSize = spv.size() * 4; ci.pCode = spv.data();
- return vkCreateShaderModule(device, &ci, nullptr, &out) == VK_SUCCESS;
- }
-
- // Creates one geodesic render target (image + memory + view + framebuffer)
- // at w x h against the shared geo_render_pass. Used for the low- and high-res
- // progressive targets.
- auto VulkanRenderer::Impl::create_geo_target(GeoTarget& t, uint32_t w, uint32_t h) -> bool
- {
- const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
- t.w = w; t.h = h;
-
- VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { w, h, 1 };
- ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT;
- ici.tiling = VK_IMAGE_TILING_OPTIMAL;
- ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
- VK_CHECK(vkCreateImage(device, &ici, nullptr, &t.image));
- VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(device, t.image, &im_req);
- VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- im_alloc.allocationSize = im_req.size;
- im_alloc.memoryTypeIndex = find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(device, &im_alloc, nullptr, &t.mem));
- VK_CHECK(vkBindImageMemory(device, t.image, t.mem, 0));
- VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- vci.image = t.image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt;
- vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(device, &vci, nullptr, &t.view));
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = geo_render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &t.view;
- fbci.width = w; fbci.height = h; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &t.fb));
- return true;
- }
-
- auto VulkanRenderer::Impl::create_geodesic_resources() -> bool
- {
- const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
- const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
- const float SagA_rs = 1.269e10f;
-
- VkAttachmentDescription color{};
- color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
- subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref;
- VkSubpassDependency deps[2]{};
- deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0;
- deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
- deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
- deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color;
- rpci.subpassCount = 1; rpci.pSubpasses = &subpass;
- rpci.dependencyCount = 2; rpci.pDependencies = deps;
- VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &geo_render_pass));
-
- if (!create_geo_target(geo_lo, GEO_LO_W, GEO_LO_H)) return false;
- if (!create_geo_target(geo_hi, GEO_HI_W, GEO_HI_H)) return false;
-
- create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem);
- create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem);
- create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem);
- create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem);
-
- camera.set_camera_mode(CameraMode::Orbital);
- camera.set_orbital_target(glm::vec3(0.0f));
- camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk
- camera.set_orbital_limits(2.2e11, 1.5e12);
- camera.set_orbital_speed(0.01f);
- camera.set_zoom_speed(3e10);
- camera.set_azimuth(0.0f);
- camera.set_elevation(1.25f);
-
- void* p = nullptr;
- vkMapMemory(device, cam_mem, 0, 128, 0, &cam_mapped); // camera UBO refilled every frame
- vkMapMemory(device, disk_mem, 0, 32, 0, &disk_mapped); // disk UBO refilled from bh_params every frame
-
- std::vector<uint8_t> obj_data(800, 0);
- int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4);
- float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16);
- float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16);
- vkMapMemory(device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(device, obj_mem);
-
- vkMapMemory(device, sim_mem, 0, 16, 0, &sim_mapped);
-
- if (!create_hdri_cubemap("assets/hdri/HDR_blue_nebulae-1.hdr")) return false;
-
- VkDescriptorSetLayoutBinding binds[5]{};
- for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; }
- binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
- dslci.bindingCount = 5; dslci.pBindings = binds;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &geo_set_layout));
- VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } };
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
- dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes;
- VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &geo_pool));
- VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
- dsai.descriptorPool = geo_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &geo_set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &geo_set));
- VkDescriptorBufferInfo bi[4] = { { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } };
- VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet writes[5]{};
- for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = geo_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; }
- writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = geo_set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = &cube_info;
- vkUpdateDescriptorSets(device, 5, writes, 0, nullptr);
-
- float quad[] = {
- -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f,
- -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f,
- };
- create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, quad_vb, quad_vb_mem);
- vkMapMemory(device, quad_vb_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(device, quad_vb_mem);
-
- VkShaderModule vmod, fmod;
- if (!create_shader_module("assets/shaders/generated/Geodesic.vertexMain.spv", vmod)) return false;
- if (!create_shader_module("assets/shaders/generated/Geodesic.fragmentMain.spv", fmod)) return false;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
- plci.setLayoutCount = 1; plci.pSetLayouts = &geo_set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &geo_pipeline_layout));
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
- VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } };
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib;
- vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via;
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- // Dynamic viewport/scissor: the same pipeline renders into either the
- // low- or high-res target, sized per frame in record_command_buffer.
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1;
- VkDynamicState dyn_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
- VkPipelineDynamicStateCreateInfo dyn{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dyn.dynamicStateCount = 2; dyn.pDynamicStates = dyn_states;
- VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba;
- VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- gpci.stageCount = 2; gpci.pStages = stages;
- gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
- gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb;
- gpci.pDynamicState = &dyn;
- gpci.layout = geo_pipeline_layout; gpci.renderPass = geo_render_pass; gpci.subpass = 0;
- VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &geo_pipeline);
- vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr);
- if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: geodesic pipeline creation failed ({})", (int)pr); return false; }
-
- start_time = glfwGetTime();
- update_geodesic_uniforms();
- DONUT_INFO("Vulkan: geodesic resources ready ({}x{} moving / {}x{} settled)",
- (int)GEO_LO_W, (int)GEO_LO_H, (int)GEO_HI_W, (int)GEO_HI_H);
- return true;
- }
-
- auto VulkanRenderer::Impl::create_hdri_cubemap(const char* path) -> bool
- {
- hdri_path = path;
- const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
- const uint32_t FACE = 1024;
- const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT;
- uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; // full chain (11 for 1024)
-
- VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
- cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 };
- cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT;
- cci.tiling = VK_IMAGE_TILING_OPTIMAL;
- // COLOR_ATTACHMENT renders the 6 faces (mip 0); TRANSFER_SRC/DST build the
- // mip chain by blitting; SAMPLED for shader reads (incl. mip-LOD sampling).
- cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT
- | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
- VK_CHECK(vkCreateImage(device, &cci, nullptr, &cube_image));
- VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(device, cube_image, &creq);
- VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(device, &cai, nullptr, &cube_mem));
- VK_CHECK(vkBindImageMemory(device, cube_image, cube_mem, 0));
- VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- cvci.image = cube_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt;
- cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 };
- VK_CHECK(vkCreateImageView(device, &cvci, nullptr, &cube_view));
- VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
- csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR;
- csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS;
- csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
- VK_CHECK(vkCreateSampler(device, &csm, nullptr, &cube_sampler));
-
- int w = 0, h = 0, ch = 0;
- float* pixels = stbi_loadf(path, &w, &h, &ch, 4);
- if (!pixels)
- {
- DONUT_WARN("Vulkan: HDRI '{}' could not be loaded; using a dark background", path);
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
- cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
- VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd));
- VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
- vkBeginCommandBuffer(cmd, &bi);
- VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
- tb.image = cube_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 };
- tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb);
- VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f;
- VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 };
- vkCmdClearColorImage(cmd, cube_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng);
- VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb);
- vkEndCommandBuffer(cmd);
- VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
- vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(graphics_queue);
- vkFreeCommandBuffers(device, command_pool, 1, &cmd);
- return true;
- }
-
- // Apple GPUs can't linearly filter RGBA32F, so store the equirect as
- // RGBA16F (convert the loaded floats to half on the way into staging).
- const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT;
- size_t texel_count = (size_t)w * h * 4;
- VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t);
- VkBuffer eq_staging; VkDeviceMemory eq_staging_mem;
- if (!create_buffer(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem)) { stbi_image_free(pixels); return false; }
- void* mp = nullptr; vkMapMemory(device, eq_staging_mem, 0, eq_size, 0, &mp);
- uint16_t* dst = (uint16_t*)mp;
- for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; memcpy(&dst[i], &hf, sizeof(uint16_t)); }
- vkUnmapMemory(device, eq_staging_mem);
- stbi_image_free(pixels);
-
- VkImage eq_image; VkDeviceMemory eq_mem;
- VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
- eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 };
- eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT;
- eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
- VK_CHECK(vkCreateImage(device, &eci, nullptr, &eq_image));
- VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(device, eq_image, &ereq);
- VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
- eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
- VK_CHECK(vkAllocateMemory(device, &eai, nullptr, &eq_mem));
- VK_CHECK(vkBindImageMemory(device, eq_image, eq_mem, 0));
- VkImageView eq_view;
- VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt;
- evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- VK_CHECK(vkCreateImageView(device, &evci, nullptr, &eq_view));
- VkSampler eq_sampler;
- VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
- esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR;
- esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; // longitude wraps
- esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; // latitude clamps
- esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
- VK_CHECK(vkCreateSampler(device, &esm, nullptr, &eq_sampler));
-
- VkImageView face_views[6];
- for (uint32_t i = 0; i < 6; ++i)
- {
- VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
- fvci.image = cube_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt;
- fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 };
- VK_CHECK(vkCreateImageView(device, &fvci, nullptr, &face_views[i]));
- }
-
- VkAttachmentDescription color{};
- color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT;
- color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
- color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
- color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
- VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref;
- VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL;
- dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
- VkRenderPass rp;
- VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
- rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep;
- VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &rp));
- VkFramebuffer face_fb[6];
- for (uint32_t i = 0; i < 6; ++i)
- {
- VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
- fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1;
- VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &face_fb[i]));
- }
-
- VkDescriptorSetLayoutBinding binds[2]{};
- binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
- binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayout set_layout;
- VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &set_layout));
- VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } };
- VkDescriptorPool pool;
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes;
- VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &pool));
-
- VkShaderModule vmod, fmod;
- if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.vertexMain.spv", vmod)) return false;
- if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.fragmentMain.spv", fmod)) return false;
- VkPipelineLayout playout;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &playout));
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
- VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 };
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via;
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } };
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &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;
- VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline);
- vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr);
- if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: equirect pipeline failed ({})", (int)pr); return false; }
-
- float cube_verts[] = {
- -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1,
- -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1,
- 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1,
- -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1,
- -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1,
- -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1,
- };
- VkBuffer cube_vb; VkDeviceMemory cube_vb_mem;
- create_buffer(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem);
- vkMapMemory(device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(device, cube_vb_mem);
-
- glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
- proj[1][1] *= -1.0f; // Vulkan clip space is Y-down vs OpenGL
- glm::mat4 views[6] = {
- glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)),
- glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)),
- glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)),
- glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)),
- glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)),
- glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)),
- };
- VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6];
- for (uint32_t i = 0; i < 6; ++i)
- {
- create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]);
- glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; // SPIR-V expects row-major
- vkMapMemory(device, ubo_mem[i], 0, 128, 0, &mp); memcpy(mp, mats, 128); vkUnmapMemory(device, ubo_mem[i]);
- VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &sets[i]));
- VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE };
- VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet ws[2]{};
- ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info;
- ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info;
- vkUpdateDescriptorSets(device, 2, ws, 0, nullptr);
- }
-
- VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
- cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
- VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd));
- VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
- VK_CHECK(vkBeginCommandBuffer(cmd, &bi));
- VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
- to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
- to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst);
- VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 };
- vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &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);
- }
-
- // Build the mip chain (all 6 faces) by successive linear down-blits, so
- // divergence-based LOD sampling can read a blurred sky for lensed rays.
- // Mip 0 (every layer) is SHADER_READ_ONLY from the render passes above.
- {
- VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- src0.image = cube_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 };
- src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
- src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0);
-
- int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE;
- for (uint32_t m = 1; m < CUBE_MIPS; ++m)
- {
- int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1;
- VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- bd.image = cube_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 };
- bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
- bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd);
-
- VkImageBlit blit{};
- blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 };
- blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 };
- vkCmdBlitImage(cmd, cube_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
- cube_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
-
- VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- bs.image = cube_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 };
- bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
- bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs);
- mipW = nW; mipH = nH;
- }
- VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
- fin.image = cube_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 };
- fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
- fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
- vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin);
- }
-
- VK_CHECK(vkEndCommandBuffer(cmd));
- VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
- VK_CHECK(vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE));
- VK_CHECK(vkQueueWaitIdle(graphics_queue));
-
- vkFreeCommandBuffers(device, command_pool, 1, &cmd);
- for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(device, ubo[i], nullptr); vkFreeMemory(device, ubo_mem[i], nullptr); vkDestroyFramebuffer(device, face_fb[i], nullptr); vkDestroyImageView(device, face_views[i], nullptr); }
- vkDestroyBuffer(device, cube_vb, nullptr); vkFreeMemory(device, cube_vb_mem, nullptr);
- vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, playout, nullptr);
- vkDestroyDescriptorPool(device, pool, nullptr); vkDestroyDescriptorSetLayout(device, set_layout, nullptr);
- vkDestroyRenderPass(device, rp, nullptr);
- vkDestroySampler(device, eq_sampler, nullptr); vkDestroyImageView(device, eq_view, nullptr);
- vkDestroyImage(device, eq_image, nullptr); vkFreeMemory(device, eq_mem, nullptr);
- vkDestroyBuffer(device, eq_staging, nullptr); vkFreeMemory(device, eq_staging_mem, nullptr);
- DONUT_INFO("Vulkan: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE);
- return true;
- }
-
- // Runtime HDRI switch: drain the device, tear down the old cubemap, build the
- // new one, and repoint the geodesic set's samplerCube (binding 4) at it.
- auto VulkanRenderer::Impl::rebuild_hdri_cubemap(const char* path) -> void
- {
- vkDeviceWaitIdle(device);
-
- if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr);
- if (cube_view) vkDestroyImageView(device, cube_view, nullptr);
- if (cube_image) vkDestroyImage(device, cube_image, nullptr);
- if (cube_mem) vkFreeMemory(device, cube_mem, nullptr);
- cube_sampler = VK_NULL_HANDLE; cube_view = VK_NULL_HANDLE;
- cube_image = VK_NULL_HANDLE; cube_mem = VK_NULL_HANDLE;
-
- create_hdri_cubemap(path);
-
- VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
- write.dstSet = geo_set; write.dstBinding = 4; write.descriptorCount = 1;
- write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &cube_info;
- vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
-
- // The scene sphere samples the same cube (binding 1) — repoint it too.
- if (sphere_set)
- {
- VkWriteDescriptorSet sw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
- sw.dstSet = sphere_set; sw.dstBinding = 1; sw.descriptorCount = 1;
- sw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw.pImageInfo = &cube_info;
- vkUpdateDescriptorSets(device, 1, &sw, 0, nullptr);
- }
- if (skybox_set)
- {
- VkWriteDescriptorSet kw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
- kw.dstSet = skybox_set; kw.dstBinding = 1; kw.descriptorCount = 1;
- kw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw.pImageInfo = &cube_info;
- vkUpdateDescriptorSets(device, 1, &kw, 0, nullptr);
- }
- }
-
- auto VulkanRenderer::Impl::create_present_resources() -> bool
- {
- VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
- smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR;
- smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
- VK_CHECK(vkCreateSampler(device, &smci, nullptr, &present_sampler));
-
- VkDescriptorSetLayoutBinding bind{}; bind.binding = 0; bind.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bind.descriptorCount = 1; bind.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
- dslci.bindingCount = 1; dslci.pBindings = &bind;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &present_set_layout));
- // One present set per geodesic target (low-/high-res), each sampling its
- // own image; record_command_buffer binds whichever was rendered this frame.
- VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 };
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
- dpci.maxSets = 2; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize;
- VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &present_pool));
- for (GeoTarget* t : { &geo_lo, &geo_hi })
- {
- VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
- dsai.descriptorPool = present_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &present_set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &t->present_set));
- VkDescriptorImageInfo ii{ present_sampler, t->view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
- write.dstSet = t->present_set; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &ii;
- vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
- }
-
- VkShaderModule vmod, fmod;
- if (!create_shader_module("assets/shaders/generated/TexturedQuad.vertexMain.spv", vmod)) return false;
- if (!create_shader_module("assets/shaders/generated/TexturedQuad.fragmentMain.spv", fmod)) return false;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
- plci.setLayoutCount = 1; plci.pSetLayouts = &present_set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &present_pipeline_layout));
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
- VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } };
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib;
- vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via;
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1;
- VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
- VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn;
- VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba;
- VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- gpci.stageCount = 2; gpci.pStages = stages;
- gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
- gpci.pDynamicState = &dsci;
- gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb;
- gpci.layout = present_pipeline_layout; gpci.renderPass = render_pass; gpci.subpass = 0;
- VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &present_pipeline);
- vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr);
- if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: present pipeline creation failed ({})", (int)pr); return false; }
- DONUT_INFO("Vulkan: present pipeline ready");
- return true;
- }
-
- auto VulkanRenderer::Impl::create_scene_resources() -> bool
- {
- const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
-
- // Scene camera: normal-scale orbital viewer (matches the OpenGL world-builder).
- scene_camera.set_camera_mode(CameraMode::Orbital);
- scene_camera.set_orbital_target(glm::vec3(0.0f));
- scene_camera.set_orbital_radius(15.0);
- scene_camera.set_orbital_limits(2.0, 200.0);
- scene_camera.set_orbital_speed(0.01f);
- scene_camera.set_zoom_speed(2.0);
- scene_camera.set_azimuth(0.0f);
- scene_camera.set_elevation((float)std::numbers::pi / 3.0f);
- scene_camera.update_orbital();
-
- // Line grid on the XZ plane (+/-50 units, 1-unit cells). Grid.slang scales
- // the position by u_GridSize/50, so u_GridSize = 50 keeps it 1:1.
- std::vector<glm::vec3> lines;
- const int N = 50;
- for (int i = -N; i <= N; ++i)
- {
- lines.push_back({ (float)i, 0.0f, (float)-N });
- lines.push_back({ (float)i, 0.0f, (float) N });
- lines.push_back({ (float)-N, 0.0f, (float)i });
- lines.push_back({ (float) N, 0.0f, (float)i });
- }
- grid_vertex_count = (uint32_t)lines.size();
- VkDeviceSize vbsize = lines.size() * sizeof(glm::vec3);
- if (!create_buffer(vbsize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, grid_vb, grid_vb_mem)) return false;
- void* mp = nullptr; vkMapMemory(device, grid_vb_mem, 0, vbsize, 0, &mp); memcpy(mp, lines.data(), vbsize); vkUnmapMemory(device, grid_vb_mem);
-
- // $Globals UBO (set 0, binding 0), std140, 176 bytes; refilled each frame.
- if (!create_buffer(176, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, grid_ubo, grid_ubo_mem)) return false;
- vkMapMemory(device, grid_ubo_mem, 0, 176, 0, &grid_ubo_mapped);
-
- VkDescriptorSetLayoutBinding b{}; b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 1; dslci.pBindings = &b;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &grid_set_layout));
- VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 };
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 1; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize;
- VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &grid_pool));
- VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = grid_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &grid_set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &grid_set));
- VkDescriptorBufferInfo bi{ grid_ubo, 0, VK_WHOLE_SIZE };
- VkWriteDescriptorSet w{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; w.dstSet = grid_set; w.dstBinding = 0; w.descriptorCount = 1; w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &bi;
- vkUpdateDescriptorSets(device, 1, &w, 0, nullptr);
-
- VkShaderModule vmod, fmod;
- if (!create_shader_module("assets/shaders/generated/Grid.vertexMain.spv", vmod)) return false;
- if (!create_shader_module("assets/shaders/generated/Grid.fragmentMain.spv", fmod)) return false;
- VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &grid_set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &grid_pipeline_layout));
- VkPipelineShaderStageCreateInfo stages[2]{};
- stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main";
- stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main";
- VkVertexInputBindingDescription vib{ 0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 };
- VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib;
- vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via;
- VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
- VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1;
- VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
- VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn;
- VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
- ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; // transparent lines: test but don't write
- VkPipelineColorBlendAttachmentState cba{};
- cba.blendEnable = VK_TRUE;
- cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD;
- cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD;
- cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba;
- VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- gpci.stageCount = 2; gpci.pStages = stages;
- gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps;
- gpci.pDynamicState = &dsci;
- gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDepthStencilState = &ds;
- gpci.layout = grid_pipeline_layout; gpci.renderPass = scene_render_pass; gpci.subpass = 0;
- VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &grid_pipeline);
- vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr);
- if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: grid pipeline creation failed ({})", (int)pr); return false; }
-
- // Lit sphere: unit UV-sphere mesh (pos+normal, stride 24), placed/scaled by
- // u_Transform; samples the geodesic HDRI cubemap for ambient (binding 1).
- {
- std::vector<float> sv; std::vector<uint32_t> si;
- const int RINGS = 24, SECTORS = 48;
- for (int r = 0; r <= RINGS; ++r)
- {
- float phi = (float)std::numbers::pi * r / RINGS;
- for (int s = 0; s <= SECTORS; ++s)
- {
- float theta = 2.0f * (float)std::numbers::pi * s / SECTORS;
- float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta);
- sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit)
- sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position
- }
- }
- for (int r = 0; r < RINGS; ++r)
- for (int s = 0; s < SECTORS; ++s)
- {
- uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1;
- si.push_back(a); si.push_back(b); si.push_back(a + 1);
- si.push_back(b); si.push_back(b + 1); si.push_back(a + 1);
- }
- sphere_index_count = (uint32_t)si.size();
- VkDeviceSize svsz = sv.size() * sizeof(float), sisz = si.size() * sizeof(uint32_t);
- if (!create_buffer(svsz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, sphere_vb, sphere_vb_mem)) return false;
- if (!create_buffer(sisz, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_vis, sphere_ib, sphere_ib_mem)) return false;
- void* sp = nullptr;
- vkMapMemory(device, sphere_vb_mem, 0, svsz, 0, &sp); memcpy(sp, sv.data(), svsz); vkUnmapMemory(device, sphere_vb_mem);
- vkMapMemory(device, sphere_ib_mem, 0, sisz, 0, &sp); memcpy(sp, si.data(), sisz); vkUnmapMemory(device, sphere_ib_mem);
-
- VkPhysicalDeviceProperties pdp{}; vkGetPhysicalDeviceProperties(physical, &pdp);
- VkDeviceSize min_align = pdp.limits.minUniformBufferOffsetAlignment;
- sphere_ubo_stride = ((208 + min_align - 1) / min_align) * min_align; // per-object slot
- VkDeviceSize sphere_ubo_size = sphere_ubo_stride * MAX_SCENE_OBJECTS;
- if (!create_buffer(sphere_ubo_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sphere_ubo, sphere_ubo_mem)) return false;
- vkMapMemory(device, sphere_ubo_mem, 0, sphere_ubo_size, 0, &sphere_ubo_mapped);
-
- VkDescriptorSetLayoutBinding sb[2]{};
- sb[0].binding = 0; sb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sb[0].descriptorCount = 1; sb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
- sb[1].binding = 1; sb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sb[1].descriptorCount = 1; sb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayoutCreateInfo sdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; sdslci.bindingCount = 2; sdslci.pBindings = sb;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &sdslci, nullptr, &sphere_set_layout));
- VkDescriptorPoolSize sps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } };
- VkDescriptorPoolCreateInfo sdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; sdpci.maxSets = 1; sdpci.poolSizeCount = 2; sdpci.pPoolSizes = sps;
- VK_CHECK(vkCreateDescriptorPool(device, &sdpci, nullptr, &sphere_pool));
- VkDescriptorSetAllocateInfo sdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; sdsai.descriptorPool = sphere_pool; sdsai.descriptorSetCount = 1; sdsai.pSetLayouts = &sphere_set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &sdsai, &sphere_set));
- VkDescriptorBufferInfo sbi{ sphere_ubo, 0, 208 }; // per-draw size; the dynamic offset selects the object slot
- VkDescriptorImageInfo sii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet sw[2]{};
- sw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[0].dstSet = sphere_set; sw[0].dstBinding = 0; sw[0].descriptorCount = 1; sw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sw[0].pBufferInfo = &sbi;
- sw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[1].dstSet = sphere_set; sw[1].dstBinding = 1; sw[1].descriptorCount = 1; sw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw[1].pImageInfo = &sii;
- vkUpdateDescriptorSets(device, 2, sw, 0, nullptr);
-
- VkShaderModule svmod, sfmod;
- if (!create_shader_module("assets/shaders/generated/Sphere.vertexMain.spv", svmod)) return false;
- if (!create_shader_module("assets/shaders/generated/Sphere.fragmentMain.spv", sfmod)) return false;
- VkPipelineLayoutCreateInfo splci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; splci.setLayoutCount = 1; splci.pSetLayouts = &sphere_set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &splci, nullptr, &sphere_pipeline_layout));
- VkPipelineShaderStageCreateInfo sstages[2]{};
- sstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; sstages[0].module = svmod; sstages[0].pName = "main";
- sstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; sstages[1].module = sfmod; sstages[1].pName = "main";
- VkVertexInputBindingDescription svib{ 0, 6 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription svia[2] = { { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * (uint32_t)sizeof(float) } };
- VkPipelineVertexInputStateCreateInfo svin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- svin.vertexBindingDescriptionCount = 1; svin.pVertexBindingDescriptions = &svib;
- svin.vertexAttributeDescriptionCount = 2; svin.pVertexAttributeDescriptions = svia;
- VkPipelineInputAssemblyStateCreateInfo sia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; sia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkPipelineViewportStateCreateInfo svps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; svps.viewportCount = 1; svps.scissorCount = 1;
- VkDynamicState sdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
- VkPipelineDynamicStateCreateInfo sdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; sdsci.dynamicStateCount = 2; sdsci.pDynamicStates = sdyn;
- VkPipelineRasterizationStateCreateInfo srs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; srs.polygonMode = VK_POLYGON_MODE_FILL; srs.cullMode = VK_CULL_MODE_NONE; srs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; srs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo sms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; sms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineDepthStencilStateCreateInfo sds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
- sds.depthTestEnable = VK_TRUE; sds.depthWriteEnable = VK_TRUE; sds.depthCompareOp = VK_COMPARE_OP_LESS;
- VkPipelineColorBlendAttachmentState scba{}; scba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo scb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; scb.attachmentCount = 1; scb.pAttachments = &scba;
- VkGraphicsPipelineCreateInfo sgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- sgpci.stageCount = 2; sgpci.pStages = sstages;
- sgpci.pVertexInputState = &svin; sgpci.pInputAssemblyState = &sia; sgpci.pViewportState = &svps;
- sgpci.pDynamicState = &sdsci;
- sgpci.pRasterizationState = &srs; sgpci.pMultisampleState = &sms; sgpci.pColorBlendState = &scb; sgpci.pDepthStencilState = &sds;
- sgpci.layout = sphere_pipeline_layout; sgpci.renderPass = scene_render_pass; sgpci.subpass = 0;
- VkResult spr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &sgpci, nullptr, &sphere_pipeline);
- vkDestroyShaderModule(device, svmod, nullptr); vkDestroyShaderModule(device, sfmod, nullptr);
- if (spr != VK_SUCCESS) { DONUT_ERROR("Vulkan: sphere pipeline creation failed ({})", (int)spr); return false; }
- }
-
- // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex
- // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry.
- {
- const float cube[] = {
- -1,-1,-1, 1,-1,-1, 1, 1,-1, 1, 1,-1, -1, 1,-1, -1,-1,-1,
- -1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1,-1, 1,
- -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1,
- 1, 1, 1, 1, 1,-1, 1,-1,-1, 1,-1,-1, 1,-1, 1, 1, 1, 1,
- -1,-1,-1, 1,-1,-1, 1,-1, 1, 1,-1, 1, -1,-1, 1, -1,-1,-1,
- -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1
- };
- if (!create_buffer(sizeof(cube), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, skybox_vb, skybox_vb_mem)) return false;
- void* kp = nullptr; vkMapMemory(device, skybox_vb_mem, 0, sizeof(cube), 0, &kp); memcpy(kp, cube, sizeof(cube)); vkUnmapMemory(device, skybox_vb_mem);
-
- // $Globals UBO (128 B: u_Projection@0, u_View@64); u_Skybox at binding 1.
- if (!create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, skybox_ubo, skybox_ubo_mem)) return false;
- vkMapMemory(device, skybox_ubo_mem, 0, 128, 0, &skybox_ubo_mapped);
-
- VkDescriptorSetLayoutBinding kb[2]{};
- kb[0].binding = 0; kb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kb[0].descriptorCount = 1; kb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
- kb[1].binding = 1; kb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kb[1].descriptorCount = 1; kb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
- VkDescriptorSetLayoutCreateInfo kdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; kdslci.bindingCount = 2; kdslci.pBindings = kb;
- VK_CHECK(vkCreateDescriptorSetLayout(device, &kdslci, nullptr, &skybox_set_layout));
- VkDescriptorPoolSize kps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } };
- VkDescriptorPoolCreateInfo kdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; kdpci.maxSets = 1; kdpci.poolSizeCount = 2; kdpci.pPoolSizes = kps;
- VK_CHECK(vkCreateDescriptorPool(device, &kdpci, nullptr, &skybox_pool));
- VkDescriptorSetAllocateInfo kdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; kdsai.descriptorPool = skybox_pool; kdsai.descriptorSetCount = 1; kdsai.pSetLayouts = &skybox_set_layout;
- VK_CHECK(vkAllocateDescriptorSets(device, &kdsai, &skybox_set));
- VkDescriptorBufferInfo kbi{ skybox_ubo, 0, VK_WHOLE_SIZE };
- VkDescriptorImageInfo kii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
- VkWriteDescriptorSet kw[2]{};
- kw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[0].dstSet = skybox_set; kw[0].dstBinding = 0; kw[0].descriptorCount = 1; kw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kw[0].pBufferInfo = &kbi;
- kw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[1].dstSet = skybox_set; kw[1].dstBinding = 1; kw[1].descriptorCount = 1; kw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw[1].pImageInfo = &kii;
- vkUpdateDescriptorSets(device, 2, kw, 0, nullptr);
-
- VkShaderModule kvmod, kfmod;
- if (!create_shader_module("assets/shaders/generated/Skybox.vertexMain.spv", kvmod)) return false;
- if (!create_shader_module("assets/shaders/generated/Skybox.fragmentMain.spv", kfmod)) return false;
- VkPipelineLayoutCreateInfo kplci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; kplci.setLayoutCount = 1; kplci.pSetLayouts = &skybox_set_layout;
- VK_CHECK(vkCreatePipelineLayout(device, &kplci, nullptr, &skybox_pipeline_layout));
- VkPipelineShaderStageCreateInfo kstages[2]{};
- kstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; kstages[0].module = kvmod; kstages[0].pName = "main";
- kstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; kstages[1].module = kfmod; kstages[1].pName = "main";
- VkVertexInputBindingDescription kvib{ 0, 3 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX };
- VkVertexInputAttributeDescription kvia{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 };
- VkPipelineVertexInputStateCreateInfo kvin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
- kvin.vertexBindingDescriptionCount = 1; kvin.pVertexBindingDescriptions = &kvib;
- kvin.vertexAttributeDescriptionCount = 1; kvin.pVertexAttributeDescriptions = &kvia;
- VkPipelineInputAssemblyStateCreateInfo kia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; kia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
- VkPipelineViewportStateCreateInfo kvps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; kvps.viewportCount = 1; kvps.scissorCount = 1;
- VkDynamicState kdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
- VkPipelineDynamicStateCreateInfo kdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; kdsci.dynamicStateCount = 2; kdsci.pDynamicStates = kdyn;
- VkPipelineRasterizationStateCreateInfo krs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; krs.polygonMode = VK_POLYGON_MODE_FILL; krs.cullMode = VK_CULL_MODE_NONE; krs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; krs.lineWidth = 1.0f;
- VkPipelineMultisampleStateCreateInfo kms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; kms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
- VkPipelineDepthStencilStateCreateInfo kds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
- kds.depthTestEnable = VK_FALSE; kds.depthWriteEnable = VK_FALSE; // background: neither tests nor writes depth
- VkPipelineColorBlendAttachmentState kcba{}; kcba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
- VkPipelineColorBlendStateCreateInfo kcb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; kcb.attachmentCount = 1; kcb.pAttachments = &kcba;
- VkGraphicsPipelineCreateInfo kgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
- kgpci.stageCount = 2; kgpci.pStages = kstages;
- kgpci.pVertexInputState = &kvin; kgpci.pInputAssemblyState = &kia; kgpci.pViewportState = &kvps;
- kgpci.pDynamicState = &kdsci;
- kgpci.pRasterizationState = &krs; kgpci.pMultisampleState = &kms; kgpci.pColorBlendState = &kcb; kgpci.pDepthStencilState = &kds;
- kgpci.layout = skybox_pipeline_layout; kgpci.renderPass = scene_render_pass; kgpci.subpass = 0;
- VkResult kpr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &kgpci, nullptr, &skybox_pipeline);
- vkDestroyShaderModule(device, kvmod, nullptr); vkDestroyShaderModule(device, kfmod, nullptr);
- if (kpr != VK_SUCCESS) { DONUT_ERROR("Vulkan: skybox pipeline creation failed ({})", (int)kpr); return false; }
- }
-
- DONUT_INFO("Vulkan: scene resources ready ({} grid verts, {} sphere indices, skybox)", grid_vertex_count, sphere_index_count);
- return true;
- }
-
- auto VulkanRenderer::Impl::update_scene_uniforms() -> void
- {
- struct GridUBO {
- glm::mat4 view_projection; // 0 (SPIR-V RowMajor -> upload transposed)
- glm::mat4 transform; // 64
- float grid_size; float p0[3]; // 128
- glm::vec3 grid_color; // 144
- float grid_alpha; // 156
- glm::vec3 camera_pos; // 160
- float p1; // 172
- } g{};
- static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch");
-
- uint32_t h = swapchain_extent.height ? swapchain_extent.height : 1;
- float aspect = (float)swapchain_extent.width / (float)h;
- scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f);
- glm::mat4 vp = scene_camera.get_projection_matrix() * scene_camera.get_view_matrix();
-
- // Slang's mul(M,v) + the SPIR-V RowMajor decoration means glm's column-major
- // matrices upload directly here (no transpose) to read as the intended M.
- g.view_projection = vp;
- g.transform = glm::mat4(1.0f);
- g.grid_size = 50.0f;
- g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f);
- g.grid_alpha = 0.75f;
- g.camera_pos = scene_camera.get_orbital_position();
- if (grid_ubo_mapped) memcpy(grid_ubo_mapped, &g, sizeof(g));
-
- struct SphereUBO {
- glm::mat4 view_projection; // 0
- glm::mat4 transform; // 64
- glm::vec3 color; float specular; // 128, 140
- float emission; float p0[3]; // 144
- glm::vec3 light_pos; float p1; // 160, 172
- glm::vec3 camera_pos; int is_selected; // 176, 188
- glm::vec3 outline_color; float outline_width; // 192, 204
- };
- static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch");
- glm::vec3 cam_pos = scene_camera.get_orbital_position();
- uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS);
- for (uint32_t i = 0; i < obj_count; ++i)
- {
- const SceneObject& o = scene_objects[i];
- SphereUBO s{};
- s.view_projection = vp; // same no-transpose rule as the grid
- s.transform = glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius));
- s.color = o.color;
- s.specular = 0.6f;
- s.emission = 0.0f;
- s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f);
- s.camera_pos = cam_pos;
- s.is_selected = ((int)i == selected_object) ? 1 : 0;
- s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f);
- s.outline_width = 0.15f;
- if (sphere_ubo_mapped) memcpy((char*)sphere_ubo_mapped + (VkDeviceSize)i * sphere_ubo_stride, &s, sizeof(s));
- }
-
- struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; } sky{};
- static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch");
- sky.projection = scene_camera.get_projection_matrix(); // no transpose (mul(M,v))
- sky.view = glm::mat4(glm::mat3(scene_camera.get_view_matrix())); // strip translation so the sky stays centered
- if (skybox_ubo_mapped) memcpy(skybox_ubo_mapped, &sky, sizeof(sky));
- }
-
- auto VulkanRenderer::Impl::update_geodesic_uniforms() -> void
- {
- struct CamUBO {
- glm::vec3 pos; float p0; glm::vec3 right; float p1;
- glm::vec3 up; float p2; glm::vec3 fwd; float p3;
- float tan_half_fov; float aspect; uint32_t moving; int p4;
- } cam_data{};
- glm::vec3 pos, fwd;
- if (camera.get_camera_mode() == CameraMode::FPS)
- {
- pos = camera.get_position();
- fwd = camera.get_forward_direction();
- }
- else
- {
- pos = camera.get_orbital_position();
- fwd = glm::normalize(camera.get_orbital_target() - pos);
- }
- glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
- cam_data.pos = pos; cam_data.right = right; cam_data.up = glm::cross(right, fwd); cam_data.fwd = fwd;
- cam_data.tan_half_fov = (float)tan(glm::radians(bh_params.fov_degrees * 0.5f));
- cam_data.aspect = (float)GEO_HI_W / (float)GEO_HI_H; // 16:9, same for both targets
- cam_data.moving = user_moving ? 1u : 0u;
- if (cam_mapped) memcpy(cam_mapped, &cam_data, sizeof(cam_data));
-
- // Integration budget drives how deep the geodesics are traced: too few
- // steps and grazing rays exhaust the loop mid-lensing, so the shader
- // paints them with the HDRI (Geodesic.slang) and the hole looks sliced
- // off "up to a certain depth" -- and near face-on it vanishes entirely.
- // Measured: at ~8000 steps the disk disappears at steep poses, ~15000 is
- // needed for it to resolve, INDEPENDENT of resolution. So we DON'T drop
- // steps while moving (that would make the hole flicker out mid-drag);
- // responsiveness comes from the lower-res target instead (see record).
- // Sourced from settings.toml (max_steps_static), capped GPU-safe.
- constexpr int kStepCeil = 15000;
- struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim;
- sim.steps_static = std::clamp(bh_params.quality_steps, 1000, kStepCeil);
- sim.steps_moving = sim.steps_static; // same budget both frames; resolution is the lever
- sim.early_exit = SettingsManager::get_early_exit_distance();
- sim.time = (float)(glfwGetTime() - start_time);
- if (sim_mapped) memcpy(sim_mapped, &sim, sizeof(sim));
-
- // Disk UBO, refilled live from the UI-tunable parameters. Layout matches
- // the shader Disk struct: r1, r2, turbulence, slab thickness, brightness,
- // temperature. Radii are in Schwarzschild radii (x SagA_rs).
- const float SagA_rs = 1.269e10f;
- float disk_data[8] = {
- std::max(bh_params.disk_inner_rs, 3.0f) * SagA_rs,
- std::max(bh_params.disk_outer_rs, bh_params.disk_inner_rs + 0.5f) * SagA_rs,
- std::max(bh_params.turbulence, 0.0f),
- SagA_rs * 0.1f, // slab half-thickness (fixed)
- std::max(bh_params.brightness, 0.0f),
- std::max(bh_params.temperature, 1000.0f),
- 0.0f, 0.0f,
- };
- if (disk_mapped) memcpy(disk_mapped, disk_data, sizeof(disk_data));
- }
-
- static double g_ScrollAccum = 0.0;
- static GLFWscrollfun g_PrevScroll = nullptr;
- static void donut_vk_scroll_callback(GLFWwindow* w, double x, double y)
- {
- if (g_PrevScroll) g_PrevScroll(w, x, y); // keep ImGui's scroll handling intact
- g_ScrollAccum += y;
- }
-
- auto VulkanRenderer::Impl::process_input() -> void
- {
- bool over_ui = imgui_init && ImGui::GetIO().WantCaptureMouse;
-
- double now = glfwGetTime();
- float dt = last_frame_time > 0.0 ? (float)(now - last_frame_time) : 0.0f;
- last_frame_time = now;
-
- double mx = 0, my = 0;
- glfwGetCursorPos(window, &mx, &my);
- bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
-
- if (!scene_mode && camera.get_camera_mode() == CameraMode::FPS)
- {
- // Left-drag looks around (screen-up looks up); WASD/QE move.
- if (left_down && !left_was_down) { fps_last_x = mx; fps_last_y = my; }
- if (left_down && !over_ui)
- camera.on_mouse_move(float(mx - fps_last_x), float(fps_last_y - my));
- fps_last_x = mx; fps_last_y = my;
- left_was_down = left_down;
-
- bool over_kb = imgui_init && ImGui::GetIO().WantCaptureKeyboard;
- bool moved = false;
- if (!over_kb && dt > 0.0f)
- {
- float mdt = dt * (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f);
- if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { camera.move_forward(mdt); moved = true; }
- if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { camera.move_backward(mdt); moved = true; }
- if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { camera.move_left(mdt); moved = true; }
- if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { camera.move_right(mdt); moved = true; }
- if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { camera.move_up(mdt); moved = true; }
- if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { camera.move_down(mdt); moved = true; }
- }
- g_ScrollAccum = 0.0; // scroll unused in free-fly
- user_moving = moved || (left_down && !over_ui);
- }
- else
- {
- Camera& cam = scene_mode ? scene_camera : camera;
- if (left_down && !left_was_down && !over_ui)
- cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0);
- else if (!left_down && left_was_down)
- cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0);
- left_was_down = left_down;
-
- cam.process_orbital_mouse_move(mx, my); // orbits only while dragging
-
- double scroll = g_ScrollAccum; g_ScrollAccum = 0.0;
- if (scroll != 0.0 && !over_ui)
- cam.process_orbital_scroll(0.0, scroll);
-
- user_moving = cam.is_dragging() || cam.is_panning();
- }
- }
-
- auto VulkanRenderer::Impl::destroy_geodesic_resources() -> void
- {
- if (grid_pipeline) vkDestroyPipeline(device, grid_pipeline, nullptr);
- if (grid_pipeline_layout) vkDestroyPipelineLayout(device, grid_pipeline_layout, nullptr);
- if (grid_pool) vkDestroyDescriptorPool(device, grid_pool, nullptr);
- if (grid_set_layout) vkDestroyDescriptorSetLayout(device, grid_set_layout, nullptr);
- if (grid_ubo_mapped) { vkUnmapMemory(device, grid_ubo_mem); grid_ubo_mapped = nullptr; }
- if (grid_ubo) vkDestroyBuffer(device, grid_ubo, nullptr);
- if (grid_ubo_mem) vkFreeMemory(device, grid_ubo_mem, nullptr);
- if (grid_vb) vkDestroyBuffer(device, grid_vb, nullptr);
- if (grid_vb_mem) vkFreeMemory(device, grid_vb_mem, nullptr);
-
- if (sphere_pipeline) vkDestroyPipeline(device, sphere_pipeline, nullptr);
- if (sphere_pipeline_layout) vkDestroyPipelineLayout(device, sphere_pipeline_layout, nullptr);
- if (sphere_pool) vkDestroyDescriptorPool(device, sphere_pool, nullptr);
- if (sphere_set_layout) vkDestroyDescriptorSetLayout(device, sphere_set_layout, nullptr);
- if (sphere_ubo_mapped) { vkUnmapMemory(device, sphere_ubo_mem); sphere_ubo_mapped = nullptr; }
- if (sphere_ubo) vkDestroyBuffer(device, sphere_ubo, nullptr);
- if (sphere_ubo_mem) vkFreeMemory(device, sphere_ubo_mem, nullptr);
- if (sphere_ib) vkDestroyBuffer(device, sphere_ib, nullptr);
- if (sphere_ib_mem) vkFreeMemory(device, sphere_ib_mem, nullptr);
- if (sphere_vb) vkDestroyBuffer(device, sphere_vb, nullptr);
- if (sphere_vb_mem) vkFreeMemory(device, sphere_vb_mem, nullptr);
-
- if (skybox_pipeline) vkDestroyPipeline(device, skybox_pipeline, nullptr);
- if (skybox_pipeline_layout) vkDestroyPipelineLayout(device, skybox_pipeline_layout, nullptr);
- if (skybox_pool) vkDestroyDescriptorPool(device, skybox_pool, nullptr);
- if (skybox_set_layout) vkDestroyDescriptorSetLayout(device, skybox_set_layout, nullptr);
- if (skybox_ubo_mapped) { vkUnmapMemory(device, skybox_ubo_mem); skybox_ubo_mapped = nullptr; }
- if (skybox_ubo) vkDestroyBuffer(device, skybox_ubo, nullptr);
- if (skybox_ubo_mem) vkFreeMemory(device, skybox_ubo_mem, nullptr);
- if (skybox_vb) vkDestroyBuffer(device, skybox_vb, nullptr);
- if (skybox_vb_mem) vkFreeMemory(device, skybox_vb_mem, nullptr);
-
- if (present_pipeline) vkDestroyPipeline(device, present_pipeline, nullptr);
- if (present_pipeline_layout) vkDestroyPipelineLayout(device, present_pipeline_layout, nullptr);
- if (present_pool) vkDestroyDescriptorPool(device, present_pool, nullptr);
- if (present_set_layout) vkDestroyDescriptorSetLayout(device, present_set_layout, nullptr);
- if (present_sampler) vkDestroySampler(device, present_sampler, nullptr);
-
- if (geo_pipeline) vkDestroyPipeline(device, geo_pipeline, nullptr);
- if (geo_pipeline_layout) vkDestroyPipelineLayout(device, geo_pipeline_layout, nullptr);
- if (geo_pool) vkDestroyDescriptorPool(device, geo_pool, nullptr);
- if (geo_set_layout) vkDestroyDescriptorSetLayout(device, geo_set_layout, nullptr);
- if (quad_vb) vkDestroyBuffer(device, quad_vb, nullptr);
- if (quad_vb_mem) vkFreeMemory(device, quad_vb_mem, nullptr);
- if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr);
- if (cube_view) vkDestroyImageView(device, cube_view, nullptr);
- if (cube_image) vkDestroyImage(device, cube_image, nullptr);
- if (cube_mem) vkFreeMemory(device, cube_mem, nullptr);
- if (cam_mapped) { vkUnmapMemory(device, cam_mem); cam_mapped = nullptr; }
- if (sim_mapped) { vkUnmapMemory(device, sim_mem); sim_mapped = nullptr; }
- if (disk_mapped) { vkUnmapMemory(device, disk_mem); disk_mapped = nullptr; }
- VkBuffer ubos[4] = { cam_buf, disk_buf, obj_buf, sim_buf };
- VkDeviceMemory umem[4] = { cam_mem, disk_mem, obj_mem, sim_mem };
- for (int i = 0; i < 4; ++i) { if (ubos[i]) vkDestroyBuffer(device, ubos[i], nullptr); if (umem[i]) vkFreeMemory(device, umem[i], nullptr); }
- for (GeoTarget* t : { &geo_lo, &geo_hi })
- {
- if (t->fb) vkDestroyFramebuffer(device, t->fb, nullptr);
- if (t->view) vkDestroyImageView(device, t->view, nullptr);
- if (t->image) vkDestroyImage(device, t->image, nullptr);
- if (t->mem) vkFreeMemory(device, t->mem, nullptr);
- }
- if (geo_render_pass) vkDestroyRenderPass(device, geo_render_pass, nullptr);
- }
-
- auto VulkanRenderer::Impl::record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool
- {
- VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
- VK_CHECK(vkBeginCommandBuffer(cmd, &begin));
-
- if (scene_mode)
- {
- // Scene view: opaque sphere (writes depth) then the transparent grid on
- // top, into a color+depth swapchain pass, then ImGui.
- VkClearValue cvs[2]{};
- cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } };
- cvs[1].depthStencil = { 1.0f, 0 };
- VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = scene_render_pass; rpbi.framebuffer = scene_framebuffers[image_index];
- rpbi.renderArea = { { 0, 0 }, swapchain_extent };
- rpbi.clearValueCount = 2; rpbi.pClearValues = cvs;
- vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
-
- // Negative-height viewport flips Y so the scene reads like the GL path.
- VkViewport gvp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 };
- VkRect2D gsc{ { 0, 0 }, swapchain_extent };
- vkCmdSetViewport(cmd, 0, 1, &gvp);
- vkCmdSetScissor(cmd, 0, 1, &gsc);
-
- // Skybox background first (forces depth 1; no depth test/write).
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline_layout, 0, 1, &skybox_set, 0, nullptr);
- VkDeviceSize skoff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &skybox_vb, &skoff);
- vkCmdDraw(cmd, 36, 1, 0, 0);
-
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline);
- VkDeviceSize soff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &sphere_vb, &soff);
- vkCmdBindIndexBuffer(cmd, sphere_ib, 0, VK_INDEX_TYPE_UINT32);
- uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS);
- for (uint32_t i = 0; i < obj_count; ++i)
- {
- uint32_t dyn = (uint32_t)((VkDeviceSize)i * sphere_ubo_stride); // select this object's UBO slot
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline_layout, 0, 1, &sphere_set, 1, &dyn);
- vkCmdDrawIndexed(cmd, sphere_index_count, 1, 0, 0, 0);
- }
-
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline_layout, 0, 1, &grid_set, 0, nullptr);
- VkDeviceSize goff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &grid_vb, &goff);
- vkCmdDraw(cmd, grid_vertex_count, 1, 0, 0);
-
- if (draw_data)
- ImGui_ImplVulkan_RenderDrawData(draw_data, cmd);
- vkCmdEndRenderPass(cmd);
- VK_CHECK(vkEndCommandBuffer(cmd));
- return true;
- }
-
- // Geodesic offscreen pass. Progressive resolution: low-res while the
- // camera moves (4x fewer pixels -> responsive), high-res once it settles
- // (resolves the photon ring). Step count is the same for both (the disk
- // vanishes below ~15000 steps at steep poses), so resolution is the lever.
- GeoTarget& gt = user_moving ? geo_lo : geo_hi;
- VkClearValue geo_clear{}; geo_clear.color = { { 0, 0, 0, 1 } };
- VkRenderPassBeginInfo grp{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- grp.renderPass = geo_render_pass; grp.framebuffer = gt.fb;
- grp.renderArea = { { 0, 0 }, { gt.w, gt.h } };
- grp.clearValueCount = 1; grp.pClearValues = &geo_clear;
- vkCmdBeginRenderPass(cmd, &grp, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline);
- VkViewport gvp{ 0, 0, (float)gt.w, (float)gt.h, 0, 1 }; VkRect2D gsc{ { 0, 0 }, { gt.w, gt.h } };
- vkCmdSetViewport(cmd, 0, 1, &gvp); vkCmdSetScissor(cmd, 0, 1, &gsc);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline_layout, 0, 1, &geo_set, 0, nullptr);
- VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off);
- vkCmdDraw(cmd, 6, 1, 0, 0);
- vkCmdEndRenderPass(cmd);
-
- // Swapchain pass: upscale the geodesic image, then the ImGui UI on top
- VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } };
- VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
- rpbi.renderPass = render_pass;
- rpbi.framebuffer = framebuffers[image_index];
- rpbi.renderArea = { { 0, 0 }, swapchain_extent };
- rpbi.clearValueCount = 1; rpbi.pClearValues = &cv;
- vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
- vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline);
- // Negative-height viewport flips the geodesic image vertically so the scene
- // reads the same as the OpenGL path (Vulkan's clip space is Y-down). Only
- // this draw is affected; ImGui sets its own viewport.
- VkViewport vp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 };
- VkRect2D scissor{ { 0, 0 }, swapchain_extent };
- vkCmdSetViewport(cmd, 0, 1, &vp);
- vkCmdSetScissor(cmd, 0, 1, &scissor);
- vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline_layout, 0, 1, &gt.present_set, 0, nullptr);
- vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off);
- vkCmdDraw(cmd, 6, 1, 0, 0);
- if (draw_data)
- ImGui_ImplVulkan_RenderDrawData(draw_data, cmd);
- vkCmdEndRenderPass(cmd);
-
- VK_CHECK(vkEndCommandBuffer(cmd));
- return true;
- }
-
- auto VulkanRenderer::Impl::cleanup_swapchain() -> void
- {
- destroy_scene_targets();
- for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
- framebuffers.clear();
- for (auto iv : image_views) vkDestroyImageView(device, iv, nullptr);
- image_views.clear();
- if (render_pass) { vkDestroyRenderPass(device, render_pass, nullptr); render_pass = VK_NULL_HANDLE; }
- if (swapchain) { vkDestroySwapchainKHR(device, swapchain, nullptr); swapchain = VK_NULL_HANDLE; }
- }
-
- auto VulkanRenderer::Impl::recreate_swapchain() -> bool
- {
- // Wait until the window has a non-zero size (e.g. after un-minimizing).
- int w = 0, h = 0;
- glfwGetFramebufferSize(window, &w, &h);
- while (w == 0 || h == 0)
- {
- glfwGetFramebufferSize(window, &w, &h);
- glfwWaitEvents();
- }
- width = w; height = h;
- vkDeviceWaitIdle(device);
-
- cleanup_swapchain();
- // render_finished are tied to image count; recreate below via sync if it changed.
- if (!create_swapchain()) return false;
- if (!create_image_views()) return false;
- if (!create_render_pass()) return false;
- if (!create_framebuffers())return false;
- if (!create_scene_targets())return false;
- images_in_flight.assign(images.size(), VK_NULL_HANDLE);
- return true;
- }
-
- VulkanRenderer::VulkanRenderer() { m_impl = new Impl(); }
- VulkanRenderer::~VulkanRenderer() { shutdown(); delete m_impl; m_impl = nullptr; }
-
- auto VulkanRenderer::init(void* glfwWindow, int width, int height) -> bool
- {
- Impl& v = *m_impl;
- v.window = (GLFWwindow*)glfwWindow;
- v.width = width; v.height = height;
-
- if (!v.create_instance()) return false;
- if (!v.pick_physical_and_device()) return false;
- if (!v.create_swapchain()) return false;
- if (!v.create_image_views()) return false;
- if (!v.create_render_pass()) return false;
- if (!v.create_framebuffers()) return false;
- if (!v.create_scene_targets()) return false;
- if (!v.create_command_buffers()) return false;
- if (!v.create_sync_objects()) return false;
- if (!v.create_geodesic_resources()) return false;
- if (!v.create_present_resources()) return false;
- if (!v.create_scene_resources()) return false;
-
- DONUT_INFO("Vulkan renderer ready: {} swapchain images, {}x{}",
- (int)v.images.size(), v.swapchain_extent.width, v.swapchain_extent.height);
- return true;
- }
-
- auto VulkanRenderer::init_ui() -> bool
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return false;
-
- VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 };
- VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
- dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
- dpci.maxSets = 1000;
- dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size;
- VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &v.imgui_pool));
-
- IMGUI_CHECKVERSION();
- ImGui::CreateContext();
- ImGuiIO& io = ImGui::GetIO();
- io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
- io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
- ImGui::StyleColorsDark();
-
- ImGui_ImplGlfw_InitForVulkan(v.window, true);
- g_PrevScroll = glfwSetScrollCallback(v.window, donut_vk_scroll_callback); // chain ImGui + camera zoom
- ImGui_ImplVulkan_InitInfo info{};
- info.ApiVersion = VK_API_VERSION_1_2;
- info.Instance = v.instance;
- info.PhysicalDevice = v.physical;
- info.Device = v.device;
- info.QueueFamily = v.graphics_family;
- info.Queue = v.graphics_queue;
- info.DescriptorPool = v.imgui_pool;
- info.RenderPass = v.render_pass;
- info.MinImageCount = 2;
- info.ImageCount = (uint32_t)v.images.size();
- info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
- if (!ImGui_ImplVulkan_Init(&info))
- {
- DONUT_ERROR("Vulkan: ImGui_ImplVulkan_Init failed");
- return false;
- }
-
- v.imgui_init = true;
- DONUT_INFO("Vulkan: ImGui backend initialized");
- return true;
- }
-
- auto VulkanRenderer::resize(int width, int height) -> void
- {
- m_impl->framebuffer_resized = true;
- m_impl->width = width; m_impl->height = height;
- }
-
- auto VulkanRenderer::set_hdri(const std::string& path) -> void
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE || v.geo_set == VK_NULL_HANDLE) return;
- if (v.hdri_path == path) return;
- v.rebuild_hdri_cubemap(path.c_str());
- }
-
- auto VulkanRenderer::current_hdri() const -> const std::string&
- {
- return m_impl->hdri_path;
- }
-
- auto VulkanRenderer::set_free_fly(bool enabled) -> void
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return;
- const bool is_fps = v.camera.get_camera_mode() == CameraMode::FPS;
- if (enabled == is_fps) return;
-
- if (enabled)
- {
- // Seed the fly pose from the current orbital framing so the view is continuous.
- glm::vec3 pos = v.camera.get_orbital_position();
- glm::vec3 fwd = glm::normalize(v.camera.get_orbital_target() - pos);
- float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f)));
- float yaw = glm::degrees(atan2(fwd.z, fwd.x));
- v.camera.set_camera_mode(CameraMode::FPS);
- v.camera.set_movement_speed(2.0e10f); // scene spans ~1e11 units
- v.camera.set_mouse_sensitivity(0.15f);
- v.camera.set_position(pos);
- v.camera.set_rotation(glm::vec3(pitch, yaw, 0.0f));
- }
- else
- {
- v.camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact
- }
- }
-
- auto VulkanRenderer::is_free_fly() const -> bool
- {
- return m_impl->camera.get_camera_mode() == CameraMode::FPS;
- }
-
- auto VulkanRenderer::set_scene_mode(bool enabled) -> void
- {
- if (m_impl) m_impl->scene_mode = enabled;
- }
-
- auto VulkanRenderer::is_scene_mode() const -> bool
- {
- return m_impl && m_impl->scene_mode;
- }
-
- auto VulkanRenderer::scene_objects() -> std::vector<SceneObject>&
- {
- return m_impl->scene_objects;
- }
-
- auto VulkanRenderer::set_selected_object(int index) -> void
- {
- if (m_impl) m_impl->selected_object = index;
- }
-
- auto VulkanRenderer::black_hole_params() -> BlackHoleParams&
- {
- return m_impl->bh_params;
- }
-
- auto VulkanRenderer::reset_camera() -> void
- {
- if (!m_impl) return;
- Camera& cam = m_impl->camera;
- cam.set_camera_mode(CameraMode::Orbital); // also flips is_free_fly() back
- cam.set_orbital_radius(4e11);
- cam.set_azimuth(0.0f);
- cam.set_elevation(1.25f);
- }
-
- auto VulkanRenderer::device_name() const -> const std::string&
- {
- return m_impl->gpu_name;
- }
-
- auto VulkanRenderer::draw_frame(const glm::vec4& clear_color, const std::function<void()>& build_ui) -> void
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) return;
-
- ImDrawData* draw_data = nullptr;
- if (v.imgui_init)
- {
- ImGui_ImplVulkan_NewFrame();
- ImGui_ImplGlfw_NewFrame();
- ImGui::NewFrame();
- if (build_ui) build_ui();
- ImGui::Render();
- draw_data = ImGui::GetDrawData();
- }
-
- vkWaitForFences(v.device, 1, &v.in_flight[v.current_frame], VK_TRUE, UINT64_MAX);
-
- uint32_t image_index = 0;
- VkResult r = vkAcquireNextImageKHR(v.device, v.swapchain, UINT64_MAX,
- v.image_available[v.current_frame], VK_NULL_HANDLE, &image_index);
- if (r == VK_ERROR_OUT_OF_DATE_KHR) { v.recreate_swapchain(); return; }
- if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan: acquire failed ({})", (int)r); return; }
-
- if (v.images_in_flight[image_index] != VK_NULL_HANDLE)
- vkWaitForFences(v.device, 1, &v.images_in_flight[image_index], VK_TRUE, UINT64_MAX);
- v.images_in_flight[image_index] = v.in_flight[v.current_frame];
-
- // The geodesic offscreen image is shared across frames in flight; wait for
- // the previous frame to finish reading it before overwriting it this frame.
- if (v.geo_in_use != VK_NULL_HANDLE)
- vkWaitForFences(v.device, 1, &v.geo_in_use, VK_TRUE, UINT64_MAX);
- v.process_input();
- if (v.scene_mode) v.update_scene_uniforms(); else v.update_geodesic_uniforms();
-
- vkResetCommandBuffer(v.command_buffers[v.current_frame], 0);
- if (!v.record_command_buffer(v.command_buffers[v.current_frame], image_index, clear_color, draw_data)) return;
-
- VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
- VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
- submit.waitSemaphoreCount = 1;
- submit.pWaitSemaphores = &v.image_available[v.current_frame];
- submit.pWaitDstStageMask = &wait_stage;
- submit.commandBufferCount = 1;
- submit.pCommandBuffers = &v.command_buffers[v.current_frame];
- submit.signalSemaphoreCount = 1;
- submit.pSignalSemaphores = &v.render_finished[image_index];
-
- vkResetFences(v.device, 1, &v.in_flight[v.current_frame]);
- if (vkQueueSubmit(v.graphics_queue, 1, &submit, v.in_flight[v.current_frame]) != VK_SUCCESS)
- { DONUT_ERROR("Vulkan: queue submit failed"); return; }
- v.geo_in_use = v.in_flight[v.current_frame];
-
- VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
- present.waitSemaphoreCount = 1;
- present.pWaitSemaphores = &v.render_finished[image_index];
- present.swapchainCount = 1;
- present.pSwapchains = &v.swapchain;
- present.pImageIndices = &image_index;
- r = vkQueuePresentKHR(v.present_queue, &present);
- if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || v.framebuffer_resized)
- {
- v.framebuffer_resized = false;
- v.recreate_swapchain();
- }
-
- v.current_frame = (v.current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
- }
-
- auto VulkanRenderer::shutdown() -> void
- {
- Impl& v = *m_impl;
- if (v.device == VK_NULL_HANDLE) { if (v.instance && v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } return; }
-
- vkDeviceWaitIdle(v.device);
- v.destroy_geodesic_resources();
- if (v.imgui_init)
- {
- ImGui_ImplVulkan_Shutdown();
- ImGui_ImplGlfw_Shutdown();
- ImGui::DestroyContext();
- v.imgui_init = false;
- }
- if (v.imgui_pool) { vkDestroyDescriptorPool(v.device, v.imgui_pool, nullptr); v.imgui_pool = VK_NULL_HANDLE; }
- for (auto s : v.render_finished) vkDestroySemaphore(v.device, s, nullptr);
- for (auto s : v.image_available) vkDestroySemaphore(v.device, s, nullptr);
- for (auto f : v.in_flight) vkDestroyFence(v.device, f, nullptr);
- v.render_finished.clear(); v.image_available.clear(); v.in_flight.clear();
- if (v.command_pool) { vkDestroyCommandPool(v.device, v.command_pool, nullptr); v.command_pool = VK_NULL_HANDLE; }
- v.cleanup_swapchain();
- vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE;
- if (v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; }
- if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; }
- }
-}
diff --git a/src/platform/vulkan/vulkan_renderer.h b/src/platform/vulkan/vulkan_renderer.h
deleted file mode 100644
index d583f07..0000000
--- a/src/platform/vulkan/vulkan_renderer.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-
-#include "rendering/renderer_backend.h"
-
-// Live-window Vulkan backend: owns the instance, surface, device, swapchain,
-// render pass, framebuffers and per-frame synchronization, and drives the
-// acquire -> record -> submit -> present loop. Implements the shared
-// RendererBackend interface (SceneObject / BlackHoleParams live there). Pure-C++
-// header (no vulkan.h / glfw leak); the GLFW window is passed as an opaque handle.
-namespace Donut
-{
- // Must be called BEFORE glfwInit() when the Vulkan API is selected: points
- // GLFW at the loader the app links against (GLFW's own dlopen fails on
- // macOS/Homebrew) and configures the MoltenVK ICD / layer paths.
- auto vulkan_prepare_glfw() -> void;
-
- class VulkanRenderer : public RendererBackend
- {
- public:
- VulkanRenderer();
- ~VulkanRenderer() override;
-
- // glfwWindow must be a GLFW window created with GLFW_NO_API.
- auto init(void* glfwWindow, int width, int height) -> bool override;
- auto init_ui() -> bool override;
- auto shutdown() -> void override;
- auto resize(int width, int height) -> void override;
-
- auto draw_frame(const glm::vec4& clear_color,
- const std::function<void()>& build_ui) -> void override;
-
- auto set_scene_mode(bool enabled) -> void override;
- auto is_scene_mode() const -> bool override;
- auto set_free_fly(bool enabled) -> void override; // seeds the fly pose from the orbital framing
- auto is_free_fly() const -> bool override;
- auto reset_camera() -> void override;
-
- auto set_hdri(const std::string& path) -> void override; // rebuilds the cubemap (device-idle)
- auto current_hdri() const -> const std::string& override;
- auto scene_objects() -> std::vector<SceneObject>& override;
- auto set_selected_object(int index) -> void override;
- auto black_hole_params() -> BlackHoleParams& override;
-
- auto device_name() const -> const std::string& override;
-
- private:
- struct Impl;
- Impl* m_impl = nullptr;
- };
-}
diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp
new file mode 100644
index 0000000..46b002c
--- /dev/null
+++ b/src/rendering/black_hole_renderer.cpp
@@ -0,0 +1,144 @@
+#include "black_hole_renderer.h"
+
+#include "core/log.h"
+#include "core/settings_manager.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+
+namespace Donut
+{
+ using namespace RHI;
+
+ namespace
+ {
+ constexpr float SagA_rs = 1.269e10f; // Schwarzschild radius of the modelled hole
+
+ struct CamUBO {
+ glm::vec3 pos; float p0; glm::vec3 right; float p1;
+ glm::vec3 up; float p2; glm::vec3 fwd; float p3;
+ float tan_half_fov; float aspect; uint32_t moving; int p4;
+ };
+ struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; };
+ }
+
+ auto BlackHoleRenderer::init(RHI::Device& device) -> bool
+ {
+ m_device = &device;
+
+ m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, false, Filter::Linear);
+ m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, false, Filter::Linear);
+
+ // Fullscreen quad shared by the geodesic and present passes: pos.xy + uv.
+ const float quad[] = {
+ -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f,
+ -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f,
+ };
+ m_quad_vb = device.create_buffer(BufferType::Vertex, sizeof(quad), quad);
+
+ m_cam_ubo = device.create_buffer(BufferType::Uniform, 128);
+ m_disk_ubo = device.create_buffer(BufferType::Uniform, 32);
+ m_obj_ubo = device.create_buffer(BufferType::Uniform, 800);
+ m_sim_ubo = device.create_buffer(BufferType::Uniform, 16);
+
+ // The single "object" is the black hole itself (position 0, radius = r_s).
+ std::vector<uint8_t> obj(800, 0);
+ int num_objects = 1; std::memcpy(obj.data() + 0, &num_objects, 4);
+ float pos_radius[4] = { 0, 0, 0, SagA_rs }; std::memcpy(obj.data() + 16, pos_radius, 16);
+ float color[4] = { 0, 0, 0, 1 }; std::memcpy(obj.data() + 272, color, 16);
+ m_obj_ubo->update(obj.data(), obj.size());
+
+ {
+ PipelineDesc d;
+ d.shader = "geodesic";
+ d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } };
+ d.resources = {
+ { ResourceKind::UniformBuffer, 0, "Camera" },
+ { ResourceKind::UniformBuffer, 1, "Disk" },
+ { ResourceKind::UniformBuffer, 2, "Objects" },
+ { ResourceKind::UniformBuffer, 3, "Simulation" },
+ { ResourceKind::Texture, 4, "u_HDRIEnvironment" },
+ };
+ d.topology = Topology::Triangles;
+ d.has_depth = false; // color-only off-screen target
+ m_geo_pipeline = device.create_pipeline(d);
+ }
+ {
+ PipelineDesc d;
+ d.shader = "textured_quad";
+ d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } };
+ d.resources = { { ResourceKind::Texture, 0, "u_ScreenTexture" } };
+ d.topology = Topology::Triangles;
+ d.has_depth = true; // swapchain target carries depth (unused here)
+ m_present_pipeline = device.create_pipeline(d);
+ }
+
+ DONUT_INFO("BlackHoleRenderer ready ({}x{} moving / {}x{} settled)",
+ GEO_LO_W, GEO_LO_H, GEO_HI_W, GEO_HI_H);
+ return true;
+ }
+
+ auto BlackHoleRenderer::render_geodesic(RHI::CommandList& cmd, const GeodesicView& view,
+ const BlackHoleParams& params, RHI::Texture* cubemap) -> void
+ {
+ CamUBO cam{};
+ cam.pos = view.position; cam.right = view.right; cam.up = view.up; cam.fwd = view.forward;
+ cam.tan_half_fov = view.tan_half_fov;
+ cam.aspect = view.aspect;
+ cam.moving = view.moving ? 1u : 0u;
+ m_cam_ubo->update(&cam, sizeof(cam));
+
+ // Same integration budget whether moving or settled (the disk vanishes at
+ // steep poses below ~15000 steps); responsiveness comes from the lower-res
+ // target instead. Sourced from the UI, clamped GPU-safe.
+ constexpr int kStepCeil = 15000;
+ SimUBO sim{};
+ sim.steps_static = std::clamp(params.quality_steps, 1000, kStepCeil);
+ sim.steps_moving = sim.steps_static;
+ sim.early_exit = SettingsManager::get_early_exit_distance();
+ sim.time = view.time;
+ m_sim_ubo->update(&sim, sizeof(sim));
+
+ // Disk struct: r_in, r_out, turbulence, slab half-thickness, brightness,
+ // temperature (radii in Schwarzschild radii).
+ float disk[8] = {
+ std::max(params.disk_inner_rs, 3.0f) * SagA_rs,
+ std::max(params.disk_outer_rs, params.disk_inner_rs + 0.5f) * SagA_rs,
+ std::max(params.turbulence, 0.0f),
+ SagA_rs * 0.1f,
+ std::max(params.brightness, 0.0f),
+ std::max(params.temperature, 1000.0f),
+ 0.0f, 0.0f,
+ };
+ m_disk_ubo->update(disk, sizeof(disk));
+
+ // Progressive resolution: small target while moving, large once settled.
+ RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get();
+ m_last_target = target;
+
+ cmd.begin_render_pass(target, glm::vec4(0, 0, 0, 1));
+ cmd.set_viewport(0, 0, target->width(), target->height(), false);
+ cmd.bind_pipeline(m_geo_pipeline.get());
+ cmd.bind_uniform(0, m_cam_ubo.get());
+ cmd.bind_uniform(1, m_disk_ubo.get());
+ cmd.bind_uniform(2, m_obj_ubo.get());
+ cmd.bind_uniform(3, m_sim_ubo.get());
+ cmd.bind_texture(4, cubemap);
+ cmd.bind_vertex_buffer(m_quad_vb.get());
+ cmd.draw(6);
+ cmd.end_render_pass();
+ }
+
+ auto BlackHoleRenderer::blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void
+ {
+ if (!m_last_target) return;
+ // flip_y matches the shared top-left orientation (Vulkan flips via a
+ // negative-height viewport; GL is a no-op).
+ cmd.set_viewport(0, 0, fb_width, fb_height, true);
+ cmd.bind_pipeline(m_present_pipeline.get());
+ cmd.bind_texture(0, m_last_target->color_texture());
+ cmd.bind_vertex_buffer(m_quad_vb.get());
+ cmd.draw(6);
+ }
+}
diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h
new file mode 100644
index 0000000..b9c158b
--- /dev/null
+++ b/src/rendering/black_hole_renderer.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "rhi.h"
+#include "renderer_backend.h" // BlackHoleParams
+#include <glm/glm.hpp>
+
+namespace Donut
+{
+ // The black-hole view, written ONCE against the RHI. Each frame it ray-traces
+ // the geodesic shader into an off-screen target (progressive resolution: a
+ // small target while the camera moves, a larger one once it settles) and then
+ // upscales that target onto the swapchain. The Application owns the camera and
+ // the HDRI cubemap and passes them in.
+ struct GeodesicView
+ {
+ glm::vec3 position, right, up, forward;
+ float tan_half_fov = 1.0f;
+ float aspect = 16.0f / 9.0f;
+ bool moving = false;
+ float time = 0.0f;
+ };
+
+ class BlackHoleRenderer
+ {
+ public:
+ static constexpr int GEO_LO_W = 480, GEO_LO_H = 270;
+ static constexpr int GEO_HI_W = 960, GEO_HI_H = 540;
+
+ auto init(RHI::Device& device) -> bool;
+
+ // Off-screen geodesic pass (opens/closes its own render pass). Call before
+ // the swapchain pass; `cubemap` is the shared HDRI environment.
+ auto render_geodesic(RHI::CommandList& cmd, const GeodesicView& view,
+ const BlackHoleParams& params, RHI::Texture* cubemap) -> void;
+
+ // Upscales the geodesic target onto the swapchain (call inside the open
+ // swapchain render pass, before ImGui).
+ auto blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void;
+
+ private:
+ RHI::Device* m_device = nullptr;
+
+ Ref<RHI::Pipeline> m_geo_pipeline;
+ Ref<RHI::RenderTarget> m_geo_lo;
+ Ref<RHI::RenderTarget> m_geo_hi;
+ Ref<RHI::Buffer> m_quad_vb;
+ Ref<RHI::Buffer> m_cam_ubo;
+ Ref<RHI::Buffer> m_disk_ubo;
+ Ref<RHI::Buffer> m_obj_ubo;
+ Ref<RHI::Buffer> m_sim_ubo;
+
+ Ref<RHI::Pipeline> m_present_pipeline;
+ RHI::RenderTarget* m_last_target = nullptr; // target chosen this frame
+ };
+}
diff --git a/src/rendering/framebuffer.cpp b/src/rendering/framebuffer.cpp
deleted file mode 100644
index 3da4b48..0000000
--- a/src/rendering/framebuffer.cpp
+++ /dev/null
@@ -1,251 +0,0 @@
-#include "framebuffer.h"
-#include "core/log.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- namespace Utils
- {
- static GLenum TextureTarget(bool multisampled)
- {
- return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
- }
-
- static void bind_texture(bool multisampled, uint32_t id)
- {
- glBindTexture(TextureTarget(multisampled), id);
- }
-
- static void CreateTextures(bool multisampled, uint32_t* out_id, uint32_t count)
- {
- // glCreateTextures is 4.5 DSA; macOS caps at 4.1. Callers bind each
- // texture (with the correct target) before use.
- glGenTextures(count, out_id);
- }
-
- static void AttachColorTexture(uint32_t id, int samples, GLenum internal_format, GLenum format, uint32_t width, uint32_t height, int index)
- {
- bool multisampled = samples > 1;
- if (multisampled)
- {
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internal_format, width, height, GL_FALSE);
- }
- else
- {
- // Integer color formats require an integer pixel type even when
- // data is null, or macOS's strict core profile rejects the call.
- GLenum type = (format == GL_RED_INTEGER) ? GL_INT : GL_UNSIGNED_BYTE;
- glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, type, nullptr);
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- }
-
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0);
- }
-
- static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachment_type, uint32_t width, uint32_t height)
- {
- bool multisampled = samples > 1;
- if (multisampled)
- {
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE);
- }
- else
- {
- // glTexStorage2D is 4.2; use mutable storage for macOS (4.1).
- GLenum depth_format = (format == GL_DEPTH24_STENCIL8) ? GL_DEPTH_STENCIL : GL_DEPTH_COMPONENT;
- GLenum depth_type = (format == GL_DEPTH24_STENCIL8) ? GL_UNSIGNED_INT_24_8 : GL_FLOAT;
- glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, depth_format, depth_type, nullptr);
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- }
-
- glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_type, TextureTarget(multisampled), id, 0);
- }
-
- static bool IsDepthFormat(FramebufferTextureFormat format)
- {
- switch (format)
- {
- case FramebufferTextureFormat::DEPTH24STENCIL8: return true;
- }
- return false;
- }
-
- static GLenum DonutFBTextureFormatToGL(FramebufferTextureFormat format)
- {
- switch (format)
- {
- case FramebufferTextureFormat::RGBA8: return GL_RGBA8;
- case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER;
- }
-
- return 0;
- }
- }
-
- Framebuffer::Framebuffer(const FramebufferSpecification& spec)
- : m_specification(spec)
- {
- for (auto spec : m_specification.attachments.attachments)
- {
- if (!Utils::IsDepthFormat(spec.texture_format))
- m_color_attachment_specifications.emplace_back(spec);
- else
- m_depth_attachment_specification = spec;
- }
-
- invalidate();
- }
-
- Framebuffer::~Framebuffer()
- {
- glDeleteFramebuffers(1, &m_renderer_id);
- glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data());
- glDeleteTextures(1, &m_depth_attachment);
- }
-
- auto Framebuffer::invalidate() -> void
- {
- if (m_renderer_id)
- {
- glDeleteFramebuffers(1, &m_renderer_id);
- glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data());
- glDeleteTextures(1, &m_depth_attachment);
-
- m_color_attachments.clear();
- m_depth_attachment = 0;
- }
-
- glGenFramebuffers(1, &m_renderer_id); // glCreateFramebuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
-
- bool multisample = m_specification.Samples > 1;
-
- if (m_color_attachment_specifications.size())
- {
- m_color_attachments.resize(m_color_attachment_specifications.size());
- Utils::CreateTextures(multisample, m_color_attachments.data(), static_cast<uint32_t>(m_color_attachments.size()));
-
- for (size_t i = 0; i < m_color_attachments.size(); i++)
- {
- Utils::bind_texture(multisample, m_color_attachments[i]);
- switch (m_color_attachment_specifications[i].texture_format)
- {
- case FramebufferTextureFormat::RGBA8:
- Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_RGBA8, GL_RGBA, m_specification.Width, m_specification.Height, static_cast<int>(i));
- break;
- case FramebufferTextureFormat::RED_INTEGER:
- Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_R32I, GL_RED_INTEGER, m_specification.Width, m_specification.Height, static_cast<int>(i));
- break;
- }
- }
- }
-
- if (m_depth_attachment_specification.texture_format != FramebufferTextureFormat::None)
- {
- Utils::CreateTextures(multisample, &m_depth_attachment, 1);
- Utils::bind_texture(multisample, m_depth_attachment);
- switch (m_depth_attachment_specification.texture_format)
- {
- case FramebufferTextureFormat::DEPTH24STENCIL8:
- Utils::AttachDepthTexture(m_depth_attachment, m_specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_specification.Width, m_specification.Height);
- break;
- }
- }
-
- if (m_color_attachments.size() > 1)
- {
- GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
- glDrawBuffers(static_cast<GLsizei>(m_color_attachments.size()), buffers);
- }
- else if (m_color_attachments.empty())
- glDrawBuffer(GL_NONE);
-
- if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
- {
- GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
- switch (status)
- {
- case GL_FRAMEBUFFER_UNDEFINED:
- DONUT_ERROR("Framebuffer is undefined");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
- DONUT_ERROR("Framebuffer has incomplete attachment");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
- DONUT_ERROR("Framebuffer is missing attachment");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
- DONUT_ERROR("Framebuffer has incomplete draw buffer");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
- DONUT_ERROR("Framebuffer has incomplete read buffer");
- break;
- case GL_FRAMEBUFFER_UNSUPPORTED:
- DONUT_ERROR("Framebuffer format is unsupported");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
- DONUT_ERROR("Framebuffer has incomplete multisample");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
- DONUT_ERROR("Framebuffer has incomplete layer targets");
- break;
- default:
- DONUT_ERROR("Framebuffer is incomplete (unknown error: {})", status);
- break;
- }
- }
-
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
-
- auto Framebuffer::bind() -> void
- {
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
- glViewport(0, 0, m_specification.Width, m_specification.Height);
- }
-
- auto Framebuffer::unbind() -> void
- {
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
-
- auto Framebuffer::resize(uint32_t width, uint32_t height) -> void
- {
- m_specification.Width = width;
- m_specification.Height = height;
-
- invalidate();
- }
-
- auto Framebuffer::read_pixel(uint32_t attachment_index, int x, int y) -> int
- {
- glReadBuffer(GL_COLOR_ATTACHMENT0 + attachment_index);
- int pixel_data;
- glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixel_data);
- return pixel_data;
- }
-
- auto Framebuffer::clear_attachment(uint32_t attachment_index, int value) -> void
- {
- // glClearTexImage is 4.4 and unavailable on macOS. clear the integer
- // attachment by binding this framebuffer and clearing its draw buffer.
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
- glClearBufferiv(GL_COLOR, static_cast<GLint>(attachment_index), &value);
- }
-
- auto Framebuffer::create(const FramebufferSpecification& spec) -> Ref<Framebuffer>
- {
- return create_ref<Framebuffer>(spec);
- }
-};
diff --git a/src/rendering/framebuffer.h b/src/rendering/framebuffer.h
deleted file mode 100644
index cf1fbef..0000000
--- a/src/rendering/framebuffer.h
+++ /dev/null
@@ -1,80 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "texture.h"
-
-#include <vector>
-
-namespace Donut
-{
- enum class FramebufferTextureFormat
- {
- None = 0,
-
- RGBA8,
- RED_INTEGER,
-
- DEPTH24STENCIL8,
-
- Depth = DEPTH24STENCIL8
- };
-
- struct FramebufferTextureSpecification
- {
- FramebufferTextureSpecification() = default;
- FramebufferTextureSpecification(FramebufferTextureFormat format)
- : texture_format(format) {}
-
- FramebufferTextureFormat texture_format = FramebufferTextureFormat::None;
- };
-
- struct FramebufferAttachmentSpecification
- {
- FramebufferAttachmentSpecification() = default;
- FramebufferAttachmentSpecification(std::initializer_list<FramebufferTextureSpecification> attachments)
- : attachments(attachments) {}
-
- std::vector<FramebufferTextureSpecification> attachments;
- };
-
- struct FramebufferSpecification
- {
- uint32_t Width = 0, Height = 0;
- FramebufferAttachmentSpecification attachments;
- uint32_t Samples = 1;
-
- bool SwapChainTarget = false;
- };
-
- class Framebuffer
- {
- public:
- Framebuffer(const FramebufferSpecification& spec);
- ~Framebuffer();
-
- auto bind() -> void;
- auto unbind() -> void;
-
- auto resize(uint32_t width, uint32_t height) -> void;
- auto read_pixel(uint32_t attachment_index, int x, int y) -> int;
-
- auto clear_attachment(uint32_t attachment_index, int value) -> void;
- auto get_color_attachment_renderer_id(uint32_t index = 0) const -> uint32_t { return m_color_attachments[index]; }
-
- auto get_specification() const -> const FramebufferSpecification& { return m_specification; }
-
- static auto create(const FramebufferSpecification& spec) -> Ref<Framebuffer>;
-
- private:
- auto invalidate() -> void;
-
- uint32_t m_renderer_id = 0;
- FramebufferSpecification m_specification;
-
- std::vector<FramebufferTextureSpecification> m_color_attachment_specifications;
- FramebufferTextureSpecification m_depth_attachment_specification = FramebufferTextureFormat::None;
-
- std::vector<uint32_t> m_color_attachments;
- uint32_t m_depth_attachment = 0;
- };
-};
diff --git a/src/rendering/index_buffer.cpp b/src/rendering/index_buffer.cpp
deleted file mode 100644
index 2c933e0..0000000
--- a/src/rendering/index_buffer.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-#include "index_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- IndexBuffer::IndexBuffer(const uint32_t* indices, uint32_t count)
- : m_count(count)
- {
- glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW);
- }
-
- IndexBuffer::~IndexBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto IndexBuffer::bind() const -> void
- {
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id);
- }
-
- auto IndexBuffer::unbind() const -> void
- {
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
- }
-
- auto IndexBuffer::create(const uint32_t* indices, uint32_t count) -> IndexBuffer*
- {
- return new IndexBuffer(indices, count);
- }
-};
diff --git a/src/rendering/index_buffer.h b/src/rendering/index_buffer.h
deleted file mode 100644
index 0b1b2b6..0000000
--- a/src/rendering/index_buffer.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include <cstdint>
-
-namespace Donut
-{
- class IndexBuffer
- {
- public:
- IndexBuffer(const uint32_t* indices, uint32_t count);
- ~IndexBuffer();
-
- auto bind() const -> void;
- auto unbind() const -> void;
- auto get_count() const -> uint32_t { return m_count; }
-
- static auto create(const uint32_t* indices, uint32_t count) -> IndexBuffer*;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_count = 0;
- };
-};
diff --git a/src/rendering/render_api.h b/src/rendering/render_api.h
new file mode 100644
index 0000000..62742c9
--- /dev/null
+++ b/src/rendering/render_api.h
@@ -0,0 +1,25 @@
+#pragma once
+
+namespace Donut
+{
+ // Which graphics backend the app runs on, selected once at startup (from
+ // settings) before the window is created. The rendering itself goes through
+ // the RHI (rendering/rhi.h) and the shared renderers; this is only the
+ // backend selector the window + application branch on.
+ class RendererAPI
+ {
+ public:
+ enum class API
+ {
+ None = 0,
+ OpenGL = 1,
+ Vulkan = 2,
+ };
+
+ static auto get_api() -> API { return s_api; }
+ static auto set_api(API api) -> void { s_api = api; }
+
+ private:
+ inline static API s_api = API::None;
+ };
+}
diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp
deleted file mode 100644
index 1c82447..0000000
--- a/src/rendering/renderer.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-#include "renderer.h"
-
-#include "core/log.h"
-
-#include <glad/glad.h>
-#include <GLFW/glfw3.h>
-#include <glm/gtc/matrix_transform.hpp>
-
-namespace Donut
-{
- RendererAPI::API RendererAPI::s_api = RendererAPI::API::OpenGL;
-
- auto RendererAPI::create() -> Scope<RendererAPI>
- {
- return create_scope<RendererAPI>();
- }
-
- auto RendererAPI::init() -> void
- {
- if (!glfwGetCurrentContext())
- {
- DONUT_ERROR("No OpenGL context is current! Cannot initialize GLAD.");
- return;
- }
-
- if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
- {
- DONUT_ERROR("Failed to initialize GLAD!");
- return;
- }
-
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- glEnable(GL_DEPTH_TEST);
- glDepthFunc(GL_LESS);
-
- glEnable(GL_CULL_FACE);
- glCullFace(GL_BACK);
- glFrontFace(GL_CCW);
- }
-
- auto RendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
- {
- glViewport(x, y, width, height);
- }
-
- auto RendererAPI::set_clear_color(const glm::vec4& color) -> void
- {
- glClearColor(color.r, color.g, color.b, color.a);
- }
-
- auto RendererAPI::clear() -> void
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- }
-
- auto RendererAPI::enable_depth_test() -> void
- {
- glEnable(GL_DEPTH_TEST);
- }
-
- auto RendererAPI::disable_depth_test() -> void
- {
- glDisable(GL_DEPTH_TEST);
- }
-
- auto RendererAPI::set_face_culling(bool enabled) -> void
- {
- if (enabled)
- {
- glEnable(GL_CULL_FACE);
- glCullFace(GL_BACK);
- glFrontFace(GL_CCW);
- }
- else
- glDisable(GL_CULL_FACE);
- }
-
- auto RendererAPI::enable_blending() -> void
- {
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- }
-
- auto RendererAPI::disable_blending() -> void
- {
- glDisable(GL_BLEND);
- }
-
- auto RendererAPI::draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
- {
- uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count();
- glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr);
- glBindTexture(GL_TEXTURE_2D, 0);
- }
-
- auto RendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void
- {
- glDrawArrays(GL_TRIANGLES, first, vertex_count);
- }
-
- auto RendererAPI::draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
- {
- uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count();
- glDrawElements(GL_LINES, count, GL_UNSIGNED_INT, nullptr);
- }
-
- auto RendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void
- {
- glActiveTexture(GL_TEXTURE0 + slot);
- glBindTexture(GL_TEXTURE_2D, texture_id);
- }
-
- auto RendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void
- {
- // Image load/store is OpenGL 4.2; the pointer is null on macOS (4.1).
- if (glBindImageTexture == nullptr)
- return;
- glBindImageTexture(slot, texture_id, 0, GL_FALSE, 0,
- read_only ? GL_READ_ONLY : GL_WRITE_ONLY, GL_RGBA8);
- }
-
- auto RendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void
- {
- glReadPixels(x, y, width, height, format, type, pixels);
- }
-
- auto Renderer::init() -> void
- {
- RenderCommand::init();
- RenderCommand::enable_depth_test();
- }
-
- auto Renderer::shutdown() -> void
- {
- }
-
- auto Renderer::on_window_resize(uint32_t width, uint32_t height) -> void
- {
- RenderCommand::set_viewport(0, 0, width, height);
- }
-
- auto Renderer::submit(const Ref<Shader>& shader,
- const Ref<VertexArray>& vertex_array,
- const glm::mat4& transform,
- const glm::mat4& view_projection) -> void
- {
- shader->bind();
- shader->set_mat4("u_ViewProjection", view_projection);
- shader->set_mat4("u_Transform", transform);
-
- vertex_array->bind();
- RenderCommand::draw_indexed(vertex_array);
- }
-
- Scope<RendererAPI> RenderCommand::s_renderer_api = RendererAPI::create();
-
- auto Renderer::set_clear_color(const glm::vec4& color) -> void
- {
- RenderCommand::set_clear_color(color);
- }
-
- auto Renderer::clear() -> void
- {
- RenderCommand::clear();
- }
-};
diff --git a/src/rendering/renderer.h b/src/rendering/renderer.h
deleted file mode 100644
index d7855a4..0000000
--- a/src/rendering/renderer.h
+++ /dev/null
@@ -1,155 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "vertex_array.h"
-#include "shader.h"
-#include "framebuffer.h"
-
-#include <glm/glm.hpp>
-
-namespace Donut
-{
- // Single concrete backend: the OpenGL renderer. (Vulkan runs through its own
- // dedicated VulkanRenderer, not this immediate-mode command layer.)
- class RendererAPI
- {
- public:
- enum class API
- {
- None = 0,
- OpenGL = 1,
- Vulkan = 2,
- };
-
- public:
- RendererAPI() = default;
- ~RendererAPI() = default;
-
- auto init() -> void;
- auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void;
- auto set_clear_color(const glm::vec4& color) -> void;
- auto clear() -> void;
- auto enable_depth_test() -> void;
- auto disable_depth_test() -> void;
- auto set_face_culling(bool enabled) -> void;
- auto enable_blending() -> void;
- auto disable_blending() -> void;
-
- auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void;
- auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void;
- auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void;
- auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void;
- auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void;
- auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void;
-
- inline static auto get_api() -> API { return s_api; }
- inline static auto set_api(API api) -> void { s_api = api; }
- static auto create() -> Scope<RendererAPI>;
-
- private:
- static API s_api;
- };
-
- class RenderCommand
- {
- public:
- inline static auto init() -> void
- {
- s_renderer_api->init();
- }
-
- inline static auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
- {
- s_renderer_api->set_viewport(x, y, width, height);
- }
-
- inline static auto set_clear_color(const glm::vec4& color) -> void
- {
- s_renderer_api->set_clear_color(color);
- }
-
- inline static auto clear() -> void
- {
- s_renderer_api->clear();
- }
-
- inline static auto enable_depth_test() -> void
- {
- s_renderer_api->enable_depth_test();
- }
-
- inline static auto disable_depth_test() -> void
- {
- s_renderer_api->disable_depth_test();
- }
-
- inline static auto set_face_culling(bool enabled) -> void
- {
- s_renderer_api->set_face_culling(enabled);
- }
-
- inline static auto enable_blending() -> void
- {
- s_renderer_api->enable_blending();
- }
-
- inline static auto disable_blending() -> void
- {
- s_renderer_api->disable_blending();
- }
-
- inline static auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void
- {
- s_renderer_api->draw_indexed(vertex_array, index_count);
- }
-
- inline static auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void
- {
- s_renderer_api->draw_arrays(vertex_count, first);
- }
-
- inline static auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void
- {
- s_renderer_api->draw_lines(vertex_array, index_count);
- }
-
- inline static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void
- {
- s_renderer_api->bind_texture(texture_id, slot);
- }
-
- inline static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void
- {
- s_renderer_api->bind_image_texture(texture_id, slot, read_only);
- }
-
- inline static auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void
- {
- s_renderer_api->read_pixels(x, y, width, height, format, type, pixels);
- }
-
- private:
- static Scope<RendererAPI> s_renderer_api;
- };
-
- class Renderer
- {
- public:
- static auto init() -> void;
- static auto shutdown() -> void;
-
- static auto on_window_resize(uint32_t width, uint32_t height) -> void;
-
- static auto submit(const Ref<Shader>& shader,
- const Ref<VertexArray>& vertex_array,
- const glm::mat4& transform,
- const glm::mat4& view_projection) -> void;
-
- static auto set_clear_color(const glm::vec4& color) -> void;
- static auto clear() -> void;
-
- inline static auto get_api() -> RendererAPI::API { return RendererAPI::get_api(); }
- };
-};
diff --git a/src/rendering/renderer_backend.h b/src/rendering/renderer_backend.h
index 6a4f1af..d34983f 100644
--- a/src/rendering/renderer_backend.h
+++ b/src/rendering/renderer_backend.h
@@ -1,14 +1,15 @@
#pragma once
#include <glm/glm.hpp>
-#include <functional>
-#include <string>
-#include <vector>
namespace Donut
{
- // A placeable/editable sphere in the world-builder scene view. Backend-
- // agnostic: the app owns the list, every backend renders it the same way.
+ // Backend-agnostic scene/black-hole data shared by the Application and the
+ // portable renderers (SceneRenderer, BlackHoleRenderer). The rendering itself
+ // is written once on the RHI (rendering/rhi.h); these are just the inputs.
+
+ // A placeable/editable sphere in the world-builder scene view. The app owns
+ // the list; SceneRenderer draws it the same way on every backend.
struct SceneObject
{
glm::vec3 position = { 0.0f, 2.0f, 0.0f };
@@ -28,46 +29,4 @@ namespace Donut
int quality_steps = 15000; // geodesic integration steps
float fov_degrees = 60.0f; // camera field of view
};
-
- // The swappable high-level renderer. Each graphics backend (OpenGL, Vulkan,
- // and later Metal / D3D12) implements this one interface; the Application,
- // states and UI drive it identically, so the app behaves the same no matter
- // which backend is active. Backends own their own device/swapchain/pipelines
- // and translate these high-level calls into their native API.
- class RendererBackend
- {
- public:
- virtual ~RendererBackend() = default;
-
- // glfwWindow is an opaque GLFW handle (created NO_API for Vulkan/Metal,
- // with a GL context for OpenGL). Returns false on failure.
- virtual auto init(void* glfwWindow, int width, int height) -> bool = 0;
-
- // Creates the ImGui context + this backend's ImGui platform/render impl.
- virtual auto init_ui() -> bool = 0;
- virtual auto shutdown() -> void = 0;
- virtual auto resize(int width, int height) -> void = 0;
-
- // Renders one frame: draws the current view (black hole or scene) from
- // the shared state, then invokes build_ui to draw the ImGui panel on top.
- virtual auto draw_frame(const glm::vec4& clear_color,
- const std::function<void()>& build_ui) -> void = 0;
-
- // --- shared view / camera state ------------------------------------
- virtual auto set_scene_mode(bool enabled) -> void = 0; // black hole vs scene view
- virtual auto is_scene_mode() const -> bool = 0;
- virtual auto set_free_fly(bool enabled) -> void = 0; // free-fly vs orbital camera
- virtual auto is_free_fly() const -> bool = 0;
- virtual auto reset_camera() -> void = 0;
-
- // --- shared content ------------------------------------------------
- virtual auto set_hdri(const std::string& path) -> void = 0;
- virtual auto current_hdri() const -> const std::string& = 0;
- virtual auto scene_objects() -> std::vector<SceneObject>& = 0;
- virtual auto set_selected_object(int index) -> void = 0;
- virtual auto black_hole_params() -> BlackHoleParams& = 0;
-
- // --- introspection for the UI --------------------------------------
- virtual auto device_name() const -> const std::string& = 0;
- };
}
diff --git a/src/rendering/rhi/rhi.h b/src/rendering/rhi.h
index f64252c..65f10b5 100644
--- a/src/rendering/rhi/rhi.h
+++ b/src/rendering/rhi.h
@@ -14,7 +14,7 @@
#include <vector>
#include <functional>
-namespace Donut::rhi
+namespace Donut::RHI
{
enum class Format
{
@@ -35,9 +35,11 @@ namespace Donut::rhi
struct VertexLayout { uint32_t stride = 0; std::vector<VertexAttribute> attributes; };
// A shader resource slot the pipeline exposes. `binding` is the set-0 binding
- // index shared by the GLSL/SPIR-V (the app binds resources to these before a draw).
+ // index used by Vulkan; `name` is the GLSL block/sampler identifier used by the
+ // OpenGL backend (GL 4.1 has no binding qualifier, so it binds by name). The app
+ // declares both when building a pipeline (it knows its own shader).
enum class ResourceKind { UniformBuffer, Texture };
- struct ResourceSlot { ResourceKind kind; uint32_t binding; };
+ struct ResourceSlot { ResourceKind kind; uint32_t binding; std::string name; };
// --- opaque GPU resources (backends subclass) --------------------------
class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; };
@@ -113,13 +115,12 @@ namespace Donut::rhi
Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref<RenderTarget> = 0;
virtual auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> = 0;
- // Frame loop: begin_frame returns the swapchain command list (or nullptr if
- // the frame is skipped, e.g. minimised); record into it, then end_frame
- // submits + presents. record_offscreen runs a self-contained pass whose
- // command list targets a RenderTarget (used before the swapchain frame).
+ // Frame loop: begin_frame returns the frame's command list (or nullptr if
+ // the frame is skipped, e.g. minimised); record one or more render passes
+ // into it — off-screen passes into RenderTargets first, then the swapchain
+ // pass (begin_render_pass(nullptr, ...)) — then end_frame submits + presents.
virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0;
virtual auto end_frame() -> void = 0;
- virtual auto record_offscreen(const std::function<void(CommandList&)>& record) -> void = 0;
// ImGui lives above the RHI but its platform/render backend is per-device.
virtual auto init_imgui() -> void = 0;
diff --git a/src/rendering/scene_renderer.cpp b/src/rendering/scene_renderer.cpp
new file mode 100644
index 0000000..e276074
--- /dev/null
+++ b/src/rendering/scene_renderer.cpp
@@ -0,0 +1,217 @@
+#include "scene_renderer.h"
+
+#include "core/log.h"
+
+#include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
+#include <cmath>
+#include <numbers>
+#include <vector>
+
+namespace Donut
+{
+ using namespace RHI;
+
+ namespace
+ {
+ // std140 layouts, matching the Slang ConstantBuffer structs exactly. glm's
+ // column-major matrices upload directly (the shaders decorate the members
+ // row_major and use mul(M,v), so the same bytes read correctly on every
+ // backend, with no transpose).
+ struct GridUBO {
+ glm::mat4 view_projection; // 0
+ glm::mat4 transform; // 64
+ float grid_size; float p0[3]; // 128
+ glm::vec3 grid_color; // 144
+ float grid_alpha; // 156
+ glm::vec3 camera_pos; // 160
+ float p1; // 172
+ };
+ static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch");
+
+ struct SphereUBO {
+ glm::mat4 view_projection; // 0
+ glm::mat4 transform; // 64
+ glm::vec3 color; float specular; // 128, 140
+ float emission; float p0[3]; // 144
+ glm::vec3 light_pos; float p1; // 160, 172
+ glm::vec3 camera_pos; int is_selected; // 176, 188
+ glm::vec3 outline_color; float outline_width; // 192, 204
+ };
+ static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch");
+
+ struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; };
+ static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch");
+ }
+
+ auto SceneRenderer::init(RHI::Device& device) -> bool
+ {
+ m_device = &device;
+
+ // Reference grid: line list on the XZ plane (+/-50, 1-unit cells). Grid.slang
+ // scales by u_GridSize/50, so u_GridSize = 50 keeps it 1:1.
+ {
+ std::vector<glm::vec3> lines;
+ const int N = 50;
+ for (int i = -N; i <= N; ++i)
+ {
+ lines.push_back({ (float)i, 0.0f, (float)-N });
+ lines.push_back({ (float)i, 0.0f, (float) N });
+ lines.push_back({ (float)-N, 0.0f, (float)i });
+ lines.push_back({ (float) N, 0.0f, (float)i });
+ }
+ m_grid_vertex_count = (int)lines.size();
+ m_grid_vb = device.create_buffer(BufferType::Vertex, lines.size() * sizeof(glm::vec3), lines.data());
+ m_grid_ubo = device.create_buffer(BufferType::Uniform, sizeof(GridUBO));
+
+ PipelineDesc d;
+ d.shader = "grid";
+ d.vertex_layout = { sizeof(glm::vec3), { { 0, 3, 0 } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "GridU" } };
+ d.topology = Topology::Lines;
+ d.blend = BlendMode::AlphaBlend;
+ d.depth_test = true; d.depth_write = false; d.depth_op = CompareOp::LessEqual;
+ d.has_depth = true;
+ m_grid_pipeline = device.create_pipeline(d);
+ }
+
+ // Lit sphere: unit UV-sphere (pos + normal), placed/scaled per object.
+ {
+ std::vector<float> sv; std::vector<uint32_t> si;
+ const int RINGS = 24, SECTORS = 48;
+ for (int r = 0; r <= RINGS; ++r)
+ {
+ float phi = (float)std::numbers::pi * r / RINGS;
+ for (int s = 0; s <= SECTORS; ++s)
+ {
+ float theta = 2.0f * (float)std::numbers::pi * s / SECTORS;
+ float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta);
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit)
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position
+ }
+ }
+ for (int r = 0; r < RINGS; ++r)
+ for (int s = 0; s < SECTORS; ++s)
+ {
+ uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1;
+ si.push_back(a); si.push_back(b); si.push_back(a + 1);
+ si.push_back(b); si.push_back(b + 1); si.push_back(a + 1);
+ }
+ m_sphere_index_count = (int)si.size();
+ m_sphere_vb = device.create_buffer(BufferType::Vertex, sv.size() * sizeof(float), sv.data());
+ m_sphere_ib = device.create_buffer(BufferType::Index, si.size() * sizeof(uint32_t), si.data());
+
+ PipelineDesc d;
+ d.shader = "sphere";
+ d.vertex_layout = { 6 * sizeof(float), { { 0, 3, 0 }, { 1, 3, 3 * sizeof(float) } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "SphereU" },
+ { ResourceKind::Texture, 1, "u_HDRIEnvironment" } };
+ d.topology = Topology::Triangles;
+ d.depth_test = true; d.depth_write = true; d.depth_op = CompareOp::Less;
+ d.has_depth = true;
+ m_sphere_pipeline = device.create_pipeline(d);
+ }
+
+ // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex
+ // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry.
+ {
+ const float cube[] = {
+ -1,-1,-1, 1,-1,-1, 1, 1,-1, 1, 1,-1, -1, 1,-1, -1,-1,-1,
+ -1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1,-1, 1,
+ -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1,
+ 1, 1, 1, 1, 1,-1, 1,-1,-1, 1,-1,-1, 1,-1, 1, 1, 1, 1,
+ -1,-1,-1, 1,-1,-1, 1,-1, 1, 1,-1, 1, -1,-1, 1, -1,-1,-1,
+ -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1
+ };
+ m_skybox_vb = device.create_buffer(BufferType::Vertex, sizeof(cube), cube);
+ m_skybox_ubo = device.create_buffer(BufferType::Uniform, sizeof(SkyboxUBO));
+
+ PipelineDesc d;
+ d.shader = "skybox";
+ d.vertex_layout = { 3 * sizeof(float), { { 0, 3, 0 } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "SkyboxU" },
+ { ResourceKind::Texture, 1, "u_Skybox" } };
+ d.topology = Topology::Triangles;
+ d.depth_test = false; d.depth_write = false;
+ d.has_depth = true;
+ m_skybox_pipeline = device.create_pipeline(d);
+ }
+
+ DONUT_INFO("SceneRenderer ready ({} grid verts, {} sphere indices, skybox)",
+ m_grid_vertex_count, m_sphere_index_count);
+ return true;
+ }
+
+ auto SceneRenderer::render(RHI::CommandList& cmd, const CameraView& cam,
+ const std::vector<SceneObject>& objects, int selected,
+ RHI::Texture* cubemap) -> void
+ {
+ // Full-window viewport; flip_y lets the backend match the shared top-left
+ // orientation (Vulkan flips via a negative-height viewport, GL is a no-op).
+ cmd.set_viewport(0, 0, cam.fb_width, cam.fb_height, true);
+
+ glm::mat4 vp = cam.projection * cam.view;
+
+ // Skybox background first (depth 1, no depth test/write).
+ {
+ SkyboxUBO sky{};
+ sky.projection = cam.projection;
+ sky.view = glm::mat4(glm::mat3(cam.view)); // strip translation
+ m_skybox_ubo->update(&sky, sizeof(sky));
+ cmd.bind_pipeline(m_skybox_pipeline.get());
+ cmd.bind_uniform(0, m_skybox_ubo.get());
+ cmd.bind_texture(1, cubemap);
+ cmd.bind_vertex_buffer(m_skybox_vb.get());
+ cmd.draw(36);
+ }
+
+ // Opaque spheres (write depth). One UBO per object slot avoids aliasing the
+ // per-draw uniforms across the deferred command stream.
+ {
+ int count = std::min((int)objects.size(), MAX_OBJECTS);
+ while ((int)m_sphere_ubos.size() < count)
+ m_sphere_ubos.push_back(m_device->create_buffer(BufferType::Uniform, sizeof(SphereUBO)));
+
+ cmd.bind_pipeline(m_sphere_pipeline.get());
+ for (int i = 0; i < count; ++i)
+ {
+ const SceneObject& o = objects[i];
+ SphereUBO s{};
+ s.view_projection = vp;
+ s.transform = glm::translate(glm::mat4(1.0f), o.position)
+ * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius));
+ s.color = o.color;
+ s.specular = 0.6f;
+ s.emission = 0.0f;
+ s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f);
+ s.camera_pos = cam.position;
+ s.is_selected = (i == selected) ? 1 : 0;
+ s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f);
+ s.outline_width = 0.15f;
+ m_sphere_ubos[i]->update(&s, sizeof(s));
+
+ cmd.bind_uniform(0, m_sphere_ubos[i].get());
+ cmd.bind_texture(1, cubemap);
+ cmd.bind_vertex_buffer(m_sphere_vb.get());
+ cmd.bind_index_buffer(m_sphere_ib.get());
+ cmd.draw_indexed(m_sphere_index_count);
+ }
+ }
+
+ // Transparent grid on top (tests depth, doesn't write).
+ {
+ GridUBO g{};
+ g.view_projection = vp;
+ g.transform = glm::mat4(1.0f);
+ g.grid_size = 50.0f;
+ g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f);
+ g.grid_alpha = 0.75f;
+ g.camera_pos = cam.position;
+ m_grid_ubo->update(&g, sizeof(g));
+ cmd.bind_pipeline(m_grid_pipeline.get());
+ cmd.bind_uniform(0, m_grid_ubo.get());
+ cmd.bind_vertex_buffer(m_grid_vb.get());
+ cmd.draw(m_grid_vertex_count);
+ }
+ }
+}
diff --git a/src/rendering/scene_renderer.h b/src/rendering/scene_renderer.h
new file mode 100644
index 0000000..eafd56e
--- /dev/null
+++ b/src/rendering/scene_renderer.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "rhi.h"
+#include "renderer_backend.h" // SceneObject
+#include <glm/glm.hpp>
+#include <vector>
+
+namespace Donut
+{
+ // The world-builder scene view (skybox + lit spheres + reference grid),
+ // written ONCE against the RHI so it renders identically on every backend.
+ // The Application owns the camera, the object list and the HDRI cubemap and
+ // passes them in each frame; this class owns only the GPU resources (meshes,
+ // pipelines, per-object uniform buffers) and records the draws.
+ struct CameraView
+ {
+ glm::mat4 view;
+ glm::mat4 projection;
+ glm::vec3 position;
+ int fb_width = 1;
+ int fb_height = 1;
+ };
+
+ class SceneRenderer
+ {
+ public:
+ // Builds the grid/sphere/skybox meshes and pipelines on the given device.
+ auto init(RHI::Device& device) -> bool;
+
+ // Records the scene into the currently-open swapchain render pass. `cubemap`
+ // is the shared HDRI environment (sphere ambient + skybox background).
+ auto render(RHI::CommandList& cmd, const CameraView& cam,
+ const std::vector<SceneObject>& objects, int selected,
+ RHI::Texture* cubemap) -> void;
+
+ private:
+ static constexpr int MAX_OBJECTS = 64;
+
+ RHI::Device* m_device = nullptr;
+
+ Ref<RHI::Pipeline> m_grid_pipeline;
+ Ref<RHI::Buffer> m_grid_vb;
+ Ref<RHI::Buffer> m_grid_ubo;
+ int m_grid_vertex_count = 0;
+
+ Ref<RHI::Pipeline> m_sphere_pipeline;
+ Ref<RHI::Buffer> m_sphere_vb;
+ Ref<RHI::Buffer> m_sphere_ib;
+ int m_sphere_index_count = 0;
+ std::vector<Ref<RHI::Buffer>> m_sphere_ubos; // one per object slot
+
+ Ref<RHI::Pipeline> m_skybox_pipeline;
+ Ref<RHI::Buffer> m_skybox_vb;
+ Ref<RHI::Buffer> m_skybox_ubo;
+ };
+}
diff --git a/src/rendering/texture_manager.cpp b/src/rendering/texture_manager.cpp
deleted file mode 100644
index 3afba0d..0000000
--- a/src/rendering/texture_manager.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-#include "texture_manager.h"
-#include "renderer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- auto TextureManager::create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D>
- {
- return create_ref<Texture2D>(width, height);
- }
-
- auto TextureManager::bind_texture(uint32_t texture_id, uint32_t slot) -> void
- {
- RenderCommand::bind_texture(texture_id, slot);
- }
-
- auto TextureManager::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void
- {
- RenderCommand::bind_image_texture(texture_id, slot, read_only);
- }
-
- auto TextureManager::set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void
- {
- glBindTexture(GL_TEXTURE_2D, texture_id);
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
- }
-
- auto TextureManager::resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void
- {
- glBindTexture(GL_TEXTURE_2D, texture_id);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
- }
-};
diff --git a/src/rendering/texture_manager.h b/src/rendering/texture_manager.h
deleted file mode 100644
index 02c32b1..0000000
--- a/src/rendering/texture_manager.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "rendering/texture.h"
-#include <cstdint>
-
-namespace Donut
-{
- class TextureManager
- {
- public:
- static auto create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D>;
-
- static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void;
- static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void;
- static auto set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void;
- static auto resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void;
- };
-};
diff --git a/src/rendering/uniform_buffer.cpp b/src/rendering/uniform_buffer.cpp
deleted file mode 100644
index c4118b8..0000000
--- a/src/rendering/uniform_buffer.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#include "uniform_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- UniformBuffer::UniformBuffer(uint32_t size, uint32_t binding)
- : m_size(size), m_binding(binding)
- {
- glGenBuffers(1, &m_renderer_id);
- glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id);
- glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);
- glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id);
- }
-
- UniformBuffer::~UniformBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto UniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void
- {
- glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id);
- glBufferSubData(GL_UNIFORM_BUFFER, offset, size, data);
- }
-
- auto UniformBuffer::bind(uint32_t binding) -> void
- {
- glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id);
- }
-
- auto UniformBuffer::create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer>
- {
- return create_ref<UniformBuffer>(size, binding);
- }
-};
diff --git a/src/rendering/uniform_buffer.h b/src/rendering/uniform_buffer.h
deleted file mode 100644
index b5617b9..0000000
--- a/src/rendering/uniform_buffer.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include <cstdint>
-
-namespace Donut
-{
- class UniformBuffer
- {
- public:
- UniformBuffer(uint32_t size, uint32_t binding);
- ~UniformBuffer();
-
- auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void;
- auto bind(uint32_t binding) -> void;
-
- static auto create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer>;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_size = 0;
- uint32_t m_binding = 0;
- };
-};
diff --git a/src/rendering/vertex_array.cpp b/src/rendering/vertex_array.cpp
deleted file mode 100644
index cddcf35..0000000
--- a/src/rendering/vertex_array.cpp
+++ /dev/null
@@ -1,61 +0,0 @@
-#include <glad/glad.h>
-
-#include "vertex_array.h"
-
-namespace Donut
-{
- VertexArray::VertexArray()
- {
- // glCreateVertexArrays is 4.5 DSA; macOS caps at 4.1. glGenVertexArrays
- // reserves the name and the VAO is created on first bind (done below).
- glGenVertexArrays(1, &m_renderer_id);
- }
-
- VertexArray::~VertexArray()
- {
- glDeleteVertexArrays(1, &m_renderer_id);
- }
-
- auto VertexArray::bind() const -> void
- {
- glBindVertexArray(m_renderer_id);
- }
-
- auto VertexArray::unbind() const -> void
- {
- glBindVertexArray(0);
- }
-
- auto VertexArray::add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void
- {
- glBindVertexArray(m_renderer_id);
- vertex_buffer->bind();
-
- const auto& layout = vertex_buffer->get_layout();
- for (const auto& element : layout.get_elements())
- {
- glEnableVertexAttribArray(m_vertex_buffer_index);
- glVertexAttribPointer(m_vertex_buffer_index,
- element.count,
- element.type,
- element.normalized ? GL_TRUE : GL_FALSE,
- layout.get_stride(),
- reinterpret_cast<const void*>(static_cast<uintptr_t>(element.offset)));
- m_vertex_buffer_index++;
- }
-
- m_vertex_buffers.push_back(vertex_buffer);
- }
-
- auto VertexArray::set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void
- {
- glBindVertexArray(m_renderer_id);
- index_buffer->bind();
- m_index_buffer = index_buffer;
- }
-
- auto VertexArray::create() -> VertexArray*
- {
- return new VertexArray();
- }
-};
diff --git a/src/rendering/vertex_array.h b/src/rendering/vertex_array.h
deleted file mode 100644
index 1f06758..0000000
--- a/src/rendering/vertex_array.h
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-
-#include "vertex_buffer.h"
-#include "index_buffer.h"
-
-#include <vector>
-
-namespace Donut
-{
- class VertexArray
- {
- public:
- VertexArray();
- ~VertexArray();
-
- auto bind() const -> void;
- auto unbind() const -> void;
-
- auto add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void;
- auto set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void;
-
- auto get_vertex_buffers() const -> const std::vector<Ref<VertexBuffer>>& { return m_vertex_buffers; }
- auto get_index_buffer() const -> const Ref<IndexBuffer>& { return m_index_buffer; }
-
- static auto create() -> VertexArray*;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_vertex_buffer_index = 0;
- std::vector<Ref<VertexBuffer>> m_vertex_buffers;
- Ref<IndexBuffer> m_index_buffer;
- };
-};
diff --git a/src/rendering/vertex_buffer.cpp b/src/rendering/vertex_buffer.cpp
deleted file mode 100644
index 1bc471c..0000000
--- a/src/rendering/vertex_buffer.cpp
+++ /dev/null
@@ -1,50 +0,0 @@
-#include "vertex_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- auto VertexBufferElement::get_size_of_type(uint32_t type) -> uint32_t
- {
- switch (type)
- {
- case 0x1406: return 4; // GL_FLOAT
- case 0x1405: return 4; // GL_UNSIGNED_INT
- case 0x1401: return 1; // GL_UNSIGNED_BYTE
- default: return 0;
- }
- }
-
- VertexBuffer::VertexBuffer(const void* data, uint32_t size)
- {
- glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
- }
-
- VertexBuffer::~VertexBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto VertexBuffer::bind() const -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- }
-
- auto VertexBuffer::unbind() const -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, 0);
- }
-
- auto VertexBuffer::set_data(const void* data, uint32_t size) -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
- }
-
- auto VertexBuffer::create(const void* data, uint32_t size) -> VertexBuffer*
- {
- return new VertexBuffer(data, size);
- }
-};
diff --git a/src/rendering/vertex_buffer.h b/src/rendering/vertex_buffer.h
deleted file mode 100644
index 4e61700..0000000
--- a/src/rendering/vertex_buffer.h
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma once
-
-#include <cstdint>
-#include <vector>
-
-namespace Donut
-{
- struct VertexBufferElement
- {
- uint32_t type;
- uint32_t count;
- uint8_t normalized;
- uint32_t offset;
-
- static auto get_size_of_type(uint32_t type) -> uint32_t;
- };
-
- class VertexBufferLayout
- {
- public:
- VertexBufferLayout() = default;
- ~VertexBufferLayout() = default;
-
- template<typename T>
- auto push(uint32_t count) -> void
- {
- static_assert(false);
- }
-
- template<>
- void push<float>(uint32_t count)
- {
- m_elements.push_back({ 0x1406, count, 0, m_stride }); // GL_FLOAT
- m_stride += count * VertexBufferElement::get_size_of_type(0x1406);
- }
-
- template<>
- void push<uint32_t>(uint32_t count)
- {
- m_elements.push_back({ 0x1405, count, 0, m_stride }); // GL_UNSIGNED_INT
- m_stride += count * VertexBufferElement::get_size_of_type(0x1405);
- }
-
- template<>
- void push<uint8_t>(uint32_t count)
- {
- m_elements.push_back({ 0x1401, count, 1, m_stride }); // GL_UNSIGNED_BYTE
- m_stride += count * VertexBufferElement::get_size_of_type(0x1401);
- }
-
- inline auto get_elements() const -> const std::vector<VertexBufferElement>& { return m_elements; }
- inline auto get_stride() const -> uint32_t { return m_stride; }
-
- private:
- std::vector<VertexBufferElement> m_elements;
- uint32_t m_stride = 0;
- };
-
- class VertexBuffer
- {
- public:
- VertexBuffer(const void* data, uint32_t size);
- ~VertexBuffer();
-
- auto bind() const -> void;
- auto unbind() const -> void;
- auto set_data(const void* data, uint32_t size) -> void;
-
- auto get_layout() const -> const VertexBufferLayout& { return m_layout; }
- auto set_layout(const VertexBufferLayout& layout) -> void { m_layout = layout; }
-
- static auto create(const void* data, uint32_t size) -> VertexBuffer*;
-
- private:
- uint32_t m_renderer_id = 0;
- VertexBufferLayout m_layout;
- };
-};
diff --git a/src/states/config_state.cpp b/src/states/config_state.cpp
deleted file mode 100644
index d5fca87..0000000
--- a/src/states/config_state.cpp
+++ /dev/null
@@ -1,307 +0,0 @@
-#include "config_state.h"
-#include "rendering/renderer.h"
-#include "core/application.h"
-#include "core/theme_manager.h"
-#include "core/settings_manager.h"
-
-#include <imgui.h>
-#include <imgui_internal.h>
-#include <glad/glad.h>
-
-namespace Donut
-{
- auto ConfigState::on_enter() -> void
- {
- DONUT_INFO("Entering Config State");
-
- const auto& settings = SettingsManager::get_settings_const();
-
- m_selected_api = (settings.graphics.render_api == "Vulkan") ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL;
- m_selected_theme = (settings.graphics.selected_theme == "Light") ? 1 :
- (settings.graphics.selected_theme == "Blue") ? 2 : 0;
- m_target_fps = settings.simulation.target_fps;
- m_compute_height = settings.simulation.compute_height;
- m_max_steps_moving = settings.simulation.max_steps_moving;
- m_max_steps_static = settings.simulation.max_steps_static;
- m_early_exit_distance = settings.simulation.early_exit_distance;
- m_gravity_enabled = settings.simulation.gravity_enabled;
- m_v_sync_enabled = settings.graphics.v_sync_enabled;
- m_show_fps = settings.graphics.show_fps;
- m_show_performance_metrics = settings.graphics.show_performance_metrics;
- m_show_debug_info = settings.graphics.show_debug_info;
- m_enable_anti_aliasing = settings.graphics.enable_anti_aliasing;
- }
-
- auto ConfigState::on_exit() -> void
- {
- DONUT_INFO("Exiting Config State");
- }
-
- auto ConfigState::on_update(float delta_time) -> void
- {
- if (m_show_restart_message)
- {
- m_restart_message_timer += delta_time;
- if (m_restart_message_timer > 3.0f)
- {
- m_show_restart_message = false;
- m_restart_message_timer = 0.0f;
- }
- }
- }
-
- auto ConfigState::on_render() -> void
- {
- Renderer::set_clear_color({ 0.1f, 0.1f, 0.1f, 1.0f });
- Renderer::clear();
- }
-
- auto ConfigState::on_event(Event& event) -> void
- {
- }
-
- auto ConfigState::on_im_ui_render() -> void
- {
- ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
- ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
- ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
-
- ImGui::Begin("Donut Configuration", nullptr, ImGuiWindowFlags_NoCollapse);
-
- ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
- ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Donut Configuration");
- ImGui::PopFont();
- ImGui::Separator();
-
- ImGui::Columns(2, "ConfigColumns", true);
-
- float available_height = ImGui::GetWindowHeight() - 140;
-
- ImGui::BeginChild("GraphicsSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Graphics Settings");
- ImGui::Separator();
-
- ImGui::Text("Render API:");
- ImGui::SameLine();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(requires restart)");
-
- const char* api_names[] = { "OpenGL", "Vulkan" };
- static int current_api = (int)m_selected_api - 1;
-
- if (ImGui::BeginCombo("##RenderAPI", api_names[current_api]))
- {
- for (int i = 0; i < IM_ARRAYSIZE(api_names); i++)
- {
- const bool is_selected = (current_api == i);
- if (ImGui::Selectable(api_names[i], is_selected))
- {
- current_api = i;
- m_selected_api = (RendererAPI::API)(i + 1);
- m_show_restart_message = true;
- m_restart_message_timer = 0.0f;
- }
-
- if (is_selected)
- ImGui::SetItemDefaultFocus();
- }
-
- ImGui::EndCombo();
- }
-
- ImGui::Text("Current API: ");
- ImGui::SameLine();
- const char* current_api_name = (Renderer::get_api() == RendererAPI::API::OpenGL) ? "OpenGL" : "Vulkan";
- ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), current_api_name);
-
- ImGui::Spacing();
-
- ImGui::Checkbox("Enable VSync", &m_v_sync_enabled);
- ImGui::SameLine();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(recommended)");
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Display");
- ImGui::Separator();
-
- ImGui::Text("Window Size: 1280x720");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Fullscreen: Not implemented yet");
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "System Info");
- ImGui::Separator();
-
- ImGui::Text("OpenGL Version: %s", glGetString(GL_VERSION));
- ImGui::Text("GPU: %s", glGetString(GL_RENDERER));
- ImGui::Text("Vendor: %s", glGetString(GL_VENDOR));
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance");
- ImGui::Separator();
-
- ImGui::SliderInt("Target FPS", &m_target_fps, 30, 120, "%d FPS");
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Target frame rate for the simulation");
-
- ImGui::Checkbox("Show FPS Counter", &m_show_fps);
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Display current FPS in the simulation");
-
- ImGui::Checkbox("Show Performance Metrics", &m_show_performance_metrics);
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Show detailed performance information");
-
- ImGui::Checkbox("Show Debug Info", &m_show_debug_info);
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Display debug information and statistics");
-
- ImGui::Checkbox("Enable Anti-Aliasing", &m_enable_anti_aliasing);
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Enable anti-aliasing for smoother rendering");
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Theme");
- ImGui::Separator();
-
- const char* theme_names[] = { "Dark", "Light", "Blue" };
- if (ImGui::Combo("UI Theme", &m_selected_theme, theme_names, IM_ARRAYSIZE(theme_names)))
- ThemeManager::set_theme(static_cast<Theme>(m_selected_theme));
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Choose the application theme");
-
- ImGui::EndChild();
-
- ImGui::NextColumn();
-
- ImGui::BeginChild("SimulationSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Settings");
- ImGui::Separator();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality");
- ImGui::Separator();
-
- ImGui::SliderInt("Compute Height", &m_compute_height, 64, 2048, "%d px");
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Resolution of the compute shader. Higher values give better quality but lower performance.");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Higher = better quality, lower performance");
-
- ImGui::SliderInt("Max Steps (Moving)", &m_max_steps_moving, 1000, 60000, "%d");
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Maximum ray marching steps when camera is moving");
-
- ImGui::SliderInt("Max Steps (Static)", &m_max_steps_static, 1000, 30000, "%d");
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Maximum ray marching steps when camera is stationary");
-
- ImGui::SliderFloat("Early Exit Distance", &m_early_exit_distance, 1e11f, 1e13f, "%.2e");
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Distance at which ray marching stops to improve performance");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Distance at which ray marching stops");
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics");
- ImGui::Separator();
-
- ImGui::Checkbox("Enable Gravity", &m_gravity_enabled);
-
- ImGui::EndChild();
-
- ImGui::Columns(1);
-
- ImGui::Spacing();
- ImGui::Separator();
- ImGui::Spacing();
-
- float button_width = (ImGui::GetWindowWidth() - 120) / 5.0f;
-
- if (ImGui::Button("World Builder", ImVec2(button_width, 35)))
- {
- apply_settings();
- Application::get().get_state_manager().switch_to_state("WorldBuilder");
- }
-
- ImGui::SameLine();
- if (ImGui::Button("Reset to Defaults", ImVec2(button_width, 35)))
- reset_to_defaults();
-
- ImGui::SameLine();
- if (ImGui::Button("Apply Settings", ImVec2(button_width, 35)))
- apply_settings();
-
- ImGui::SameLine();
- if (ImGui::Button("Save Settings", ImVec2(button_width, 35)))
- {
- apply_settings();
- DONUT_INFO("Settings saved manually");
- }
-
- ImGui::SameLine();
- if (ImGui::Button("Exit", ImVec2(button_width, 35)))
- Application::get().close();
-
- if (m_show_restart_message)
- {
- ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10);
- ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
- ImGui::TextWrapped("Warning: Render API changed! Please restart the application for changes to take effect.");
- ImGui::PopStyleColor();
- }
-
- ImGui::Spacing();
- ImGui::Separator();
- ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Donut Engine v1.0.0 | Black Hole Simulation");
-
- ImGui::End();
- }
-
- auto ConfigState::apply_settings() -> void
- {
- SimulationSettings sim_settings;
- sim_settings.target_fps = m_target_fps;
- sim_settings.compute_height = m_compute_height;
- sim_settings.max_steps_moving = m_max_steps_moving;
- sim_settings.max_steps_static = m_max_steps_static;
- sim_settings.early_exit_distance = m_early_exit_distance;
- sim_settings.gravity_enabled = m_gravity_enabled;
-
- GraphicsSettings gfx_settings;
- gfx_settings.render_api = (m_selected_api == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL";
- gfx_settings.v_sync_enabled = m_v_sync_enabled;
- gfx_settings.show_fps = m_show_fps;
- gfx_settings.show_performance_metrics = m_show_performance_metrics;
- gfx_settings.show_debug_info = m_show_debug_info;
- gfx_settings.enable_anti_aliasing = m_enable_anti_aliasing;
- gfx_settings.selected_theme = (m_selected_theme == 1) ? "Light" :
- (m_selected_theme == 2) ? "Blue" : "Dark";
-
- SettingsManager::set_simulation_settings(sim_settings);
- SettingsManager::set_graphics_settings(gfx_settings);
-
- RendererAPI::set_api(m_selected_api);
- DONUT_INFO("Settings applied and saved");
- }
-
- auto ConfigState::reset_to_defaults() -> void
- {
- m_selected_api = RendererAPI::API::OpenGL;
- m_target_fps = 60;
- m_compute_height = 512;
- m_max_steps_moving = 30000;
- m_max_steps_static = 15000;
- m_early_exit_distance = 5e12f;
- m_gravity_enabled = true;
- m_v_sync_enabled = true;
- m_show_fps = true;
- m_show_performance_metrics = true;
- m_show_debug_info = false;
- m_enable_anti_aliasing = true;
- m_selected_theme = 0;
-
- apply_settings();
- DONUT_INFO("Settings reset to defaults and saved");
- }
-};
diff --git a/src/states/config_state.h b/src/states/config_state.h
deleted file mode 100644
index e64889b..0000000
--- a/src/states/config_state.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#pragma once
-
-#include "core/state.h"
-#include "core/event.h"
-#include "core/log.h"
-#include "rendering/renderer.h"
-
-namespace Donut
-{
- class ConfigState
- : public State
- {
- public:
- ~ConfigState() = default;
-
- auto on_enter() -> void override;
- auto on_exit() -> void override;
- auto on_update(float delta_time) -> void override;
- auto on_render() -> void override;
- auto on_im_ui_render() -> void override;
- auto on_event(Event& event) -> void override;
-
- private:
- auto apply_settings() -> void;
- auto reset_to_defaults() -> void;
-
- private:
- RendererAPI::API m_selected_api = RendererAPI::API::OpenGL;
- bool m_show_restart_message = false;
- float m_restart_message_timer = 0.0f;
-
- int m_target_fps = 60;
- int m_compute_height = 512;
- int m_max_steps_moving = 30000;
- int m_max_steps_static = 15000;
- float m_early_exit_distance = 5e12f;
- bool m_gravity_enabled = true;
-
- bool m_v_sync_enabled = true;
- bool m_show_fps = true;
- bool m_show_performance_metrics = true;
- bool m_show_debug_info = false;
- bool m_enable_anti_aliasing = true;
-
- int m_selected_theme = 0; // 0=Dark, 1=Light, 2=Blue
- };
-}; \ No newline at end of file
diff --git a/src/states/simulation_state.cpp b/src/states/simulation_state.cpp
deleted file mode 100644
index 06955ff..0000000
--- a/src/states/simulation_state.cpp
+++ /dev/null
@@ -1,386 +0,0 @@
-#include "simulation_state.h"
-#include "rendering/renderer.h"
-#include "core/application.h"
-#include "core/hdri_manager.h"
-#include "core/window.h"
-#include "core/event.h"
-#include "core/settings_manager.h"
-
-#include <imgui.h>
-#include <GLFW/glfw3.h>
-#include <chrono>
-#include <iomanip>
-#include <sstream>
-#include <vector>
-
-namespace Donut
-{
- auto SimulationState::on_enter() -> void
- {
- DONUT_INFO("Entering Simulation State");
-
- const auto& settings = SettingsManager::get_settings_const();
- auto& engine = Application::get().get_engine();
-
- engine.set_target_fps(settings.simulation.target_fps);
- engine.set_compute_height(settings.simulation.compute_height);
- engine.set_max_steps_moving(settings.simulation.max_steps_moving);
- engine.set_max_steps_static(settings.simulation.max_steps_static);
- engine.set_early_exit_distance(settings.simulation.early_exit_distance);
- engine.get_gravity() = settings.simulation.gravity_enabled;
-
- engine.set_disk_thickness(settings.simulation.disk_thickness);
- engine.set_disk_density(settings.simulation.disk_density);
- engine.set_rotation_speed(settings.simulation.rotation_speed);
- engine.set_blur_strength(settings.simulation.blur_strength);
- engine.set_glow_intensity(settings.simulation.glow_intensity);
-
- engine.update_compute_dimensions();
- m_initialized = true;
- }
-
- auto SimulationState::on_exit() -> void
- {
- DONUT_INFO("Exiting Simulation State");
- }
-
- auto SimulationState::on_update(float delta_time) -> void
- {
- auto& engine = Application::get().get_engine();
- engine.update_performance(delta_time);
- engine.update_window_dimensions();
- engine.update_physics(delta_time);
-
- if (engine.get_camera().is_dragging())
- {
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- double xpos, ypos;
- glfwGetCursorPos(window, &xpos, &ypos);
- engine.get_camera().process_orbital_mouse_move(xpos, ypos);
- }
- }
-
- auto SimulationState::on_render() -> void
- {
- auto& engine = Application::get().get_engine();
- RenderCommand::set_clear_color(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
- RenderCommand::clear();
- RenderCommand::set_viewport(0, 0, static_cast<uint32_t>(engine.get_width()), static_cast<uint32_t>(engine.get_height()));
-
- engine.dispatch_compute(engine.get_camera());
- engine.draw_blur_pass();
- }
-
- auto SimulationState::on_event(Event& event) -> void
- {
- auto& engine = Application::get().get_engine();
-
- if (event.get_event_type() == EventType::MouseButtonPressed)
- {
- MouseButtonPressedEvent& e = (MouseButtonPressedEvent&)event;
- int button = e.get_mouse_button();
-
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- Camera& camera = engine.get_camera();
- double last_x = camera.get_last_x();
- double last_y = camera.get_last_y();
- glfwGetCursorPos(window, &last_x, &last_y);
-
- camera.process_orbital_mouse_button(button, GLFW_PRESS, 0);
- }
- else if (event.get_event_type() == EventType::MouseButtonReleased)
- {
- MouseButtonReleasedEvent& e = (MouseButtonReleasedEvent&)event;
- int button = e.get_mouse_button();
- engine.get_camera().process_orbital_mouse_button(button, GLFW_RELEASE, 0);
- }
-
- if (event.get_event_type() == EventType::MouseScrolled)
- {
- MouseScrolledEvent& e = (MouseScrolledEvent&)event;
- engine.get_camera().process_orbital_scroll(e.get_x_offset(), e.get_y_offset());
- }
-
- if (event.get_event_type() == EventType::KeyPressed)
- {
- KeyPressedEvent& e = (KeyPressedEvent&)event;
- if (e.get_key_code() == GLFW_KEY_G)
- {
- engine.get_gravity() = !engine.get_gravity();
- DONUT_INFO("Gravity turned {}", engine.get_gravity() ? "ON" : "OFF");
- }
- }
- }
-
- auto SimulationState::on_im_ui_render() -> void
- {
- auto& engine = Application::get().get_engine();
-
- ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver);
- ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 420, 20), ImGuiCond_FirstUseEver);
-
- ImGui::Begin("Simulation Controls", nullptr, ImGuiWindowFlags_NoCollapse);
-
- ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Black Hole Simulation");
- ImGui::SameLine();
- if (ImGui::Button("Back to Config"))
- Application::get().get_state_manager().switch_to_state("Config");
- ImGui::SameLine();
- if (ImGui::Button("Save Settings"))
- {
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.target_fps = engine.get_target_fps();
- settings.compute_height = engine.get_compute_height();
- settings.max_steps_moving = engine.get_max_steps_moving();
- settings.max_steps_static = engine.get_max_steps_static();
- settings.early_exit_distance = engine.get_early_exit_distance();
- settings.gravity_enabled = engine.get_gravity();
- SettingsManager::set_simulation_settings(settings);
- DONUT_INFO("Simulation settings saved manually");
- }
-
- ImGui::Separator();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance");
- ImGui::Separator();
-
- ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
- ImGui::Text("Frame Time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate);
- ImGui::Text("Engine FPS: %.1f", engine.get_current_fps());
-
- int target_fps = engine.get_target_fps();
- if (ImGui::SliderInt("Target FPS", &target_fps, 30, 120))
- {
- engine.set_target_fps(target_fps);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.target_fps = target_fps;
- SettingsManager::set_simulation_settings(settings);
- }
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Info");
- ImGui::Separator();
-
- ImGui::Text("Resolution: %dx%d", engine.get_width(), engine.get_height());
- ImGui::Text("Compute Resolution: %dx%d", engine.get_compute_width(), engine.get_compute_height());
- ImGui::Text("Objects: %zu", engine.get_objects().size());
-
- if (ImGui::Button("Print Object Info"))
- engine.print_object_info();
-
- int compute_height = engine.get_compute_height();
- if (ImGui::SliderInt("Compute Height", &compute_height, 64, 2048))
- {
- engine.set_compute_height(compute_height);
- engine.update_compute_dimensions();
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.compute_height = compute_height;
- SettingsManager::set_simulation_settings(settings);
- }
-
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Compute Width: %d (auto-calculated)", engine.get_compute_width());
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality Settings");
- ImGui::Separator();
-
- int max_steps_moving = engine.get_max_steps_moving();
- if (ImGui::SliderInt("Max Steps (Moving)", &max_steps_moving, 1000, 60000))
- {
- engine.set_max_steps_moving(max_steps_moving);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.max_steps_moving = max_steps_moving;
- SettingsManager::set_simulation_settings(settings);
- }
- int max_steps_static = engine.get_max_steps_static();
- if (ImGui::SliderInt("Max Steps (Static)", &max_steps_static, 1000, 30000))
- {
- engine.set_max_steps_static(max_steps_static);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.max_steps_static = max_steps_static;
- SettingsManager::set_simulation_settings(settings);
- }
- float early_exit_distance = engine.get_early_exit_distance();
- if (ImGui::SliderFloat("Early Exit Distance", &early_exit_distance, 1e11f, 1e13f, "%.2e"))
- {
- engine.set_early_exit_distance(early_exit_distance);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.early_exit_distance = early_exit_distance;
- SettingsManager::set_simulation_settings(settings);
- }
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics");
- ImGui::Separator();
-
- bool& gravity = engine.get_gravity();
- if (ImGui::Checkbox("Gravity Enabled", &gravity))
- {
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.gravity_enabled = gravity;
- SettingsManager::set_simulation_settings(settings);
- }
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Accretion Disk");
- ImGui::Separator();
-
- float disk_thickness = engine.get_disk_thickness();
- if (ImGui::SliderFloat("Cloud Thickness", &disk_thickness, 0.1f, 2.0f, "%.2f"))
- {
- engine.set_disk_thickness(disk_thickness);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.disk_thickness = disk_thickness;
- SettingsManager::set_simulation_settings(settings);
- }
- ImGui::TextDisabled("Thickness relative to Schwarzschild radius");
-
- float disk_density = engine.get_disk_density();
- if (ImGui::SliderFloat("Cloud Density", &disk_density, 0.1f, 3.0f, "%.2f"))
- {
- engine.set_disk_density(disk_density);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.disk_density = disk_density;
- SettingsManager::set_simulation_settings(settings);
- }
- ImGui::TextDisabled("Overall density multiplier");
-
- float rotation_speed = engine.get_rotation_speed();
- if (ImGui::SliderFloat("Rotation Speed", &rotation_speed, 0.0f, 3.0f, "%.2f"))
- {
- engine.set_rotation_speed(rotation_speed);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.rotation_speed = rotation_speed;
- SettingsManager::set_simulation_settings(settings);
- }
- ImGui::TextDisabled("Rotation speed multiplier (0 = no rotation)");
-
- float blur_strength = engine.get_blur_strength();
- if (ImGui::SliderFloat("Blur Strength", &blur_strength, 0.5f, 5.0f, "%.2f"))
- {
- engine.set_blur_strength(blur_strength);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.blur_strength = blur_strength;
- SettingsManager::set_simulation_settings(settings);
- }
- ImGui::TextDisabled("Blur radius for glow effect");
-
- float glow_intensity = engine.get_glow_intensity();
- if (ImGui::SliderFloat("Glow Intensity", &glow_intensity, 0.1f, 3.0f, "%.2f"))
- {
- engine.set_glow_intensity(glow_intensity);
- SimulationSettings settings = SettingsManager::get_settings_const().simulation;
- settings.glow_intensity = glow_intensity;
- SettingsManager::set_simulation_settings(settings);
- }
- ImGui::TextDisabled("Intensity of the glow effect");
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Environment");
- ImGui::Separator();
-
- static int selected_hdri = 0;
- ImGui::PushID("SimulationHDRI");
- auto& hdri_manager = HDRIManager::get();
- const auto& available_hdri = hdri_manager.get_available_hdri();
-
- static std::vector<std::string> hdri_option_names;
- static std::vector<const char*> hdri_options;
-
- if (hdri_option_names.size() != available_hdri.size())
- {
- hdri_option_names.clear();
- hdri_options.clear();
-
- for (const auto& path : available_hdri)
- {
- hdri_option_names.push_back(hdri_manager.get_hdri_name(path));
- hdri_options.push_back(hdri_option_names.back().c_str());
- }
- }
-
- if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size())))
- {
- auto& hdri_manager = HDRIManager::get();
- hdri_manager.set_current_hdri(available_hdri[selected_hdri]);
- engine.set_hdri_environment(hdri_manager.get_current_hdri());
- }
- ImGui::TextDisabled("HDRI provides background and lighting for the simulation");
- ImGui::PopID();
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera");
- ImGui::Separator();
-
- ImGui::Text("Position: (%.2e, %.2e, %.2e)",
- engine.get_camera().get_orbital_position().x,
- engine.get_camera().get_orbital_position().y,
- engine.get_camera().get_orbital_position().z);
- ImGui::Text("Radius: %.2e", engine.get_camera().get_orbital_radius());
- ImGui::Text("Azimuth: %.2f", engine.get_camera().get_azimuth());
- ImGui::Text("Elevation: %.2f", engine.get_camera().get_elevation());
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Export");
- ImGui::Separator();
-
- if (ImGui::Button("Export Frame (1080p)", ImVec2(-1, 30)))
- {
- auto& engine = Application::get().get_engine();
- auto now = std::chrono::system_clock::now();
- auto time_t = std::chrono::system_clock::to_time_t(now);
- std::stringstream ss;
- ss << "frame_1080p_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
- engine.export_high_res_frame(ss.str(), 1920, 1080);
- }
-
- if (ImGui::Button("Export High-Res Frame (4K)", ImVec2(-1, 30)))
- {
- auto& engine = Application::get().get_engine();
- auto now = std::chrono::system_clock::now();
- auto time_t = std::chrono::system_clock::to_time_t(now);
- std::stringstream ss;
- ss << "high_res_frame_4k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
- engine.export_high_res_frame(ss.str(), 4096, 3072);
- }
-
- if (ImGui::Button("Export High-Res Frame (8K)", ImVec2(-1, 30)))
- {
- auto& engine = Application::get().get_engine();
- auto now = std::chrono::system_clock::now();
- auto time_t = std::chrono::system_clock::to_time_t(now);
- std::stringstream ss;
- ss << "high_res_frame_8k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png";
- engine.export_high_res_frame(ss.str(), 8192, 6144);
- }
-
- if (ImGui::Button("Export Ultra High-Res Frame (16K)", ImVec2(-1, 30)))
- {
- auto& engine = Application::get().get_engine();
- auto now = std::chrono::system_clock::now();
- auto time_t = std::chrono::system_clock::to_time_t(now);
- std::stringstream ss;
- ss << "high_res_frame_16k_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".png";
- engine.export_high_res_frame(ss.str(), 16384, 12288);
- }
-
- ImGui::Spacing();
-
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Controls");
- ImGui::Separator();
-
- ImGui::Text("Left Mouse: Orbit camera");
- ImGui::Text("Scroll: Zoom in/out");
- ImGui::Text("G: Toggle gravity");
- ImGui::Text("Right Mouse: Enable gravity (hold)");
-
- ImGui::End();
- }
-};
diff --git a/src/states/simulation_state.h b/src/states/simulation_state.h
deleted file mode 100644
index 625d19e..0000000
--- a/src/states/simulation_state.h
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-
-#include "core/state.h"
-#include "core/event.h"
-#include "core/log.h"
-#include "engine/engine.h"
-#include <vector>
-
-namespace Donut
-{
- class SimulationState
- : public State
- {
- public:
- ~SimulationState() = default;
-
- auto on_enter() -> void override;
- auto on_exit() -> void override;
- auto on_update(float delta_time) -> void override;
- auto on_render() -> void override;
- auto on_im_ui_render() -> void override;
- auto on_event(Event& event) -> void override;
-
- private:
- bool m_initialized = false;
- };
-};
diff --git a/src/states/world_builder_state.cpp b/src/states/world_builder_state.cpp
deleted file mode 100644
index 09283c3..0000000
--- a/src/states/world_builder_state.cpp
+++ /dev/null
@@ -1,1113 +0,0 @@
-#include "world_builder_state.h"
-
-#include "core/application.h"
-#include "core/window.h"
-#include "core/hdri_manager.h"
-
-#include "rendering/renderer.h"
-#include "rendering/shader.h"
-#include "rendering/vertex_array.h"
-#include "rendering/vertex_buffer.h"
-#include "rendering/index_buffer.h"
-#include "rendering/texture.h"
-
-#include <imgui.h>
-#include <ImGuizmo.h>
-#include <GLFW/glfw3.h>
-
-#include <glm/gtc/matrix_transform.hpp>
-#include <glm/gtc/type_ptr.hpp>
-
-#include <nlohmann/json.hpp>
-
-#include <numbers>
-#include <fstream>
-#include <sstream>
-#include <limits>
-#include <vector>
-
-namespace Donut
-{
- auto WorldBuilderState::on_enter() -> void
- {
- DONUT_INFO("Entering World Builder State");
-
- ImGuizmo::Enable(true);
-
- m_camera.set_camera_mode(CameraMode::Orbital);
- m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f));
- m_camera.set_orbital_radius(15.0f);
- m_camera.set_orbital_limits(2.0f, 200.0f);
- m_camera.set_orbital_speed(0.01f);
- m_camera.set_zoom_speed(2.0f);
- m_camera.set_azimuth(0.0f);
- m_camera.set_elevation(static_cast<float>(std::numbers::pi) / 3.0f);
- m_camera.update_orbital();
-
- m_sphere_shader = Ref<Shader>(Shader::create("assets/shaders/Sphere.glsl"));
- m_skybox_shader = Ref<Shader>(Shader::create("assets/shaders/Skybox.glsl"));
- m_grid_shader = Ref<Shader>(Shader::create("assets/shaders/Grid.glsl"));
-
- if (!m_sphere_shader)
- DONUT_ERROR("Failed to create sphere shader");
- if (!m_skybox_shader)
- DONUT_ERROR("Failed to create skybox shader");
- if (!m_grid_shader)
- DONUT_ERROR("Failed to create grid shader");
-
- initialize_sphere_geometry();
- initialize_skybox_geometry();
- initialize_grid_geometry();
-
- auto& hdri_manager = HDRIManager::get();
- m_hdri_environment = hdri_manager.get_current_hdri();
- if (!m_hdri_environment)
- {
- hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr");
- m_hdri_environment = hdri_manager.get_current_hdri();
- if (!m_hdri_environment)
- DONUT_WARN("Failed to load default HDRI for WorldBuilder, using fallback");
- }
-
- Material black_hole_material(glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, 0.0f);
- m_black_hole = Object(glm::vec3(0.0f, 0.0f, 0.0f), 2.0f, black_hole_material);
- m_black_hole_initialized = true;
-
- // Set default grid size
- m_grid_size = 10.0f;
-
- m_initialized = true;
- }
-
- auto WorldBuilderState::on_exit() -> void
- {
- DONUT_INFO("Exiting World Builder State");
- }
-
- auto WorldBuilderState::on_update(float delta_time) -> void
- {
- if (m_camera_dragging &&
- !ImGuizmo::IsUsing())
- {
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- double xpos, ypos;
- glfwGetCursorPos(window, &xpos, &ypos);
-
- glm::vec2 current_mouse_pos(xpos, ypos);
- glm::vec2 delta = current_mouse_pos - m_last_mouse_pos;
-
- float sensitivity = 0.005f;
- float azimuth_delta = delta.x * sensitivity;
- float elevation_delta = -delta.y * sensitivity;
-
- float new_azimuth = m_camera.get_azimuth() + azimuth_delta;
- float new_elevation = m_camera.get_elevation() + elevation_delta;
-
- new_elevation = glm::clamp(new_elevation, 0.01f, static_cast<float>(std::numbers::pi) - 0.01f);
-
- m_camera.set_azimuth(new_azimuth);
- m_camera.set_elevation(new_elevation);
- m_camera.update_orbital();
-
- m_last_mouse_pos = current_mouse_pos;
- }
- }
-
- auto WorldBuilderState::on_render() -> void
- {
- auto& hdri_manager = HDRIManager::get();
- m_hdri_environment = hdri_manager.get_current_hdri();
-
- RenderCommand::set_clear_color(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f));
- RenderCommand::clear();
-
- if (m_hdri_environment)
- render_skybox();
-
- if (m_show_grid)
- render_grid();
-
- render_scene();
- }
-
- auto WorldBuilderState::on_event(Event& event) -> void
- {
- EventDispatcher dispatcher(event);
-
- dispatcher.dispatch<MouseButtonPressedEvent>([this](MouseButtonPressedEvent& e)
- {
- if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT)
- {
- if (ImGuizmo::IsUsing() ||
- ImGuizmo::IsOver())
- return false;
-
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- double xpos, ypos;
- glfwGetCursorPos(window, &xpos, &ypos);
-
- int width, height;
- glfwGetFramebufferSize(window, &width, &height);
-
- float ndc_x = (2.0f * static_cast<float>(xpos)) / static_cast<float>(width) - 1.0f;
- float ndc_y = 1.0f - (2.0f * static_cast<float>(ypos)) / static_cast<float>(height);
-
- glm::vec4 rayStart_NDC(ndc_x, ndc_y, -1.0f, 1.0f);
- glm::vec4 rayEnd_NDC(ndc_x, ndc_y, 0.0f, 1.0f);
-
- glm::mat4 inv_vp = glm::inverse(m_camera.get_projection_matrix() * m_camera.get_view_matrix());
- glm::vec4 rayStart_World = inv_vp * rayStart_NDC;
- glm::vec4 rayEnd_World = inv_vp * rayEnd_NDC;
-
- rayStart_World /= rayStart_World.w;
- rayEnd_World /= rayEnd_World.w;
-
- glm::vec3 ray_dir = glm::normalize(glm::vec3(rayEnd_World - rayStart_World));
- glm::vec3 ray_origin = glm::vec3(rayStart_World);
-
- float closest_distance = std::numeric_limits<float>::max();
- int closest_object_index = -1;
-
- if (m_black_hole_initialized)
- {
- glm::vec3 oc = ray_origin - m_black_hole.m_centre;
- float a = glm::dot(ray_dir, ray_dir);
- float b = 2.0f * glm::dot(oc, ray_dir);
- float c = glm::dot(oc, oc) - m_black_hole.m_radius * m_black_hole.m_radius;
- float discriminant = b * b - 4 * a * c;
-
- if (discriminant > 0)
- {
- float t1 = (-b - sqrt(discriminant)) / (2.0f * a);
- float t2 = (-b + sqrt(discriminant)) / (2.0f * a);
-
- if (t1 > 0 && t1 < closest_distance)
- {
- closest_distance = t1;
- closest_object_index = -2;
- }
- else if (t2 > 0 && t2 < closest_distance)
- {
- closest_distance = t2;
- closest_object_index = -2;
- }
- }
- }
-
- for (size_t i = 0; i < m_scene.objs.size(); ++i)
- {
- const Object& obj = m_scene.objs[i];
-
- glm::vec3 oc = ray_origin - obj.m_centre;
- float a = glm::dot(ray_dir, ray_dir);
- float b = 2.0f * glm::dot(oc, ray_dir);
- float c = glm::dot(oc, oc) - obj.m_radius * obj.m_radius;
- float discriminant = b * b - 4 * a * c;
-
- if (discriminant > 0)
- {
- float t1 = (-b - sqrt(discriminant)) / (2.0f * a);
- float t2 = (-b + sqrt(discriminant)) / (2.0f * a);
-
- if (t1 > 0 && t1 < closest_distance)
- {
- closest_distance = t1;
- closest_object_index = static_cast<int>(i);
- }
- else if (t2 > 0 && t2 < closest_distance)
- {
- closest_distance = t2;
- closest_object_index = static_cast<int>(i);
- }
- }
- }
-
- if (closest_object_index >= 0)
- {
- m_selected_object_index = closest_object_index;
- DONUT_INFO("Selected object {}", closest_object_index);
- return true;
- }
- else if (closest_object_index == -2)
- {
- m_selected_object_index = -1;
- DONUT_INFO("Black hole clicked (not selectable)");
- return true;
- }
- else
- {
- m_selected_object_index = -1;
- m_camera_dragging = true;
- m_last_mouse_pos = glm::vec2(xpos, ypos);
- }
-
- return true;
- }
- return false;
- });
-
- dispatcher.dispatch<MouseButtonReleasedEvent>([this](MouseButtonReleasedEvent& e)
- {
- if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT)
- {
- if (!ImGuizmo::IsUsing())
- m_camera_dragging = false;
- return true;
- }
-
- return false;
- });
-
- dispatcher.dispatch<WindowResizeEvent>([this](WindowResizeEvent& e)
- {
- int new_width = e.get_width();
- int new_height = e.get_height();
- if (new_width > 0 && new_height > 0)
- {
- float aspect = static_cast<float>(new_width) / static_cast<float>(new_height);
- m_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f);
- }
- return false;
- });
-
- dispatcher.dispatch<MouseScrolledEvent>([this](MouseScrolledEvent& e)
- {
- float zoom_speed = 0.1f;
- float zoom_delta = e.get_y_offset() * zoom_speed;
-
- double current_radius = m_camera.get_orbital_radius();
- double new_radius = current_radius - zoom_delta * current_radius * 0.1f;
-
- double min_radius = 2.0f;
- double max_radius = 200.0f;
- new_radius = glm::clamp(new_radius, min_radius, max_radius);
-
- m_camera.set_orbital_radius(new_radius);
- m_camera.update_orbital();
-
- return true;
- });
-
- dispatcher.dispatch<KeyPressedEvent>([this](KeyPressedEvent& e)
- {
- if (m_selected_object_index >= 0 &&
- m_selected_object_index < static_cast<int>(m_scene.objs.size()))
- {
- switch (e.get_key_code())
- {
- case GLFW_KEY_M:
- m_gizmo_operation = ImGuizmo::TRANSLATE;
- return true;
- case GLFW_KEY_R:
- m_gizmo_operation = ImGuizmo::ROTATE;
- return true;
- case GLFW_KEY_S:
- m_gizmo_operation = ImGuizmo::SCALE;
- return true;
- }
- }
- return false;
- });
- }
-
- auto WorldBuilderState::on_im_ui_render() -> void
- {
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- int width, height;
- glfwGetFramebufferSize(window, &width, &height);
-
- ImGuiViewport* vp = ImGui::GetMainViewport();
- ImGuizmo::SetRect(vp->Pos.x, vp->Pos.y, vp->Size.x, vp->Size.y);
-
- ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver);
- ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver);
-
- ImGui::Begin("World Builder", nullptr, ImGuiWindowFlags_NoCollapse);
-
- ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
- ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "World Builder");
- ImGui::PopFont();
- ImGui::Separator();
-
- if (ImGui::CollapsingHeader("Scene Info"))
- {
- ImGui::Text("Objects in scene: %zu/16 (+ 1 black hole)", m_scene.objs.size());
-
- ImGui::Separator();
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera Info:");
-
- glm::vec3 camera_pos = m_camera.get_orbital_position();
- glm::vec3 target = m_camera.get_orbital_target();
-
- ImGui::Text("Position: (%.2f, %.2f, %.2f)", camera_pos.x, camera_pos.y, camera_pos.z);
- ImGui::Text("Target: (%.2f, %.2f, %.2f)", target.x, target.y, target.z);
- ImGui::Text("Distance: %.2f", m_camera.get_orbital_radius());
- ImGui::Text("Azimuth: %.1f°", glm::degrees(m_camera.get_azimuth()));
- ImGui::Text("Elevation: %.1f°", glm::degrees(m_camera.get_elevation()));
-
- ImGui::Separator();
-
- if (ImGui::Button("Reset Camera"))
- {
- m_camera.set_orbital_radius(15.0f);
- m_camera.set_azimuth(0.0f);
- m_camera.set_elevation(static_cast<float>(std::numbers::pi) / 3.0f);
- m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f));
- m_camera.update_orbital();
- }
-
- ImGui::SameLine();
- if (ImGui::Button("Clear Scene"))
- clear_scene();
-
- ImGui::SameLine();
- if (ImGui::Button("Focus on Objects"))
- {
- if (!m_scene.objs.empty())
- {
- glm::vec3 center(0.0f);
- for (const auto& obj : m_scene.objs)
- center += obj.m_centre;
- center /= static_cast<float>(m_scene.objs.size());
- m_camera.set_orbital_target(center);
- m_camera.update_orbital();
- }
- }
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("create Object"))
- {
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "New Sphere");
- ImGui::Separator();
-
- ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Objects: %zu/16", m_scene.objs.size());
-
- ImGui::Text("Position:");
- ImGui::DragFloat3("##Position", &m_new_object_position.x, 0.1f);
-
- ImGui::Text("Radius:");
- ImGui::DragFloat("##Radius", &m_new_object_radius, 0.1f, 0.1f, 10.0f);
-
- ImGui::Text("Color:");
- ImGui::ColorEdit3("##Color", &m_new_object_color.x);
-
- ImGui::Text("Specular:");
- ImGui::SliderFloat("##Specular", &m_new_object_specular, 0.0f, 1.0f);
-
- ImGui::Text("Emission:");
- ImGui::SliderFloat("##Emission", &m_new_object_emission, 0.0f, 1.0f);
-
- ImGui::Spacing();
-
- if (m_scene.objs.size() >= 16)
- {
- ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
- ImGui::Button("add Sphere (Limit Reached)", ImVec2(ImGui::GetWindowWidth() - 20, 30));
- ImGui::PopStyleVar();
- }
- else
- {
- if (ImGui::Button("add Sphere", ImVec2(ImGui::GetWindowWidth() - 20, 30)))
- add_sphere();
- }
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("Scene Objects"))
- {
- if (m_black_hole_initialized)
- {
- ImGui::PushID(-1);
-
- ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "Black Hole (Center)");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Position: (0.00, 0.00, 0.00)");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Radius: %.2f", m_black_hole.m_radius);
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Color: Black (with white outline)");
- ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Cannot be removed or modified");
-
- ImGui::PopID();
- ImGui::Separator();
- }
-
- if (m_scene.objs.empty())
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No additional objects in scene");
- else
- {
- for (size_t i = 0; i < m_scene.objs.size(); ++i)
- {
- Object& obj = m_scene.objs[i];
-
- ImGui::PushID(static_cast<int>(i));
-
- bool is_selected = (m_selected_object_index == static_cast<int>(i));
- if (ImGui::Selectable(("Sphere " + std::to_string(i)).c_str(), is_selected))
- m_selected_object_index = static_cast<int>(i);
-
- if (is_selected)
- {
- ImGui::SameLine();
- if (ImGui::Button("Remove"))
- {
- remove_selected_object();
- }
-
- ImGui::Text("Position: (%.2f, %.2f, %.2f)",
- obj.m_centre.x, obj.m_centre.y, obj.m_centre.z);
- ImGui::Text("Radius: %.2f", obj.m_radius);
- ImGui::Text("Color: (%.2f, %.2f, %.2f)",
- obj.m_material.m_color.x, obj.m_material.m_color.y, obj.m_material.m_color.z);
- }
-
- ImGui::PopID();
- }
- }
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("Gizmo Controls"))
- {
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Gizmo Operation:");
-
- if (ImGui::RadioButton("Translate", m_gizmo_operation == ImGuizmo::TRANSLATE))
- m_gizmo_operation = ImGuizmo::TRANSLATE;
- ImGui::SameLine();
- if (ImGui::RadioButton("Rotate", m_gizmo_operation == ImGuizmo::ROTATE))
- m_gizmo_operation = ImGuizmo::ROTATE;
- ImGui::SameLine();
- if (ImGui::RadioButton("Scale", m_gizmo_operation == ImGuizmo::SCALE))
- m_gizmo_operation = ImGuizmo::SCALE;
-
- ImGui::Spacing();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Hotkeys:");
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "M = Move, R = Rotate, S = Scale");
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("Selection Outline"))
- {
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Outline Settings:");
-
- ImGui::Text("Outline Color:");
- ImGui::ColorEdit3("##OutlineColor", &m_outline_color.x);
-
- ImGui::Text("Outline Width:");
- ImGui::SliderFloat("##OutlineWidth", &m_outline_width, 0.01f, 0.9f, "%.2f");
-
- ImGui::Spacing();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "White rim around sphere silhouette");
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("Grid Settings"))
- {
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Grid Settings:");
-
- ImGui::Text("Show Grid:");
- ImGui::SameLine();
- ImGui::Checkbox("##ShowGrid", &m_show_grid);
-
- ImGui::Text("Grid Color:");
- ImGui::ColorEdit3("##GridColor", &m_grid_color.x);
-
- ImGui::Text("Grid Alpha:");
- ImGui::SliderFloat("##GridAlpha", &m_grid_alpha, 0.0f, 1.0f, "%.2f");
-
- ImGui::Text("Grid Size:");
- ImGui::SliderFloat("##GridSize", &m_grid_size, 1.0f, 500.0f, "%.1f");
-
- ImGui::Spacing();
- if (ImGui::Button("Regenerate Grid", ImVec2(ImGui::GetWindowWidth() - 20, 25)))
- initialize_grid_geometry();
-
- ImGui::Spacing();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Reference grid for spatial orientation");
- }
-
- ImGui::Spacing();
-
- if (ImGui::CollapsingHeader("HDRI Environment", nullptr))
- {
- ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Settings:");
-
- static int selected_hdri = 0;
- ImGui::PushID("WorldBuilderHDRI");
- auto& hdri_manager = HDRIManager::get();
- const auto& available_hdri = hdri_manager.get_available_hdri();
-
- static std::vector<std::string> hdri_option_names;
- static std::vector<const char*> hdri_options;
-
- if (hdri_option_names.size() != available_hdri.size())
- {
- hdri_option_names.clear();
- hdri_options.clear();
-
- for (const auto& path : available_hdri)
- {
- hdri_option_names.push_back(hdri_manager.get_hdri_name(path));
- hdri_options.push_back(hdri_option_names.back().c_str());
- }
- }
-
- if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast<int>(hdri_options.size())))
- {
- auto& hdri_manager = HDRIManager::get();
- hdri_manager.set_current_hdri(available_hdri[selected_hdri]);
- m_hdri_environment = hdri_manager.get_current_hdri();
- }
-
- ImGui::Spacing();
- ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "HDRI provides background skybox and lighting for the scene");
- ImGui::PopID();
- }
-
- ImGui::Spacing();
- ImGui::Separator();
- ImGui::Spacing();
-
- float button_width = (ImGui::GetWindowWidth() - 30) / 2.0f;
-
- if (ImGui::Button("Save Scene", ImVec2(button_width, 30)))
- save_scene();
-
- ImGui::SameLine();
- if (ImGui::Button("load Scene", ImVec2(button_width, 30)))
- load_scene();
-
- ImGui::Spacing();
-
- if (ImGui::Button("Start Simulation", ImVec2(ImGui::GetWindowWidth() - 20, 30)))
- {
- auto& engine = Application::get().get_engine();
- engine.load_objects_from_scene(m_scene.objs);
- Application::get().get_state_manager().switch_to_state("Simulation");
- }
-
- ImGui::Spacing();
-
- if (ImGui::Button("Back to Config", ImVec2(ImGui::GetWindowWidth() - 20, 30)))
- Application::get().get_state_manager().switch_to_state("Config");
-
- ImGui::Spacing();
- ImGui::Separator();
- ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Camera: Left mouse = rotate, Scroll = zoom");
- ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Gizmo: M=Move, R=Rotate, S=Scale");
-
- ImGui::End();
-
- if (m_selected_object_index >= 0 &&
- m_selected_object_index < static_cast<int>(m_scene.objs.size()) &&
- m_selected_object_index != -2)
- {
- glm::mat4 view = m_camera.get_view_matrix();
- glm::mat4 projection = m_camera.get_projection_matrix();
-
- Object& selected_obj = m_scene.objs[m_selected_object_index];
-
- glm::mat4 object_transform = glm::mat4(1.0f);
- object_transform = glm::translate(object_transform, selected_obj.m_centre);
- object_transform = glm::scale(object_transform, glm::vec3(selected_obj.m_radius));
-
- DONUT_INFO("Object position: ({}, {}, {})", selected_obj.m_centre.x, selected_obj.m_centre.y, selected_obj.m_centre.z);
- glm::vec3 camera_pos = m_camera.get_orbital_position();
- DONUT_INFO("Camera position: ({}, {}, {})", camera_pos.x, camera_pos.y, camera_pos.z);
-
- glm::vec3 view_translation = glm::vec3(view[3][0], view[3][1], view[3][2]);
- DONUT_INFO("View translation: ({}, {}, {})", view_translation.x, view_translation.y, view_translation.z);
-
- ImGuizmo::SetDrawlist(ImGui::GetForegroundDrawList());
- if (ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection),
- m_gizmo_operation, m_gizmo_mode,
- glm::value_ptr(object_transform),
- nullptr, nullptr))
- {
- glm::vec3 new_position = glm::vec3(object_transform[3][0], object_transform[3][1], object_transform[3][2]);
- selected_obj.m_centre = new_position;
-
- glm::vec3 scale = glm::vec3
- (
- glm::length(glm::vec3(object_transform[0])),
- glm::length(glm::vec3(object_transform[1])),
- glm::length(glm::vec3(object_transform[2]))
- );
- selected_obj.m_radius = (scale.x + scale.y + scale.z) / 3.0f;
-
- DONUT_INFO("Matrix [3]: ({}, {}, {}, {})", object_transform[3][0], object_transform[3][1], object_transform[3][2], object_transform[3][3]);
- DONUT_INFO("New position: ({}, {}, {})", new_position.x, new_position.y, new_position.z);
- }
- }
- }
-
- auto WorldBuilderState::add_sphere() -> void
- {
- if (m_scene.objs.size() >= 16)
- {
- DONUT_WARN("Cannot add more objects. Maximum of 16 objects reached.");
- return;
- }
-
- Material material(m_new_object_color, m_new_object_specular, m_new_object_emission);
- Object sphere(m_new_object_position, m_new_object_radius, material);
- m_scene.objs.push_back(sphere);
-
- m_selected_object_index = static_cast<int>(m_scene.objs.size() - 1);
-
- m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f);
- m_new_object_radius = 1.0f;
- m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f);
- m_new_object_specular = 0.5f;
- m_new_object_emission = 0.0f;
-
- DONUT_INFO("Added sphere to scene and selected it");
- }
-
- auto WorldBuilderState::remove_selected_object() -> void
- {
- if (m_selected_object_index >= 0 && m_selected_object_index < static_cast<int>(m_scene.objs.size()))
- {
- m_scene.objs.erase(m_scene.objs.begin() + m_selected_object_index);
- m_selected_object_index = -1;
- DONUT_INFO("Removed object from scene");
- }
- }
-
- auto WorldBuilderState::clear_scene() -> void
- {
- m_scene.objs.clear();
- m_selected_object_index = -1;
- DONUT_INFO("Cleared scene (black hole remains at center)");
- }
-
- auto WorldBuilderState::save_scene() -> void
- {
- nlohmann::json scene_data;
- scene_data["objects"] = nlohmann::json::array();
-
- for (const auto& obj : m_scene.objs)
- {
- nlohmann::json sphere_data;
- sphere_data["position"] =
- {
- obj.m_centre.x,
- obj.m_centre.y,
- obj.m_centre.z
- };
-
- sphere_data["color"] =
- {
- obj.m_material.m_color.x,
- obj.m_material.m_color.y,
- obj.m_material.m_color.z
- };
-
- sphere_data["radius"] = obj.m_radius;
- sphere_data["specular"] = obj.m_material.m_specular;
- sphere_data["emission"] = obj.m_material.m_emission;
- scene_data["objects"].push_back(sphere_data);
- }
-
- std::ofstream file("Scene.json");
- if (file.is_open())
- {
- file << scene_data.dump(4);
- file.close();
- DONUT_INFO("Scene saved to Scene.json (black hole always present at center)");
- }
- else
- DONUT_ERROR("Failed to save scene");
- }
-
- auto WorldBuilderState::load_scene() -> void
- {
- std::ifstream file("Scene.json");
- if (file.is_open())
- {
- m_scene.objs.clear();
- m_selected_object_index = -1;
-
- nlohmann::json scene_data;
- file >> scene_data;
-
- if (scene_data.contains("objects"))
- {
- for (const auto& sphere_data : scene_data["objects"])
- {
- glm::vec3 position;
- glm::vec3 color;
- float radius;
- float specular;
- float emission;
-
- auto pos_array = sphere_data["position"];
- position.x = pos_array[0].get<float>();
- position.y = pos_array[1].get<float>();
- position.z = pos_array[2].get<float>();
-
- auto color_array = sphere_data["color"];
- color.x = color_array[0].get<float>();
- color.y = color_array[1].get<float>();
- color.z = color_array[2].get<float>();
-
- radius = sphere_data["radius"].get<float>();
- specular = sphere_data["specular"].get<float>();
- emission = sphere_data["emission"].get<float>();
-
- Material material(color, specular, emission);
- Object sphere(position, radius, material);
- m_scene.objs.push_back(sphere);
- }
- }
-
- file.close();
- DONUT_INFO("Scene loaded from Scene.json (black hole remains at center)");
- }
- else
- DONUT_ERROR("Failed to load scene");
- }
-
- auto WorldBuilderState::initialize_sphere_geometry() -> void
- {
- std::vector<float> vertices;
- std::vector<uint32_t> indices;
-
- const int segments = 32;
- const int rings = 16;
-
- for (int ring = 0; ring <= rings; ++ring)
- {
- float phi = static_cast<float>(std::numbers::pi) * ring / rings;
- float sin_phi = sin(phi);
- float cos_phi = cos(phi);
-
- for (int segment = 0; segment <= segments; ++segment)
- {
- float theta = 2.0f * static_cast<float>(std::numbers::pi) * segment / segments;
- float sin_theta = sin(theta);
- float cos_theta = cos(theta);
-
- float x = cos_theta * sin_phi;
- float y = cos_phi;
- float z = sin_theta * sin_phi;
-
- float nx = x;
- float ny = y;
- float nz = z;
-
- vertices.push_back(x);
- vertices.push_back(y);
- vertices.push_back(z);
- vertices.push_back(nx);
- vertices.push_back(ny);
- vertices.push_back(nz);
- }
- }
-
- for (int ring = 0; ring < rings; ++ring)
- {
- for (int segment = 0; segment < segments; ++segment)
- {
- uint32_t first = ring * (segments + 1) + segment;
- uint32_t second = first + segments + 1;
-
- indices.push_back(first);
- indices.push_back(second);
- indices.push_back(first + 1);
-
- indices.push_back(second);
- indices.push_back(second + 1);
- indices.push_back(first + 1);
- }
- }
-
- auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float))));
- VertexBufferLayout layout;
- layout.push<float>(3);
- layout.push<float>(3);
- vertex_buffer->set_layout(layout);
-
- auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size())));
-
- m_sphere_vao = Ref<VertexArray>(VertexArray::create());
- m_sphere_vao->add_vertex_buffer(vertex_buffer);
- m_sphere_vao->set_index_buffer(index_buffer);
- }
-
- auto WorldBuilderState::render_scene() -> void
- {
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- int width, height;
- glfwGetFramebufferSize(window, &width, &height);
-
- RenderCommand::set_viewport(0, 0, width, height);
- RenderCommand::enable_depth_test();
-
- glm::mat4 view = m_camera.get_view_matrix();
- glm::mat4 projection = m_camera.get_projection_matrix();
- glm::mat4 view_projection = projection * view;
-
- glm::vec3 light_pos = m_scene.m_light_pos;
- glm::vec3 camera_pos = m_camera.get_orbital_position();
-
- m_sphere_shader->bind();
- m_sphere_shader->set_mat4("u_ViewProjection", view_projection);
- m_sphere_shader->set_float3("u_LightPos", light_pos);
- m_sphere_shader->set_float3("u_CameraPos", camera_pos);
-
- m_sphere_shader->set_float3("u_OutlineColor", m_outline_color);
- m_sphere_shader->set_float("u_OutlineWidth", m_outline_width);
-
- if (m_hdri_environment)
- {
- m_hdri_environment->bind(1);
- m_sphere_shader->set_int("u_HDRIEnvironment", 1);
- }
-
- if (m_black_hole_initialized)
- {
- glm::mat4 black_hole_transform = glm::translate(glm::mat4(1.0f), m_black_hole.m_centre);
- black_hole_transform = glm::scale(black_hole_transform, glm::vec3(m_black_hole.m_radius));
-
- m_sphere_shader->set_mat4("u_Transform", black_hole_transform);
- m_sphere_shader->set_int("u_IsSelected", 1);
- m_sphere_shader->set_float3("u_Color", m_black_hole.m_material.m_color);
- m_sphere_shader->set_float("u_Emission", m_black_hole.m_material.m_emission);
- m_sphere_shader->set_float("u_Specular", m_black_hole.m_material.m_specular);
- m_sphere_shader->set_float("u_OutlineWidth", 0.3f);
-
- m_sphere_vao->bind();
- RenderCommand::draw_indexed(m_sphere_vao);
-
- m_sphere_shader->set_float("u_OutlineWidth", m_outline_width);
- }
-
- for (size_t i = 0; i < m_scene.objs.size(); ++i)
- {
- const auto& obj = m_scene.objs[i];
-
- glm::mat4 transform = glm::translate(glm::mat4(1.0f), obj.m_centre);
- transform = glm::scale(transform, glm::vec3(obj.m_radius));
-
- m_sphere_shader->set_mat4("u_Transform", transform);
-
- bool is_selected = (m_selected_object_index == static_cast<int>(i));
- m_sphere_shader->set_int("u_IsSelected", is_selected ? 1 : 0);
- if (is_selected)
- {
- glm::vec3 highlight_color = obj.m_material.m_color * 1.5f;
- highlight_color = glm::clamp(highlight_color, 0.0f, 1.0f);
- m_sphere_shader->set_float3("u_Color", highlight_color);
- m_sphere_shader->set_float("u_Emission", 0.2f);
- }
- else
- {
- m_sphere_shader->set_float3("u_Color", obj.m_material.m_color);
- m_sphere_shader->set_float("u_Emission", obj.m_material.m_emission);
- }
-
- m_sphere_shader->set_float("u_Specular", obj.m_material.m_specular);
-
- m_sphere_vao->bind();
- RenderCommand::draw_indexed(m_sphere_vao);
- }
-
- RenderCommand::disable_depth_test();
- }
-
-
-
- auto WorldBuilderState::initialize_skybox_geometry() -> void
- {
- float skybox_vertices[] =
- {
- -1.0f, 1.0f, -1.0f,
- -1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
- 1.0f, 1.0f, -1.0f,
- -1.0f, 1.0f, -1.0f,
-
- -1.0f, -1.0f, 1.0f,
- -1.0f, -1.0f, -1.0f,
- -1.0f, 1.0f, -1.0f,
- -1.0f, 1.0f, -1.0f,
- -1.0f, 1.0f, 1.0f,
- -1.0f, -1.0f, 1.0f,
-
- 1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, 1.0f,
- 1.0f, 1.0f, 1.0f,
- 1.0f, 1.0f, 1.0f,
- 1.0f, 1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
-
- -1.0f, -1.0f, 1.0f,
- -1.0f, 1.0f, 1.0f,
- 1.0f, 1.0f, 1.0f,
- 1.0f, 1.0f, 1.0f,
- 1.0f, -1.0f, 1.0f,
- -1.0f, -1.0f, 1.0f,
-
- -1.0f, 1.0f, -1.0f,
- 1.0f, 1.0f, -1.0f,
- 1.0f, 1.0f, 1.0f,
- 1.0f, 1.0f, 1.0f,
- -1.0f, 1.0f, 1.0f,
- -1.0f, 1.0f, -1.0f,
-
- -1.0f, -1.0f, -1.0f,
- -1.0f, -1.0f, 1.0f,
- 1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
- -1.0f, -1.0f, 1.0f,
- 1.0f, -1.0f, 1.0f
- };
-
- auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(skybox_vertices, sizeof(skybox_vertices)));
- VertexBufferLayout layout;
- layout.push<float>(3);
- vertex_buffer->set_layout(layout);
-
- m_skybox_vao = Ref<VertexArray>(VertexArray::create());
- m_skybox_vao->add_vertex_buffer(vertex_buffer);
- }
-
- auto WorldBuilderState::render_skybox() -> void
- {
- if (!m_skybox_shader || !m_skybox_vao || !m_hdri_environment)
- return;
-
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- int width, height;
- glfwGetFramebufferSize(window, &width, &height);
-
- RenderCommand::set_viewport(0, 0, width, height);
- RenderCommand::disable_depth_test();
-
- glm::mat4 view = m_camera.get_view_matrix();
- glm::mat4 projection = m_camera.get_projection_matrix();
-
- view = glm::mat4(glm::mat3(view));
-
- m_skybox_shader->bind();
- m_skybox_shader->set_mat4("u_View", view);
- m_skybox_shader->set_mat4("u_Projection", projection);
-
- m_hdri_environment->bind(0);
- m_skybox_shader->set_int("u_Skybox", 0);
-
- m_skybox_vao->bind();
- RenderCommand::draw_arrays(36);
-
- RenderCommand::enable_depth_test();
- }
-
- auto WorldBuilderState::initialize_grid_geometry() -> void
- {
- if (m_grid_vao)
- {
- m_grid_vao->unbind();
- m_grid_vao.reset();
- }
-
- const float grid_size = 50.0f;
- const int grid_lines = 101;
- const float half_size = grid_size * 0.5f;
- const float step = grid_size / (grid_lines - 1);
-
- std::vector<float> vertices;
- std::vector<uint32_t> indices;
-
- for (int i = 0; i < grid_lines; ++i)
- {
- float z = -half_size + i * step;
-
- vertices.push_back(-half_size);
- vertices.push_back(0.0f);
- vertices.push_back(z);
-
- vertices.push_back(half_size);
- vertices.push_back(0.0f);
- vertices.push_back(z);
-
- uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2;
- indices.push_back(base_index);
- indices.push_back(base_index + 1);
- }
-
- for (int i = 0; i < grid_lines; ++i)
- {
- float x = -half_size + i * step;
-
- vertices.push_back(x);
- vertices.push_back(0.0f);
- vertices.push_back(-half_size);
-
- vertices.push_back(x);
- vertices.push_back(0.0f);
- vertices.push_back(half_size);
-
- uint32_t base_index = static_cast<uint32_t>(vertices.size() / 3) - 2;
- indices.push_back(base_index);
- indices.push_back(base_index + 1);
- }
-
- auto vertex_buffer = Ref<VertexBuffer>(VertexBuffer::create(vertices.data(), static_cast<uint32_t>(vertices.size() * sizeof(float))));
- VertexBufferLayout layout;
- layout.push<float>(3);
- vertex_buffer->set_layout(layout);
-
- auto index_buffer = Ref<IndexBuffer>(IndexBuffer::create(indices.data(), static_cast<uint32_t>(indices.size())));
-
- m_grid_vao = Ref<VertexArray>(VertexArray::create());
- m_grid_vao->add_vertex_buffer(vertex_buffer);
- m_grid_vao->set_index_buffer(index_buffer);
-
- m_grid_vao->unbind();
- }
-
- auto WorldBuilderState::render_grid() -> void
- {
- if (!m_grid_shader || !m_grid_vao)
- return;
-
- GLFWwindow* window = static_cast<GLFWwindow*>(Application::get().get_window().get_native_window());
- int width, height;
- glfwGetFramebufferSize(window, &width, &height);
-
- RenderCommand::set_viewport(0, 0, width, height);
- RenderCommand::enable_depth_test();
- RenderCommand::enable_blending();
-
- glm::mat4 view = m_camera.get_view_matrix();
- glm::mat4 projection = m_camera.get_projection_matrix();
- glm::mat4 view_projection = projection * view;
- glm::mat4 transform = glm::mat4(1.0f);
-
- glm::vec3 camera_pos = m_camera.get_orbital_position();
-
- m_grid_shader->bind();
- m_grid_shader->set_mat4("u_ViewProjection", view_projection);
- m_grid_shader->set_mat4("u_Transform", transform);
- m_grid_shader->set_float3("u_GridColor", m_grid_color);
- m_grid_shader->set_float("u_GridAlpha", m_grid_alpha);
- m_grid_shader->set_float("u_GridSize", m_grid_size);
- m_grid_shader->set_float3("u_CameraPos", camera_pos);
-
- m_grid_vao->bind();
- RenderCommand::draw_lines(m_grid_vao);
-
- // Clean up state to prevent conflicts with scene rendering
- m_grid_vao->unbind();
- m_grid_shader->unbind();
- RenderCommand::disable_blending();
-
- // Ensure depth test is still enabled for scene rendering
- RenderCommand::enable_depth_test();
- }
-};
diff --git a/src/states/world_builder_state.h b/src/states/world_builder_state.h
deleted file mode 100644
index 8b4678a..0000000
--- a/src/states/world_builder_state.h
+++ /dev/null
@@ -1,96 +0,0 @@
-#pragma once
-
-#include "core/camera.h"
-#include "core/state.h"
-#include "core/event.h"
-#include "core/log.h"
-
-#include "engine/scene.h"
-#include "engine/object.h"
-
-#include "rendering/renderer.h"
-#include "rendering/shader.h"
-#include "rendering/vertex_array.h"
-
-#include <imgui.h>
-#include <ImGuizmo.h>
-
-#include <vector>
-#include <memory>
-
-namespace Donut
-{
- class WorldBuilderState
- : public State
- {
- public:
- ~WorldBuilderState() = default;
-
- auto on_enter() -> void override;
- auto on_exit() -> void override;
- auto on_update(float delta_time) -> void override;
- auto on_render() -> void override;
- auto on_im_ui_render() -> void override;
- auto on_event(Event& event) -> void override;
- private:
- auto add_sphere() -> void;
- auto remove_selected_object() -> void;
- auto clear_scene() -> void;
- auto save_scene() -> void;
- auto load_scene() -> void;
- auto render_scene() -> void;
- auto initialize_sphere_geometry() -> void;
- auto initialize_grid_geometry() -> void;
- auto render_grid() -> void;
- private:
- Scene m_scene;
- Camera m_camera;
- bool m_initialized = false;
-
- Ref<Shader> m_sphere_shader;
- Ref<VertexArray> m_sphere_vao;
- Ref<CubemapTexture> m_hdri_environment;
-
- Ref<Shader> m_skybox_shader;
- Ref<VertexArray> m_skybox_vao;
-
- Ref<Shader> m_grid_shader;
- Ref<VertexArray> m_grid_vao;
-
- glm::vec3 m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f);
- float m_new_object_radius = 1.0f;
- glm::vec3 m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f);
- float m_new_object_specular = 0.5f;
- float m_new_object_emission = 0.0f;
-
- int m_selected_object_index = -1;
- bool m_camera_dragging = false;
- glm::vec2 m_last_mouse_pos = glm::vec2(0.0f, 0.0f);
-
- ImGuizmo::OPERATION m_gizmo_operation = ImGuizmo::TRANSLATE;
- ImGuizmo::MODE m_gizmo_mode = ImGuizmo::LOCAL;
-
- bool m_show_object_list = true;
- bool m_show_object_creator = true;
- bool m_show_scene_info = true;
- bool m_show_gizmo_controls = true;
- bool m_show_outline_controls = true;
- bool m_show_grid = true;
-
- glm::vec3 m_outline_color = glm::vec3(1.0f, 1.0f, 1.0f);
- float m_outline_width = 0.25f;
-
- glm::vec3 m_grid_color = glm::vec3(0.5f, 0.5f, 0.5f);
- float m_grid_alpha = 0.5f;
- float m_grid_size = 50.0f;
-
- Object m_black_hole;
- bool m_black_hole_initialized = false;
-
- auto set_hdri_environment(Ref<CubemapTexture> hdri) -> void { m_hdri_environment = hdri; }
- auto get_hdri_environment() const -> Ref<CubemapTexture> { return m_hdri_environment; }
-
- auto initialize_skybox_geometry() -> void;
- auto render_skybox() -> void;
- };
-};