feat: add gfx pipeline abstraction with render targets

Add cbt/gfx layer (pipeline + render_target) inspired by Sokol but
C++-friendly with RAII and pipeline_desc. Supports building full
graphics pipelines (VS/IA/raster/PS/OM), render-to-texture (FBO),
and post-processing steps (scene -> RT -> fullscreen quad with
sampler + vignette).

- Depth/state/viewport/texture cleanup for reliable scene switching.
- Updated cube/sphere to demonstrate (sphere uses RT+post).
- Vulkan backend easy via PIMPL in impl (same public API).
- Fixed depth test, viewport restore, and state leakage on switch.

Followed coding conventions (snake_case, trailing returns, east const,
include order, no ; after ns/class, etc.).
This commit is contained in:
2026-05-06 00:43:00 +02:00
parent 98673b57ff
commit 5ec8cfc735
7 changed files with 565 additions and 163 deletions
+313
View File
@@ -0,0 +1,313 @@
#include <cstddef>
#include <utility>
#include "cbt/gfx.hpp"
#include "glad/glad.h"
#include "glm/gtc/type_ptr.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/vao.hpp"
#include "cbt/opengl/texture.hpp"
namespace cbt::gfx {
struct pipeline::impl {
opengl::shader m_prog;
opengl::buffer m_vbo;
opengl::buffer m_ebo{opengl::buffer_type::index};
opengl::vao m_vao;
GLint m_loc_proj = -1;
GLint m_loc_view = -1;
GLint m_loc_model = -1;
GLsizei m_index_count = 0;
GLsizei m_vertex_count = 0;
bool m_valid = false;
bool m_uses_index = false;
bool m_depth_test = true;
GLenum m_draw_mode = GL_TRIANGLES;
GLenum m_index_gl_type = GL_UNSIGNED_INT;
auto build(pipeline_desc const& desc) -> bool;
auto bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void;
};
auto pipeline::impl::build(pipeline_desc const& desc) -> bool {
if (!desc.vertex_shader_src || !desc.fragment_shader_src ||
desc.vertex_data.empty() || desc.attributes.empty() || desc.vertex_stride == 0) {
return false;
}
if (!m_prog.compile_vertex(desc.vertex_shader_src) ||
!m_prog.compile_fragment(desc.fragment_shader_src) ||
!m_prog.link()) {
return false;
}
m_loc_proj = glGetUniformLocation(m_prog.id(), "u_proj");
m_loc_view = glGetUniformLocation(m_prog.id(), "u_view");
m_loc_model = glGetUniformLocation(m_prog.id(), "u_model");
m_vbo.upload(desc.vertex_data.data(), desc.vertex_data.size());
m_vertex_count = static_cast<GLsizei>(desc.vertex_data.size() / desc.vertex_stride);
m_uses_index = !desc.index_data.empty();
if (m_uses_index) {
m_ebo.upload(desc.index_data.data(), desc.index_data.size());
std::size_t const idx_size = (desc.index_type_ == index_type::uint16)
? sizeof(std::uint16_t)
: sizeof(std::uint32_t);
m_index_count = static_cast<GLsizei>(desc.index_data.size() / idx_size);
m_index_gl_type = (desc.index_type_ == index_type::uint16)
? GL_UNSIGNED_SHORT
: GL_UNSIGNED_INT;
}
m_draw_mode = GL_TRIANGLES;
m_depth_test = desc.depth_test;
m_vao.bind();
m_vbo.bind();
if (m_uses_index) {
m_ebo.bind();
}
for (auto const& attr : desc.attributes) {
glEnableVertexAttribArray(attr.location);
glVertexAttribPointer(
attr.location,
static_cast<GLint>(attr.num_components),
GL_FLOAT,
GL_FALSE,
static_cast<GLsizei>(desc.vertex_stride),
reinterpret_cast<void*>(static_cast<std::uintptr_t>(attr.offset))
);
}
m_vao.unbind();
m_vbo.unbind();
if (m_uses_index) {
m_ebo.unbind();
}
if (desc.depth_test) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
m_valid = true;
return true;
}
auto pipeline::impl::bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void {
if (!m_prog.valid()) {
return;
}
GLint loc = glGetUniformLocation(m_prog.id(), sampler_name);
if (loc != -1) {
glUniform1i(loc, static_cast<GLint>(unit));
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, texture_id);
}
}
pipeline::pipeline() : m_impl(std::make_unique<impl>()) {}
pipeline::pipeline(pipeline_desc const& desc) : m_impl(std::make_unique<impl>()) {
m_impl->build(desc);
}
pipeline::pipeline(pipeline&& other) noexcept
: m_impl(std::move(other.m_impl)) {}
auto pipeline::operator=(pipeline&& other) noexcept -> pipeline& {
if (this != &other) {
m_impl = std::move(other.m_impl);
}
return *this;
}
pipeline::~pipeline() = default;
auto pipeline::valid() const -> bool {
return m_impl && m_impl->m_valid;
}
auto pipeline::draw(
glm::mat4 const& model,
glm::mat4 const& view,
glm::mat4 const& proj
) const -> void {
if (!valid()) {
return;
}
auto& impl = *m_impl;
impl.m_prog.use();
if (impl.m_loc_proj != -1) {
glUniformMatrix4fv(impl.m_loc_proj, 1, GL_FALSE, glm::value_ptr(proj));
}
if (impl.m_loc_view != -1) {
glUniformMatrix4fv(impl.m_loc_view, 1, GL_FALSE, glm::value_ptr(view));
}
if (impl.m_loc_model != -1) {
glUniformMatrix4fv(impl.m_loc_model, 1, GL_FALSE, glm::value_ptr(model));
}
if (impl.m_depth_test) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
impl.m_vao.bind();
if (impl.m_uses_index) {
glDrawElements(impl.m_draw_mode, impl.m_index_count, impl.m_index_gl_type, nullptr);
} else {
glDrawArrays(impl.m_draw_mode, 0, impl.m_vertex_count);
}
impl.m_vao.unbind();
impl.m_prog.unuse();
// Reset texture state to prevent leakage on scene switches or between passes
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
}
auto pipeline::bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void {
if (m_impl) {
m_impl->bind_texture(sampler_name, texture_id, unit);
}
}
struct render_target::impl {
GLuint m_fbo = 0;
opengl::texture m_color{opengl::texture_target::_2d};
GLuint m_depth_rbo = 0;
int m_width = 0;
int m_height = 0;
bool m_valid = false;
auto build(int width, int height) -> bool;
auto cleanup() -> void;
};
auto render_target::impl::build(int width, int height) -> bool {
m_width = width;
m_height = height;
glGenFramebuffers(1, &m_fbo);
if (m_fbo == 0) {
return false;
}
// Color texture
m_color = opengl::texture(opengl::texture_target::_2d);
m_color.bind(0);
m_color.upload(nullptr, width, height, opengl::texture_format::rgba, opengl::texture_type::ubyte);
m_color.set_filter(GL_LINEAR, GL_LINEAR);
m_color.set_wrap(GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
m_color.unbind();
// Depth renderbuffer
glGenRenderbuffers(1, &m_depth_rbo);
glBindRenderbuffer(GL_RENDERBUFFER, m_depth_rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// Attach to FBO
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color.id(), 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depth_rbo);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
cleanup();
return false;
}
m_valid = true;
return true;
}
auto render_target::impl::cleanup() -> void {
if (m_fbo != 0) {
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
if (m_depth_rbo != 0) {
glDeleteRenderbuffers(1, &m_depth_rbo);
m_depth_rbo = 0;
}
m_valid = false;
}
render_target::render_target(int width, int height) : m_impl(std::make_unique<impl>()) {
m_impl->build(width, height);
}
render_target::render_target(render_target&& other) noexcept
: m_impl(std::move(other.m_impl)) {}
auto render_target::operator=(render_target&& other) noexcept -> render_target& {
if (this != &other) {
m_impl = std::move(other.m_impl);
}
return *this;
}
render_target::~render_target() {
if (m_impl) {
m_impl->cleanup();
}
}
auto render_target::bind() const -> void {
if (!valid()) return;
glBindFramebuffer(GL_FRAMEBUFFER, m_impl->m_fbo);
glViewport(0, 0, m_impl->m_width, m_impl->m_height);
}
auto render_target::unbind() const -> void {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
auto render_target::color_id() const -> std::uint32_t {
return m_impl ? m_impl->m_color.id() : 0;
}
auto render_target::width() const -> int {
return m_impl ? m_impl->m_width : 0;
}
auto render_target::height() const -> int {
return m_impl ? m_impl->m_height : 0;
}
auto render_target::valid() const -> bool {
return m_impl && m_impl->m_valid;
}
auto render_target::resize(int width, int height) -> void {
if (m_impl && m_impl->m_width == width && m_impl->m_height == height) {
return;
}
if (m_impl) {
m_impl->cleanup();
}
if (!m_impl) {
m_impl = std::make_unique<impl>();
}
m_impl->build(width, height);
}
} // namespace cbt::gfx
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
#include <vector>
#include "glm/glm.hpp"
namespace cbt::gfx {
enum class primitive_type {
triangles
};
enum class index_type {
uint16,
uint32
};
struct attribute_desc {
std::uint32_t location = 0;
std::uint32_t num_components = 3;
std::uint32_t offset = 0;
};
struct pipeline_desc {
std::span<std::byte const> vertex_data{};
std::span<std::byte const> index_data{};
std::vector<attribute_desc> attributes{};
std::uint32_t vertex_stride = 0;
char const* vertex_shader_src = nullptr;
char const* fragment_shader_src = nullptr;
bool depth_test = true;
primitive_type primitive = primitive_type::triangles;
index_type index_type_ = index_type::uint32;
};
class pipeline {
public:
pipeline();
explicit pipeline(pipeline_desc const& desc);
pipeline(pipeline const&) = delete;
pipeline(pipeline&& other) noexcept;
auto operator=(pipeline const&) -> pipeline& = delete;
auto operator=(pipeline&& other) noexcept -> pipeline&;
~pipeline();
auto valid() const -> bool;
auto draw(glm::mat4 const& model, glm::mat4 const& view, glm::mat4 const& proj) const -> void;
auto bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit = 0) const -> void;
private:
struct impl;
std::unique_ptr<impl> m_impl;
};
class render_target {
public:
explicit render_target(int width, int height);
render_target(render_target const&) = delete;
render_target(render_target&& other) noexcept;
auto operator=(render_target const&) -> render_target& = delete;
auto operator=(render_target&& other) noexcept -> render_target&;
~render_target();
auto bind() const -> void;
auto unbind() const -> void;
auto color_id() const -> std::uint32_t;
auto width() const -> int;
auto height() const -> int;
auto valid() const -> bool;
auto resize(int width, int height) -> void;
private:
struct impl;
std::unique_ptr<impl> m_impl;
};
} // namespace cbt::gfx