aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform')
-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_framebuffer.cpp246
-rw-r--r--src/platform/opengl/opengl_framebuffer.h35
-rw-r--r--src/platform/opengl/opengl_index_buffer.cpp28
-rw-r--r--src/platform/opengl/opengl_index_buffer.h22
-rw-r--r--src/platform/opengl/opengl_renderer_api.cpp118
-rw-r--r--src/platform/opengl/opengl_renderer_api.h44
-rw-r--r--src/platform/opengl/opengl_shader.cpp302
-rw-r--r--src/platform/opengl/opengl_shader.h58
-rw-r--r--src/platform/opengl/opengl_texture.cpp272
-rw-r--r--src/platform/opengl/opengl_texture.h68
-rw-r--r--src/platform/opengl/opengl_uniform_buffer.cpp29
-rw-r--r--src/platform/opengl/opengl_uniform_buffer.h21
-rw-r--r--src/platform/opengl/opengl_vertex_array.cpp58
-rw-r--r--src/platform/opengl/opengl_vertex_array.h41
-rw-r--r--src/platform/opengl/opengl_vertex_buffer.cpp35
-rw-r--r--src/platform/opengl/opengl_vertex_buffer.h25
-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_index_buffer.cpp25
-rw-r--r--src/platform/vulkan/vulkan_index_buffer.h22
-rw-r--r--src/platform/vulkan/vulkan_renderer.cpp1235
-rw-r--r--src/platform/vulkan/vulkan_renderer.h40
-rw-r--r--src/platform/vulkan/vulkan_renderer_api.cpp86
-rw-r--r--src/platform/vulkan/vulkan_renderer_api.h38
-rw-r--r--src/platform/vulkan/vulkan_shader.cpp146
-rw-r--r--src/platform/vulkan/vulkan_shader.h52
-rw-r--r--src/platform/vulkan/vulkan_texture.cpp73
-rw-r--r--src/platform/vulkan/vulkan_texture.h60
-rw-r--r--src/platform/vulkan/vulkan_uniform_buffer.cpp25
-rw-r--r--src/platform/vulkan/vulkan_uniform_buffer.h19
-rw-r--r--src/platform/vulkan/vulkan_vertex_array.cpp36
-rw-r--r--src/platform/vulkan/vulkan_vertex_array.h28
-rw-r--r--src/platform/vulkan/vulkan_vertex_buffer.cpp34
-rw-r--r--src/platform/vulkan/vulkan_vertex_buffer.h26
36 files changed, 4317 insertions, 0 deletions
diff --git a/src/platform/metal/metal_context.h b/src/platform/metal/metal_context.h
new file mode 100644
index 0000000..f1e376b
--- /dev/null
+++ b/src/platform/metal/metal_context.h
@@ -0,0 +1,16 @@
+#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
new file mode 100644
index 0000000..71e8551
--- /dev/null
+++ b/src/platform/metal/metal_context.mm
@@ -0,0 +1,124 @@
+#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_framebuffer.cpp b/src/platform/opengl/opengl_framebuffer.cpp
new file mode 100644
index 0000000..1624439
--- /dev/null
+++ b/src/platform/opengl/opengl_framebuffer.cpp
@@ -0,0 +1,246 @@
+#include "opengl_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;
+ }
+ }
+
+ OpenGLFramebuffer::OpenGLFramebuffer(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();
+ }
+
+ OpenGLFramebuffer::~OpenGLFramebuffer()
+ {
+ glDeleteFramebuffers(1, &m_renderer_id);
+ glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data());
+ glDeleteTextures(1, &m_depth_attachment);
+ }
+
+ auto OpenGLFramebuffer::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 OpenGLFramebuffer::bind() -> void
+ {
+ glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
+ glViewport(0, 0, m_specification.Width, m_specification.Height);
+ }
+
+ auto OpenGLFramebuffer::unbind() -> void
+ {
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ }
+
+ auto OpenGLFramebuffer::resize(uint32_t width, uint32_t height) -> void
+ {
+ m_specification.Width = width;
+ m_specification.Height = height;
+
+ invalidate();
+ }
+
+ auto OpenGLFramebuffer::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 OpenGLFramebuffer::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);
+ }
+};
diff --git a/src/platform/opengl/opengl_framebuffer.h b/src/platform/opengl/opengl_framebuffer.h
new file mode 100644
index 0000000..456d924
--- /dev/null
+++ b/src/platform/opengl/opengl_framebuffer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "rendering/framebuffer.h"
+
+namespace Donut
+{
+ class OpenGLFramebuffer : public Framebuffer
+ {
+ public:
+ OpenGLFramebuffer(const FramebufferSpecification& spec);
+ virtual ~OpenGLFramebuffer();
+
+ auto invalidate() -> void;
+
+ virtual auto bind() -> void override;
+ virtual auto unbind() -> void override;
+
+ virtual auto resize(uint32_t width, uint32_t height) -> void override;
+ virtual auto read_pixel(uint32_t attachment_index, int x, int y) -> int override;
+
+ virtual auto clear_attachment(uint32_t attachment_index, int value) -> void override;
+ virtual auto get_color_attachment_renderer_id(uint32_t index = 0) const -> uint32_t override{ return m_color_attachments[index]; }
+
+ virtual auto get_specification() const -> const FramebufferSpecification& override{ return m_specification; }
+ private:
+ 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/platform/opengl/opengl_index_buffer.cpp b/src/platform/opengl/opengl_index_buffer.cpp
new file mode 100644
index 0000000..faa6b81
--- /dev/null
+++ b/src/platform/opengl/opengl_index_buffer.cpp
@@ -0,0 +1,28 @@
+#include "opengl_index_buffer.h"
+#include <glad/glad.h>
+
+namespace Donut
+{
+ OpenGLIndexBuffer::OpenGLIndexBuffer(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);
+ }
+
+ OpenGLIndexBuffer::~OpenGLIndexBuffer()
+ {
+ glDeleteBuffers(1, &m_renderer_id);
+ }
+
+ auto OpenGLIndexBuffer::bind() const -> void
+ {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id);
+ }
+
+ auto OpenGLIndexBuffer::unbind() const -> void
+ {
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ }
+};
diff --git a/src/platform/opengl/opengl_index_buffer.h b/src/platform/opengl/opengl_index_buffer.h
new file mode 100644
index 0000000..397da43
--- /dev/null
+++ b/src/platform/opengl/opengl_index_buffer.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "rendering/index_buffer.h"
+
+namespace Donut
+{
+ class OpenGLIndexBuffer
+ : public IndexBuffer
+ {
+ public:
+ OpenGLIndexBuffer(const uint32_t* indices, uint32_t count);
+ virtual ~OpenGLIndexBuffer();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+ virtual auto get_count() const -> uint32_t override{ return m_count; }
+
+ private:
+ uint32_t m_renderer_id;
+ uint32_t m_count;
+ };
+};
diff --git a/src/platform/opengl/opengl_renderer_api.cpp b/src/platform/opengl/opengl_renderer_api.cpp
new file mode 100644
index 0000000..39ed786
--- /dev/null
+++ b/src/platform/opengl/opengl_renderer_api.cpp
@@ -0,0 +1,118 @@
+#include "opengl_renderer_api.h"
+
+#include <glad/glad.h>
+#include <GLFW/glfw3.h>
+
+namespace Donut
+{
+ auto OpenGLRendererAPI::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 OpenGLRendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
+ {
+ glViewport(x, y, width, height);
+ }
+
+ auto OpenGLRendererAPI::set_clear_color(const glm::vec4& color) -> void
+ {
+ glClearColor(color.r, color.g, color.b, color.a);
+ }
+
+ auto OpenGLRendererAPI::clear() -> void
+ {
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ }
+
+ auto OpenGLRendererAPI::enable_depth_test() -> void
+ {
+ glEnable(GL_DEPTH_TEST);
+ }
+
+ auto OpenGLRendererAPI::disable_depth_test() -> void
+ {
+ glDisable(GL_DEPTH_TEST);
+ }
+
+ auto OpenGLRendererAPI::set_face_culling(bool enabled) -> void
+ {
+ if (enabled)
+ {
+ glEnable(GL_CULL_FACE);
+ glCullFace(GL_BACK);
+ glFrontFace(GL_CCW);
+ }
+ else
+ glDisable(GL_CULL_FACE);
+ }
+
+ auto OpenGLRendererAPI::enable_blending() -> void
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ auto OpenGLRendererAPI::disable_blending() -> void
+ {
+ glDisable(GL_BLEND);
+ }
+
+ auto OpenGLRendererAPI::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 OpenGLRendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void
+ {
+ glDrawArrays(GL_TRIANGLES, first, vertex_count);
+ }
+
+ auto OpenGLRendererAPI::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 OpenGLRendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void
+ {
+ glActiveTexture(GL_TEXTURE0 + slot);
+ glBindTexture(GL_TEXTURE_2D, texture_id);
+ }
+
+ auto OpenGLRendererAPI::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 OpenGLRendererAPI::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);
+ }
+};
diff --git a/src/platform/opengl/opengl_renderer_api.h b/src/platform/opengl/opengl_renderer_api.h
new file mode 100644
index 0000000..02f18ab
--- /dev/null
+++ b/src/platform/opengl/opengl_renderer_api.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "core/memory.h"
+#include "core/log.h"
+
+#include "rendering/renderer.h"
+
+#include <glad/glad.h>
+
+namespace Donut
+{
+ class OpenGLRendererAPI
+ : public RendererAPI
+ {
+ public:
+ virtual auto init() -> void override;
+ virtual void set_viewport(uint32_t x, uint32_t y,
+ uint32_t width, uint32_t height) override;
+ virtual auto set_clear_color(const glm::vec4& color) -> void override;
+ virtual auto clear() -> void override;
+ virtual auto enable_depth_test() -> void override;
+ virtual auto disable_depth_test() -> void override;
+ virtual auto set_face_culling(bool enabled) -> void override;
+ virtual auto enable_blending() -> void override;
+ virtual auto disable_blending() -> void override;
+
+ virtual void draw_indexed(const Ref<VertexArray>& vertex_array,
+ uint32_t index_count = 0) override;
+
+ virtual void draw_arrays(uint32_t vertex_count,
+ uint32_t first = 0) override;
+ virtual void draw_lines(const Ref<VertexArray>& vertex_array,
+ uint32_t index_count = 0) override;
+ virtual void bind_texture(uint32_t texture_id,
+ uint32_t slot = 0) override;
+ virtual void bind_image_texture(uint32_t texture_id,
+ uint32_t slot = 0,
+ bool read_only = false) override;
+ virtual void read_pixels(uint32_t x, uint32_t y,
+ uint32_t width, uint32_t height,
+ uint32_t format, uint32_t type,
+ void* pixels) override;
+ };
+};
diff --git a/src/platform/opengl/opengl_shader.cpp b/src/platform/opengl/opengl_shader.cpp
new file mode 100644
index 0000000..d15d7e0
--- /dev/null
+++ b/src/platform/opengl/opengl_shader.cpp
@@ -0,0 +1,302 @@
+#include "opengl_shader.h"
+
+#include <glad/glad.h>
+#include <glm/gtc/type_ptr.hpp>
+
+#include <fstream>
+#include <iostream>
+
+namespace Donut
+{
+ static uint32_t ShaderTypeFromString(const std::string& type)
+ {
+ if (type == "vertex")
+ return GL_VERTEX_SHADER;
+ if (type == "fragment" || type == "pixel")
+ return GL_FRAGMENT_SHADER;
+ if (type == "compute")
+ return GL_COMPUTE_SHADER;
+ return 0;
+ }
+
+ // Shaders are authored in Slang and compiled to assets/shaders/generated/
+ // <name>.glsl by Tools/compile-shaders.sh. Given a legacy ".../<name>.glsl"
+ // path, prefer that generated file when present; otherwise fall back to the
+ // hand-written GLSL (e.g. shaders not yet ported to Slang).
+ static std::string ResolveShaderPath(const std::string& filepath)
+ {
+ size_t slash = filepath.find_last_of("/\\");
+ std::string dir = (slash == std::string::npos) ? std::string() : filepath.substr(0, slash + 1);
+ std::string file = (slash == std::string::npos) ? filepath : filepath.substr(slash + 1);
+ size_t dot = file.rfind('.');
+ std::string base = (dot == std::string::npos) ? file : file.substr(0, dot);
+
+ std::string generated = dir + "generated/" + base + ".glsl";
+ std::ifstream test(generated);
+ if (test.good())
+ return generated;
+ return filepath;
+ }
+
+ OpenGLShader::OpenGLShader(const std::string& filepath)
+ {
+ std::string resolved = ResolveShaderPath(filepath);
+ m_is_slang = (resolved != filepath);
+ std::string source = read_file(resolved);
+ auto shader_sources = pre_process(source);
+ compile(shader_sources);
+
+ auto last_slash = filepath.find_last_of("/\\");
+ last_slash = last_slash == std::string::npos ? 0 : last_slash + 1;
+ auto last_dot = filepath.rfind('.');
+ auto count = last_dot == std::string::npos ? filepath.size() - last_slash : last_dot - last_slash;
+ m_name = filepath.substr(last_slash, count);
+ }
+
+ OpenGLShader::OpenGLShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src)
+ : m_name(name)
+ {
+ std::unordered_map<uint32_t, std::string> sources;
+ sources[GL_VERTEX_SHADER] = vertex_src;
+ sources[GL_FRAGMENT_SHADER] = fragment_src;
+ compile(sources);
+ }
+
+ OpenGLShader::OpenGLShader(const std::string& name, const std::string& compute_src)
+ : m_name(name)
+ {
+ std::unordered_map<uint32_t, std::string> sources;
+ sources[GL_COMPUTE_SHADER] = compute_src;
+ compile(sources);
+ }
+
+ OpenGLShader::~OpenGLShader()
+ {
+ glDeleteProgram(m_renderer_id);
+ }
+
+ auto OpenGLShader::read_file(const std::string& filepath) -> std::string
+ {
+ std::string result;
+ std::ifstream in(filepath, std::ios::in |
+ std::ios::binary);
+
+ if (in)
+ {
+ in.seekg(0, std::ios::end);
+ size_t size = in.tellg();
+ if (size != -1)
+ {
+ result.resize(size);
+ in.seekg(0, std::ios::beg);
+ in.read(&result[0], size);
+ }
+ }
+ return result;
+ }
+
+ auto OpenGLShader::pre_process(const std::string& source) -> std::unordered_map<uint32_t, std::string>
+ {
+ std::unordered_map<uint32_t, std::string> shader_sources;
+
+ const char* type_token = "#type";
+ size_t type_token_length = strlen(type_token);
+ size_t pos = source.find(type_token, 0);
+
+ while (pos != std::string::npos)
+ {
+ size_t eol = source.find_first_of("\r\n", pos);
+ size_t begin = pos + type_token_length + 1;
+ std::string type = source.substr(begin, eol - begin);
+
+ size_t next_line_pos = source.find_first_not_of("\r\n", eol);
+ pos = source.find(type_token, next_line_pos);
+ shader_sources[ShaderTypeFromString(type)] = (pos == std::string::npos) ? source.substr(next_line_pos) :
+ source.substr(next_line_pos, pos - next_line_pos);
+ }
+
+ return shader_sources;
+ }
+
+ auto OpenGLShader::compile(const std::unordered_map<uint32_t, std::string>& shader_sources) -> void
+ {
+ uint32_t program = glCreateProgram();
+ std::vector<uint32_t> glShaderIDs(shader_sources.size());
+ for (auto& kv : shader_sources)
+ {
+ uint32_t type = kv.first;
+ const std::string& source = kv.second;
+
+ uint32_t shader = glCreateShader(type);
+ const char* source_c_str = source.c_str();
+ glShaderSource(shader, 1, &source_c_str, 0);
+ glCompileShader(shader);
+
+ int is_compiled = 0;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &is_compiled);
+ if (is_compiled == GL_FALSE)
+ {
+ int max_length = 0;
+ glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length);
+ std::vector<char> info_log(max_length);
+ glGetShaderInfoLog(shader, max_length, &max_length, &info_log[0]);
+ glDeleteShader(shader);
+ for (auto id : glShaderIDs)
+ glDeleteShader(id);
+ glDeleteProgram(program);
+ m_renderer_id = 0;
+ // info_log.data() is null when the driver returns an empty log
+ // (e.g. macOS rejecting a compute shader); streaming a null
+ // char* into std::cout calls strlen(NULL) and crashes.
+ const char* log = info_log.empty() ? "" : info_log.data();
+ std::cout << "Shader compilation failure!" << std::endl << log << std::endl;
+ return;
+ }
+ glAttachShader(program, shader);
+ glShaderIDs.push_back(shader);
+ }
+
+ m_renderer_id = program;
+ glLinkProgram(m_renderer_id);
+
+ int is_linked = 0;
+ glGetProgramiv(m_renderer_id, GL_LINK_STATUS, (int*)&is_linked);
+ if (is_linked == GL_FALSE)
+ {
+ int max_length = 0;
+ glGetProgramiv(m_renderer_id, GL_INFO_LOG_LENGTH, &max_length);
+ std::vector<char> info_log(max_length);
+ glGetProgramInfoLog(m_renderer_id, max_length, &max_length, &info_log[0]);
+ glDeleteProgram(m_renderer_id);
+ for (auto id : glShaderIDs)
+ glDeleteShader(id);
+ m_renderer_id = 0;
+ const char* log = info_log.empty() ? "" : info_log.data();
+ std::cout << "Shader link failure!" << std::endl << log << std::endl;
+ return;
+ }
+
+ for (auto id : glShaderIDs)
+ {
+ glDetachShader(m_renderer_id, id);
+ glDeleteShader(id);
+ }
+ }
+
+ auto OpenGLShader::bind() const -> void
+ {
+ glUseProgram(m_renderer_id);
+ }
+
+ auto OpenGLShader::unbind() const -> void
+ {
+ glUseProgram(0);
+ }
+
+ auto OpenGLShader::set_int(const std::string& name, int value) -> void
+ {
+ upload_uniform_int(name, value);
+ }
+
+ auto OpenGLShader::set_int_array(const std::string& name, int* values, uint32_t count) -> void
+ {
+ upload_uniform_int_array(name, values, count);
+ }
+
+ auto OpenGLShader::set_float(const std::string& name, float value) -> void
+ {
+ upload_uniform_float(name, value);
+ }
+
+ auto OpenGLShader::set_float2(const std::string& name, const glm::vec2& value) -> void
+ {
+ upload_uniform_float2(name, value);
+ }
+
+ auto OpenGLShader::set_float3(const std::string& name, const glm::vec3& value) -> void
+ {
+ upload_uniform_float3(name, value);
+ }
+
+ auto OpenGLShader::set_float4(const std::string& name, const glm::vec4& value) -> void
+ {
+ upload_uniform_float4(name, value);
+ }
+
+ auto OpenGLShader::set_mat4(const std::string& name, const glm::mat4& value) -> void
+ {
+ upload_uniform_mat4(name, value);
+ }
+
+ auto OpenGLShader::upload_uniform_int(const std::string& name, int value) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform1i(location, value);
+ }
+
+ auto OpenGLShader::upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform1iv(location, count, values);
+ }
+
+ auto OpenGLShader::upload_uniform_float(const std::string& name, float value) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform1f(location, value);
+ }
+
+ auto OpenGLShader::upload_uniform_float2(const std::string& name, const glm::vec2& value) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform2f(location, value.x, value.y);
+ }
+
+ auto OpenGLShader::upload_uniform_float3(const std::string& name, const glm::vec3& value) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform3f(location, value.x, value.y, value.z);
+ }
+
+ auto OpenGLShader::upload_uniform_float4(const std::string& name, const glm::vec4& value) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniform4f(location, value.x, value.y, value.z, value.w);
+ }
+
+ auto OpenGLShader::upload_uniform_mat3(const std::string& name, const glm::mat3& matrix) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniformMatrix3fv(location, 1, m_is_slang ? GL_TRUE : GL_FALSE, glm::value_ptr(matrix));
+ }
+
+ auto OpenGLShader::upload_uniform_mat4(const std::string& name, const glm::mat4& matrix) -> void
+ {
+ int location = glGetUniformLocation(m_renderer_id, name.c_str());
+ glUniformMatrix4fv(location, 1, m_is_slang ? GL_TRUE : GL_FALSE, glm::value_ptr(matrix));
+ }
+
+ auto OpenGLShader::dispatch(uint32_t x, uint32_t y, uint32_t z) -> void
+ {
+ // Compute shaders require OpenGL 4.3+. On drivers that cap out earlier
+ // (e.g. macOS, which is frozen at 4.1) glDispatchCompute is never
+ // loaded and the pointer is null. Guard so we no-op instead of crash.
+ if (m_renderer_id == 0 || glDispatchCompute == nullptr)
+ return;
+ glDispatchCompute(x, y, z);
+ }
+
+ auto OpenGLShader::dispatch_indirect(uint32_t offset) -> void
+ {
+ if (m_renderer_id == 0 || glDispatchComputeIndirect == nullptr)
+ return;
+ glDispatchComputeIndirect(offset);
+ }
+
+ auto OpenGLShader::memory_barrier(uint32_t barriers) -> void
+ {
+ if (glMemoryBarrier == nullptr)
+ return;
+ glMemoryBarrier(barriers);
+ }
+};
diff --git a/src/platform/opengl/opengl_shader.h b/src/platform/opengl/opengl_shader.h
new file mode 100644
index 0000000..289bfac
--- /dev/null
+++ b/src/platform/opengl/opengl_shader.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "rendering/shader.h"
+
+#include <unordered_map>
+#include <glm/glm.hpp>
+
+namespace Donut
+{
+ class OpenGLShader
+ : public Shader
+ {
+ public:
+ OpenGLShader(const std::string& filepath);
+ OpenGLShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src);
+ OpenGLShader(const std::string& name, const std::string& compute_src);
+ virtual ~OpenGLShader();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto set_int( const std::string& name, int value) -> void override;
+ virtual auto set_int_array(const std::string& name, int* values, uint32_t count) -> void override;
+ virtual auto set_float( const std::string& name, float value) -> void override;
+ virtual auto set_float2( const std::string& name, const glm::vec2& value) -> void override;
+ virtual auto set_float3( const std::string& name, const glm::vec3& value) -> void override;
+ virtual auto set_float4( const std::string& name, const glm::vec4& value) -> void override;
+ virtual auto set_mat4( const std::string& name, const glm::mat4& value) -> void override;
+
+ virtual auto dispatch(uint32_t x, uint32_t y = 1, uint32_t z = 1) -> void override;
+ virtual auto dispatch_indirect(uint32_t offset = 0) -> void override;
+ virtual auto memory_barrier(uint32_t barriers) -> void override;
+
+ virtual auto get_name() const -> const std::string& override{ return m_name; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ auto upload_uniform_int( const std::string& name, int value) -> void;
+ auto upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void;
+ auto upload_uniform_float( const std::string& name, float value) -> void;
+ auto upload_uniform_float2( const std::string& name, const glm::vec2& value) -> void;
+ auto upload_uniform_float3( const std::string& name, const glm::vec3& value) -> void;
+ auto upload_uniform_float4( const std::string& name, const glm::vec4& value) -> void;
+ auto upload_uniform_mat3( const std::string& name, const glm::mat3& matrix) -> void;
+ auto upload_uniform_mat4( const std::string& name, const glm::mat4& matrix) -> void;
+
+ private:
+ auto read_file(const std::string& filepath) -> std::string;
+ auto pre_process(const std::string& source) -> std::unordered_map<uint32_t, std::string>;
+ auto compile(const std::unordered_map<uint32_t, std::string>& shader_sources) -> void;
+ private:
+ uint32_t m_renderer_id = 0;
+ std::string m_name;
+ // True when loaded from a Slang-compiled GLSL. Slang expects row-major
+ // matrix data, so matrix uniforms are transposed on upload (glm is
+ // column-major) to keep all matrix math correct.
+ bool m_is_slang = false;
+ };
+};
diff --git a/src/platform/opengl/opengl_texture.cpp b/src/platform/opengl/opengl_texture.cpp
new file mode 100644
index 0000000..6bcb9cd
--- /dev/null
+++ b/src/platform/opengl/opengl_texture.cpp
@@ -0,0 +1,272 @@
+#include "opengl_texture.h"
+
+#include "rendering/shader.h"
+
+#define STB_IMAGE_IMPLEMENTATION
+#include "stb_image.h"
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/type_ptr.hpp>
+
+// NOTE: This file targets OpenGL 4.1 (the maximum macOS exposes). It uses the
+// classic bind-based texture API rather than 4.5 Direct State Access
+// (glCreateTextures / glTextureStorage2D / glTextureParameteri / glBindTextureUnit),
+// none of which exist on macOS.
+
+namespace Donut
+{
+ OpenGLTexture2D::OpenGLTexture2D(uint32_t width, uint32_t height)
+ : m_width(width), m_height(height)
+ {
+ m_internal_format = GL_RGBA8;
+ m_data_format = GL_RGBA;
+
+ glGenTextures(1, &m_renderer_id);
+ glBindTexture(GL_TEXTURE_2D, m_renderer_id);
+ glTexImage2D(GL_TEXTURE_2D, 0, m_internal_format, m_width, m_height, 0, m_data_format, GL_UNSIGNED_BYTE, 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_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+ }
+
+ OpenGLTexture2D::OpenGLTexture2D(const std::string& path)
+ : m_path(path)
+ {
+ m_width = 1;
+ m_height = 1;
+ m_internal_format = GL_RGBA8;
+ m_data_format = GL_RGBA;
+
+ glGenTextures(1, &m_renderer_id);
+ glBindTexture(GL_TEXTURE_2D, m_renderer_id);
+ glTexImage2D(GL_TEXTURE_2D, 0, m_internal_format, m_width, m_height, 0, m_data_format, GL_UNSIGNED_BYTE, 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_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+
+ uint32_t white_pixel = 0xFFFFFFFF;
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, m_data_format, GL_UNSIGNED_BYTE, &white_pixel);
+
+ DONUT_INFO("Created default texture (stb_image not available for loading: ", path, ")");
+ }
+
+ OpenGLTexture2D::~OpenGLTexture2D()
+ {
+ glDeleteTextures(1, &m_renderer_id);
+ }
+
+ auto OpenGLTexture2D::set_data(void* data, uint32_t size) -> void
+ {
+ uint32_t bpp = m_data_format == GL_RGBA ? 4 : 3;
+ if (size != m_width * m_height * bpp)
+ {
+ DONUT_ERROR("Data must be entire texture!");
+ return;
+ }
+
+ glBindTexture(GL_TEXTURE_2D, m_renderer_id);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, m_data_format, GL_UNSIGNED_BYTE, data);
+ }
+
+ auto OpenGLTexture2D::bind(uint32_t slot) const -> void
+ {
+ glActiveTexture(GL_TEXTURE0 + slot);
+ glBindTexture(GL_TEXTURE_2D, m_renderer_id);
+ }
+
+ auto OpenGLTexture2D::bind_as_image(uint32_t slot, bool read_only) const -> void
+ {
+ // Image load/store is OpenGL 4.2 and unavailable on macOS. Guard the
+ // function pointer so this degrades to a no-op instead of crashing.
+ if (glBindImageTexture == nullptr)
+ return;
+ GLenum access = read_only ? GL_READ_ONLY : GL_WRITE_ONLY;
+ glBindImageTexture(slot, m_renderer_id, 0, GL_FALSE, 0, access, m_internal_format);
+ }
+
+ OpenGLCubemapTexture::OpenGLCubemapTexture(uint32_t width, uint32_t height)
+ : m_width(width), m_height(height)
+ {
+ m_internal_format = GL_RGBA16F;
+ m_data_format = GL_RGBA;
+
+ glGenTextures(1, &m_renderer_id);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, m_renderer_id);
+ for (uint32_t i = 0; i < 6; ++i)
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, m_internal_format, m_width, m_height, 0, m_data_format, GL_FLOAT, nullptr);
+
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+ }
+
+ OpenGLCubemapTexture::OpenGLCubemapTexture(const std::string& path)
+ : m_path(path)
+ {
+ m_width = 1024;
+ m_height = 1024;
+ m_internal_format = GL_RGBA16F;
+ m_data_format = GL_RGBA;
+
+ glGenTextures(1, &m_renderer_id);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, m_renderer_id);
+ for (uint32_t i = 0; i < 6; ++i)
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, m_internal_format, m_width, m_height, 0, m_data_format, GL_FLOAT, nullptr);
+
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+
+ LoadHDRI(path);
+ }
+
+ OpenGLCubemapTexture::~OpenGLCubemapTexture()
+ {
+ glDeleteTextures(1, &m_renderer_id);
+ }
+
+ auto OpenGLCubemapTexture::LoadHDRI(const std::string& path) -> void
+ {
+ stbi_set_flip_vertically_on_load(true);
+ int width, height, channels;
+ float* hdr_data = stbi_loadf(path.c_str(), &width, &height, &channels, 3);
+
+ if (!hdr_data)
+ {
+ DONUT_ERROR("Failed to load HDRI: {}", path);
+ float default_sky[6 * 4] =
+ {
+ 0.5f, 0.7f, 1.0f, 1.0f, // Right
+ 0.5f, 0.7f, 1.0f, 1.0f, // Left
+ 0.5f, 0.7f, 1.0f, 1.0f, // Top
+ 0.5f, 0.7f, 1.0f, 1.0f, // Bottom
+ 0.5f, 0.7f, 1.0f, 1.0f, // Front
+ 0.5f, 0.7f, 1.0f, 1.0f // Back
+ };
+
+ glBindTexture(GL_TEXTURE_CUBE_MAP, m_renderer_id);
+ for (int i = 0; i < 6; ++i)
+ glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, 1, 1, GL_RGBA, GL_FLOAT, &default_sky[i * 4]);
+ return;
+ }
+
+ convert_equirectangular_to_cubemap(hdr_data, width, height);
+ stbi_image_free(hdr_data);
+
+ DONUT_INFO("Successfully loaded HDRI: {} ({}x{})", path, width, height);
+ }
+
+ auto OpenGLCubemapTexture::convert_equirectangular_to_cubemap(float* hdr_data, int width, int height) -> void
+ {
+ uint32_t capture_fbo, capture_rbo;
+ glGenFramebuffers(1, &capture_fbo);
+ glGenRenderbuffers(1, &capture_rbo);
+
+ glBindFramebuffer(GL_FRAMEBUFFER, capture_fbo);
+ glBindRenderbuffer(GL_RENDERBUFFER, capture_rbo);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, m_width, m_height);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, capture_rbo);
+
+ uint32_t hdr_texture;
+ glGenTextures(1, &hdr_texture);
+ glBindTexture(GL_TEXTURE_2D, hdr_texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, hdr_data);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ auto equirect_shader = Shader::create("assets/shaders/EquirectToCubemap.glsl");
+ if (!equirect_shader)
+ {
+ DONUT_ERROR("Failed to create equirectangular to cubemap shader");
+ return;
+ }
+
+ uint32_t shader_program = equirect_shader->get_renderer_id();
+
+ float 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
+ };
+
+ uint32_t cube_vao, cube_vbo;
+ glGenVertexArrays(1, &cube_vao);
+ glGenBuffers(1, &cube_vbo);
+ glBindVertexArray(cube_vao);
+ glBindBuffer(GL_ARRAY_BUFFER, cube_vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
+
+ glm::mat4 capture_projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
+ glm::mat4 capture_views[] =
+ {
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
+ glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f))
+ };
+
+ glUseProgram(shader_program);
+ glUniform1i(glGetUniformLocation(shader_program, "u_EquirectangularMap"), 0);
+ // EquirectToCubemap is authored in Slang (row-major); transpose glm's
+ // column-major matrices on upload (GL_TRUE) to match.
+ glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_Projection"), 1, GL_TRUE, &capture_projection[0][0]);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, hdr_texture);
+
+ glViewport(0, 0, m_width, m_height);
+ glBindFramebuffer(GL_FRAMEBUFFER, capture_fbo);
+ for (unsigned int i = 0; i < 6; ++i)
+ {
+ glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_View"), 1, GL_TRUE, &capture_views[i][0][0]);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, m_renderer_id, 0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ glBindVertexArray(cube_vao);
+ glDrawArrays(GL_TRIANGLES, 0, 36);
+ }
+ glBindVertexArray(0);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ glDeleteVertexArrays(1, &cube_vao);
+ glDeleteBuffers(1, &cube_vbo);
+ glDeleteTextures(1, &hdr_texture);
+ glDeleteFramebuffers(1, &capture_fbo);
+ glDeleteRenderbuffers(1, &capture_rbo);
+ }
+
+ auto OpenGLCubemapTexture::set_data(void* data, uint32_t size) -> void
+ {
+ DONUT_WARN("set_data not implemented for cubemaps");
+ }
+
+ auto OpenGLCubemapTexture::bind(uint32_t slot) const -> void
+ {
+ glActiveTexture(GL_TEXTURE0 + slot);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, m_renderer_id);
+ }
+
+ auto OpenGLCubemapTexture::bind_as_image(uint32_t slot, bool read_only) const -> void
+ {
+ if (glBindImageTexture == nullptr)
+ return;
+ GLenum access = read_only ? GL_READ_ONLY : GL_WRITE_ONLY;
+ glBindImageTexture(slot, m_renderer_id, 0, GL_TRUE, 0, access, m_internal_format);
+ }
+};
diff --git a/src/platform/opengl/opengl_texture.h b/src/platform/opengl/opengl_texture.h
new file mode 100644
index 0000000..7aa87ea
--- /dev/null
+++ b/src/platform/opengl/opengl_texture.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "rendering/texture.h"
+#include "core/log.h"
+
+#include <glad/glad.h>
+
+namespace Donut
+{
+ class OpenGLTexture2D
+ : public Texture2D
+ {
+ public:
+ OpenGLTexture2D(uint32_t width, uint32_t height);
+ OpenGLTexture2D(const std::string& path);
+ virtual ~OpenGLTexture2D();
+
+ virtual auto get_width() const -> uint32_t override{ return m_width; }
+ virtual auto get_height() const -> uint32_t override{ return m_height; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ virtual auto set_data(void* data, uint32_t size) -> void override;
+ virtual auto bind(uint32_t slot = 0) const -> void override;
+ virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override;
+
+ virtual bool operator==(const Texture& other) const override
+ {
+ return m_renderer_id == other.get_renderer_id();
+ }
+
+ private:
+ std::string m_path;
+ uint32_t m_width, m_height;
+ uint32_t m_renderer_id;
+ GLenum m_internal_format, m_data_format;
+ };
+
+ class OpenGLCubemapTexture
+ : public CubemapTexture
+ {
+ public:
+ OpenGLCubemapTexture(uint32_t width, uint32_t height);
+ OpenGLCubemapTexture(const std::string& path);
+ virtual ~OpenGLCubemapTexture();
+
+ virtual auto get_width() const -> uint32_t override{ return m_width; }
+ virtual auto get_height() const -> uint32_t override{ return m_height; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ virtual auto set_data(void* data, uint32_t size) -> void override;
+ virtual auto bind(uint32_t slot = 0) const -> void override;
+ virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override;
+
+ virtual bool operator==(const Texture& other) const override
+ {
+ return m_renderer_id == other.get_renderer_id();
+ }
+
+ private:
+ void LoadHDRI(const std::string& path);
+ auto convert_equirectangular_to_cubemap(float* hdr_data, int width, int height) -> void;
+
+ std::string m_path;
+ uint32_t m_width, m_height;
+ uint32_t m_renderer_id;
+ GLenum m_internal_format, m_data_format;
+ };
+}
diff --git a/src/platform/opengl/opengl_uniform_buffer.cpp b/src/platform/opengl/opengl_uniform_buffer.cpp
new file mode 100644
index 0000000..9b3b6d2
--- /dev/null
+++ b/src/platform/opengl/opengl_uniform_buffer.cpp
@@ -0,0 +1,29 @@
+#include "opengl_uniform_buffer.h"
+
+namespace Donut
+{
+ OpenGLUniformBuffer::OpenGLUniformBuffer(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);
+ }
+
+ OpenGLUniformBuffer::~OpenGLUniformBuffer()
+ {
+ glDeleteBuffers(1, &m_renderer_id);
+ }
+
+ auto OpenGLUniformBuffer::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 OpenGLUniformBuffer::bind(uint32_t binding) -> void
+ {
+ glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id);
+ }
+};
diff --git a/src/platform/opengl/opengl_uniform_buffer.h b/src/platform/opengl/opengl_uniform_buffer.h
new file mode 100644
index 0000000..db137a0
--- /dev/null
+++ b/src/platform/opengl/opengl_uniform_buffer.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "rendering/uniform_buffer.h"
+#include <glad/glad.h>
+
+namespace Donut
+{
+ class OpenGLUniformBuffer : public UniformBuffer
+ {
+ public:
+ OpenGLUniformBuffer(uint32_t size, uint32_t binding);
+ virtual ~OpenGLUniformBuffer();
+
+ virtual auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void override;
+ virtual auto bind(uint32_t binding) -> void override;
+ private:
+ uint32_t m_renderer_id = 0;
+ uint32_t m_size = 0;
+ uint32_t m_binding = 0;
+ };
+};
diff --git a/src/platform/opengl/opengl_vertex_array.cpp b/src/platform/opengl/opengl_vertex_array.cpp
new file mode 100644
index 0000000..01e6f56
--- /dev/null
+++ b/src/platform/opengl/opengl_vertex_array.cpp
@@ -0,0 +1,58 @@
+#include <glad/glad.h>
+
+#include "opengl_vertex_array.h"
+#include "rendering/vertex_buffer.h"
+#include "rendering/index_buffer.h"
+
+namespace Donut
+{
+ OpenGLVertexArray::OpenGLVertexArray()
+ {
+ // 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);
+ }
+
+ OpenGLVertexArray::~OpenGLVertexArray()
+ {
+ glDeleteVertexArrays(1, &m_renderer_id);
+ }
+
+ auto OpenGLVertexArray::bind() const -> void
+ {
+ glBindVertexArray(m_renderer_id);
+ }
+
+ auto OpenGLVertexArray::unbind() const -> void
+ {
+ glBindVertexArray(0);
+ }
+
+ auto OpenGLVertexArray::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 OpenGLVertexArray::set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void
+ {
+ glBindVertexArray(m_renderer_id);
+ index_buffer->bind();
+ m_index_buffer = index_buffer;
+ }
+};
diff --git a/src/platform/opengl/opengl_vertex_array.h b/src/platform/opengl/opengl_vertex_array.h
new file mode 100644
index 0000000..5115830
--- /dev/null
+++ b/src/platform/opengl/opengl_vertex_array.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "core/memory.h"
+
+#include "rendering/vertex_array.h"
+#include "rendering/vertex_buffer.h"
+#include "rendering/index_buffer.h"
+
+#include <vector>
+
+namespace Donut
+{
+ class OpenGLVertexArray
+ : public VertexArray
+ {
+ public:
+ OpenGLVertexArray();
+ virtual ~OpenGLVertexArray();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void override;
+ virtual auto set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void override;
+
+ virtual const std::vector<Ref<VertexBuffer>>& get_vertex_buffers() const override
+ {
+ return m_vertex_buffers;
+ }
+
+ virtual const Ref<IndexBuffer>& get_index_buffer() const override
+ {
+ return m_index_buffer;
+ }
+ private:
+ uint32_t m_renderer_id;
+ uint32_t m_vertex_buffer_index = 0;
+ std::vector<Ref<VertexBuffer>> m_vertex_buffers;
+ Ref<IndexBuffer> m_index_buffer;
+ };
+};
diff --git a/src/platform/opengl/opengl_vertex_buffer.cpp b/src/platform/opengl/opengl_vertex_buffer.cpp
new file mode 100644
index 0000000..5cd4b82
--- /dev/null
+++ b/src/platform/opengl/opengl_vertex_buffer.cpp
@@ -0,0 +1,35 @@
+#include "opengl_vertex_buffer.h"
+#include "rendering/vertex_buffer.h"
+
+#include <glad/glad.h>
+
+namespace Donut
+{
+ OpenGLVertexBuffer::OpenGLVertexBuffer(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);
+ }
+
+ OpenGLVertexBuffer::~OpenGLVertexBuffer()
+ {
+ glDeleteBuffers(1, &m_renderer_id);
+ }
+
+ auto OpenGLVertexBuffer::bind() const -> void
+ {
+ glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
+ }
+
+ auto OpenGLVertexBuffer::unbind() const -> void
+ {
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ auto OpenGLVertexBuffer::set_data(const void* data, uint32_t size) -> void
+ {
+ glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
+ glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
+ }
+};
diff --git a/src/platform/opengl/opengl_vertex_buffer.h b/src/platform/opengl/opengl_vertex_buffer.h
new file mode 100644
index 0000000..df2e0e6
--- /dev/null
+++ b/src/platform/opengl/opengl_vertex_buffer.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "rendering/vertex_buffer.h"
+
+namespace Donut
+{
+ class OpenGLVertexBuffer
+ : public VertexBuffer
+ {
+ public:
+ OpenGLVertexBuffer(const void* data, uint32_t size);
+ virtual ~OpenGLVertexBuffer();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+ virtual auto set_data(const void* data, uint32_t size) -> void override;
+
+ virtual auto get_layout() const -> const VertexBufferLayout& override{ return m_layout; }
+ virtual auto set_layout(const VertexBufferLayout& layout) -> void override{ m_layout = layout; }
+
+ private:
+ uint32_t m_renderer_id;
+ VertexBufferLayout m_layout;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_context.cpp b/src/platform/vulkan/vulkan_context.cpp
new file mode 100644
index 0000000..ece7700
--- /dev/null
+++ b/src/platform/vulkan/vulkan_context.cpp
@@ -0,0 +1,789 @@
+#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
new file mode 100644
index 0000000..de57c90
--- /dev/null
+++ b/src/platform/vulkan/vulkan_context.h
@@ -0,0 +1,41 @@
+#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_index_buffer.cpp b/src/platform/vulkan/vulkan_index_buffer.cpp
new file mode 100644
index 0000000..a6e5912
--- /dev/null
+++ b/src/platform/vulkan/vulkan_index_buffer.cpp
@@ -0,0 +1,25 @@
+#include "vulkan_index_buffer.h"
+
+namespace Donut
+{
+ VulkanIndexBuffer::VulkanIndexBuffer(uint32_t* indices, uint32_t count)
+ : m_count(count)
+ {
+ // TODO(Hachem): Implement Vulkan index buffer creation
+ }
+
+ VulkanIndexBuffer::~VulkanIndexBuffer()
+ {
+ // TODO(Hachem): Implement Vulkan index buffer cleanup
+ }
+
+ auto VulkanIndexBuffer::bind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan index buffer binding
+ }
+
+ auto VulkanIndexBuffer::unbind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan index buffer unbinding
+ }
+};
diff --git a/src/platform/vulkan/vulkan_index_buffer.h b/src/platform/vulkan/vulkan_index_buffer.h
new file mode 100644
index 0000000..a0734a8
--- /dev/null
+++ b/src/platform/vulkan/vulkan_index_buffer.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "rendering/index_buffer.h"
+
+namespace Donut
+{
+ class VulkanIndexBuffer
+ : public IndexBuffer
+ {
+ public:
+ VulkanIndexBuffer(uint32_t* indices, uint32_t count);
+ virtual ~VulkanIndexBuffer();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto get_count() const -> uint32_t override{ return m_count; }
+ private:
+ uint32_t m_renderer_id;
+ uint32_t m_count;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp
new file mode 100644
index 0000000..95a546b
--- /dev/null
+++ b/src/platform/vulkan/vulkan_renderer.cpp
@@ -0,0 +1,1235 @@
+#include "vulkan_renderer.h"
+#include "core/log.h"
+#include "core/camera.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{};
+
+ // Geodesic scene, rendered every frame into a fixed low-resolution
+ // offscreen image (keeps each draw well under the Metal GPU watchdog),
+ // then upscaled onto the swapchain by the present pass below.
+ static constexpr uint32_t GEO_W = 480, GEO_H = 270;
+ VkImage geo_image = VK_NULL_HANDLE;
+ VkDeviceMemory geo_image_mem = VK_NULL_HANDLE;
+ VkImageView geo_image_view = VK_NULL_HANDLE;
+ VkRenderPass geo_render_pass = VK_NULL_HANDLE;
+ VkFramebuffer geo_framebuffer = VK_NULL_HANDLE;
+ 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;
+ Camera camera{ 60.0f, (float)GEO_W / (float)GEO_H, 0.1f, 100.0f };
+ bool left_was_down = false;
+ VkImage cube_image = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE;
+ VkImageView cube_view = VK_NULL_HANDLE; VkSampler cube_sampler = VK_NULL_HANDLE;
+ 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;
+ VkDescriptorSet present_set = VK_NULL_HANDLE;
+ VkPipelineLayout present_pipeline_layout = VK_NULL_HANDLE;
+ VkPipeline present_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_hdri_cubemap(const char* path) -> bool;
+ auto create_present_resources() -> bool;
+ auto process_input() -> void;
+ auto update_geodesic_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);
+ DONUT_INFO("Vulkan device: {}", props.deviceName);
+ 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;
+ }
+
+ 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;
+ }
+
+ 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;
+
+ VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
+ ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { GEO_W, GEO_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, &geo_image));
+ VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(device, geo_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, &geo_image_mem));
+ VK_CHECK(vkBindImageMemory(device, geo_image, geo_image_mem, 0));
+ VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
+ vci.image = geo_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, &geo_image_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_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));
+ VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
+ fbci.renderPass = geo_render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &geo_image_view;
+ fbci.width = GEO_W; fbci.height = GEO_H; fbci.layers = 1;
+ VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &geo_framebuffer));
+
+ 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(1e11);
+ camera.set_orbital_limits(4e10, 3e11);
+ camera.set_orbital_speed(0.01f);
+ camera.set_zoom_speed(1e10);
+ camera.set_azimuth(0.0f);
+ camera.set_elevation(1.25f);
+
+ void* p = nullptr;
+ vkMapMemory(device, cam_mem, 0, 128, 0, &cam_mapped); // camera UBO is refilled every frame
+
+ float disk_data[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 };
+ vkMapMemory(device, disk_mem, 0, 32, 0, &p); memcpy(p, disk_data, sizeof(disk_data)); vkUnmapMemory(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(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;
+ VkViewport vp{ 0, 0, (float)GEO_W, (float)GEO_H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { GEO_W, GEO_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;
+ 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 = 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{} offscreen)", (int)GEO_W, (int)GEO_H);
+ return true;
+ }
+
+ auto VulkanRenderer::Impl::create_hdri_cubemap(const char* path) -> bool
+ {
+ 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;
+
+ 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 = 1; 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_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, 1, 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.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);
+ }
+ 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;
+ }
+
+ 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));
+ VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 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, &present_pool));
+ 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, &present_set));
+ VkDescriptorImageInfo ii{ present_sampler, geo_image_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
+ VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
+ write.dstSet = 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::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 = camera.get_orbital_position();
+ glm::vec3 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(60.0f * 0.5f));
+ cam_data.aspect = (float)GEO_W / (float)GEO_H;
+ cam_data.moving = camera.is_dragging() || camera.is_panning() ? 1u : 0u;
+ if (cam_mapped) memcpy(cam_mapped, &cam_data, sizeof(cam_data));
+
+ // Fewer integration steps while the camera moves keeps dragging responsive;
+ // more steps once it settles renders the disk in full.
+ struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim;
+ sim.steps_moving = 3500; sim.steps_static = 5000; sim.early_exit = 5e12f;
+ sim.time = (float)(glfwGetTime() - start_time);
+ if (sim_mapped) memcpy(sim_mapped, &sim, sizeof(sim));
+ }
+
+ 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;
+
+ bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
+ if (left_down && !left_was_down && !over_ui)
+ camera.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0);
+ else if (!left_down && left_was_down)
+ camera.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0);
+ left_was_down = left_down;
+
+ double mx = 0, my = 0;
+ glfwGetCursorPos(window, &mx, &my);
+ camera.process_orbital_mouse_move(mx, my); // tracks last position internally; orbits only while dragging
+
+ double scroll = g_ScrollAccum; g_ScrollAccum = 0.0;
+ if (scroll != 0.0 && !over_ui)
+ camera.process_orbital_scroll(0.0, scroll);
+ }
+
+ auto VulkanRenderer::Impl::destroy_geodesic_resources() -> void
+ {
+ 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; }
+ 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); }
+ if (geo_framebuffer) vkDestroyFramebuffer(device, geo_framebuffer, nullptr);
+ if (geo_render_pass) vkDestroyRenderPass(device, geo_render_pass, nullptr);
+ if (geo_image_view) vkDestroyImageView(device, geo_image_view, nullptr);
+ if (geo_image) vkDestroyImage(device, geo_image, nullptr);
+ if (geo_image_mem) vkFreeMemory(device, geo_image_mem, 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));
+
+ // Geodesic offscreen pass
+ 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 = geo_framebuffer;
+ grp.renderArea = { { 0, 0 }, { GEO_W, GEO_H } };
+ grp.clearValueCount = 1; grp.pClearValues = &geo_clear;
+ vkCmdBeginRenderPass(cmd, &grp, VK_SUBPASS_CONTENTS_INLINE);
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline_layout, 0, 1, &geo_set, 0, nullptr);
+ VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off);
+ vkCmdDraw(cmd, 6, 1, 0, 0);
+ vkCmdEndRenderPass(cmd);
+
+ // Swapchain pass: upscale the geodesic image, then the ImGui UI on top
+ VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } };
+ VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
+ rpbi.renderPass = render_pass;
+ rpbi.framebuffer = framebuffers[image_index];
+ rpbi.renderArea = { { 0, 0 }, swapchain_extent };
+ rpbi.clearValueCount = 1; rpbi.pClearValues = &cv;
+ vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline);
+ // Negative-height viewport flips the geodesic image vertically so the scene
+ // reads the same as the OpenGL path (Vulkan's clip space is Y-down). Only
+ // this draw is affected; ImGui sets its own viewport.
+ VkViewport vp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 };
+ VkRect2D scissor{ { 0, 0 }, swapchain_extent };
+ vkCmdSetViewport(cmd, 0, 1, &vp);
+ vkCmdSetScissor(cmd, 0, 1, &scissor);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline_layout, 0, 1, &present_set, 0, nullptr);
+ vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off);
+ vkCmdDraw(cmd, 6, 1, 0, 0);
+ if (draw_data)
+ ImGui_ImplVulkan_RenderDrawData(draw_data, cmd);
+ vkCmdEndRenderPass(cmd);
+
+ VK_CHECK(vkEndCommandBuffer(cmd));
+ return true;
+ }
+
+ auto VulkanRenderer::Impl::cleanup_swapchain() -> void
+ {
+ 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;
+ 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_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;
+
+ 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_im_gui() -> 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::on_resize(int width, int height) -> void
+ {
+ m_impl->framebuffer_resized = true;
+ m_impl->width = width; m_impl->height = height;
+ }
+
+ 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();
+ 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
new file mode 100644
index 0000000..97a4e38
--- /dev/null
+++ b/src/platform/vulkan/vulkan_renderer.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <glm/glm.hpp>
+#include <functional>
+
+// 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. 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:
+ VulkanRenderer();
+ ~VulkanRenderer();
+
+ // glfwWindow must be a GLFW window created with GLFW_NO_API.
+ auto init(void* glfwWindow, int width, int height) -> bool;
+ auto shutdown() -> void;
+
+ // Creates the ImGui context + Vulkan/GLFW backends. Call after init().
+ auto init_im_gui() -> bool;
+
+ // Renders + presents one frame: clears to clearColor, then (if init_im_gui
+ // ran) opens an ImGui frame, invokes buildUI to populate it, and draws it.
+ auto draw_frame(const glm::vec4& clearColor, const std::function<void()>& buildUI = {}) -> void;
+
+ auto on_resize(int width, int height) -> void;
+
+ private:
+ struct Impl;
+ Impl* m_impl = nullptr;
+ };
+}
diff --git a/src/platform/vulkan/vulkan_renderer_api.cpp b/src/platform/vulkan/vulkan_renderer_api.cpp
new file mode 100644
index 0000000..56499f4
--- /dev/null
+++ b/src/platform/vulkan/vulkan_renderer_api.cpp
@@ -0,0 +1,86 @@
+#include "vulkan_renderer_api.h"
+
+namespace Donut
+{
+ auto VulkanRendererAPI::init() -> void
+ {
+ // TODO(Hachem): Implement Vulkan renderer API initialization
+ }
+
+ auto VulkanRendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
+ {
+ // TODO(Hachem): Implement Vulkan viewport setting
+ }
+
+ auto VulkanRendererAPI::set_clear_color(const glm::vec4& color) -> void
+ {
+ // TODO(Hachem): Implement Vulkan clear color setting
+ }
+
+ auto VulkanRendererAPI::clear() -> void
+ {
+ // TODO(Hachem): Implement Vulkan clear
+ }
+
+ auto VulkanRendererAPI::enable_depth_test() -> void
+ {
+ // TODO(Hachem): Implement Vulkan depth test enabling
+ }
+
+ auto VulkanRendererAPI::disable_depth_test() -> void
+ {
+ // TODO(Hachem): Implement Vulkan depth test disabling
+ }
+
+ auto VulkanRendererAPI::set_face_culling(bool enabled) -> void
+ {
+ // TODO(Hachem): Implement Vulkan face culling setting
+ }
+
+ auto VulkanRendererAPI::enable_blending() -> void
+ {
+ // TODO(Hachem): Implement Vulkan blending enabling
+ }
+
+ auto VulkanRendererAPI::disable_blending() -> void
+ {
+ // TODO(Hachem): Implement Vulkan blending disabling
+ }
+
+ auto VulkanRendererAPI::draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
+ {
+ // TODO(Hachem): Implement Vulkan indexed drawing
+ }
+
+ auto VulkanRendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void
+ {
+ // TODO(Hachem): Implement Vulkan array drawing
+ }
+
+ auto VulkanRendererAPI::draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
+ {
+ // TODO(Hachem): Implement Vulkan line drawing
+ }
+
+ auto VulkanRendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void
+ {
+ // TODO(Hachem): Implement Vulkan texture binding
+ }
+
+ auto VulkanRendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void
+ {
+ // TODO(Hachem): Implement Vulkan image texture binding
+ }
+
+ auto VulkanRendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
+ uint32_t format, uint32_t type, void* pixels) -> void
+ {
+ // TODO(Hachem): Implement Vulkan pixel reading
+ // For now, this is a placeholder implementation
+ // In a real Vulkan implementation, this would involve:
+ // 1. Creating a staging buffer
+ // 2. Copying the framebuffer to the staging buffer
+ // 3. Mapping the staging buffer and copying to the pixels array
+ // 4. Unmapping and destroying the staging buffer
+ }
+};
diff --git a/src/platform/vulkan/vulkan_renderer_api.h b/src/platform/vulkan/vulkan_renderer_api.h
new file mode 100644
index 0000000..15bf76d
--- /dev/null
+++ b/src/platform/vulkan/vulkan_renderer_api.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "core/memory.h"
+#include "rendering/renderer.h"
+
+namespace Donut
+{
+ class VulkanRendererAPI
+ : public RendererAPI
+ {
+ public:
+ virtual auto init() -> void override;
+ virtual void set_viewport(uint32_t x, uint32_t y,
+ uint32_t width, uint32_t height) override;
+ virtual auto set_clear_color(const glm::vec4& color) -> void override;
+ virtual auto clear() -> void override;
+ virtual auto enable_depth_test() -> void override;
+ virtual auto disable_depth_test() -> void override;
+ virtual auto set_face_culling(bool enabled) -> void override;
+ virtual auto enable_blending() -> void override;
+ virtual auto disable_blending() -> void override;
+
+ virtual void draw_indexed(const Ref<VertexArray>& vertex_array,
+ uint32_t index_count = 0) override;
+
+ virtual void draw_arrays(uint32_t vertex_count,
+ uint32_t first = 0) override;
+ virtual void draw_lines(const Ref<VertexArray>& vertex_array,
+ uint32_t index_count = 0) override;
+ virtual void bind_texture(uint32_t texture_id,
+ uint32_t slot = 0) override;
+ virtual void bind_image_texture(uint32_t texture_id,
+ uint32_t slot = 0,
+ bool read_only = false) override;
+ virtual void read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
+ uint32_t format, uint32_t type, void* pixels) override;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_shader.cpp b/src/platform/vulkan/vulkan_shader.cpp
new file mode 100644
index 0000000..7549761
--- /dev/null
+++ b/src/platform/vulkan/vulkan_shader.cpp
@@ -0,0 +1,146 @@
+#include "vulkan_shader.h"
+
+#include <fstream>
+#include <glm/gtc/type_ptr.hpp>
+
+namespace Donut
+{
+ VulkanShader::VulkanShader(const std::string& filepath)
+ {
+ // TODO(Hachem): Implement Vulkan shader creation from filepath
+ }
+
+ VulkanShader::VulkanShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src)
+ : m_name(name)
+ {
+ // TODO(Hachem): Implement Vulkan shader creation from source
+ }
+
+ VulkanShader::VulkanShader(const std::string& name, const std::string& compute_src)
+ : m_name(name)
+ {
+ // TODO(Hachem): Implement Vulkan compute shader creation
+ }
+
+ VulkanShader::~VulkanShader()
+ {
+ // TODO(Hachem): Implement Vulkan shader cleanup
+ }
+
+ auto VulkanShader::bind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader binding
+ }
+
+ auto VulkanShader::unbind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader unbinding
+ }
+
+ auto VulkanShader::set_int(const std::string& name, int value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader int uniform setting
+ }
+
+ auto VulkanShader::set_int_array(const std::string& name, int* values, uint32_t count) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader int array uniform setting
+ }
+
+ auto VulkanShader::set_float(const std::string& name, float value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader float uniform setting
+ }
+
+ auto VulkanShader::set_float2(const std::string& name, const glm::vec2& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader float2 uniform setting
+ }
+
+ auto VulkanShader::set_float3(const std::string& name, const glm::vec3& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader float3 uniform setting
+ }
+
+ auto VulkanShader::set_float4(const std::string& name, const glm::vec4& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader float4 uniform setting
+ }
+
+ auto VulkanShader::set_mat4(const std::string& name, const glm::mat4& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader mat4 uniform setting
+ }
+
+ auto VulkanShader::dispatch(uint32_t x, uint32_t y, uint32_t z) -> void
+ {
+ // TODO(Hachem): Implement Vulkan compute shader dispatch
+ }
+
+ auto VulkanShader::dispatch_indirect(uint32_t offset) -> void
+ {
+ // TODO(Hachem): Implement Vulkan indirect compute shader dispatch
+ }
+
+ auto VulkanShader::memory_barrier(uint32_t barriers) -> void
+ {
+ // TODO(Hachem): Implement Vulkan memory barrier
+ }
+
+ auto VulkanShader::upload_uniform_int(const std::string& name, int value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform int upload
+ }
+
+ auto VulkanShader::upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform int array upload
+ }
+
+ auto VulkanShader::upload_uniform_float(const std::string& name, float value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform float upload
+ }
+
+ auto VulkanShader::upload_uniform_float2(const std::string& name, const glm::vec2& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform float2 upload
+ }
+
+ auto VulkanShader::upload_uniform_float3(const std::string& name, const glm::vec3& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform float3 upload
+ }
+
+ auto VulkanShader::upload_uniform_float4(const std::string& name, const glm::vec4& value) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform float4 upload
+ }
+
+ auto VulkanShader::upload_uniform_mat3(const std::string& name, const glm::mat3& matrix) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform mat3 upload
+ }
+
+ auto VulkanShader::upload_uniform_mat4(const std::string& name, const glm::mat4& matrix) -> void
+ {
+ // TODO(Hachem): Implement Vulkan uniform mat4 upload
+ }
+
+ auto VulkanShader::read_file(const std::string& filepath) -> std::string
+ {
+ // TODO(Hachem): Implement file reading for Vulkan shader
+ return "";
+ }
+
+ auto VulkanShader::pre_process(const std::string& source) -> std::unordered_map<uint32_t, std::string>
+ {
+ // TODO(Hachem): Implement shader preprocessing for Vulkan
+ return {};
+ }
+
+ auto VulkanShader::compile(const std::unordered_map<uint32_t, std::string>& shader_sources) -> void
+ {
+ // TODO(Hachem): Implement Vulkan shader compilation
+ }
+};
diff --git a/src/platform/vulkan/vulkan_shader.h b/src/platform/vulkan/vulkan_shader.h
new file mode 100644
index 0000000..9453130
--- /dev/null
+++ b/src/platform/vulkan/vulkan_shader.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "rendering/shader.h"
+
+#include <string>
+#include <unordered_map>
+
+namespace Donut
+{
+ class VulkanShader
+ : public Shader
+ {
+ public:
+ VulkanShader(const std::string& filepath);
+ VulkanShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src);
+ VulkanShader(const std::string& name, const std::string& compute_src);
+ virtual ~VulkanShader();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto set_int(const std::string& name, int value) -> void override;
+ virtual auto set_int_array(const std::string& name, int* values, uint32_t count) -> void override;
+ virtual auto set_float(const std::string& name, float value) -> void override;
+ virtual auto set_float2(const std::string& name, const glm::vec2& value) -> void override;
+ virtual auto set_float3(const std::string& name, const glm::vec3& value) -> void override;
+ virtual auto set_float4(const std::string& name, const glm::vec4& value) -> void override;
+ virtual auto set_mat4(const std::string& name, const glm::mat4& value) -> void override;
+
+ virtual auto dispatch(uint32_t x, uint32_t y = 1, uint32_t z = 1) -> void override;
+ virtual auto dispatch_indirect(uint32_t offset = 0) -> void override;
+ virtual auto memory_barrier(uint32_t barriers) -> void override;
+
+ virtual auto get_name() const -> const std::string& override{ return m_name; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ auto upload_uniform_int(const std::string& name, int value) -> void;
+ auto upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void;
+ auto upload_uniform_float(const std::string& name, float value) -> void;
+ auto upload_uniform_float2(const std::string& name, const glm::vec2& value) -> void;
+ auto upload_uniform_float3(const std::string& name, const glm::vec3& value) -> void;
+ auto upload_uniform_float4(const std::string& name, const glm::vec4& value) -> void;
+ auto upload_uniform_mat3(const std::string& name, const glm::mat3& matrix) -> void;
+ auto upload_uniform_mat4(const std::string& name, const glm::mat4& matrix) -> void;
+ private:
+ auto read_file(const std::string& filepath) -> std::string;
+ auto pre_process(const std::string& source) -> std::unordered_map<uint32_t, std::string>;
+ auto compile(const std::unordered_map<uint32_t, std::string>& shader_sources) -> void;
+ uint32_t m_renderer_id;
+ std::string m_name;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_texture.cpp b/src/platform/vulkan/vulkan_texture.cpp
new file mode 100644
index 0000000..a970a53
--- /dev/null
+++ b/src/platform/vulkan/vulkan_texture.cpp
@@ -0,0 +1,73 @@
+#include "vulkan_texture.h"
+
+namespace Donut
+{
+ VulkanTexture2D::VulkanTexture2D(uint32_t width, uint32_t height)
+ : m_width(width), m_height(height)
+ {
+ m_internal_format = 0;
+ m_data_format = 0;
+ m_renderer_id = 0;
+ }
+
+ VulkanTexture2D::VulkanTexture2D(const std::string& path)
+ : m_path(path)
+ {
+ m_width = 1;
+ m_height = 1;
+ m_internal_format = 0;
+ m_data_format = 0;
+ m_renderer_id = 0;
+ }
+
+ VulkanTexture2D::~VulkanTexture2D()
+ {
+ }
+
+ auto VulkanTexture2D::set_data(void* data, uint32_t size) -> void
+ {
+ }
+
+ auto VulkanTexture2D::bind(uint32_t slot) const -> void
+ {
+ }
+
+ auto VulkanTexture2D::bind_as_image(uint32_t slot, bool read_only) const -> void
+ {
+ }
+
+ // Vulkan Cubemap Implementation (Placeholder)
+ VulkanCubemapTexture::VulkanCubemapTexture(uint32_t width, uint32_t height)
+ : m_width(width), m_height(height)
+ {
+ m_internal_format = 0;
+ m_data_format = 0;
+ m_renderer_id = 0;
+ }
+
+ VulkanCubemapTexture::VulkanCubemapTexture(const std::string& path)
+ : m_path(path)
+ {
+ m_width = 1024;
+ m_height = 1024;
+ m_internal_format = 0;
+ m_data_format = 0;
+ m_renderer_id = 0;
+ }
+
+ VulkanCubemapTexture::~VulkanCubemapTexture()
+ {
+ }
+
+ auto VulkanCubemapTexture::set_data(void* data, uint32_t size) -> void
+ {
+ }
+
+ auto VulkanCubemapTexture::bind(uint32_t slot) const -> void
+ {
+ }
+
+ auto VulkanCubemapTexture::bind_as_image(uint32_t slot, bool read_only) const -> void
+ {
+ }
+};
diff --git a/src/platform/vulkan/vulkan_texture.h b/src/platform/vulkan/vulkan_texture.h
new file mode 100644
index 0000000..ea8471b
--- /dev/null
+++ b/src/platform/vulkan/vulkan_texture.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "rendering/texture.h"
+
+namespace Donut
+{
+ class VulkanTexture2D
+ : public Texture2D
+ {
+ public:
+ VulkanTexture2D(uint32_t width, uint32_t height);
+ VulkanTexture2D(const std::string& path);
+ virtual ~VulkanTexture2D();
+
+ virtual auto get_width() const -> uint32_t override{ return m_width; }
+ virtual auto get_height() const -> uint32_t override{ return m_height; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ virtual auto set_data(void* data, uint32_t size) -> void override;
+ virtual auto bind(uint32_t slot = 0) const -> void override;
+ virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override;
+
+ virtual bool operator==(const Texture& other) const override
+ {
+ return m_renderer_id == ((VulkanTexture2D&)other).m_renderer_id;
+ }
+ private:
+ std::string m_path;
+ uint32_t m_width, m_height;
+ uint32_t m_renderer_id;
+ uint32_t m_internal_format, m_data_format;
+ };
+
+ class VulkanCubemapTexture
+ : public CubemapTexture
+ {
+ public:
+ VulkanCubemapTexture(uint32_t width, uint32_t height);
+ VulkanCubemapTexture(const std::string& path);
+ virtual ~VulkanCubemapTexture();
+
+ virtual auto get_width() const -> uint32_t override{ return m_width; }
+ virtual auto get_height() const -> uint32_t override{ return m_height; }
+ virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; }
+
+ virtual auto set_data(void* data, uint32_t size) -> void override;
+ virtual auto bind(uint32_t slot = 0) const -> void override;
+ virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override;
+
+ virtual bool operator==(const Texture& other) const override
+ {
+ return m_renderer_id == ((VulkanCubemapTexture&)other).m_renderer_id;
+ }
+ private:
+ std::string m_path;
+ uint32_t m_width, m_height;
+ uint32_t m_renderer_id;
+ uint32_t m_internal_format, m_data_format;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_uniform_buffer.cpp b/src/platform/vulkan/vulkan_uniform_buffer.cpp
new file mode 100644
index 0000000..1a64099
--- /dev/null
+++ b/src/platform/vulkan/vulkan_uniform_buffer.cpp
@@ -0,0 +1,25 @@
+#include "vulkan_uniform_buffer.h"
+
+namespace Donut
+{
+ VulkanUniformBuffer::VulkanUniformBuffer(uint32_t size, uint32_t binding)
+ : m_size(size), m_binding(binding)
+ {
+ // TODO: Implement Vulkan uniform buffer
+ }
+
+ VulkanUniformBuffer::~VulkanUniformBuffer()
+ {
+ // TODO: Implement Vulkan uniform buffer cleanup
+ }
+
+ auto VulkanUniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void
+ {
+ // TODO: Implement Vulkan uniform buffer data setting
+ }
+
+ auto VulkanUniformBuffer::bind(uint32_t binding) -> void
+ {
+ // TODO: Implement Vulkan uniform buffer binding
+ }
+};
diff --git a/src/platform/vulkan/vulkan_uniform_buffer.h b/src/platform/vulkan/vulkan_uniform_buffer.h
new file mode 100644
index 0000000..7d9ad0a
--- /dev/null
+++ b/src/platform/vulkan/vulkan_uniform_buffer.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "rendering/uniform_buffer.h"
+
+namespace Donut
+{
+ class VulkanUniformBuffer : public UniformBuffer
+ {
+ public:
+ VulkanUniformBuffer(uint32_t size, uint32_t binding);
+ virtual ~VulkanUniformBuffer();
+
+ virtual auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void override;
+ virtual auto bind(uint32_t binding) -> void override;
+ private:
+ uint32_t m_size;
+ uint32_t m_binding;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_vertex_array.cpp b/src/platform/vulkan/vulkan_vertex_array.cpp
new file mode 100644
index 0000000..73219b1
--- /dev/null
+++ b/src/platform/vulkan/vulkan_vertex_array.cpp
@@ -0,0 +1,36 @@
+#include "vulkan_vertex_array.h"
+
+namespace Donut
+{
+ VulkanVertexArray::VulkanVertexArray()
+ {
+ // TODO(Hachem): Implement Vulkan vertex array creation
+ }
+
+ VulkanVertexArray::~VulkanVertexArray()
+ {
+ // TODO(Hachem): Implement Vulkan vertex array cleanup
+ }
+
+ auto VulkanVertexArray::bind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex array binding
+ }
+
+ auto VulkanVertexArray::unbind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex array unbinding
+ }
+
+ auto VulkanVertexArray::add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer addition
+ m_vertex_buffers.push_back(vertex_buffer);
+ }
+
+ auto VulkanVertexArray::set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void
+ {
+ // TODO(Hachem): Implement Vulkan index buffer setting
+ m_index_buffer = index_buffer;
+ }
+};
diff --git a/src/platform/vulkan/vulkan_vertex_array.h b/src/platform/vulkan/vulkan_vertex_array.h
new file mode 100644
index 0000000..0d282a2
--- /dev/null
+++ b/src/platform/vulkan/vulkan_vertex_array.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "rendering/vertex_array.h"
+#include "core/memory.h"
+
+namespace Donut
+{
+ class VulkanVertexArray
+ : public VertexArray
+ {
+ public:
+ VulkanVertexArray();
+ virtual ~VulkanVertexArray();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void override;
+ virtual auto set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void override;
+
+ virtual auto get_vertex_buffers() const -> const std::vector<Ref<VertexBuffer>>& { return m_vertex_buffers; }
+ virtual auto get_index_buffer() const -> const Ref<IndexBuffer>& { return m_index_buffer; }
+ private:
+ uint32_t m_renderer_id;
+ std::vector<Ref<VertexBuffer>> m_vertex_buffers;
+ Ref<IndexBuffer> m_index_buffer;
+ };
+};
diff --git a/src/platform/vulkan/vulkan_vertex_buffer.cpp b/src/platform/vulkan/vulkan_vertex_buffer.cpp
new file mode 100644
index 0000000..6a222c1
--- /dev/null
+++ b/src/platform/vulkan/vulkan_vertex_buffer.cpp
@@ -0,0 +1,34 @@
+#include "vulkan_vertex_buffer.h"
+
+namespace Donut
+{
+ VulkanVertexBuffer::VulkanVertexBuffer(uint32_t size)
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer creation with size
+ }
+
+ VulkanVertexBuffer::VulkanVertexBuffer(float* vertices, uint32_t size)
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer creation with data
+ }
+
+ VulkanVertexBuffer::~VulkanVertexBuffer()
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer cleanup
+ }
+
+ auto VulkanVertexBuffer::bind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer binding
+ }
+
+ auto VulkanVertexBuffer::unbind() const -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer unbinding
+ }
+
+ auto VulkanVertexBuffer::set_data(const void* data, uint32_t size) -> void
+ {
+ // TODO(Hachem): Implement Vulkan vertex buffer data setting
+ }
+}; \ No newline at end of file
diff --git a/src/platform/vulkan/vulkan_vertex_buffer.h b/src/platform/vulkan/vulkan_vertex_buffer.h
new file mode 100644
index 0000000..3218021
--- /dev/null
+++ b/src/platform/vulkan/vulkan_vertex_buffer.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "rendering/vertex_buffer.h"
+
+namespace Donut
+{
+ class VulkanVertexBuffer
+ : public VertexBuffer
+ {
+ public:
+ VulkanVertexBuffer(uint32_t size);
+ VulkanVertexBuffer(float* vertices, uint32_t size);
+ virtual ~VulkanVertexBuffer();
+
+ virtual auto bind() const -> void override;
+ virtual auto unbind() const -> void override;
+
+ virtual auto set_data(const void* data, uint32_t size) -> void override;
+
+ virtual auto get_layout() const -> const VertexBufferLayout& override{ return m_layout; }
+ virtual auto set_layout(const VertexBufferLayout& layout) -> void override{ m_layout = layout; }
+ private:
+ uint32_t m_renderer_id;
+ VertexBufferLayout m_layout;
+ };
+};