aboutsummaryrefslogtreecommitdiff
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
parente3aa33fc9a99d8b817bda42a567a4c347e18ab32 (diff)
[feat]: complete RHI api
-rw-r--r--README.md6
-rw-r--r--assets/hdri/hdr_blue_nebulae_1.hdr (renamed from assets/hdri/HDR_blue_nebulae-1.hdr)bin62208386 -> 62208386 bytes
-rw-r--r--assets/hdri/hdr_subdued_blue_nebulae.hdr (renamed from assets/hdri/HDR_subdued_blue_nebulae.hdr)bin62041084 -> 62041084 bytes
-rw-r--r--assets/hdri/hdr_subdued_multi_nebulae.hdr (renamed from assets/hdri/HDR_subdued_multi_nebulae.hdr)bin64541302 -> 64541302 bytes
-rw-r--r--assets/shaders/Grid.slang34
-rw-r--r--assets/shaders/Sphere.slang67
-rw-r--r--assets/shaders/blur.slang (renamed from assets/shaders/Blur.slang)0
-rw-r--r--assets/shaders/equirect_to_cubemap.slang (renamed from assets/shaders/EquirectToCubemap.slang)0
-rw-r--r--assets/shaders/geodesic.slang (renamed from assets/shaders/Geodesic.slang)0
-rw-r--r--assets/shaders/grid.slang39
-rw-r--r--assets/shaders/skybox.slang (renamed from assets/shaders/Skybox.slang)12
-rw-r--r--assets/shaders/sphere.slang74
-rw-r--r--assets/shaders/textured_quad.slang (renamed from assets/shaders/TexturedQuad.slang)0
-rw-r--r--assets/shaders/vk_pipeline_test.slang (renamed from assets/shaders/VkPipelineTest.slang)0
-rw-r--r--branding/logo_black.jpg (renamed from branding/Logo-Black.jpg)bin31049 -> 31049 bytes
-rw-r--r--branding/logo_monochrome_black.jpg (renamed from branding/Logo-Monochrome-Black.jpg)bin25555 -> 25555 bytes
-rw-r--r--branding/logo_monochrome_white.jpg (renamed from branding/Logo-Monochrome-White.jpg)bin26074 -> 26074 bytes
-rw-r--r--branding/logo_white.jpg (renamed from branding/Logo-White.jpg)bin31870 -> 31870 bytes
-rw-r--r--branding/screenshot_1.png (renamed from branding/Screenshot1.png)bin566012 -> 566012 bytes
-rw-r--r--branding/screenshot_2.png (renamed from branding/Screenshot2.png)bin916806 -> 916806 bytes
-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
-rwxr-xr-xtools/compile-shaders.sh2
73 files changed, 2514 insertions, 8099 deletions
diff --git a/README.md b/README.md
index 4931a65..bf6df12 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-![Logo](branding/Logo-Monochrome-White.jpg)
+![Logo](branding/logo_monochrome_white.jpg)
Donut is a real-time renderer for Sagittarius A* (Sgr A*) that traces light through the curved spacetime surrounding the black hole. Each pixel is represented by a light ray originating from the camera. The ray is integrated through the Schwarzschild metric, allowing the renderer to reproduce gravitational lensing and the distortion of the surrounding accretion disk and background. The renderer runs on the GPU using compute shaders, allowing the scene to be explored interactively in real time.
## Screenshots
-![Black Hole with Accretion Disk](branding/Screenshot1.png)
-![Black Hole with HDRI](branding/Screenshot2.png)
+![Black Hole with Accretion Disk](branding/screenshot_1.png)
+![Black Hole with HDRI](branding/screenshot_2.png)
## Physics
Donut uses the Schwarzschild solution to describe the spacetime around Sgr A*. The metric is (with $r_s = \frac{2GM}{c^2}$):
diff --git a/assets/hdri/HDR_blue_nebulae-1.hdr b/assets/hdri/hdr_blue_nebulae_1.hdr
index f34b25d..f34b25d 100644
--- a/assets/hdri/HDR_blue_nebulae-1.hdr
+++ b/assets/hdri/hdr_blue_nebulae_1.hdr
Binary files differ
diff --git a/assets/hdri/HDR_subdued_blue_nebulae.hdr b/assets/hdri/hdr_subdued_blue_nebulae.hdr
index ea34298..ea34298 100644
--- a/assets/hdri/HDR_subdued_blue_nebulae.hdr
+++ b/assets/hdri/hdr_subdued_blue_nebulae.hdr
Binary files differ
diff --git a/assets/hdri/HDR_subdued_multi_nebulae.hdr b/assets/hdri/hdr_subdued_multi_nebulae.hdr
index da88396..da88396 100644
--- a/assets/hdri/HDR_subdued_multi_nebulae.hdr
+++ b/assets/hdri/hdr_subdued_multi_nebulae.hdr
Binary files differ
diff --git a/assets/shaders/Grid.slang b/assets/shaders/Grid.slang
deleted file mode 100644
index ea7d263..0000000
--- a/assets/shaders/Grid.slang
+++ /dev/null
@@ -1,34 +0,0 @@
-uniform float4x4 u_ViewProjection;
-uniform float4x4 u_Transform;
-uniform float u_GridSize;
-
-struct VSInput { float3 position : POSITION; };
-struct VSOutput { float4 position : SV_Position; };
-
-[shader("vertex")]
-VSOutput vertexMain(VSInput input)
-{
- VSOutput output;
- float3 scaledPosition = input.position * (u_GridSize / 50.0);
- output.position = mul(u_ViewProjection, mul(u_Transform, float4(scaledPosition, 1.0)));
- return output;
-}
-
-uniform float3 u_GridColor;
-uniform float u_GridAlpha;
-uniform float3 u_CameraPos;
-
-[shader("fragment")]
-float4 fragmentMain(VSOutput input) : SV_Target
-{
- float dist = length(u_CameraPos);
- float fadeFactor = 1.0 - clamp(dist / 100.0, 0.0, 0.8);
-
- float3 color = u_GridColor;
- float alpha = u_GridAlpha * fadeFactor;
-
- float gridFade = 1.0 - clamp(dist / 50.0, 0.0, 0.9);
- alpha *= gridFade;
-
- return float4(color, alpha);
-}
diff --git a/assets/shaders/Sphere.slang b/assets/shaders/Sphere.slang
deleted file mode 100644
index 0871438..0000000
--- a/assets/shaders/Sphere.slang
+++ /dev/null
@@ -1,67 +0,0 @@
-uniform float4x4 u_ViewProjection;
-uniform float4x4 u_Transform;
-
-struct VSInput
-{
- float3 position : POSITION;
- float3 normal : NORMAL;
-};
-
-struct VSOutput
-{
- float4 position : SV_Position;
- float3 normal : TEXCOORD0;
- float3 worldPos : TEXCOORD1;
-};
-
-[shader("vertex")]
-VSOutput vertexMain(VSInput input)
-{
- VSOutput output;
- output.worldPos = mul(u_Transform, float4(input.position, 1.0)).xyz;
- float3x3 normalMatrix = (float3x3)u_Transform;
- output.normal = mul(normalMatrix, input.normal);
- output.position = mul(u_ViewProjection, float4(output.worldPos, 1.0));
- return output;
-}
-
-uniform float3 u_Color;
-uniform float u_Specular;
-uniform float u_Emission;
-uniform float3 u_LightPos;
-uniform float3 u_CameraPos;
-uniform int u_IsSelected;
-uniform float3 u_OutlineColor;
-uniform float u_OutlineWidth;
-SamplerCube u_HDRIEnvironment;
-
-[shader("fragment")]
-float4 fragmentMain(VSOutput input) : SV_Target
-{
- float3 normal = normalize(input.normal);
- float3 lightDir = normalize(u_LightPos - input.worldPos);
- float3 viewDir = normalize(u_CameraPos - input.worldPos);
- float3 reflectDir = reflect(-lightDir, normal);
-
- float3 hdriLight = u_HDRIEnvironment.Sample(normal).rgb;
- float hdriIntensity = 0.3;
-
- float diff = max(dot(normal, lightDir), 0.0);
- float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
-
- float3 diffuse = u_Color * (diff + hdriIntensity * hdriLight);
- float3 specular = float3(u_Specular, u_Specular, u_Specular) * spec;
- float3 emission = u_Color * u_Emission;
-
- float3 result = diffuse + specular + emission;
-
- if (u_IsSelected > 0)
- {
- float ndotv = max(dot(normal, viewDir), 0.0);
- float rim = 1.0 - ndotv;
- float outlineMask = step(1.0 - u_OutlineWidth, rim);
- result = lerp(result, u_OutlineColor, outlineMask);
- }
-
- return float4(result, 1.0);
-}
diff --git a/assets/shaders/Blur.slang b/assets/shaders/blur.slang
index 17e4eae..17e4eae 100644
--- a/assets/shaders/Blur.slang
+++ b/assets/shaders/blur.slang
diff --git a/assets/shaders/EquirectToCubemap.slang b/assets/shaders/equirect_to_cubemap.slang
index 9b2ea4b..9b2ea4b 100644
--- a/assets/shaders/EquirectToCubemap.slang
+++ b/assets/shaders/equirect_to_cubemap.slang
diff --git a/assets/shaders/Geodesic.slang b/assets/shaders/geodesic.slang
index 7acbd78..7acbd78 100644
--- a/assets/shaders/Geodesic.slang
+++ b/assets/shaders/geodesic.slang
diff --git a/assets/shaders/grid.slang b/assets/shaders/grid.slang
new file mode 100644
index 0000000..f549cb1
--- /dev/null
+++ b/assets/shaders/grid.slang
@@ -0,0 +1,39 @@
+// Reference grid on the XZ plane. Uniforms live in a ConstantBuffer (UBO) so both
+// backends drive it through the same RHI path (bind a uniform buffer at binding 0).
+struct GridU
+{
+ float4x4 u_ViewProjection;
+ float4x4 u_Transform;
+ float u_GridSize;
+ float3 u_GridColor;
+ float u_GridAlpha;
+ float3 u_CameraPos;
+};
+ConstantBuffer<GridU> gu;
+
+struct VSInput { float3 position : POSITION; };
+struct VSOutput { float4 position : SV_Position; };
+
+[shader("vertex")]
+VSOutput vertexMain(VSInput input)
+{
+ VSOutput output;
+ float3 scaledPosition = input.position * (gu.u_GridSize / 50.0);
+ output.position = mul(gu.u_ViewProjection, mul(gu.u_Transform, float4(scaledPosition, 1.0)));
+ return output;
+}
+
+[shader("fragment")]
+float4 fragmentMain(VSOutput input) : SV_Target
+{
+ float dist = length(gu.u_CameraPos);
+ float fadeFactor = 1.0 - clamp(dist / 100.0, 0.0, 0.8);
+
+ float3 color = gu.u_GridColor;
+ float alpha = gu.u_GridAlpha * fadeFactor;
+
+ float gridFade = 1.0 - clamp(dist / 50.0, 0.0, 0.9);
+ alpha *= gridFade;
+
+ return float4(color, alpha);
+}
diff --git a/assets/shaders/Skybox.slang b/assets/shaders/skybox.slang
index 6ba81bc..6447bea 100644
--- a/assets/shaders/Skybox.slang
+++ b/assets/shaders/skybox.slang
@@ -1,5 +1,11 @@
-uniform float4x4 u_Projection;
-uniform float4x4 u_View;
+// Fullscreen skybox cube. View/projection in a ConstantBuffer (UBO, binding 0)
+// so both backends drive it through the RHI; the cubemap is a sampler at binding 1.
+struct SkyboxU
+{
+ float4x4 u_Projection;
+ float4x4 u_View;
+};
+ConstantBuffer<SkyboxU> sb;
struct VSInput { float3 position : POSITION; };
struct VSOutput { float4 position : SV_Position; float3 texCoords : TEXCOORD0; };
@@ -9,7 +15,7 @@ VSOutput vertexMain(VSInput input)
{
VSOutput output;
output.texCoords = input.position;
- float4 pos = mul(u_Projection, mul(u_View, float4(input.position, 1.0)));
+ float4 pos = mul(sb.u_Projection, mul(sb.u_View, float4(input.position, 1.0)));
output.position = pos.xyww; // force depth = 1 so the skybox stays behind everything
return output;
}
diff --git a/assets/shaders/sphere.slang b/assets/shaders/sphere.slang
new file mode 100644
index 0000000..e37700d
--- /dev/null
+++ b/assets/shaders/sphere.slang
@@ -0,0 +1,74 @@
+// Lit sphere (Blinn-Phong + HDRI ambient + selection rim). Uniforms in a
+// ConstantBuffer (UBO, binding 0) so both backends drive it through the RHI;
+// the environment cubemap is a separate sampler at binding 1.
+struct SphereU
+{
+ float4x4 u_ViewProjection;
+ float4x4 u_Transform;
+ float3 u_Color;
+ float u_Specular;
+ float u_Emission;
+ float3 u_LightPos;
+ float3 u_CameraPos;
+ int u_IsSelected;
+ float3 u_OutlineColor;
+ float u_OutlineWidth;
+};
+ConstantBuffer<SphereU> su;
+
+struct VSInput
+{
+ float3 position : POSITION;
+ float3 normal : NORMAL;
+};
+
+struct VSOutput
+{
+ float4 position : SV_Position;
+ float3 normal : TEXCOORD0;
+ float3 worldPos : TEXCOORD1;
+};
+
+[shader("vertex")]
+VSOutput vertexMain(VSInput input)
+{
+ VSOutput output;
+ output.worldPos = mul(su.u_Transform, float4(input.position, 1.0)).xyz;
+ float3x3 normalMatrix = (float3x3)su.u_Transform;
+ output.normal = mul(normalMatrix, input.normal);
+ output.position = mul(su.u_ViewProjection, float4(output.worldPos, 1.0));
+ return output;
+}
+
+SamplerCube u_HDRIEnvironment;
+
+[shader("fragment")]
+float4 fragmentMain(VSOutput input) : SV_Target
+{
+ float3 normal = normalize(input.normal);
+ float3 lightDir = normalize(su.u_LightPos - input.worldPos);
+ float3 viewDir = normalize(su.u_CameraPos - input.worldPos);
+ float3 reflectDir = reflect(-lightDir, normal);
+
+ float3 hdriLight = u_HDRIEnvironment.Sample(normal).rgb;
+ float hdriIntensity = 0.3;
+
+ float diff = max(dot(normal, lightDir), 0.0);
+ float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
+
+ float3 diffuse = su.u_Color * (diff + hdriIntensity * hdriLight);
+ float3 specular = float3(su.u_Specular, su.u_Specular, su.u_Specular) * spec;
+ float3 emission = su.u_Color * su.u_Emission;
+
+ float3 result = diffuse + specular + emission;
+
+ if (su.u_IsSelected > 0)
+ {
+ float ndotv = max(dot(normal, viewDir), 0.0);
+ float rim = 1.0 - ndotv;
+ float outlineMask = step(1.0 - su.u_OutlineWidth, rim);
+ result = lerp(result, su.u_OutlineColor, outlineMask);
+ }
+
+ return float4(result, 1.0);
+}
diff --git a/assets/shaders/TexturedQuad.slang b/assets/shaders/textured_quad.slang
index 52d46ed..52d46ed 100644
--- a/assets/shaders/TexturedQuad.slang
+++ b/assets/shaders/textured_quad.slang
diff --git a/assets/shaders/VkPipelineTest.slang b/assets/shaders/vk_pipeline_test.slang
index f4d1897..f4d1897 100644
--- a/assets/shaders/VkPipelineTest.slang
+++ b/assets/shaders/vk_pipeline_test.slang
diff --git a/branding/Logo-Black.jpg b/branding/logo_black.jpg
index 5960c8e..5960c8e 100644
--- a/branding/Logo-Black.jpg
+++ b/branding/logo_black.jpg
Binary files differ
diff --git a/branding/Logo-Monochrome-Black.jpg b/branding/logo_monochrome_black.jpg
index 65181e4..65181e4 100644
--- a/branding/Logo-Monochrome-Black.jpg
+++ b/branding/logo_monochrome_black.jpg
Binary files differ
diff --git a/branding/Logo-Monochrome-White.jpg b/branding/logo_monochrome_white.jpg
index 4397006..4397006 100644
--- a/branding/Logo-Monochrome-White.jpg
+++ b/branding/logo_monochrome_white.jpg
Binary files differ
diff --git a/branding/Logo-White.jpg b/branding/logo_white.jpg
index 7981c4b..7981c4b 100644
--- a/branding/Logo-White.jpg
+++ b/branding/logo_white.jpg
Binary files differ
diff --git a/branding/Screenshot1.png b/branding/screenshot_1.png
index 7175ab6..7175ab6 100644
--- a/branding/Screenshot1.png
+++ b/branding/screenshot_1.png
Binary files differ
diff --git a/branding/Screenshot2.png b/branding/screenshot_2.png
index 2aece5d..2aece5d 100644
--- a/branding/Screenshot2.png
+++ b/branding/screenshot_2.png
Binary files differ
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;
- };
-};
diff --git a/tools/compile-shaders.sh b/tools/compile-shaders.sh
index 1f58c2a..5da5d9a 100755
--- a/tools/compile-shaders.sh
+++ b/tools/compile-shaders.sh
@@ -16,7 +16,7 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SLANGC="${SLANGC:-$ROOT/tools/slang/bin/slangc}"
SPIRV_CROSS="${SPIRV_CROSS:-$(command -v spirv-cross || echo spirv-cross)}"
-SRC="$ROOT/assets/Shaders"
+SRC="$ROOT/assets/shaders"
OUT="$SRC/generated"
[ -x "$SLANGC" ] || { echo "compile-shaders: slangc not found at $SLANGC (run tools/fetch-slang.sh)"; exit 1; }