diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5876df0..1fe21e3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25)
include(FetchContent)
-set(BLT_GRAPHICS_VERSION 2.0.10)
+set(BLT_GRAPHICS_VERSION 2.0.11)
set(BLT_GRAPHICS_TEST_VERSION 0.0.1)
project(BLT_WITH_GRAPHICS VERSION ${BLT_GRAPHICS_VERSION})
diff --git a/include/blt/gfx/vao.h b/include/blt/gfx/vao.h
new file mode 100644
index 0000000..64ebb0e
--- /dev/null
+++ b/include/blt/gfx/vao.h
@@ -0,0 +1,198 @@
+#pragma once
+/*
+ * Copyright (C) 2024 Brett Terpstra
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef BLT_GFX_VAO_H
+#define BLT_GFX_VAO_H
+
+#include
+#include
+#include
+#include
+
+namespace blt::gfx
+{
+ class unique_vao_t;
+
+ namespace detail
+ {
+ struct vao_vbo_storage_t
+ {
+ std::unique_ptr vbo;
+ std::optional> attribute_numbers;
+
+ [[nodiscard]] bool is_element() const
+ {
+ return vbo->get_buffer_type() == GL_ELEMENT_ARRAY_BUFFER;
+ }
+
+ explicit vao_vbo_storage_t(unique_vbo_t&& vbo): vbo(std::make_unique(std::move(vbo)))
+ {}
+ };
+
+ class vao_vbo_context_t
+ {
+ friend class vao_context_t;
+ public:
+ vao_vbo_context_t(const vao_vbo_context_t& copy) = delete;
+ vao_vbo_context_t(vao_vbo_context_t&& move) = delete;
+ vao_vbo_context_t& operator=(const vao_vbo_context_t& copy) = delete;
+ vao_vbo_context_t& operator=(vao_vbo_context_t&& move) = delete;
+ /**
+ * This function takes ownership of the underlying VBO (GPU side). It will be freed when the basic vertex array is deleted
+ * @param attribute_number attribute number to bind to
+ * @param coordinate_size size of the data (number of elements, not the number of bytes)
+ * @param type GL_TYPE type of data
+ * @param stride how many bytes this data takes (for the entire per-vertex data structure) 0 will assume packed data
+ * This is in effect how many bytes until the next block of data
+ * @param offset offset into the data structure to where the data is stored
+ */
+ vao_vbo_context_t& attribute_ptr(int attribute_number, int coordinate_size, GLenum type, int stride, long offset);
+
+ vao_vbo_context_t& silence()
+ {
+ attributed = true;
+ return *this;
+ }
+
+ /**
+ * Useless function, but if it makes you feel better, feel free to use it.
+ */
+ vao_vbo_context_t& as_element()
+ {
+ return *this;
+ }
+
+ ~vao_vbo_context_t();
+ private:
+ vao_vbo_context_t(unique_vao_t& vao, vao_vbo_storage_t& vbo): vbo(vbo), vao(vao)
+ {}
+
+ vao_vbo_storage_t& vbo;
+ unique_vao_t& vao;
+ bool attributed = false;
+ };
+
+ class vao_context_t
+ {
+ friend vao_vbo_context_t;
+ friend unique_vao_t;
+ public:
+ vao_context_t(const vao_context_t& copy) = delete;
+ vao_context_t(vao_context_t&& move) = delete;
+ vao_context_t& operator=(const vao_context_t& copy) = delete;
+ vao_context_t& operator=(vao_context_t&& move) = delete;
+
+ vao_context_t& bind();
+
+ vao_context_t& unbind();
+
+ vao_vbo_context_t attach_vbo(unique_vbo_t&& vbo) const;
+
+ ~vao_context_t();
+ private:
+ [[nodiscard]] bool is_bound() const;
+
+ explicit vao_context_t(unique_vao_t& vao): vao(vao)
+ {
+ bind();
+ }
+
+ unique_vao_t& vao;
+ };
+ }
+
+ class unique_vao_t
+ {
+ friend detail::vao_vbo_context_t;
+ friend detail::vao_context_t;
+
+ public:
+ unique_vao_t(): vaoID(0)
+ {
+ glGenVertexArrays(1, &*vaoID);
+ }
+
+ unique_vao_t(const unique_vao_t&) = delete;
+
+ unique_vao_t& operator=(const unique_vao_t&) = delete;
+
+ unique_vao_t(unique_vao_t&& other) noexcept: vaoID(std::exchange(other.vaoID, std::nullopt))
+ {}
+
+ unique_vao_t& operator=(unique_vao_t&& other) noexcept
+ {
+ vaoID = std::exchange(other.vaoID, vaoID);
+ return *this;
+ }
+
+ /**
+ * This function is used for configuring the internals of VAO it will automatically unbind the VAO at the end of scope
+ */
+ detail::vao_context_t configure();
+
+ /**
+ * This function is used to bind the VAO for usage during rendering.
+ */
+ void bind() const;
+
+ [[nodiscard]] std::optional[> get_attribute(const u32 attribute) const
+ {
+ for (const auto& vbo_obj : vbo_list)
+ {
+ if (const auto attrs = vbo_obj.attribute_numbers)
+ {
+ if (attrs->contains(attribute))
+ return *vbo_obj.vbo;
+ }
+ }
+ return {};
+ }
+
+ [[nodiscard]] std::optional][> get_buffer_type(const GLuint buffer_type) const
+ {
+ for (const auto& vbo_obj : vbo_list)
+ {
+ if (vbo_obj.vbo->get_buffer_type() == buffer_type)
+ return *vbo_obj.vbo;
+ }
+ return {};
+ }
+
+ [[nodiscard]] std::optional][> get_element() const
+ {
+ for (const auto& vbo_obj : vbo_list)
+ {
+ if (vbo_obj.is_element())
+ return *vbo_obj.vbo;
+ }
+ return {};
+ }
+
+ ~unique_vao_t()
+ {
+ if (vaoID)
+ glDeleteVertexArrays(1, &*vaoID);
+ }
+
+ private:
+ std::optional vaoID;
+ std::vector vbo_list;
+ };
+}
+
+#endif //BLT_GFX_VAO_H
diff --git a/include/blt/gfx/vbo.h b/include/blt/gfx/vbo.h
new file mode 100644
index 0000000..9f58d3e
--- /dev/null
+++ b/include/blt/gfx/vbo.h
@@ -0,0 +1,214 @@
+#pragma once
+/*
+ * Copyright (C) 2024 Brett Terpstra
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef BLT_GFX_VBO_H
+#define BLT_GFX_VBO_H
+
+#include
+#include
+
+namespace blt::gfx
+{
+ class unique_vbo_t;
+
+ namespace detail
+ {
+ /**
+ * So long as this class is called from vbo.bind(), it is always valid to chain any internal functions.
+ * This system is designed to be foolproof, so don't get too clever
+ */
+ class vbo_context_t
+ {
+ friend unique_vbo_t;
+ public:
+ vbo_context_t(const vbo_context_t& copy) = delete;
+ vbo_context_t(vbo_context_t&& move) = delete;
+ vbo_context_t& operator=(const vbo_context_t& copy) = delete;
+ vbo_context_t& operator=(vbo_context_t&& move) = delete;
+ /**
+ * By default, the VBO is bound when this class is constructed (this class should only be constructed through the VBO bind() method)
+ *
+ * It is very unlikely that you need this method!
+ */
+ vbo_context_t& bind();
+
+ vbo_context_t& unbind();
+
+ /**
+ * Reserves a chunk of GPU memory for future use
+ */
+ vbo_context_t& resize(GLsizeiptr size, GLint mem_type);
+
+ /**
+ * Reserves a chunk of GPU memory for future use
+ */
+ vbo_context_t& resize(const size_t size, const GLint mem_type)
+ {
+ return resize(static_cast(size), mem_type);
+ }
+
+ /**
+ * Uploads a chunk of memory to the GPU. If the existing VBO has enough space, the memory will be reused.
+ */
+ vbo_context_t& upload(size_t size, const void* ptr, GLint mem_type);
+
+ /**
+ * Uploads a chunk of memory to the GPU. If the existing VBO has enough space, the memory will be reused.
+ */
+ template , bool> = true>
+ vbo_context_t& upload(const size_t size, T* ptr, const GLint mem_type)
+ {
+ return upload(size, static_cast(ptr), mem_type);
+ }
+
+ /**
+ * Updates an internal segment of the VBO. This function will never reallocate.
+ */
+ vbo_context_t& update(size_t offset, size_t size, const void* ptr);
+
+ /**
+ * Updates an internal segment of the VBO. This function will never reallocate.
+ */
+ template , bool> = true>
+ vbo_context_t& update(const size_t offset, const size_t size, T* ptr)
+ {
+ return update(offset, size, static_cast(ptr));
+ }
+
+ private:
+ [[nodiscard]] bool is_bound() const;
+
+ explicit vbo_context_t(unique_vbo_t& vbo): vbo(vbo)
+ {
+ bind();
+ }
+
+ unique_vbo_t& vbo;
+ };
+ }
+
+ class unique_vbo_t
+ {
+ friend class detail::vbo_context_t;
+
+ public:
+ explicit unique_vbo_t(const GLuint type): vboID(0), buffer_type(type)
+ {
+ glGenBuffers(1, &*vboID);
+ }
+
+ unique_vbo_t(const unique_vbo_t&) = delete;
+
+ unique_vbo_t& operator=(const unique_vbo_t&) = delete;
+
+ unique_vbo_t(unique_vbo_t&& other) noexcept: vboID(std::exchange(other.vboID, std::nullopt)), buffer_type(other.buffer_type),
+ size(other.size), memory_type(other.memory_type)
+ {}
+
+ unique_vbo_t& operator=(unique_vbo_t&& other) noexcept
+ {
+ vboID = std::exchange(other.vboID, vboID);
+ buffer_type = std::exchange(other.buffer_type, buffer_type);
+ size = std::exchange(other.size, size);
+ memory_type = std::exchange(other.memory_type, memory_type);
+ return *this;
+ }
+
+ /**
+ * Changes the internal buffer type of this VBO
+ */
+ GLuint change_type(const GLuint type)
+ {
+ return std::exchange(this->buffer_type, type);
+ }
+
+ /**
+ * This function binds the VBO to the current buffer_type slot and returns an object which allows you to modify or use this VBO.
+ * This allows you to use the VBO without worrying about whether an operation is valid in this context.
+ * As so long as you use this object in line, or without binding other VBOs to the same buffer_type
+ * (violating the contracts this function attempts to create) then all functions on the associated object are valid to call.
+ *
+ * You can enable the flag BLT_DEBUG_CONTRACTS which will validate VBO bind state making most of ^ irrelevant
+ */
+ detail::vbo_context_t bind();
+
+ [[nodiscard]] auto native_handle() const
+ {
+ return vboID;
+ }
+
+ [[nodiscard]] GLsizeiptr get_size() const
+ {
+ return size;
+ }
+
+ [[nodiscard]] GLint get_memory_type() const
+ {
+ return memory_type;
+ }
+
+ [[nodiscard]] GLuint get_buffer_type() const
+ {
+ return buffer_type;
+ }
+
+ ~unique_vbo_t()
+ {
+ if (vboID)
+ glDeleteBuffers(1, &*vboID);
+ }
+
+ private:
+ std::optional vboID;
+ GLuint buffer_type;
+ GLsizeiptr size = 0;
+ GLint memory_type = 0;
+ };
+
+ class unique_ssbo_t : public unique_vbo_t
+ {
+ public:
+ unique_ssbo_t(): unique_vbo_t{GL_SHADER_STORAGE_BUFFER}
+ {}
+ };
+
+ class unique_ebo_t : public unique_vbo_t
+ {
+ public:
+ unique_ebo_t(): unique_vbo_t{GL_ELEMENT_ARRAY_BUFFER}
+ {}
+ };
+
+ class unique_ubo_t : public unique_vbo_t
+ {
+ public:
+ explicit unique_ubo_t(const i32 location): unique_vbo_t{GL_UNIFORM_BUFFER}
+ {set_location(location);}
+
+ void set_location(i32 new_location);
+
+ [[nodiscard]] i32 get_location() const
+ {
+ return location;
+ }
+ private:
+ i32 location = 0;
+ };
+}
+
+#endif //BLT_GFX_VBO_H
diff --git a/libraries/BLT b/libraries/BLT
index e2dc35f..2e1bdf9 160000
--- a/libraries/BLT
+++ b/libraries/BLT
@@ -1 +1 @@
-Subproject commit e2dc35fea98cc62897169cfc50dbf59fd820cd0e
+Subproject commit 2e1bdf945e820348f7ae59922ff41355b9dd3a9c
diff --git a/src/blt/gfx/vao.cpp b/src/blt/gfx/vao.cpp
new file mode 100644
index 0000000..cea1af9
--- /dev/null
+++ b/src/blt/gfx/vao.cpp
@@ -0,0 +1,100 @@
+/*
+ *
+ * Copyright (C) 2025 Brett Terpstra
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+#include
+#include
+#include
+
+namespace blt::gfx
+{
+ #define ENSURE_CONTEXT_BOUND BLT_CONTRACT(glfwGetCurrentContext() != nullptr, "Expected active OpenGL context!")
+
+ detail::vao_vbo_context_t& detail::vao_vbo_context_t::attribute_ptr(const int attribute_number, const int coordinate_size, const GLenum type,
+ const int stride, const long offset)
+ {
+ if (!vbo.attribute_numbers)
+ vbo.attribute_numbers = hashset_t();
+ if (!vbo.attribute_numbers->contains(attribute_number))
+ vbo.attribute_numbers->insert(attribute_number);
+ glEnableVertexAttribArray(attribute_number);
+ glVertexAttribPointer(attribute_number, coordinate_size, type, GL_FALSE, stride < 0 ? 0 : stride, reinterpret_cast(offset));
+ attributed = true;
+ return *this;
+ }
+
+ detail::vao_vbo_context_t::~vao_vbo_context_t()
+ {
+ #if blt_debug_has_flag(BLT_DEBUG_CONTRACTS)
+ if (!(vbo.is_element() || attributed))
+ {
+ BLT_WARN("VBO is not an element array buffer or been assigned to an attribute, are you sure this is what you want?");
+ BLT_WARN("You can silence this warning by calling .silence()");
+ }
+ #endif
+ }
+
+ detail::vao_context_t& detail::vao_context_t::bind()
+ {
+ vao.bind();
+ return *this;
+ }
+
+ detail::vao_context_t& detail::vao_context_t::unbind() // NOLINT
+ {
+ ENSURE_CONTEXT_BOUND;
+ glBindVertexArray(0);
+ return *this;
+ }
+
+ detail::vao_vbo_context_t detail::vao_context_t::attach_vbo(unique_vbo_t&& vbo) const
+ {
+ ENSURE_CONTEXT_BOUND;
+ BLT_CONTRACT(vao.vaoID, "Expected VAO to have an associated VAO ID!");
+ BLT_CONTRACT(is_bound(), "Expected VAO to be bound before attaching VBO! (If you are using this API correctly, this has been done for you!)");
+
+ auto& vbo_storage = vao.vbo_list.emplace_back(std::move(vbo));
+ vbo_storage.vbo->bind();
+ return vao_vbo_context_t{vao, vbo_storage};
+ }
+
+ detail::vao_context_t::~vao_context_t()
+ {
+ if (is_bound())
+ unbind();
+ }
+
+ bool detail::vao_context_t::is_bound() const
+ {
+ GLint current_vao;
+ glGetIntegerv(GL_VERTEX_ARRAY_BINDING, ¤t_vao);
+ return *vao.vaoID == static_cast(current_vao);
+ }
+
+ detail::vao_context_t unique_vao_t::configure()
+ {
+ ENSURE_CONTEXT_BOUND;
+ return detail::vao_context_t{*this};
+ }
+
+ void unique_vao_t::bind() const
+ {
+ ENSURE_CONTEXT_BOUND;
+ BLT_CONTRACT(vaoID, "Expected VAO to have an associated VAO ID!");
+ glBindVertexArray(*vaoID);
+ }
+}
diff --git a/src/blt/gfx/vbo.cpp b/src/blt/gfx/vbo.cpp
new file mode 100644
index 0000000..af0c5fc
--- /dev/null
+++ b/src/blt/gfx/vbo.cpp
@@ -0,0 +1,103 @@
+/*
+ *
+ * Copyright (C) 2025 Brett Terpstra
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+#include
+#include
+#include
+
+namespace blt::gfx
+{
+ #if blt_debug_has_flag(BLT_DEBUG_CONTRACTS)
+ static hashmap_t bound_vbo_ids;
+ #endif
+
+ namespace detail
+ {
+ vbo_context_t& vbo_context_t::bind()
+ {
+ BLT_CONTRACT(vbo.vboID, "Expected VBO to have an associated VBO ID!");
+ glBindBuffer(vbo.buffer_type, *vbo.vboID);
+ #if blt_debug_has_flag(BLT_DEBUG_CONTRACTS)
+ bound_vbo_ids[vbo.buffer_type] = *vbo.vboID;
+ #endif
+ return *this;
+ }
+
+ vbo_context_t& vbo_context_t::unbind()
+ {
+ glBindBuffer(vbo.buffer_type, 0);
+ #if blt_debug_has_flag(BLT_DEBUG_CONTRACTS)
+ bound_vbo_ids[vbo.buffer_type] = 0;
+ #endif
+ return *this;
+ }
+
+ vbo_context_t& vbo_context_t::resize(const GLsizeiptr size, const GLint mem_type)
+ {
+ BLT_CONTRACT(is_bound(), "Expected VBO to be bound before resizing!");
+ vbo.size = size;
+ vbo.memory_type = mem_type;
+ glBufferData(vbo.buffer_type, size, nullptr, mem_type);
+ return *this;
+ }
+
+ vbo_context_t& vbo_context_t::upload(const size_t size, const void* ptr, const GLint mem_type)
+ {
+ BLT_CONTRACT(is_bound(), "Expected VBO to be bound before uploading!");
+ if (mem_type != vbo.memory_type || static_cast(vbo.size) < size)
+ {
+ vbo.size = static_cast(size);
+ vbo.memory_type = mem_type;
+ glBufferData(vbo.buffer_type, vbo.size, ptr, mem_type);
+ } else
+ {
+ update(0, size, ptr);
+ }
+ return *this;
+ }
+
+ vbo_context_t& vbo_context_t::update(const size_t offset, const size_t size, const void* ptr)
+ {
+ BLT_CONTRACT(is_bound(), "Expected VBO to be bound before updating!");
+ glBufferSubData(vbo.buffer_type, static_cast(offset), static_cast(size), ptr);
+ return *this;
+ }
+
+ bool vbo_context_t::is_bound() const
+ {
+ #if blt_debug_has_flag(BLT_DEBUG_CONTRACTS)
+ return bound_vbo_ids[vbo.buffer_type] == *vbo.vboID;
+ #else
+ return true;
+ #endif
+ }
+ }
+
+ detail::vbo_context_t unique_vbo_t::bind()
+ {
+ BLT_CONTRACT(glfwGetCurrentContext() != nullptr, "Expected active OpenGL context!");
+ return detail::vbo_context_t{*this};
+ }
+
+ void unique_ubo_t::set_location(const i32 new_location)
+ {
+ BLT_CONTRACT(native_handle().has_value(), "Expected UBO to have an associated buffer! (You are probably calling this on a moved-from value)");
+ location = new_location;
+ glBindBufferBase(GL_UNIFORM_BUFFER, location, *native_handle());
+ }
+}
]