aboutsummaryrefslogtreecommitdiff
path: root/src/platform/opengl
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-20 02:27:02 +0200
committerhachem <im@hachem.wtf>2026-08-20 02:40:07 +0200
commit5b361d81dbd2af0d0e99b9eb1adebea76ff05db0 (patch)
tree34db1df17e8c7248aeeb92156d635d31c2484e3f /src/platform/opengl
parente3abaaf59777258ba06705135a0cafcb5918ad12 (diff)
[chore]: MASSIVE refactor + more vulkan bs
Diffstat (limited to 'src/platform/opengl')
-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
16 files changed, 1402 insertions, 0 deletions
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;
+ };
+};