aboutsummaryrefslogtreecommitdiff
path: root/src/rendering
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-23 17:21:37 +0200
committerhachem <im@hachem.wtf>2026-08-23 17:21:37 +0200
commit3fd33ecff7472e4d6fc9e6b3905f56982e497ef2 (patch)
tree66e5812f1a18b926e88a0dd549fe14e7c7327165 /src/rendering
parente3aa33fc9a99d8b817bda42a567a4c347e18ab32 (diff)
[feat]: complete RHI api
Diffstat (limited to 'src/rendering')
-rw-r--r--src/rendering/black_hole_renderer.cpp144
-rw-r--r--src/rendering/black_hole_renderer.h55
-rw-r--r--src/rendering/framebuffer.cpp251
-rw-r--r--src/rendering/framebuffer.h80
-rw-r--r--src/rendering/index_buffer.cpp34
-rw-r--r--src/rendering/index_buffer.h23
-rw-r--r--src/rendering/render_api.h25
-rw-r--r--src/rendering/renderer.cpp168
-rw-r--r--src/rendering/renderer.h155
-rw-r--r--src/rendering/renderer_backend.h53
-rw-r--r--src/rendering/rhi.h (renamed from src/rendering/rhi/rhi.h)17
-rw-r--r--src/rendering/scene_renderer.cpp217
-rw-r--r--src/rendering/scene_renderer.h56
-rw-r--r--src/rendering/texture_manager.cpp34
-rw-r--r--src/rendering/texture_manager.h19
-rw-r--r--src/rendering/uniform_buffer.cpp36
-rw-r--r--src/rendering/uniform_buffer.h24
-rw-r--r--src/rendering/vertex_array.cpp61
-rw-r--r--src/rendering/vertex_array.h35
-rw-r--r--src/rendering/vertex_buffer.cpp50
-rw-r--r--src/rendering/vertex_buffer.h78
21 files changed, 512 insertions, 1103 deletions
diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp
new file mode 100644
index 0000000..46b002c
--- /dev/null
+++ b/src/rendering/black_hole_renderer.cpp
@@ -0,0 +1,144 @@
+#include "black_hole_renderer.h"
+
+#include "core/log.h"
+#include "core/settings_manager.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+
+namespace Donut
+{
+ using namespace RHI;
+
+ namespace
+ {
+ constexpr float SagA_rs = 1.269e10f; // Schwarzschild radius of the modelled hole
+
+ struct CamUBO {
+ glm::vec3 pos; float p0; glm::vec3 right; float p1;
+ glm::vec3 up; float p2; glm::vec3 fwd; float p3;
+ float tan_half_fov; float aspect; uint32_t moving; int p4;
+ };
+ struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; };
+ }
+
+ auto BlackHoleRenderer::init(RHI::Device& device) -> bool
+ {
+ m_device = &device;
+
+ m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, false, Filter::Linear);
+ m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, false, Filter::Linear);
+
+ // Fullscreen quad shared by the geodesic and present passes: pos.xy + uv.
+ const float quad[] = {
+ -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f,
+ -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f,
+ };
+ m_quad_vb = device.create_buffer(BufferType::Vertex, sizeof(quad), quad);
+
+ m_cam_ubo = device.create_buffer(BufferType::Uniform, 128);
+ m_disk_ubo = device.create_buffer(BufferType::Uniform, 32);
+ m_obj_ubo = device.create_buffer(BufferType::Uniform, 800);
+ m_sim_ubo = device.create_buffer(BufferType::Uniform, 16);
+
+ // The single "object" is the black hole itself (position 0, radius = r_s).
+ std::vector<uint8_t> obj(800, 0);
+ int num_objects = 1; std::memcpy(obj.data() + 0, &num_objects, 4);
+ float pos_radius[4] = { 0, 0, 0, SagA_rs }; std::memcpy(obj.data() + 16, pos_radius, 16);
+ float color[4] = { 0, 0, 0, 1 }; std::memcpy(obj.data() + 272, color, 16);
+ m_obj_ubo->update(obj.data(), obj.size());
+
+ {
+ PipelineDesc d;
+ d.shader = "geodesic";
+ d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } };
+ d.resources = {
+ { ResourceKind::UniformBuffer, 0, "Camera" },
+ { ResourceKind::UniformBuffer, 1, "Disk" },
+ { ResourceKind::UniformBuffer, 2, "Objects" },
+ { ResourceKind::UniformBuffer, 3, "Simulation" },
+ { ResourceKind::Texture, 4, "u_HDRIEnvironment" },
+ };
+ d.topology = Topology::Triangles;
+ d.has_depth = false; // color-only off-screen target
+ m_geo_pipeline = device.create_pipeline(d);
+ }
+ {
+ PipelineDesc d;
+ d.shader = "textured_quad";
+ d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } };
+ d.resources = { { ResourceKind::Texture, 0, "u_ScreenTexture" } };
+ d.topology = Topology::Triangles;
+ d.has_depth = true; // swapchain target carries depth (unused here)
+ m_present_pipeline = device.create_pipeline(d);
+ }
+
+ DONUT_INFO("BlackHoleRenderer ready ({}x{} moving / {}x{} settled)",
+ GEO_LO_W, GEO_LO_H, GEO_HI_W, GEO_HI_H);
+ return true;
+ }
+
+ auto BlackHoleRenderer::render_geodesic(RHI::CommandList& cmd, const GeodesicView& view,
+ const BlackHoleParams& params, RHI::Texture* cubemap) -> void
+ {
+ CamUBO cam{};
+ cam.pos = view.position; cam.right = view.right; cam.up = view.up; cam.fwd = view.forward;
+ cam.tan_half_fov = view.tan_half_fov;
+ cam.aspect = view.aspect;
+ cam.moving = view.moving ? 1u : 0u;
+ m_cam_ubo->update(&cam, sizeof(cam));
+
+ // Same integration budget whether moving or settled (the disk vanishes at
+ // steep poses below ~15000 steps); responsiveness comes from the lower-res
+ // target instead. Sourced from the UI, clamped GPU-safe.
+ constexpr int kStepCeil = 15000;
+ SimUBO sim{};
+ sim.steps_static = std::clamp(params.quality_steps, 1000, kStepCeil);
+ sim.steps_moving = sim.steps_static;
+ sim.early_exit = SettingsManager::get_early_exit_distance();
+ sim.time = view.time;
+ m_sim_ubo->update(&sim, sizeof(sim));
+
+ // Disk struct: r_in, r_out, turbulence, slab half-thickness, brightness,
+ // temperature (radii in Schwarzschild radii).
+ float disk[8] = {
+ std::max(params.disk_inner_rs, 3.0f) * SagA_rs,
+ std::max(params.disk_outer_rs, params.disk_inner_rs + 0.5f) * SagA_rs,
+ std::max(params.turbulence, 0.0f),
+ SagA_rs * 0.1f,
+ std::max(params.brightness, 0.0f),
+ std::max(params.temperature, 1000.0f),
+ 0.0f, 0.0f,
+ };
+ m_disk_ubo->update(disk, sizeof(disk));
+
+ // Progressive resolution: small target while moving, large once settled.
+ RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get();
+ m_last_target = target;
+
+ cmd.begin_render_pass(target, glm::vec4(0, 0, 0, 1));
+ cmd.set_viewport(0, 0, target->width(), target->height(), false);
+ cmd.bind_pipeline(m_geo_pipeline.get());
+ cmd.bind_uniform(0, m_cam_ubo.get());
+ cmd.bind_uniform(1, m_disk_ubo.get());
+ cmd.bind_uniform(2, m_obj_ubo.get());
+ cmd.bind_uniform(3, m_sim_ubo.get());
+ cmd.bind_texture(4, cubemap);
+ cmd.bind_vertex_buffer(m_quad_vb.get());
+ cmd.draw(6);
+ cmd.end_render_pass();
+ }
+
+ auto BlackHoleRenderer::blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void
+ {
+ if (!m_last_target) return;
+ // flip_y matches the shared top-left orientation (Vulkan flips via a
+ // negative-height viewport; GL is a no-op).
+ cmd.set_viewport(0, 0, fb_width, fb_height, true);
+ cmd.bind_pipeline(m_present_pipeline.get());
+ cmd.bind_texture(0, m_last_target->color_texture());
+ cmd.bind_vertex_buffer(m_quad_vb.get());
+ cmd.draw(6);
+ }
+}
diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h
new file mode 100644
index 0000000..b9c158b
--- /dev/null
+++ b/src/rendering/black_hole_renderer.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "rhi.h"
+#include "renderer_backend.h" // BlackHoleParams
+#include <glm/glm.hpp>
+
+namespace Donut
+{
+ // The black-hole view, written ONCE against the RHI. Each frame it ray-traces
+ // the geodesic shader into an off-screen target (progressive resolution: a
+ // small target while the camera moves, a larger one once it settles) and then
+ // upscales that target onto the swapchain. The Application owns the camera and
+ // the HDRI cubemap and passes them in.
+ struct GeodesicView
+ {
+ glm::vec3 position, right, up, forward;
+ float tan_half_fov = 1.0f;
+ float aspect = 16.0f / 9.0f;
+ bool moving = false;
+ float time = 0.0f;
+ };
+
+ class BlackHoleRenderer
+ {
+ public:
+ static constexpr int GEO_LO_W = 480, GEO_LO_H = 270;
+ static constexpr int GEO_HI_W = 960, GEO_HI_H = 540;
+
+ auto init(RHI::Device& device) -> bool;
+
+ // Off-screen geodesic pass (opens/closes its own render pass). Call before
+ // the swapchain pass; `cubemap` is the shared HDRI environment.
+ auto render_geodesic(RHI::CommandList& cmd, const GeodesicView& view,
+ const BlackHoleParams& params, RHI::Texture* cubemap) -> void;
+
+ // Upscales the geodesic target onto the swapchain (call inside the open
+ // swapchain render pass, before ImGui).
+ auto blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void;
+
+ private:
+ RHI::Device* m_device = nullptr;
+
+ Ref<RHI::Pipeline> m_geo_pipeline;
+ Ref<RHI::RenderTarget> m_geo_lo;
+ Ref<RHI::RenderTarget> m_geo_hi;
+ Ref<RHI::Buffer> m_quad_vb;
+ Ref<RHI::Buffer> m_cam_ubo;
+ Ref<RHI::Buffer> m_disk_ubo;
+ Ref<RHI::Buffer> m_obj_ubo;
+ Ref<RHI::Buffer> m_sim_ubo;
+
+ Ref<RHI::Pipeline> m_present_pipeline;
+ RHI::RenderTarget* m_last_target = nullptr; // target chosen this frame
+ };
+}
diff --git a/src/rendering/framebuffer.cpp b/src/rendering/framebuffer.cpp
deleted file mode 100644
index 3da4b48..0000000
--- a/src/rendering/framebuffer.cpp
+++ /dev/null
@@ -1,251 +0,0 @@
-#include "framebuffer.h"
-#include "core/log.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- namespace Utils
- {
- static GLenum TextureTarget(bool multisampled)
- {
- return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
- }
-
- static void bind_texture(bool multisampled, uint32_t id)
- {
- glBindTexture(TextureTarget(multisampled), id);
- }
-
- static void CreateTextures(bool multisampled, uint32_t* out_id, uint32_t count)
- {
- // glCreateTextures is 4.5 DSA; macOS caps at 4.1. Callers bind each
- // texture (with the correct target) before use.
- glGenTextures(count, out_id);
- }
-
- static void AttachColorTexture(uint32_t id, int samples, GLenum internal_format, GLenum format, uint32_t width, uint32_t height, int index)
- {
- bool multisampled = samples > 1;
- if (multisampled)
- {
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internal_format, width, height, GL_FALSE);
- }
- else
- {
- // Integer color formats require an integer pixel type even when
- // data is null, or macOS's strict core profile rejects the call.
- GLenum type = (format == GL_RED_INTEGER) ? GL_INT : GL_UNSIGNED_BYTE;
- glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, type, nullptr);
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- }
-
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0);
- }
-
- static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachment_type, uint32_t width, uint32_t height)
- {
- bool multisampled = samples > 1;
- if (multisampled)
- {
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE);
- }
- else
- {
- // glTexStorage2D is 4.2; use mutable storage for macOS (4.1).
- GLenum depth_format = (format == GL_DEPTH24_STENCIL8) ? GL_DEPTH_STENCIL : GL_DEPTH_COMPONENT;
- GLenum depth_type = (format == GL_DEPTH24_STENCIL8) ? GL_UNSIGNED_INT_24_8 : GL_FLOAT;
- glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, depth_format, depth_type, nullptr);
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- }
-
- glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_type, TextureTarget(multisampled), id, 0);
- }
-
- static bool IsDepthFormat(FramebufferTextureFormat format)
- {
- switch (format)
- {
- case FramebufferTextureFormat::DEPTH24STENCIL8: return true;
- }
- return false;
- }
-
- static GLenum DonutFBTextureFormatToGL(FramebufferTextureFormat format)
- {
- switch (format)
- {
- case FramebufferTextureFormat::RGBA8: return GL_RGBA8;
- case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER;
- }
-
- return 0;
- }
- }
-
- Framebuffer::Framebuffer(const FramebufferSpecification& spec)
- : m_specification(spec)
- {
- for (auto spec : m_specification.attachments.attachments)
- {
- if (!Utils::IsDepthFormat(spec.texture_format))
- m_color_attachment_specifications.emplace_back(spec);
- else
- m_depth_attachment_specification = spec;
- }
-
- invalidate();
- }
-
- Framebuffer::~Framebuffer()
- {
- glDeleteFramebuffers(1, &m_renderer_id);
- glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data());
- glDeleteTextures(1, &m_depth_attachment);
- }
-
- auto Framebuffer::invalidate() -> void
- {
- if (m_renderer_id)
- {
- glDeleteFramebuffers(1, &m_renderer_id);
- glDeleteTextures(static_cast<GLsizei>(m_color_attachments.size()), m_color_attachments.data());
- glDeleteTextures(1, &m_depth_attachment);
-
- m_color_attachments.clear();
- m_depth_attachment = 0;
- }
-
- glGenFramebuffers(1, &m_renderer_id); // glCreateFramebuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
-
- bool multisample = m_specification.Samples > 1;
-
- if (m_color_attachment_specifications.size())
- {
- m_color_attachments.resize(m_color_attachment_specifications.size());
- Utils::CreateTextures(multisample, m_color_attachments.data(), static_cast<uint32_t>(m_color_attachments.size()));
-
- for (size_t i = 0; i < m_color_attachments.size(); i++)
- {
- Utils::bind_texture(multisample, m_color_attachments[i]);
- switch (m_color_attachment_specifications[i].texture_format)
- {
- case FramebufferTextureFormat::RGBA8:
- Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_RGBA8, GL_RGBA, m_specification.Width, m_specification.Height, static_cast<int>(i));
- break;
- case FramebufferTextureFormat::RED_INTEGER:
- Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_R32I, GL_RED_INTEGER, m_specification.Width, m_specification.Height, static_cast<int>(i));
- break;
- }
- }
- }
-
- if (m_depth_attachment_specification.texture_format != FramebufferTextureFormat::None)
- {
- Utils::CreateTextures(multisample, &m_depth_attachment, 1);
- Utils::bind_texture(multisample, m_depth_attachment);
- switch (m_depth_attachment_specification.texture_format)
- {
- case FramebufferTextureFormat::DEPTH24STENCIL8:
- Utils::AttachDepthTexture(m_depth_attachment, m_specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_specification.Width, m_specification.Height);
- break;
- }
- }
-
- if (m_color_attachments.size() > 1)
- {
- GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
- glDrawBuffers(static_cast<GLsizei>(m_color_attachments.size()), buffers);
- }
- else if (m_color_attachments.empty())
- glDrawBuffer(GL_NONE);
-
- if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
- {
- GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
- switch (status)
- {
- case GL_FRAMEBUFFER_UNDEFINED:
- DONUT_ERROR("Framebuffer is undefined");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
- DONUT_ERROR("Framebuffer has incomplete attachment");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
- DONUT_ERROR("Framebuffer is missing attachment");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
- DONUT_ERROR("Framebuffer has incomplete draw buffer");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
- DONUT_ERROR("Framebuffer has incomplete read buffer");
- break;
- case GL_FRAMEBUFFER_UNSUPPORTED:
- DONUT_ERROR("Framebuffer format is unsupported");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
- DONUT_ERROR("Framebuffer has incomplete multisample");
- break;
- case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
- DONUT_ERROR("Framebuffer has incomplete layer targets");
- break;
- default:
- DONUT_ERROR("Framebuffer is incomplete (unknown error: {})", status);
- break;
- }
- }
-
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
-
- auto Framebuffer::bind() -> void
- {
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
- glViewport(0, 0, m_specification.Width, m_specification.Height);
- }
-
- auto Framebuffer::unbind() -> void
- {
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- }
-
- auto Framebuffer::resize(uint32_t width, uint32_t height) -> void
- {
- m_specification.Width = width;
- m_specification.Height = height;
-
- invalidate();
- }
-
- auto Framebuffer::read_pixel(uint32_t attachment_index, int x, int y) -> int
- {
- glReadBuffer(GL_COLOR_ATTACHMENT0 + attachment_index);
- int pixel_data;
- glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixel_data);
- return pixel_data;
- }
-
- auto Framebuffer::clear_attachment(uint32_t attachment_index, int value) -> void
- {
- // glClearTexImage is 4.4 and unavailable on macOS. clear the integer
- // attachment by binding this framebuffer and clearing its draw buffer.
- glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id);
- glClearBufferiv(GL_COLOR, static_cast<GLint>(attachment_index), &value);
- }
-
- auto Framebuffer::create(const FramebufferSpecification& spec) -> Ref<Framebuffer>
- {
- return create_ref<Framebuffer>(spec);
- }
-};
diff --git a/src/rendering/framebuffer.h b/src/rendering/framebuffer.h
deleted file mode 100644
index cf1fbef..0000000
--- a/src/rendering/framebuffer.h
+++ /dev/null
@@ -1,80 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "texture.h"
-
-#include <vector>
-
-namespace Donut
-{
- enum class FramebufferTextureFormat
- {
- None = 0,
-
- RGBA8,
- RED_INTEGER,
-
- DEPTH24STENCIL8,
-
- Depth = DEPTH24STENCIL8
- };
-
- struct FramebufferTextureSpecification
- {
- FramebufferTextureSpecification() = default;
- FramebufferTextureSpecification(FramebufferTextureFormat format)
- : texture_format(format) {}
-
- FramebufferTextureFormat texture_format = FramebufferTextureFormat::None;
- };
-
- struct FramebufferAttachmentSpecification
- {
- FramebufferAttachmentSpecification() = default;
- FramebufferAttachmentSpecification(std::initializer_list<FramebufferTextureSpecification> attachments)
- : attachments(attachments) {}
-
- std::vector<FramebufferTextureSpecification> attachments;
- };
-
- struct FramebufferSpecification
- {
- uint32_t Width = 0, Height = 0;
- FramebufferAttachmentSpecification attachments;
- uint32_t Samples = 1;
-
- bool SwapChainTarget = false;
- };
-
- class Framebuffer
- {
- public:
- Framebuffer(const FramebufferSpecification& spec);
- ~Framebuffer();
-
- auto bind() -> void;
- auto unbind() -> void;
-
- auto resize(uint32_t width, uint32_t height) -> void;
- auto read_pixel(uint32_t attachment_index, int x, int y) -> int;
-
- auto clear_attachment(uint32_t attachment_index, int value) -> void;
- auto get_color_attachment_renderer_id(uint32_t index = 0) const -> uint32_t { return m_color_attachments[index]; }
-
- auto get_specification() const -> const FramebufferSpecification& { return m_specification; }
-
- static auto create(const FramebufferSpecification& spec) -> Ref<Framebuffer>;
-
- private:
- auto invalidate() -> void;
-
- uint32_t m_renderer_id = 0;
- FramebufferSpecification m_specification;
-
- std::vector<FramebufferTextureSpecification> m_color_attachment_specifications;
- FramebufferTextureSpecification m_depth_attachment_specification = FramebufferTextureFormat::None;
-
- std::vector<uint32_t> m_color_attachments;
- uint32_t m_depth_attachment = 0;
- };
-};
diff --git a/src/rendering/index_buffer.cpp b/src/rendering/index_buffer.cpp
deleted file mode 100644
index 2c933e0..0000000
--- a/src/rendering/index_buffer.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-#include "index_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- IndexBuffer::IndexBuffer(const uint32_t* indices, uint32_t count)
- : m_count(count)
- {
- glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW);
- }
-
- IndexBuffer::~IndexBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto IndexBuffer::bind() const -> void
- {
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id);
- }
-
- auto IndexBuffer::unbind() const -> void
- {
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
- }
-
- auto IndexBuffer::create(const uint32_t* indices, uint32_t count) -> IndexBuffer*
- {
- return new IndexBuffer(indices, count);
- }
-};
diff --git a/src/rendering/index_buffer.h b/src/rendering/index_buffer.h
deleted file mode 100644
index 0b1b2b6..0000000
--- a/src/rendering/index_buffer.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include <cstdint>
-
-namespace Donut
-{
- class IndexBuffer
- {
- public:
- IndexBuffer(const uint32_t* indices, uint32_t count);
- ~IndexBuffer();
-
- auto bind() const -> void;
- auto unbind() const -> void;
- auto get_count() const -> uint32_t { return m_count; }
-
- static auto create(const uint32_t* indices, uint32_t count) -> IndexBuffer*;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_count = 0;
- };
-};
diff --git a/src/rendering/render_api.h b/src/rendering/render_api.h
new file mode 100644
index 0000000..62742c9
--- /dev/null
+++ b/src/rendering/render_api.h
@@ -0,0 +1,25 @@
+#pragma once
+
+namespace Donut
+{
+ // Which graphics backend the app runs on, selected once at startup (from
+ // settings) before the window is created. The rendering itself goes through
+ // the RHI (rendering/rhi.h) and the shared renderers; this is only the
+ // backend selector the window + application branch on.
+ class RendererAPI
+ {
+ public:
+ enum class API
+ {
+ None = 0,
+ OpenGL = 1,
+ Vulkan = 2,
+ };
+
+ static auto get_api() -> API { return s_api; }
+ static auto set_api(API api) -> void { s_api = api; }
+
+ private:
+ inline static API s_api = API::None;
+ };
+}
diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp
deleted file mode 100644
index 1c82447..0000000
--- a/src/rendering/renderer.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-#include "renderer.h"
-
-#include "core/log.h"
-
-#include <glad/glad.h>
-#include <GLFW/glfw3.h>
-#include <glm/gtc/matrix_transform.hpp>
-
-namespace Donut
-{
- RendererAPI::API RendererAPI::s_api = RendererAPI::API::OpenGL;
-
- auto RendererAPI::create() -> Scope<RendererAPI>
- {
- return create_scope<RendererAPI>();
- }
-
- auto RendererAPI::init() -> void
- {
- if (!glfwGetCurrentContext())
- {
- DONUT_ERROR("No OpenGL context is current! Cannot initialize GLAD.");
- return;
- }
-
- if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
- {
- DONUT_ERROR("Failed to initialize GLAD!");
- return;
- }
-
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- glEnable(GL_DEPTH_TEST);
- glDepthFunc(GL_LESS);
-
- glEnable(GL_CULL_FACE);
- glCullFace(GL_BACK);
- glFrontFace(GL_CCW);
- }
-
- auto RendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
- {
- glViewport(x, y, width, height);
- }
-
- auto RendererAPI::set_clear_color(const glm::vec4& color) -> void
- {
- glClearColor(color.r, color.g, color.b, color.a);
- }
-
- auto RendererAPI::clear() -> void
- {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- }
-
- auto RendererAPI::enable_depth_test() -> void
- {
- glEnable(GL_DEPTH_TEST);
- }
-
- auto RendererAPI::disable_depth_test() -> void
- {
- glDisable(GL_DEPTH_TEST);
- }
-
- auto RendererAPI::set_face_culling(bool enabled) -> void
- {
- if (enabled)
- {
- glEnable(GL_CULL_FACE);
- glCullFace(GL_BACK);
- glFrontFace(GL_CCW);
- }
- else
- glDisable(GL_CULL_FACE);
- }
-
- auto RendererAPI::enable_blending() -> void
- {
- glEnable(GL_BLEND);
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- }
-
- auto RendererAPI::disable_blending() -> void
- {
- glDisable(GL_BLEND);
- }
-
- auto RendererAPI::draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
- {
- uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count();
- glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr);
- glBindTexture(GL_TEXTURE_2D, 0);
- }
-
- auto RendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void
- {
- glDrawArrays(GL_TRIANGLES, first, vertex_count);
- }
-
- auto RendererAPI::draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count) -> void
- {
- uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count();
- glDrawElements(GL_LINES, count, GL_UNSIGNED_INT, nullptr);
- }
-
- auto RendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void
- {
- glActiveTexture(GL_TEXTURE0 + slot);
- glBindTexture(GL_TEXTURE_2D, texture_id);
- }
-
- auto RendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void
- {
- // Image load/store is OpenGL 4.2; the pointer is null on macOS (4.1).
- if (glBindImageTexture == nullptr)
- return;
- glBindImageTexture(slot, texture_id, 0, GL_FALSE, 0,
- read_only ? GL_READ_ONLY : GL_WRITE_ONLY, GL_RGBA8);
- }
-
- auto RendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void
- {
- glReadPixels(x, y, width, height, format, type, pixels);
- }
-
- auto Renderer::init() -> void
- {
- RenderCommand::init();
- RenderCommand::enable_depth_test();
- }
-
- auto Renderer::shutdown() -> void
- {
- }
-
- auto Renderer::on_window_resize(uint32_t width, uint32_t height) -> void
- {
- RenderCommand::set_viewport(0, 0, width, height);
- }
-
- auto Renderer::submit(const Ref<Shader>& shader,
- const Ref<VertexArray>& vertex_array,
- const glm::mat4& transform,
- const glm::mat4& view_projection) -> void
- {
- shader->bind();
- shader->set_mat4("u_ViewProjection", view_projection);
- shader->set_mat4("u_Transform", transform);
-
- vertex_array->bind();
- RenderCommand::draw_indexed(vertex_array);
- }
-
- Scope<RendererAPI> RenderCommand::s_renderer_api = RendererAPI::create();
-
- auto Renderer::set_clear_color(const glm::vec4& color) -> void
- {
- RenderCommand::set_clear_color(color);
- }
-
- auto Renderer::clear() -> void
- {
- RenderCommand::clear();
- }
-};
diff --git a/src/rendering/renderer.h b/src/rendering/renderer.h
deleted file mode 100644
index d7855a4..0000000
--- a/src/rendering/renderer.h
+++ /dev/null
@@ -1,155 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "vertex_array.h"
-#include "shader.h"
-#include "framebuffer.h"
-
-#include <glm/glm.hpp>
-
-namespace Donut
-{
- // Single concrete backend: the OpenGL renderer. (Vulkan runs through its own
- // dedicated VulkanRenderer, not this immediate-mode command layer.)
- class RendererAPI
- {
- public:
- enum class API
- {
- None = 0,
- OpenGL = 1,
- Vulkan = 2,
- };
-
- public:
- RendererAPI() = default;
- ~RendererAPI() = default;
-
- auto init() -> void;
- auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void;
- auto set_clear_color(const glm::vec4& color) -> void;
- auto clear() -> void;
- auto enable_depth_test() -> void;
- auto disable_depth_test() -> void;
- auto set_face_culling(bool enabled) -> void;
- auto enable_blending() -> void;
- auto disable_blending() -> void;
-
- auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void;
- auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void;
- auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void;
- auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void;
- auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void;
- auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void;
-
- inline static auto get_api() -> API { return s_api; }
- inline static auto set_api(API api) -> void { s_api = api; }
- static auto create() -> Scope<RendererAPI>;
-
- private:
- static API s_api;
- };
-
- class RenderCommand
- {
- public:
- inline static auto init() -> void
- {
- s_renderer_api->init();
- }
-
- inline static auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void
- {
- s_renderer_api->set_viewport(x, y, width, height);
- }
-
- inline static auto set_clear_color(const glm::vec4& color) -> void
- {
- s_renderer_api->set_clear_color(color);
- }
-
- inline static auto clear() -> void
- {
- s_renderer_api->clear();
- }
-
- inline static auto enable_depth_test() -> void
- {
- s_renderer_api->enable_depth_test();
- }
-
- inline static auto disable_depth_test() -> void
- {
- s_renderer_api->disable_depth_test();
- }
-
- inline static auto set_face_culling(bool enabled) -> void
- {
- s_renderer_api->set_face_culling(enabled);
- }
-
- inline static auto enable_blending() -> void
- {
- s_renderer_api->enable_blending();
- }
-
- inline static auto disable_blending() -> void
- {
- s_renderer_api->disable_blending();
- }
-
- inline static auto draw_indexed(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void
- {
- s_renderer_api->draw_indexed(vertex_array, index_count);
- }
-
- inline static auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void
- {
- s_renderer_api->draw_arrays(vertex_count, first);
- }
-
- inline static auto draw_lines(const Ref<VertexArray>& vertex_array, uint32_t index_count = 0) -> void
- {
- s_renderer_api->draw_lines(vertex_array, index_count);
- }
-
- inline static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void
- {
- s_renderer_api->bind_texture(texture_id, slot);
- }
-
- inline static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void
- {
- s_renderer_api->bind_image_texture(texture_id, slot, read_only);
- }
-
- inline static auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
- uint32_t format, uint32_t type, void* pixels) -> void
- {
- s_renderer_api->read_pixels(x, y, width, height, format, type, pixels);
- }
-
- private:
- static Scope<RendererAPI> s_renderer_api;
- };
-
- class Renderer
- {
- public:
- static auto init() -> void;
- static auto shutdown() -> void;
-
- static auto on_window_resize(uint32_t width, uint32_t height) -> void;
-
- static auto submit(const Ref<Shader>& shader,
- const Ref<VertexArray>& vertex_array,
- const glm::mat4& transform,
- const glm::mat4& view_projection) -> void;
-
- static auto set_clear_color(const glm::vec4& color) -> void;
- static auto clear() -> void;
-
- inline static auto get_api() -> RendererAPI::API { return RendererAPI::get_api(); }
- };
-};
diff --git a/src/rendering/renderer_backend.h b/src/rendering/renderer_backend.h
index 6a4f1af..d34983f 100644
--- a/src/rendering/renderer_backend.h
+++ b/src/rendering/renderer_backend.h
@@ -1,14 +1,15 @@
#pragma once
#include <glm/glm.hpp>
-#include <functional>
-#include <string>
-#include <vector>
namespace Donut
{
- // A placeable/editable sphere in the world-builder scene view. Backend-
- // agnostic: the app owns the list, every backend renders it the same way.
+ // Backend-agnostic scene/black-hole data shared by the Application and the
+ // portable renderers (SceneRenderer, BlackHoleRenderer). The rendering itself
+ // is written once on the RHI (rendering/rhi.h); these are just the inputs.
+
+ // A placeable/editable sphere in the world-builder scene view. The app owns
+ // the list; SceneRenderer draws it the same way on every backend.
struct SceneObject
{
glm::vec3 position = { 0.0f, 2.0f, 0.0f };
@@ -28,46 +29,4 @@ namespace Donut
int quality_steps = 15000; // geodesic integration steps
float fov_degrees = 60.0f; // camera field of view
};
-
- // The swappable high-level renderer. Each graphics backend (OpenGL, Vulkan,
- // and later Metal / D3D12) implements this one interface; the Application,
- // states and UI drive it identically, so the app behaves the same no matter
- // which backend is active. Backends own their own device/swapchain/pipelines
- // and translate these high-level calls into their native API.
- class RendererBackend
- {
- public:
- virtual ~RendererBackend() = default;
-
- // glfwWindow is an opaque GLFW handle (created NO_API for Vulkan/Metal,
- // with a GL context for OpenGL). Returns false on failure.
- virtual auto init(void* glfwWindow, int width, int height) -> bool = 0;
-
- // Creates the ImGui context + this backend's ImGui platform/render impl.
- virtual auto init_ui() -> bool = 0;
- virtual auto shutdown() -> void = 0;
- virtual auto resize(int width, int height) -> void = 0;
-
- // Renders one frame: draws the current view (black hole or scene) from
- // the shared state, then invokes build_ui to draw the ImGui panel on top.
- virtual auto draw_frame(const glm::vec4& clear_color,
- const std::function<void()>& build_ui) -> void = 0;
-
- // --- shared view / camera state ------------------------------------
- virtual auto set_scene_mode(bool enabled) -> void = 0; // black hole vs scene view
- virtual auto is_scene_mode() const -> bool = 0;
- virtual auto set_free_fly(bool enabled) -> void = 0; // free-fly vs orbital camera
- virtual auto is_free_fly() const -> bool = 0;
- virtual auto reset_camera() -> void = 0;
-
- // --- shared content ------------------------------------------------
- virtual auto set_hdri(const std::string& path) -> void = 0;
- virtual auto current_hdri() const -> const std::string& = 0;
- virtual auto scene_objects() -> std::vector<SceneObject>& = 0;
- virtual auto set_selected_object(int index) -> void = 0;
- virtual auto black_hole_params() -> BlackHoleParams& = 0;
-
- // --- introspection for the UI --------------------------------------
- virtual auto device_name() const -> const std::string& = 0;
- };
}
diff --git a/src/rendering/rhi/rhi.h b/src/rendering/rhi.h
index f64252c..65f10b5 100644
--- a/src/rendering/rhi/rhi.h
+++ b/src/rendering/rhi.h
@@ -14,7 +14,7 @@
#include <vector>
#include <functional>
-namespace Donut::rhi
+namespace Donut::RHI
{
enum class Format
{
@@ -35,9 +35,11 @@ namespace Donut::rhi
struct VertexLayout { uint32_t stride = 0; std::vector<VertexAttribute> attributes; };
// A shader resource slot the pipeline exposes. `binding` is the set-0 binding
- // index shared by the GLSL/SPIR-V (the app binds resources to these before a draw).
+ // index used by Vulkan; `name` is the GLSL block/sampler identifier used by the
+ // OpenGL backend (GL 4.1 has no binding qualifier, so it binds by name). The app
+ // declares both when building a pipeline (it knows its own shader).
enum class ResourceKind { UniformBuffer, Texture };
- struct ResourceSlot { ResourceKind kind; uint32_t binding; };
+ struct ResourceSlot { ResourceKind kind; uint32_t binding; std::string name; };
// --- opaque GPU resources (backends subclass) --------------------------
class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; };
@@ -113,13 +115,12 @@ namespace Donut::rhi
Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref<RenderTarget> = 0;
virtual auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> = 0;
- // Frame loop: begin_frame returns the swapchain command list (or nullptr if
- // the frame is skipped, e.g. minimised); record into it, then end_frame
- // submits + presents. record_offscreen runs a self-contained pass whose
- // command list targets a RenderTarget (used before the swapchain frame).
+ // Frame loop: begin_frame returns the frame's command list (or nullptr if
+ // the frame is skipped, e.g. minimised); record one or more render passes
+ // into it — off-screen passes into RenderTargets first, then the swapchain
+ // pass (begin_render_pass(nullptr, ...)) — then end_frame submits + presents.
virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0;
virtual auto end_frame() -> void = 0;
- virtual auto record_offscreen(const std::function<void(CommandList&)>& record) -> void = 0;
// ImGui lives above the RHI but its platform/render backend is per-device.
virtual auto init_imgui() -> void = 0;
diff --git a/src/rendering/scene_renderer.cpp b/src/rendering/scene_renderer.cpp
new file mode 100644
index 0000000..e276074
--- /dev/null
+++ b/src/rendering/scene_renderer.cpp
@@ -0,0 +1,217 @@
+#include "scene_renderer.h"
+
+#include "core/log.h"
+
+#include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
+#include <cmath>
+#include <numbers>
+#include <vector>
+
+namespace Donut
+{
+ using namespace RHI;
+
+ namespace
+ {
+ // std140 layouts, matching the Slang ConstantBuffer structs exactly. glm's
+ // column-major matrices upload directly (the shaders decorate the members
+ // row_major and use mul(M,v), so the same bytes read correctly on every
+ // backend, with no transpose).
+ struct GridUBO {
+ glm::mat4 view_projection; // 0
+ glm::mat4 transform; // 64
+ float grid_size; float p0[3]; // 128
+ glm::vec3 grid_color; // 144
+ float grid_alpha; // 156
+ glm::vec3 camera_pos; // 160
+ float p1; // 172
+ };
+ static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch");
+
+ struct SphereUBO {
+ glm::mat4 view_projection; // 0
+ glm::mat4 transform; // 64
+ glm::vec3 color; float specular; // 128, 140
+ float emission; float p0[3]; // 144
+ glm::vec3 light_pos; float p1; // 160, 172
+ glm::vec3 camera_pos; int is_selected; // 176, 188
+ glm::vec3 outline_color; float outline_width; // 192, 204
+ };
+ static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch");
+
+ struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; };
+ static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch");
+ }
+
+ auto SceneRenderer::init(RHI::Device& device) -> bool
+ {
+ m_device = &device;
+
+ // Reference grid: line list on the XZ plane (+/-50, 1-unit cells). Grid.slang
+ // scales by u_GridSize/50, so u_GridSize = 50 keeps it 1:1.
+ {
+ std::vector<glm::vec3> lines;
+ const int N = 50;
+ for (int i = -N; i <= N; ++i)
+ {
+ lines.push_back({ (float)i, 0.0f, (float)-N });
+ lines.push_back({ (float)i, 0.0f, (float) N });
+ lines.push_back({ (float)-N, 0.0f, (float)i });
+ lines.push_back({ (float) N, 0.0f, (float)i });
+ }
+ m_grid_vertex_count = (int)lines.size();
+ m_grid_vb = device.create_buffer(BufferType::Vertex, lines.size() * sizeof(glm::vec3), lines.data());
+ m_grid_ubo = device.create_buffer(BufferType::Uniform, sizeof(GridUBO));
+
+ PipelineDesc d;
+ d.shader = "grid";
+ d.vertex_layout = { sizeof(glm::vec3), { { 0, 3, 0 } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "GridU" } };
+ d.topology = Topology::Lines;
+ d.blend = BlendMode::AlphaBlend;
+ d.depth_test = true; d.depth_write = false; d.depth_op = CompareOp::LessEqual;
+ d.has_depth = true;
+ m_grid_pipeline = device.create_pipeline(d);
+ }
+
+ // Lit sphere: unit UV-sphere (pos + normal), placed/scaled per object.
+ {
+ std::vector<float> sv; std::vector<uint32_t> si;
+ const int RINGS = 24, SECTORS = 48;
+ for (int r = 0; r <= RINGS; ++r)
+ {
+ float phi = (float)std::numbers::pi * r / RINGS;
+ for (int s = 0; s <= SECTORS; ++s)
+ {
+ float theta = 2.0f * (float)std::numbers::pi * s / SECTORS;
+ float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta);
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit)
+ sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position
+ }
+ }
+ for (int r = 0; r < RINGS; ++r)
+ for (int s = 0; s < SECTORS; ++s)
+ {
+ uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1;
+ si.push_back(a); si.push_back(b); si.push_back(a + 1);
+ si.push_back(b); si.push_back(b + 1); si.push_back(a + 1);
+ }
+ m_sphere_index_count = (int)si.size();
+ m_sphere_vb = device.create_buffer(BufferType::Vertex, sv.size() * sizeof(float), sv.data());
+ m_sphere_ib = device.create_buffer(BufferType::Index, si.size() * sizeof(uint32_t), si.data());
+
+ PipelineDesc d;
+ d.shader = "sphere";
+ d.vertex_layout = { 6 * sizeof(float), { { 0, 3, 0 }, { 1, 3, 3 * sizeof(float) } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "SphereU" },
+ { ResourceKind::Texture, 1, "u_HDRIEnvironment" } };
+ d.topology = Topology::Triangles;
+ d.depth_test = true; d.depth_write = true; d.depth_op = CompareOp::Less;
+ d.has_depth = true;
+ m_sphere_pipeline = device.create_pipeline(d);
+ }
+
+ // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex
+ // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry.
+ {
+ const float cube[] = {
+ -1,-1,-1, 1,-1,-1, 1, 1,-1, 1, 1,-1, -1, 1,-1, -1,-1,-1,
+ -1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1,-1, 1,
+ -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1,
+ 1, 1, 1, 1, 1,-1, 1,-1,-1, 1,-1,-1, 1,-1, 1, 1, 1, 1,
+ -1,-1,-1, 1,-1,-1, 1,-1, 1, 1,-1, 1, -1,-1, 1, -1,-1,-1,
+ -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1
+ };
+ m_skybox_vb = device.create_buffer(BufferType::Vertex, sizeof(cube), cube);
+ m_skybox_ubo = device.create_buffer(BufferType::Uniform, sizeof(SkyboxUBO));
+
+ PipelineDesc d;
+ d.shader = "skybox";
+ d.vertex_layout = { 3 * sizeof(float), { { 0, 3, 0 } } };
+ d.resources = { { ResourceKind::UniformBuffer, 0, "SkyboxU" },
+ { ResourceKind::Texture, 1, "u_Skybox" } };
+ d.topology = Topology::Triangles;
+ d.depth_test = false; d.depth_write = false;
+ d.has_depth = true;
+ m_skybox_pipeline = device.create_pipeline(d);
+ }
+
+ DONUT_INFO("SceneRenderer ready ({} grid verts, {} sphere indices, skybox)",
+ m_grid_vertex_count, m_sphere_index_count);
+ return true;
+ }
+
+ auto SceneRenderer::render(RHI::CommandList& cmd, const CameraView& cam,
+ const std::vector<SceneObject>& objects, int selected,
+ RHI::Texture* cubemap) -> void
+ {
+ // Full-window viewport; flip_y lets the backend match the shared top-left
+ // orientation (Vulkan flips via a negative-height viewport, GL is a no-op).
+ cmd.set_viewport(0, 0, cam.fb_width, cam.fb_height, true);
+
+ glm::mat4 vp = cam.projection * cam.view;
+
+ // Skybox background first (depth 1, no depth test/write).
+ {
+ SkyboxUBO sky{};
+ sky.projection = cam.projection;
+ sky.view = glm::mat4(glm::mat3(cam.view)); // strip translation
+ m_skybox_ubo->update(&sky, sizeof(sky));
+ cmd.bind_pipeline(m_skybox_pipeline.get());
+ cmd.bind_uniform(0, m_skybox_ubo.get());
+ cmd.bind_texture(1, cubemap);
+ cmd.bind_vertex_buffer(m_skybox_vb.get());
+ cmd.draw(36);
+ }
+
+ // Opaque spheres (write depth). One UBO per object slot avoids aliasing the
+ // per-draw uniforms across the deferred command stream.
+ {
+ int count = std::min((int)objects.size(), MAX_OBJECTS);
+ while ((int)m_sphere_ubos.size() < count)
+ m_sphere_ubos.push_back(m_device->create_buffer(BufferType::Uniform, sizeof(SphereUBO)));
+
+ cmd.bind_pipeline(m_sphere_pipeline.get());
+ for (int i = 0; i < count; ++i)
+ {
+ const SceneObject& o = objects[i];
+ SphereUBO s{};
+ s.view_projection = vp;
+ s.transform = glm::translate(glm::mat4(1.0f), o.position)
+ * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius));
+ s.color = o.color;
+ s.specular = 0.6f;
+ s.emission = 0.0f;
+ s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f);
+ s.camera_pos = cam.position;
+ s.is_selected = (i == selected) ? 1 : 0;
+ s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f);
+ s.outline_width = 0.15f;
+ m_sphere_ubos[i]->update(&s, sizeof(s));
+
+ cmd.bind_uniform(0, m_sphere_ubos[i].get());
+ cmd.bind_texture(1, cubemap);
+ cmd.bind_vertex_buffer(m_sphere_vb.get());
+ cmd.bind_index_buffer(m_sphere_ib.get());
+ cmd.draw_indexed(m_sphere_index_count);
+ }
+ }
+
+ // Transparent grid on top (tests depth, doesn't write).
+ {
+ GridUBO g{};
+ g.view_projection = vp;
+ g.transform = glm::mat4(1.0f);
+ g.grid_size = 50.0f;
+ g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f);
+ g.grid_alpha = 0.75f;
+ g.camera_pos = cam.position;
+ m_grid_ubo->update(&g, sizeof(g));
+ cmd.bind_pipeline(m_grid_pipeline.get());
+ cmd.bind_uniform(0, m_grid_ubo.get());
+ cmd.bind_vertex_buffer(m_grid_vb.get());
+ cmd.draw(m_grid_vertex_count);
+ }
+ }
+}
diff --git a/src/rendering/scene_renderer.h b/src/rendering/scene_renderer.h
new file mode 100644
index 0000000..eafd56e
--- /dev/null
+++ b/src/rendering/scene_renderer.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "rhi.h"
+#include "renderer_backend.h" // SceneObject
+#include <glm/glm.hpp>
+#include <vector>
+
+namespace Donut
+{
+ // The world-builder scene view (skybox + lit spheres + reference grid),
+ // written ONCE against the RHI so it renders identically on every backend.
+ // The Application owns the camera, the object list and the HDRI cubemap and
+ // passes them in each frame; this class owns only the GPU resources (meshes,
+ // pipelines, per-object uniform buffers) and records the draws.
+ struct CameraView
+ {
+ glm::mat4 view;
+ glm::mat4 projection;
+ glm::vec3 position;
+ int fb_width = 1;
+ int fb_height = 1;
+ };
+
+ class SceneRenderer
+ {
+ public:
+ // Builds the grid/sphere/skybox meshes and pipelines on the given device.
+ auto init(RHI::Device& device) -> bool;
+
+ // Records the scene into the currently-open swapchain render pass. `cubemap`
+ // is the shared HDRI environment (sphere ambient + skybox background).
+ auto render(RHI::CommandList& cmd, const CameraView& cam,
+ const std::vector<SceneObject>& objects, int selected,
+ RHI::Texture* cubemap) -> void;
+
+ private:
+ static constexpr int MAX_OBJECTS = 64;
+
+ RHI::Device* m_device = nullptr;
+
+ Ref<RHI::Pipeline> m_grid_pipeline;
+ Ref<RHI::Buffer> m_grid_vb;
+ Ref<RHI::Buffer> m_grid_ubo;
+ int m_grid_vertex_count = 0;
+
+ Ref<RHI::Pipeline> m_sphere_pipeline;
+ Ref<RHI::Buffer> m_sphere_vb;
+ Ref<RHI::Buffer> m_sphere_ib;
+ int m_sphere_index_count = 0;
+ std::vector<Ref<RHI::Buffer>> m_sphere_ubos; // one per object slot
+
+ Ref<RHI::Pipeline> m_skybox_pipeline;
+ Ref<RHI::Buffer> m_skybox_vb;
+ Ref<RHI::Buffer> m_skybox_ubo;
+ };
+}
diff --git a/src/rendering/texture_manager.cpp b/src/rendering/texture_manager.cpp
deleted file mode 100644
index 3afba0d..0000000
--- a/src/rendering/texture_manager.cpp
+++ /dev/null
@@ -1,34 +0,0 @@
-#include "texture_manager.h"
-#include "renderer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- auto TextureManager::create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D>
- {
- return create_ref<Texture2D>(width, height);
- }
-
- auto TextureManager::bind_texture(uint32_t texture_id, uint32_t slot) -> void
- {
- RenderCommand::bind_texture(texture_id, slot);
- }
-
- auto TextureManager::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void
- {
- RenderCommand::bind_image_texture(texture_id, slot, read_only);
- }
-
- auto TextureManager::set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void
- {
- glBindTexture(GL_TEXTURE_2D, texture_id);
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
- }
-
- auto TextureManager::resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void
- {
- glBindTexture(GL_TEXTURE_2D, texture_id);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
- }
-};
diff --git a/src/rendering/texture_manager.h b/src/rendering/texture_manager.h
deleted file mode 100644
index 02c32b1..0000000
--- a/src/rendering/texture_manager.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include "rendering/texture.h"
-#include <cstdint>
-
-namespace Donut
-{
- class TextureManager
- {
- public:
- static auto create_texture(uint32_t width, uint32_t height) -> Ref<Texture2D>;
-
- static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void;
- static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void;
- static auto set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void;
- static auto resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void;
- };
-};
diff --git a/src/rendering/uniform_buffer.cpp b/src/rendering/uniform_buffer.cpp
deleted file mode 100644
index c4118b8..0000000
--- a/src/rendering/uniform_buffer.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#include "uniform_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- UniformBuffer::UniformBuffer(uint32_t size, uint32_t binding)
- : m_size(size), m_binding(binding)
- {
- glGenBuffers(1, &m_renderer_id);
- glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id);
- glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);
- glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id);
- }
-
- UniformBuffer::~UniformBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto UniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void
- {
- glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id);
- glBufferSubData(GL_UNIFORM_BUFFER, offset, size, data);
- }
-
- auto UniformBuffer::bind(uint32_t binding) -> void
- {
- glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id);
- }
-
- auto UniformBuffer::create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer>
- {
- return create_ref<UniformBuffer>(size, binding);
- }
-};
diff --git a/src/rendering/uniform_buffer.h b/src/rendering/uniform_buffer.h
deleted file mode 100644
index b5617b9..0000000
--- a/src/rendering/uniform_buffer.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-#include <cstdint>
-
-namespace Donut
-{
- class UniformBuffer
- {
- public:
- UniformBuffer(uint32_t size, uint32_t binding);
- ~UniformBuffer();
-
- auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void;
- auto bind(uint32_t binding) -> void;
-
- static auto create(uint32_t size, uint32_t binding) -> Ref<UniformBuffer>;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_size = 0;
- uint32_t m_binding = 0;
- };
-};
diff --git a/src/rendering/vertex_array.cpp b/src/rendering/vertex_array.cpp
deleted file mode 100644
index cddcf35..0000000
--- a/src/rendering/vertex_array.cpp
+++ /dev/null
@@ -1,61 +0,0 @@
-#include <glad/glad.h>
-
-#include "vertex_array.h"
-
-namespace Donut
-{
- VertexArray::VertexArray()
- {
- // glCreateVertexArrays is 4.5 DSA; macOS caps at 4.1. glGenVertexArrays
- // reserves the name and the VAO is created on first bind (done below).
- glGenVertexArrays(1, &m_renderer_id);
- }
-
- VertexArray::~VertexArray()
- {
- glDeleteVertexArrays(1, &m_renderer_id);
- }
-
- auto VertexArray::bind() const -> void
- {
- glBindVertexArray(m_renderer_id);
- }
-
- auto VertexArray::unbind() const -> void
- {
- glBindVertexArray(0);
- }
-
- auto VertexArray::add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void
- {
- glBindVertexArray(m_renderer_id);
- vertex_buffer->bind();
-
- const auto& layout = vertex_buffer->get_layout();
- for (const auto& element : layout.get_elements())
- {
- glEnableVertexAttribArray(m_vertex_buffer_index);
- glVertexAttribPointer(m_vertex_buffer_index,
- element.count,
- element.type,
- element.normalized ? GL_TRUE : GL_FALSE,
- layout.get_stride(),
- reinterpret_cast<const void*>(static_cast<uintptr_t>(element.offset)));
- m_vertex_buffer_index++;
- }
-
- m_vertex_buffers.push_back(vertex_buffer);
- }
-
- auto VertexArray::set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void
- {
- glBindVertexArray(m_renderer_id);
- index_buffer->bind();
- m_index_buffer = index_buffer;
- }
-
- auto VertexArray::create() -> VertexArray*
- {
- return new VertexArray();
- }
-};
diff --git a/src/rendering/vertex_array.h b/src/rendering/vertex_array.h
deleted file mode 100644
index 1f06758..0000000
--- a/src/rendering/vertex_array.h
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-
-#include "core/memory.h"
-
-#include "vertex_buffer.h"
-#include "index_buffer.h"
-
-#include <vector>
-
-namespace Donut
-{
- class VertexArray
- {
- public:
- VertexArray();
- ~VertexArray();
-
- auto bind() const -> void;
- auto unbind() const -> void;
-
- auto add_vertex_buffer(const Ref<VertexBuffer>& vertex_buffer) -> void;
- auto set_index_buffer(const Ref<IndexBuffer>& index_buffer) -> void;
-
- auto get_vertex_buffers() const -> const std::vector<Ref<VertexBuffer>>& { return m_vertex_buffers; }
- auto get_index_buffer() const -> const Ref<IndexBuffer>& { return m_index_buffer; }
-
- static auto create() -> VertexArray*;
-
- private:
- uint32_t m_renderer_id = 0;
- uint32_t m_vertex_buffer_index = 0;
- std::vector<Ref<VertexBuffer>> m_vertex_buffers;
- Ref<IndexBuffer> m_index_buffer;
- };
-};
diff --git a/src/rendering/vertex_buffer.cpp b/src/rendering/vertex_buffer.cpp
deleted file mode 100644
index 1bc471c..0000000
--- a/src/rendering/vertex_buffer.cpp
+++ /dev/null
@@ -1,50 +0,0 @@
-#include "vertex_buffer.h"
-
-#include <glad/glad.h>
-
-namespace Donut
-{
- auto VertexBufferElement::get_size_of_type(uint32_t type) -> uint32_t
- {
- switch (type)
- {
- case 0x1406: return 4; // GL_FLOAT
- case 0x1405: return 4; // GL_UNSIGNED_INT
- case 0x1401: return 1; // GL_UNSIGNED_BYTE
- default: return 0;
- }
- }
-
- VertexBuffer::VertexBuffer(const void* data, uint32_t size)
- {
- glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
- }
-
- VertexBuffer::~VertexBuffer()
- {
- glDeleteBuffers(1, &m_renderer_id);
- }
-
- auto VertexBuffer::bind() const -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- }
-
- auto VertexBuffer::unbind() const -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, 0);
- }
-
- auto VertexBuffer::set_data(const void* data, uint32_t size) -> void
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id);
- glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
- }
-
- auto VertexBuffer::create(const void* data, uint32_t size) -> VertexBuffer*
- {
- return new VertexBuffer(data, size);
- }
-};
diff --git a/src/rendering/vertex_buffer.h b/src/rendering/vertex_buffer.h
deleted file mode 100644
index 4e61700..0000000
--- a/src/rendering/vertex_buffer.h
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma once
-
-#include <cstdint>
-#include <vector>
-
-namespace Donut
-{
- struct VertexBufferElement
- {
- uint32_t type;
- uint32_t count;
- uint8_t normalized;
- uint32_t offset;
-
- static auto get_size_of_type(uint32_t type) -> uint32_t;
- };
-
- class VertexBufferLayout
- {
- public:
- VertexBufferLayout() = default;
- ~VertexBufferLayout() = default;
-
- template<typename T>
- auto push(uint32_t count) -> void
- {
- static_assert(false);
- }
-
- template<>
- void push<float>(uint32_t count)
- {
- m_elements.push_back({ 0x1406, count, 0, m_stride }); // GL_FLOAT
- m_stride += count * VertexBufferElement::get_size_of_type(0x1406);
- }
-
- template<>
- void push<uint32_t>(uint32_t count)
- {
- m_elements.push_back({ 0x1405, count, 0, m_stride }); // GL_UNSIGNED_INT
- m_stride += count * VertexBufferElement::get_size_of_type(0x1405);
- }
-
- template<>
- void push<uint8_t>(uint32_t count)
- {
- m_elements.push_back({ 0x1401, count, 1, m_stride }); // GL_UNSIGNED_BYTE
- m_stride += count * VertexBufferElement::get_size_of_type(0x1401);
- }
-
- inline auto get_elements() const -> const std::vector<VertexBufferElement>& { return m_elements; }
- inline auto get_stride() const -> uint32_t { return m_stride; }
-
- private:
- std::vector<VertexBufferElement> m_elements;
- uint32_t m_stride = 0;
- };
-
- class VertexBuffer
- {
- public:
- VertexBuffer(const void* data, uint32_t size);
- ~VertexBuffer();
-
- auto bind() const -> void;
- auto unbind() const -> void;
- auto set_data(const void* data, uint32_t size) -> void;
-
- auto get_layout() const -> const VertexBufferLayout& { return m_layout; }
- auto set_layout(const VertexBufferLayout& layout) -> void { m_layout = layout; }
-
- static auto create(const void* data, uint32_t size) -> VertexBuffer*;
-
- private:
- uint32_t m_renderer_id = 0;
- VertexBufferLayout m_layout;
- };
-};