diff --git a/include/gkit/gkit.hpp b/include/gkit/gkit.hpp index f2c45af..afc2d2c 100644 --- a/include/gkit/gkit.hpp +++ b/include/gkit/gkit.hpp @@ -1,41 +1,41 @@ -#pragma once - -/** core **/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** graphic **/ -#include -#include - -/** math **/ -#include -#include -#include -#include -#include -#include -#include -#include - -/** resource **/ -#include -#include -#include -#include - -/** scene **/ -#include -#include -#include -#include - -namespace gkit {} // namespace gkit +#pragma once + +/** core **/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** graphic **/ +#include +#include + +/** math **/ +#include +#include +#include +#include +#include +#include +#include +#include + +/** resource **/ +#include +#include +#include +#include + +/** scene **/ +#include +#include +#include +#include + +namespace gkit {} // namespace gkit diff --git a/include/gkit/graphic/UniformBuffer.hpp b/include/gkit/graphic/UniformBuffer.hpp deleted file mode 100644 index 7e25d41..0000000 --- a/include/gkit/graphic/UniformBuffer.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "gkit/graphic/Buffer.hpp" - -namespace gkit::graphic { - - /** - * @brief Uniform data buffer (placeholder, not implemented yet) - * - * Buffer that stores uniform data (UBO) for batching shader constants. - * - * TODO(future): implement the backend (opengl::UniformBuffer) and the Device - * factory method create_uniform_buffer(). - */ - // class UniformBuffer : public Buffer { - // public: - // ~UniformBuffer() override = default; - // }; - -} // namespace gkit::graphic diff --git a/include/gkit/graphic/config.hpp b/include/gkit/graphic/config.hpp index 6e5859e..23e6ac0 100644 --- a/include/gkit/graphic/config.hpp +++ b/include/gkit/graphic/config.hpp @@ -9,6 +9,14 @@ namespace gkit::graphic { */ const unsigned int SCR_WIDTH = 500; const unsigned int SCR_HEIGHT = 500; + /** + * @brief Engine-declared texture slot limit (fixed conservative value) + * + * Most shaders fit within 8 slots; not chasing hardware limits + * (GL_MAX_TEXTURE_IMAGE_UNITS varies by GPU, usually >= 32). + * Start up may assert the hardware supports at least this many. + */ + static constexpr uint32_t MAX_TEXTURE_SLOTS = 8; /** * @brief Texture pattern diff --git a/include/gkit/graphic/render/RenderCommand.hpp b/include/gkit/graphic/render/RenderCommand.hpp new file mode 100644 index 0000000..5f87d98 --- /dev/null +++ b/include/gkit/graphic/render/RenderCommand.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/RenderState.hpp" +#include "gkit/graphic/resource/FrameBuffer.hpp" + +#include +#include + +namespace gkit::graphic { + + /** + * @brief Viewport rectangle for a render command + * + * Each command carries its own viewport so FBO-targeted commands use the + * FBO size while screen commands use the window size (GL viewport is global + * state, so it must be set per command). + */ + struct Viewport { + int x = 0; // Left coordinate + int y = 0; // Bottom coordinate + int width = 0; // Viewport width + int height = 0; // Viewport height + }; + + class RenderObject; + + /** + * @brief A single draw command referencing a render object + * + * Value type; references (not owns) the RenderObject and its target. + * The command carries a snapshot of the geometry's render state (so two + * commands can share one RenderObject with different states) plus per-draw + * controls (target, viewport, clear, sorting metadata). + */ + struct RenderCommand { + const FrameBuffer* target = nullptr; // Render target (nullptr = screen) + RenderObject* object = nullptr; // Geometry + material source (lazily uploaded on execute) + std::optional viewport; // Viewport to set before drawing; empty = target size + RenderState state; // Render state snapshot (applied before drawing) + uint32_t instance_count = 1; // 1 = non-instanced + bool transparent = false; // Sort front-to-back (opaque) or back-to-front (transparent) + float depth_key = 0.0f; // Depth sort key (filled by upper layer) + bool clear = false; // Whether to clear the target before drawing + ClearFlags clear_flags = ClearFlags::All; // What to clear when clear is set + }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/RenderDevice.hpp b/include/gkit/graphic/render/RenderDevice.hpp similarity index 76% rename from include/gkit/graphic/RenderDevice.hpp rename to include/gkit/graphic/render/RenderDevice.hpp index 3b5c2c6..21cf09e 100644 --- a/include/gkit/graphic/RenderDevice.hpp +++ b/include/gkit/graphic/render/RenderDevice.hpp @@ -1,13 +1,15 @@ #pragma once -#include "gkit/graphic/FrameBuffer.hpp" -#include "gkit/graphic/IndexBuffer.hpp" -#include "gkit/graphic/RenderBuffer.hpp" -#include "gkit/graphic/Shader.hpp" -#include "gkit/graphic/Texture.hpp" -#include "gkit/graphic/VertexArray.hpp" -#include "gkit/graphic/VertexBuffer.hpp" #include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/RenderCommand.hpp" +#include "gkit/graphic/render/RenderState.hpp" +#include "gkit/graphic/resource/FrameBuffer.hpp" +#include "gkit/graphic/resource/IndexBuffer.hpp" +#include "gkit/graphic/resource/RenderBuffer.hpp" +#include "gkit/graphic/resource/Shader.hpp" +#include "gkit/graphic/resource/Texture.hpp" +#include "gkit/graphic/resource/VertexArray.hpp" +#include "gkit/graphic/resource/VertexBuffer.hpp" #include #include @@ -62,6 +64,19 @@ namespace gkit::graphic { */ virtual auto clear(ClearFlags flags) -> void = 0; + /** + * @brief Apply a render state snapshot incrementally + * + * Backends compare against the previously applied state and only change + * what differs (see RHI design doc §6 / render queue design §3.4). + */ + virtual auto apply_state(const RenderState& state) -> void = 0; + + /** + * @brief Set the viewport (GL viewport is global state, set per command) + */ + virtual auto set_viewport(const Viewport& viewport) -> void = 0; + /** * @brief Draw indexed geometry */ diff --git a/include/gkit/graphic/render/RenderObject.hpp b/include/gkit/graphic/render/RenderObject.hpp new file mode 100644 index 0000000..4417c9a --- /dev/null +++ b/include/gkit/graphic/render/RenderObject.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "gkit/graphic/VertexBufferLayout.hpp" +#include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" +#include "gkit/graphic/render/RenderState.hpp" +#include "gkit/graphic/resource/IndexBuffer.hpp" +#include "gkit/graphic/resource/Material.hpp" +#include "gkit/graphic/resource/VertexArray.hpp" +#include "gkit/graphic/resource/VertexBuffer.hpp" + +#include +#include +#include + +namespace gkit::graphic { + + /** + * @brief A draw unit defined by CPU data (vertices/indices + material + state) + * + * Users provide vertex/index arrays, a vertex layout, a material, and state. + * The VAO/VBO/IBO creation and binding are hidden: GPU resources are lazily + * created and cached on first draw. Only a RenderCommand built from this + * object is enqueued. + */ + class RenderObject { + public: + /** + * @brief Construct from CPU geometry data, layout, and material + * @param vertices interleaved vertex data + * @param indices index data + * @param layout vertex attribute layout (position/color/uv...) + * @param material material (shader + textures + uniforms) + * @note vertices/indices are copied into the object (owned CPU data). + */ + RenderObject(const std::vector& vertices, + const std::vector& indices, + const VertexBufferLayout& layout, + const Material& material); + + // ---- Material (reusable, replaceable) ---- + Material material; + + // ---- Render state / target ---- + RenderState state; + uint32_t instance_count = 1; // 1 = non-instanced + bool transparent = false; // Sorting class + float depth_key = 0.0f; // Depth sort key + bool clear = false; // Whether to clear the target before drawing + ClearFlags clear_flags = ClearFlags::All; // What to clear when clear is set + + /** + * @brief Lazily upload vertices/indices to GPU and return the vertex array + * @param device backend device used to create GPU buffers + * @return reference to the cached vertex array + */ + auto ensure_uploaded(RenderDevice& device) -> const VertexArray&; + + /// @brief Access the cached index buffer (valid after ensure_uploaded) + [[nodiscard]] auto index_buffer() -> const IndexBuffer&; + + /// @brief Whether GPU resources have been created + [[nodiscard]] auto is_uploaded() const -> bool { return this->uploaded; } + + private: + // CPU geometry data (provided by the user) + std::vector vertices; + std::vector indices; + VertexBufferLayout layout; + + // Lazily created GPU resources (hidden from the user) + std::unique_ptr vbo; + std::unique_ptr vao; + std::unique_ptr ibo; + bool uploaded = false; + }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/render/RenderQueue.hpp b/include/gkit/graphic/render/RenderQueue.hpp new file mode 100644 index 0000000..b322b59 --- /dev/null +++ b/include/gkit/graphic/render/RenderQueue.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "gkit/graphic/render/RenderCommand.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" + +#include + +namespace gkit::graphic { + + /** + * @brief Render queue: collects commands, sorts, and executes at flush time + * + * Collects RenderCommand values during a frame and executes them in + * `flush()` after sorting. State application and drawing go through the + * frontend RenderDevice abstraction; the queue never touches GL directly. + */ + class RenderQueue { + public: + /** + * @brief Enqueue a command (copied; command is a value type) + */ + // cmd is deliberately taken by value: the command is copied into the queue. + auto submit(RenderCommand cmd) -> void { this->commands.push_back(cmd); } + + /** + * @brief Sort and execute all queued commands, then clear + * @param device Backend device used to apply state and draw + */ + auto flush(RenderDevice& device) -> void; + + /** + * @brief Drop all queued commands without executing + */ + auto clear() -> void { this->commands.clear(); } + + /// @brief Number of queued commands + [[nodiscard]] auto size() const -> size_t { return this->commands.size(); } + + private: + std::vector commands; + }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/render/RenderState.hpp b/include/gkit/graphic/render/RenderState.hpp new file mode 100644 index 0000000..99e477d --- /dev/null +++ b/include/gkit/graphic/render/RenderState.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "gkit/graphic/config.hpp" + +#include + +namespace gkit::graphic { + + /** + * @brief Depth test state + */ + struct DepthState { + bool enabled = false; // Whether depth test is enabled + CompareFunc compare_func = CompareFunc::Less; // Depth comparison function + bool write_mask = true; // Depth write mask + }; + + /** + * @brief Blend state + */ + struct BlendState { + bool enabled = false; // Whether blending is enabled + BlendFunc src_rgb = BlendFunc::One; // Source RGB blend factor + BlendFunc dst_rgb = BlendFunc::Zero; // Destination RGB blend factor + BlendFunc src_alpha = BlendFunc::One; // Source alpha blend factor + BlendFunc dst_alpha = BlendFunc::Zero; // Destination alpha blend factor + BlendEquation equation = BlendEquation::Add; // Blend equation + }; + + /** + * @brief Cull face state + */ + struct CullFaceState { + bool enabled = false; // Whether cull face is enabled + CullFaceMode mode = CullFaceMode::Back; // Cull face mode + FrontFace front_face = FrontFace::CounterClockwise; // Front face winding order + }; + + /** + * @brief Stencil state + */ + struct StencilState { + bool enabled = false; // Whether stencil test is enabled + CompareFunc compare_func = CompareFunc::Always; // Stencil comparison function + uint32_t ref = 0; // Stencil reference value + uint32_t read_mask = 0xFF; // Stencil read mask + uint32_t write_mask = 0xFF; // Stencil write mask + StencilOp fail = StencilOp::Keep; // Stencil fail operation + StencilOp z_fail = StencilOp::Keep; // Stencil depth fail operation + StencilOp z_pass = StencilOp::Keep; // Stencil depth pass operation + }; + + /** + * @brief Composite render state snapshot (sort key, carried by command) + * + * Packs the depth/blend/cull/stencil states into one snapshot that is + * carried by RenderCommand, used for sorting/deduplication, and applied + * incrementally by StateManager. + */ + struct RenderState { + DepthState depth; // Depth test state + BlendState blend; // Blend state + CullFaceState cull_face; // Cull face state + StencilState stencil; // Stencil state + + auto operator==(const RenderState& other) const -> bool { + const auto& a = *this; + return a.depth.enabled == other.depth.enabled && a.depth.compare_func == other.depth.compare_func && + a.depth.write_mask == other.depth.write_mask && a.blend.enabled == other.blend.enabled && + a.blend.src_rgb == other.blend.src_rgb && a.blend.dst_rgb == other.blend.dst_rgb && + a.blend.src_alpha == other.blend.src_alpha && a.blend.dst_alpha == other.blend.dst_alpha && + a.blend.equation == other.blend.equation && a.cull_face.enabled == other.cull_face.enabled && + a.cull_face.mode == other.cull_face.mode && a.cull_face.front_face == other.cull_face.front_face && + a.stencil.enabled == other.stencil.enabled && a.stencil.compare_func == other.stencil.compare_func && + a.stencil.ref == other.stencil.ref && a.stencil.read_mask == other.stencil.read_mask && + a.stencil.write_mask == other.stencil.write_mask && a.stencil.fail == other.stencil.fail && + a.stencil.z_fail == other.stencil.z_fail && a.stencil.z_pass == other.stencil.z_pass; + } + + auto operator!=(const RenderState& other) const -> bool { return !(*this == other); } + }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/Renderer.hpp b/include/gkit/graphic/render/Renderer.hpp similarity index 60% rename from include/gkit/graphic/Renderer.hpp rename to include/gkit/graphic/render/Renderer.hpp index 27d78f1..be1ae08 100644 --- a/include/gkit/graphic/Renderer.hpp +++ b/include/gkit/graphic/render/Renderer.hpp @@ -1,10 +1,11 @@ #pragma once #include "gkit/core/templates/singleton.hpp" -#include "gkit/graphic/RenderDevice.hpp" #include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" +#include "gkit/graphic/render/RenderObject.hpp" +#include "gkit/graphic/render/RenderQueue.hpp" -#include #include /** @@ -38,22 +39,22 @@ namespace gkit::graphic { auto clear(ClearFlags flags = ClearFlags::All) -> void; /** - * @brief Draw indexed geometry - * @param va Vertex array containing vertex data - * @param ib Index buffer containing indices - * @param shader Shader program to use for rendering + * @brief Enqueue a draw from a reusable render object + * @param obj Render object (geometry + material + state) + * @param target render target (default nullptr = screen) + * @param viewport viewport to use for this draw (default full window) + * @note Enqueued into the render queue; executed on flush(). The object is + * non-const because its GPU resources are lazily uploaded on execute. */ - auto draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) -> void; + auto draw(RenderObject& obj, + const FrameBuffer* target = nullptr, + const Viewport& viewport = Viewport{0, 0, static_cast(SCR_WIDTH), static_cast(SCR_HEIGHT)}) + -> void; /** - * @brief Draw multiple instances of indexed geometry - * @param va Vertex array containing vertex data - * @param ib Index buffer containing indices - * @param shader Shader program to use for rendering - * @param instance_count Number of instances to draw + * @brief Execute the queued render commands (sort + apply state + draw) */ - auto draw_instance(const VertexArray& va, const IndexBuffer& ib, const Shader& shader, uint32_t instance_count) - -> void; + auto flush() -> void; /** * @brief Access the current render device @@ -62,6 +63,7 @@ namespace gkit::graphic { private: std::unique_ptr device; + RenderQueue queue; }; } // namespace gkit::graphic diff --git a/include/gkit/graphic/Buffer.hpp b/include/gkit/graphic/resource/Buffer.hpp similarity index 100% rename from include/gkit/graphic/Buffer.hpp rename to include/gkit/graphic/resource/Buffer.hpp diff --git a/include/gkit/graphic/FrameBuffer.hpp b/include/gkit/graphic/resource/FrameBuffer.hpp similarity index 76% rename from include/gkit/graphic/FrameBuffer.hpp rename to include/gkit/graphic/resource/FrameBuffer.hpp index 37d5dfa..3226911 100644 --- a/include/gkit/graphic/FrameBuffer.hpp +++ b/include/gkit/graphic/resource/FrameBuffer.hpp @@ -1,7 +1,7 @@ #pragma once -#include "gkit/graphic/RenderBuffer.hpp" -#include "gkit/graphic/Texture.hpp" +#include "gkit/graphic/resource/RenderBuffer.hpp" +#include "gkit/graphic/resource/Texture.hpp" namespace gkit::graphic { @@ -25,8 +25,12 @@ namespace gkit::graphic { /** * @brief Attach a color texture to the given slot + * + * @param texture color attachment (non-const: a framebuffer texture may be + * resized to match this FBO's dimensions on attach) + * @param slot color attachment slot */ - virtual auto attach_color_texture(const Texture& texture, int slot) -> void = 0; + virtual auto attach_color_texture(Texture& texture, int slot) -> void = 0; /** * @brief Detach the color texture from the given slot @@ -68,6 +72,16 @@ namespace gkit::graphic { */ virtual auto bind() const -> void = 0; + /** + * @brief Framebuffer width in pixels + */ + [[nodiscard]] virtual auto width() const -> int = 0; + + /** + * @brief Framebuffer height in pixels + */ + [[nodiscard]] virtual auto height() const -> int = 0; + /** * @brief Unbind, reverting to the default framebuffer (screen) */ diff --git a/include/gkit/graphic/IndexBuffer.hpp b/include/gkit/graphic/resource/IndexBuffer.hpp similarity index 95% rename from include/gkit/graphic/IndexBuffer.hpp rename to include/gkit/graphic/resource/IndexBuffer.hpp index 4621117..28c9ddd 100644 --- a/include/gkit/graphic/IndexBuffer.hpp +++ b/include/gkit/graphic/resource/IndexBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/Buffer.hpp" +#include "gkit/graphic/resource/Buffer.hpp" #include #include diff --git a/include/gkit/graphic/resource/Material.hpp b/include/gkit/graphic/resource/Material.hpp new file mode 100644 index 0000000..30cb686 --- /dev/null +++ b/include/gkit/graphic/resource/Material.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "gkit/graphic/config.hpp" +#include "gkit/graphic/resource/Shader.hpp" +#include "gkit/graphic/resource/Texture.hpp" +#include "gkit/graphic/resource/UniformBuffer.hpp" + +#include +#include + +namespace gkit::graphic { + + /** + * @brief A reusable material (shader + texture slots + uniforms) + * + * Shader and textures are held by pointer (referenced, not owned); their + * lifetime is managed by the resource system. A Material can be shared by + * multiple RenderObjects. + */ + struct Material { + Shader* shader = nullptr; // Shader (pointer reference, not owned) + std::array textures = {}; // Texture slots (pointer reference) + uint32_t texture_count = 0; // Number of slots actually used + UniformData uniforms; // Simple-path per-name uniforms + UboBlock ubo; // Batch-path UBO reference + }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/RenderBuffer.hpp b/include/gkit/graphic/resource/RenderBuffer.hpp similarity index 100% rename from include/gkit/graphic/RenderBuffer.hpp rename to include/gkit/graphic/resource/RenderBuffer.hpp diff --git a/include/gkit/graphic/Shader.hpp b/include/gkit/graphic/resource/Shader.hpp similarity index 88% rename from include/gkit/graphic/Shader.hpp rename to include/gkit/graphic/resource/Shader.hpp index 0d542b0..4bd00a8 100644 --- a/include/gkit/graphic/Shader.hpp +++ b/include/gkit/graphic/resource/Shader.hpp @@ -44,6 +44,14 @@ namespace gkit::graphic { */ virtual auto unbind() const -> void = 0; + /** + * @brief Whether the shader program compiled and linked successfully + * + * A shader created from a file whose source failed to compile/link is + * invalid; rendering with it is undefined, so the queue rejects it. + */ + [[nodiscard]] virtual auto is_valid() const -> bool = 0; + // Uniform setters (implemented by backends, mapped to the concrete API) virtual auto set_uniform_1i(const std::string& name, int value) -> void = 0; diff --git a/include/gkit/graphic/StorageBuffer.hpp b/include/gkit/graphic/resource/StorageBuffer.hpp similarity index 93% rename from include/gkit/graphic/StorageBuffer.hpp rename to include/gkit/graphic/resource/StorageBuffer.hpp index 64f609b..f502ca9 100644 --- a/include/gkit/graphic/StorageBuffer.hpp +++ b/include/gkit/graphic/resource/StorageBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/Buffer.hpp" +#include "gkit/graphic/resource/Buffer.hpp" namespace gkit::graphic { diff --git a/include/gkit/graphic/Texture.hpp b/include/gkit/graphic/resource/Texture.hpp similarity index 100% rename from include/gkit/graphic/Texture.hpp rename to include/gkit/graphic/resource/Texture.hpp diff --git a/include/gkit/graphic/resource/UniformBuffer.hpp b/include/gkit/graphic/resource/UniformBuffer.hpp new file mode 100644 index 0000000..b3e9656 --- /dev/null +++ b/include/gkit/graphic/resource/UniformBuffer.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "gkit/graphic/resource/Buffer.hpp" +#include "gkit/math/matrix3.hpp" +#include "gkit/math/matrix4.hpp" +#include "gkit/math/vector3.hpp" +#include "gkit/math/vector4.hpp" + +#include +#include +#include +#include +#include +#include + +namespace gkit::graphic { + + /** + * @brief A single uniform value (type-erased) + */ + using UniformValue = std::variant; + + /** + * @brief Simple uniform set (value-by-value assignment) + * + * Simple path: the command carries a name→value list, and the executor + * calls set_uniform_* for each entry. + */ + struct UniformData { + std::vector> values; + }; + + /** + * @brief UBO block reference (bulk upload) + * + * Batch path: the command carries a reference to the user's parameter + * struct, and the executor uploads the whole block at once. + * Holds a reference, does not own — the user struct must stay alive until + * flush finishes (lifetime contract). + */ + struct UboBlock { + const void* data = nullptr; // Pointer to the user struct (e.g. SceneParams) + size_t size = 0; // Size of the struct in bytes + uint32_t binding = 0; // UBO binding point + }; + + /** + * @brief Uniform buffer object (UBO) base class (placeholder) + * + * Buffer that stores uniform data (UBO) for batching shader constants. + * Backend (opengl::UniformBuffer) + Device factory not implemented yet. + */ + // class UniformBuffer : public Buffer { + // public: + // ~UniformBuffer() override = default; + // }; + +} // namespace gkit::graphic diff --git a/include/gkit/graphic/VertexArray.hpp b/include/gkit/graphic/resource/VertexArray.hpp similarity index 96% rename from include/gkit/graphic/VertexArray.hpp rename to include/gkit/graphic/resource/VertexArray.hpp index fdc480a..8e11a11 100644 --- a/include/gkit/graphic/VertexArray.hpp +++ b/include/gkit/graphic/resource/VertexArray.hpp @@ -1,7 +1,7 @@ #pragma once -#include "gkit/graphic/VertexBuffer.hpp" #include "gkit/graphic/VertexBufferLayout.hpp" +#include "gkit/graphic/resource/VertexBuffer.hpp" /** * @brief Vertex array (frontend abstract interface) diff --git a/include/gkit/graphic/VertexBuffer.hpp b/include/gkit/graphic/resource/VertexBuffer.hpp similarity index 95% rename from include/gkit/graphic/VertexBuffer.hpp rename to include/gkit/graphic/resource/VertexBuffer.hpp index 905cd63..3c45b34 100644 --- a/include/gkit/graphic/VertexBuffer.hpp +++ b/include/gkit/graphic/resource/VertexBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/Buffer.hpp" +#include "gkit/graphic/resource/Buffer.hpp" #include diff --git a/src/graphic/CMakeLists.txt b/src/graphic/CMakeLists.txt index d7a39ce..2753584 100644 --- a/src/graphic/CMakeLists.txt +++ b/src/graphic/CMakeLists.txt @@ -2,6 +2,8 @@ set (GKIT_GRAPHIC "gkit_graphic") set (GRAPHIC_SRC "./Renderer.cpp" + "./RenderObject.cpp" + "./RenderQueue.cpp" "./create_device.cpp" # Backend implementations (one subdirectory per graphics API) diff --git a/src/graphic/RenderObject.cpp b/src/graphic/RenderObject.cpp new file mode 100644 index 0000000..4621b0f --- /dev/null +++ b/src/graphic/RenderObject.cpp @@ -0,0 +1,30 @@ +#include "gkit/graphic/render/RenderObject.hpp" + +namespace gkit::graphic { + + RenderObject::RenderObject(const std::vector& vertices, + const std::vector& indices, + const VertexBufferLayout& layout, + const Material& material) : + material(material), vertices(vertices), indices(indices), layout(layout) {} + + auto RenderObject::ensure_uploaded(RenderDevice& device) -> const VertexArray& { + if (this->uploaded) { + return *this->vao; + } + + // Create and cache GPU resources (hidden from the user). + this->vbo = device.create_vertex_buffer(this->vertices.data(), this->vertices.size() * sizeof(float), false); + this->ibo = device.create_index_buffer(this->indices.data(), static_cast(this->indices.size())); + this->vao = device.create_vertex_array(); + this->vao->add_buffer(*this->vbo, this->layout); + + this->uploaded = true; + return *this->vao; + } + + auto RenderObject::index_buffer() -> const IndexBuffer& { + return *this->ibo; + } + +} // namespace gkit::graphic diff --git a/src/graphic/RenderQueue.cpp b/src/graphic/RenderQueue.cpp new file mode 100644 index 0000000..751d46e --- /dev/null +++ b/src/graphic/RenderQueue.cpp @@ -0,0 +1,137 @@ +#include "gkit/graphic/render/RenderQueue.hpp" + +#include "gkit/graphic/render/RenderObject.hpp" + +#include +#include + +namespace gkit::graphic { + + namespace { + + /// @brief Bind a texture slot to the shader sampler unit + auto bind_textures(const Material& material) -> void { + for (uint32_t i = 0; i < material.texture_count && i < MAX_TEXTURE_SLOTS; ++i) { + if (material.textures[i] != nullptr) { + material.textures[i]->bind(i); + } + } + } + + /// @brief Apply a single uniform value through the shader's set_uniform_* + auto apply_uniform_value(Shader& shader, const std::string& name, const UniformValue& value) -> void { + std::visit( + [&](const auto& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + shader.set_uniform_1i(name, v); + } else if constexpr (std::is_same_v) { + shader.set_uniform_1f(name, v); + } else if constexpr (std::is_same_v) { + shader.set_uniform_vec_4f(name, v); + } else if constexpr (std::is_same_v) { + shader.set_uniform_vec_3f(name, v); + } else if constexpr (std::is_same_v) { + shader.set_uniform_mat_4f(name, v); + } else if constexpr (std::is_same_v) { + shader.set_uniform_mat_3f(name, v); + } + }, + value); + } + + /// @brief Apply material uniforms (simple path) + auto apply_uniforms(const Material& material) -> void { + if (material.shader == nullptr) { + return; + } + for (const auto& [name, value] : material.uniforms.values) { + apply_uniform_value(*material.shader, name, value); + } + // TODO(graphic): upload material.ubo via a UniformBuffer backend once implemented. + } + + /// @brief Sort comparator: framebuffer commands first, then by state/transparency + auto sort_key(const RenderCommand& cmd) -> uint64_t { + // Render targets (FBO) must be drawn before screen commands, otherwise + // post-processing cannot sample the FBO attachment. So target=null (screen) + // sorts after any non-null target. Then group by state (reduce switches). + const bool blend_enabled = cmd.state.blend.enabled; + const uint64_t target_rank = (cmd.target != nullptr) ? 0 : 1; // FBO before screen + return (target_rank << 56) | (static_cast(blend_enabled) << 48) | + (static_cast(cmd.transparent) << 40); + } + + } // namespace + + auto RenderQueue::flush(RenderDevice& device) -> void { + // Sort: opaque front-to-back, transparent back-to-front; group state/shader. + std::stable_sort( + this->commands.begin(), this->commands.end(), [](const RenderCommand& a, const RenderCommand& b) { + if (sort_key(a) != sort_key(b)) { + return sort_key(a) < sort_key(b); + } + // Within the same transparency class: opaque nearer-first, transparent farther-first. + const float key_a = a.transparent ? -a.depth_key : a.depth_key; + const float key_b = b.transparent ? -b.depth_key : b.depth_key; + return key_a < key_b; + }); + + const FrameBuffer* last_target = nullptr; + for (const auto& cmd : this->commands) { + if (cmd.object == nullptr) { + continue; + } + const Material& material = cmd.object->material; + + // Switch render target: unbind the previous FBO (reverting to screen) + // before binding a different target. target=null means screen, reached + // by unbinding the previous FBO. + if (cmd.target != last_target) { + if (last_target != nullptr) { + last_target->unbind(); + } + last_target = cmd.target; + if (cmd.target != nullptr) { + cmd.target->bind(); + } + } + + // GL viewport is global state; each command sets its own viewport, + // defaulting to the render target size when none was specified. + const Viewport vp = cmd.viewport.value_or( + cmd.target != nullptr ? Viewport{.x=0, .y=0, .width=cmd.target->width(), .height=cmd.target->height()} + : Viewport{.x=0, .y=0, .width=static_cast(SCR_WIDTH), .height=static_cast(SCR_HEIGHT)}); + device.set_viewport(vp); + + // Clear the currently bound target (FBO or screen) if the command asks for it. + if (cmd.clear) { + device.clear(cmd.clear_flags); + } + + device.apply_state(cmd.state); + + // Lazily upload geometry and bind shader/textures/uniforms. + // Shader validity is enforced at enqueue time (Renderer::draw), so the + // program is guaranteed non-null and valid here. + const auto& vao = cmd.object->ensure_uploaded(device); + const auto& ibo = cmd.object->index_buffer(); + material.shader->bind(); + bind_textures(material); + apply_uniforms(material); + + if (cmd.instance_count > 1) { + device.draw_instance(vao, ibo, *material.shader, cmd.instance_count); + } else { + device.draw(vao, ibo, *material.shader); + } + } + + // End of frame: leave the default framebuffer bound (screen). + if (last_target != nullptr) { + last_target->unbind(); + } + this->commands.clear(); + } + +} // namespace gkit::graphic diff --git a/src/graphic/Renderer.cpp b/src/graphic/Renderer.cpp index 9fe99b2..99881c0 100644 --- a/src/graphic/Renderer.cpp +++ b/src/graphic/Renderer.cpp @@ -1,6 +1,7 @@ -#include "gkit/graphic/Renderer.hpp" +#include "gkit/graphic/render/Renderer.hpp" -#include "gkit/graphic/RenderDevice.hpp" +#include "gkit/core/log.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" namespace gkit::graphic { @@ -12,20 +13,46 @@ namespace gkit::graphic { this->get_device().clear(flags); } - auto Renderer::draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader) -> void { - this->get_device().draw(va, ib, shader); + auto Renderer::draw(RenderObject& obj, const FrameBuffer* target, const Viewport& viewport) -> void { + // Reject shaderless materials: a null shader cannot be bound, and one that + // failed to compile/link is undefined to render with. Drawing with either + // would dereference a null/invalid program, so refuse to enqueue instead. + const Shader* shader = obj.material.shader; + if (shader == nullptr) { + core::Log::Message msg; + msg.level = core::Log::LogLevel::Error; + msg.functions = static_cast(core::Log::LogFunction::Both); + msg.message = "Renderer::draw: object has no shader; command rejected"; + core::Log::instance().log(std::move(msg)); + return; + } + if (!shader->is_valid()) { + core::Log::Message msg; + msg.level = core::Log::LogLevel::Error; + msg.functions = static_cast(core::Log::LogFunction::Both); + msg.message = "Renderer::draw: object shader is invalid (failed to compile/link); command rejected"; + core::Log::instance().log(std::move(msg)); + return; + } + + RenderCommand cmd; + cmd.object = &obj; // lazily uploaded on execute + cmd.target = target; + cmd.viewport = viewport; + cmd.state = obj.state; // snapshot: each command carries its own state + cmd.instance_count = obj.instance_count; + cmd.transparent = obj.transparent; + cmd.depth_key = obj.depth_key; + cmd.clear = obj.clear; + cmd.clear_flags = obj.clear_flags; + this->queue.submit(cmd); } - auto Renderer::draw_instance(const VertexArray& va, - const IndexBuffer& ib, - const Shader& shader, - uint32_t instance_count) -> void { - this->get_device().draw_instance(va, ib, shader, instance_count); + auto Renderer::flush() -> void { + this->queue.flush(this->get_device()); } auto Renderer::get_device() -> RenderDevice& { - // Lazily create the default device so callers don't have to ensure - // init() was called before get_device(). if (this->device == nullptr) { this->device = create_device(Backend::OpenGL); } diff --git a/src/graphic/backend/opengl/Device.cpp b/src/graphic/backend/opengl/Device.cpp index 2d7be8e..53899b2 100644 --- a/src/graphic/backend/opengl/Device.cpp +++ b/src/graphic/backend/opengl/Device.cpp @@ -12,6 +12,10 @@ namespace gkit::graphic::opengl { + Device::Device() { + this->state_manager.force_apply_all(); + } + auto Device::create_vertex_buffer(const void* data, uint32_t size, bool dynamic) -> std::unique_ptr { // Direct new (not make_unique) so Device's friendship grants access to the @@ -49,6 +53,14 @@ namespace gkit::graphic::opengl { glClear(mask); } + auto Device::apply_state(const graphic::RenderState& state) -> void { + this->state_manager.apply(state); + } + + auto Device::set_viewport(const graphic::Viewport& viewport) -> void { + glViewport(viewport.x, viewport.y, viewport.width, viewport.height); + } + auto Device::draw(const graphic::VertexArray& va, const graphic::IndexBuffer& ib, const graphic::Shader& shader) -> void { shader.bind(); diff --git a/src/graphic/backend/opengl/Device.hpp b/src/graphic/backend/opengl/Device.hpp index f6dec67..45ed2ff 100644 --- a/src/graphic/backend/opengl/Device.hpp +++ b/src/graphic/backend/opengl/Device.hpp @@ -1,6 +1,7 @@ #pragma once -#include "gkit/graphic/RenderDevice.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" +#include "graphic/backend/opengl/StateManager.hpp" #include #include @@ -9,13 +10,13 @@ * @brief OpenGL backend render device * * Inherits frontend `graphic::RenderDevice`; implements the resource factory - * and GL render commands. + * and GL render commands. Owns a StateManager for incremental state application. */ namespace gkit::graphic::opengl { class Device final : public graphic::RenderDevice { public: - Device() = default; + Device(); ~Device() override = default; auto create_vertex_buffer(const void* data, uint32_t size, bool dynamic) @@ -29,12 +30,17 @@ namespace gkit::graphic::opengl { auto create_render_buffer(int width, int height) -> std::unique_ptr override; auto clear(ClearFlags flags) -> void override; + auto apply_state(const graphic::RenderState& state) -> void override; + auto set_viewport(const graphic::Viewport& viewport) -> void override; auto draw(const graphic::VertexArray& va, const graphic::IndexBuffer& ib, const graphic::Shader& shader) -> void override; auto draw_instance(const graphic::VertexArray& va, const graphic::IndexBuffer& ib, const graphic::Shader& shader, uint32_t instance_count) -> void override; + + private: + StateManager state_manager; // Incremental GL state application }; } // namespace gkit::graphic::opengl diff --git a/src/graphic/backend/opengl/FrameBuffer.cpp b/src/graphic/backend/opengl/FrameBuffer.cpp index 013b9ee..637b956 100644 --- a/src/graphic/backend/opengl/FrameBuffer.cpp +++ b/src/graphic/backend/opengl/FrameBuffer.cpp @@ -43,9 +43,10 @@ namespace gkit::graphic::opengl { } } - auto FrameBuffer::attach_color_texture(const graphic::Texture& texture, int slot) -> void { + auto FrameBuffer::attach_color_texture(graphic::Texture& texture, int slot) -> void { bind(); - const auto& gl_texture = static_cast(texture); + auto& gl_texture = static_cast(texture); + gl_texture.set_size(static_cast(this->fb_width), static_cast(this->fb_height)); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + slot, GL_TEXTURE_2D, gl_texture.get_renderer_id(), 0); } @@ -101,4 +102,8 @@ namespace gkit::graphic::opengl { glBindFramebuffer(GL_FRAMEBUFFER, 0); } + auto FrameBuffer::width() const -> int { return static_cast(this->fb_width); } + + auto FrameBuffer::height() const -> int { return static_cast(this->fb_height); } + } // namespace gkit::graphic::opengl diff --git a/src/graphic/backend/opengl/FrameBuffer.hpp b/src/graphic/backend/opengl/FrameBuffer.hpp index 28e3944..745e955 100644 --- a/src/graphic/backend/opengl/FrameBuffer.hpp +++ b/src/graphic/backend/opengl/FrameBuffer.hpp @@ -1,8 +1,8 @@ #pragma once -#include "gkit/graphic/FrameBuffer.hpp" -#include "gkit/graphic/RenderBuffer.hpp" -#include "gkit/graphic/Texture.hpp" +#include "gkit/graphic/resource/FrameBuffer.hpp" +#include "gkit/graphic/resource/RenderBuffer.hpp" +#include "gkit/graphic/resource/Texture.hpp" #include @@ -23,7 +23,7 @@ namespace gkit::graphic::opengl { ~FrameBuffer() override; - auto attach_color_texture(const graphic::Texture& texture, int slot) -> void override; + auto attach_color_texture(graphic::Texture& texture, int slot) -> void override; auto detach_color_texture(int slot) -> void override; auto attach_depth_stencil(const graphic::RenderBuffer& rbo) -> void override; auto detach_depth_stencil() -> void override; @@ -34,6 +34,9 @@ namespace gkit::graphic::opengl { auto bind() const -> void override; auto unbind() const -> void override; + [[nodiscard]] auto width() const -> int override; + [[nodiscard]] auto height() const -> int override; + private: explicit FrameBuffer(int width, int height); diff --git a/src/graphic/backend/opengl/IndexBuffer.hpp b/src/graphic/backend/opengl/IndexBuffer.hpp index 6355875..d82e933 100644 --- a/src/graphic/backend/opengl/IndexBuffer.hpp +++ b/src/graphic/backend/opengl/IndexBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/IndexBuffer.hpp" +#include "gkit/graphic/resource/IndexBuffer.hpp" #include diff --git a/src/graphic/backend/opengl/RenderBuffer.hpp b/src/graphic/backend/opengl/RenderBuffer.hpp index 5094ece..e85140d 100644 --- a/src/graphic/backend/opengl/RenderBuffer.hpp +++ b/src/graphic/backend/opengl/RenderBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/RenderBuffer.hpp" +#include "gkit/graphic/resource/RenderBuffer.hpp" #include diff --git a/src/graphic/backend/opengl/Shader.cpp b/src/graphic/backend/opengl/Shader.cpp index 9808c70..3eee402 100644 --- a/src/graphic/backend/opengl/Shader.cpp +++ b/src/graphic/backend/opengl/Shader.cpp @@ -171,6 +171,11 @@ namespace gkit::graphic::opengl { glUseProgram(0); } + auto Shader::is_valid() const -> bool { + // renderer_id is 0 when compilation/linking failed or the program was moved. + return this->renderer_id != 0; + } + auto Shader::set_uniform_1i(const std::string& name, int value) -> void { glUniform1i(get_uniform_location(name), value); } diff --git a/src/graphic/backend/opengl/Shader.hpp b/src/graphic/backend/opengl/Shader.hpp index 08da1c6..f828d88 100644 --- a/src/graphic/backend/opengl/Shader.hpp +++ b/src/graphic/backend/opengl/Shader.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/Shader.hpp" +#include "gkit/graphic/resource/Shader.hpp" #include #include @@ -29,6 +29,8 @@ namespace gkit::graphic::opengl { auto bind() const -> void override; auto unbind() const -> void override; + [[nodiscard]] auto is_valid() const -> bool override; + auto set_uniform_1i(const std::string& name, int value) -> void override; auto set_uniform_1f(const std::string& name, float value) -> void override; auto set_uniform_4f(const std::string& name, float v0, float v1, float v2, float v3) -> void override; diff --git a/src/graphic/backend/opengl/StateManager.cpp b/src/graphic/backend/opengl/StateManager.cpp index 77fbbed..a35e93c 100644 --- a/src/graphic/backend/opengl/StateManager.cpp +++ b/src/graphic/backend/opengl/StateManager.cpp @@ -4,127 +4,49 @@ namespace gkit::graphic::opengl { - auto StateManager::set_depth_test(bool enable) -> void { - if (this->depth_state.enabled != enable) { - this->depth_state.enabled = enable; - this->dirty_flags |= DIRTY_DEPTH; - } - } - - auto StateManager::set_depth_func(CompareFunc func) -> void { - if (this->depth_state.compare_func != func) { - this->depth_state.compare_func = func; - this->dirty_flags |= DIRTY_DEPTH; - } - } - - auto StateManager::set_depth_mask(bool write) -> void { - if (this->depth_state.write_mask != write) { - this->depth_state.write_mask = write; - this->dirty_flags |= DIRTY_DEPTH; - } - } - - auto StateManager::set_blend(bool enable) -> void { - if (this->blend_state.enabled != enable) { - this->blend_state.enabled = enable; - this->dirty_flags |= DIRTY_BLEND; - } - } - - auto StateManager::set_blend_func(BlendFunc src_rgb, BlendFunc dst_rgb, BlendFunc src_alpha, BlendFunc dst_alpha) - -> void { - if (this->blend_state.src_rgb != src_rgb || this->blend_state.dst_rgb != dst_rgb || - this->blend_state.src_alpha != src_alpha || this->blend_state.dst_alpha != dst_alpha) { - this->blend_state.src_rgb = src_rgb; - this->blend_state.dst_rgb = dst_rgb; - this->blend_state.src_alpha = src_alpha; - this->blend_state.dst_alpha = dst_alpha; - this->dirty_flags |= DIRTY_BLEND; - } - } - - auto StateManager::set_blend_equation(BlendEquation equation) -> void { - if (this->blend_state.equation != equation) { - this->blend_state.equation = equation; - this->dirty_flags |= DIRTY_BLEND; - } - } - - auto StateManager::set_cull_face(bool enable) -> void { - if (this->cull_face_state.enabled != enable) { - this->cull_face_state.enabled = enable; - this->dirty_flags |= DIRTY_CULL; - } - } - - auto StateManager::set_cull_face_mode(CullFaceMode mode) -> void { - if (this->cull_face_state.mode != mode) { - this->cull_face_state.mode = mode; - this->dirty_flags |= DIRTY_CULL; - } - } - - auto StateManager::set_front_face(FrontFace front_face) -> void { - if (this->cull_face_state.front_face != front_face) { - this->cull_face_state.front_face = front_face; - this->dirty_flags |= DIRTY_CULL; - } - } - - auto StateManager::set_stencil_test(bool enable) -> void { - if (this->stencil_state.enabled != enable) { - this->stencil_state.enabled = enable; - this->dirty_flags |= DIRTY_STENCIL; - } - } - - auto StateManager::set_stencil(CompareFunc func, uint32_t ref, uint32_t mask) -> void { - if (this->stencil_state.compare_func != func || this->stencil_state.ref != ref || - this->stencil_state.read_mask != mask) { - this->stencil_state.compare_func = func; - this->stencil_state.ref = ref; - this->stencil_state.read_mask = mask; - this->dirty_flags |= DIRTY_STENCIL; - } - } - - auto StateManager::set_stencil_op(StencilOp fail, StencilOp z_fail, StencilOp z_pass) -> void { - if (this->stencil_state.fail != fail || this->stencil_state.z_fail != z_fail || - this->stencil_state.z_pass != z_pass) { - this->stencil_state.fail = fail; - this->stencil_state.z_fail = z_fail; - this->stencil_state.z_pass = z_pass; - this->dirty_flags |= DIRTY_STENCIL; - } - } - - auto StateManager::set_stencil_mask(uint32_t mask) -> void { - if (this->stencil_state.write_mask != mask) { - this->stencil_state.write_mask = mask; - this->dirty_flags |= DIRTY_STENCIL; - } - } - - auto StateManager::apply() -> void { - if (this->dirty_flags & DIRTY_DEPTH) { + auto StateManager::apply(const graphic::RenderState& state) -> void { + // Depth + if (this->depth_state.enabled != state.depth.enabled || + this->depth_state.compare_func != state.depth.compare_func || + this->depth_state.write_mask != state.depth.write_mask) { + this->depth_state = state.depth; apply_depth_state(); } - if (this->dirty_flags & DIRTY_BLEND) { + + // Blend + if (this->blend_state.enabled != state.blend.enabled || this->blend_state.src_rgb != state.blend.src_rgb || + this->blend_state.dst_rgb != state.blend.dst_rgb || this->blend_state.src_alpha != state.blend.src_alpha || + this->blend_state.dst_alpha != state.blend.dst_alpha || + this->blend_state.equation != state.blend.equation) { + this->blend_state = state.blend; apply_blend_state(); } - if (this->dirty_flags & DIRTY_CULL) { + + // Cull face + if (this->cull_face_state.enabled != state.cull_face.enabled || + this->cull_face_state.mode != state.cull_face.mode || + this->cull_face_state.front_face != state.cull_face.front_face) { + this->cull_face_state = state.cull_face; apply_cull_face_state(); } - if (this->dirty_flags & DIRTY_STENCIL) { + + // Stencil + if (this->stencil_state.enabled != state.stencil.enabled || + this->stencil_state.compare_func != state.stencil.compare_func || + this->stencil_state.ref != state.stencil.ref || this->stencil_state.read_mask != state.stencil.read_mask || + this->stencil_state.write_mask != state.stencil.write_mask || + this->stencil_state.fail != state.stencil.fail || this->stencil_state.z_fail != state.stencil.z_fail || + this->stencil_state.z_pass != state.stencil.z_pass) { + this->stencil_state = state.stencil; apply_stencil_state(); } - this->dirty_flags = 0; } auto StateManager::force_apply_all() -> void { - this->dirty_flags = DIRTY_DEPTH | DIRTY_BLEND | DIRTY_CULL | DIRTY_STENCIL; - apply(); + apply_depth_state(); + apply_blend_state(); + apply_cull_face_state(); + apply_stencil_state(); } auto StateManager::apply_depth_state() -> void { @@ -175,20 +97,4 @@ namespace gkit::graphic::opengl { glStencilMask(this->stencil_state.write_mask); } - auto StateManager::get_depth_state() const -> const DepthState& { - return this->depth_state; - } - - auto StateManager::get_blend_state() const -> const BlendState& { - return this->blend_state; - } - - auto StateManager::get_cull_face_state() const -> const CullFaceState& { - return this->cull_face_state; - } - - auto StateManager::get_stencil_state() const -> const StencilState& { - return this->stencil_state; - } - } // namespace gkit::graphic::opengl diff --git a/src/graphic/backend/opengl/StateManager.hpp b/src/graphic/backend/opengl/StateManager.hpp index 04cb7a9..77c7591 100644 --- a/src/graphic/backend/opengl/StateManager.hpp +++ b/src/graphic/backend/opengl/StateManager.hpp @@ -1,222 +1,54 @@ #pragma once -#include "gkit/core/templates/singleton.hpp" -#include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/RenderState.hpp" #include +/** + * @brief OpenGL state manager with dirty flag mechanism + * + * Owned by opengl::Device. Applies a frontend RenderState snapshot + * incrementally: only calls GL functions for the parts that changed + * since the last applied state. + */ namespace gkit::graphic::opengl { - /** - * @brief OpenGL state manager with dirty flag mechanism - * - * Tracks current OpenGL state and only calls GL functions when state actually changes. - * Uses singleton pattern for global access. - */ - class StateManager : public core::templates::Singleton { - friend class core::templates::Singleton; - - private: - StateManager() = default; - + class StateManager { public: - /** - * @brief Depth test state structure - */ - struct DepthState { - bool enabled = false; // Whether depth test is enabled - CompareFunc compare_func = CompareFunc::Less; // Depth comparison function - bool write_mask = true; // Depth write mask - }; - - /** - * @brief Blend state structure - */ - struct BlendState { - bool enabled = false; // Whether blending is enabled - BlendFunc src_rgb = BlendFunc::One; // Source RGB blend factor - BlendFunc dst_rgb = BlendFunc::Zero; // Destination RGB blend factor - BlendFunc src_alpha = BlendFunc::One; // Source alpha blend factor - BlendFunc dst_alpha = BlendFunc::Zero; // Destination alpha blend factor - BlendEquation equation = BlendEquation::Add; // Blend equation - }; - - /** - * @brief Cull face state structure - */ - struct CullFaceState { - bool enabled = false; // Whether cull face is enabled - CullFaceMode mode = CullFaceMode::Back; // Cull face mode - FrontFace front_face = FrontFace::CounterClockwise; // Front face winding order - }; - - /** - * @brief Stencil state structure - */ - struct StencilState { - bool enabled = false; // Whether stencil test is enabled - CompareFunc compare_func = CompareFunc::Always; // Stencil comparison function - uint32_t ref = 0; // Stencil reference value - uint32_t read_mask = 0xFF; // Stencil read mask - uint32_t write_mask = 0xFF; // Stencil write mask - StencilOp fail = StencilOp::Keep; // Stencil fail operation - StencilOp z_fail = StencilOp::Keep; // Stencil depth fail operation - StencilOp z_pass = StencilOp::Keep; // Stencil depth pass operation - }; - - /** - * @brief Enable or disable depth testing - * @param enable True to enable depth testing, false to disable - */ - auto set_depth_test(bool enable) -> void; - - /** - * @brief Set depth test compare function - * @param func The comparison function to use - */ - auto set_depth_func(CompareFunc func) -> void; - - /** - * @brief Set depth write mask - * @param write True to enable depth writes, false to disable - */ - auto set_depth_mask(bool write) -> void; - - /** - * @brief Enable or disable blending - * @param enable True to enable blending, false to disable - */ - auto set_blend(bool enable) -> void; - - /** - * @brief Set blend factors for RGB and Alpha - * @param src_rgb Source RGB blend factor - * @param dst_rgb Destination RGB blend factor - * @param src_alpha Source alpha blend factor - * @param dst_alpha Destination alpha blend factor - */ - auto set_blend_func(BlendFunc src_rgb, BlendFunc dst_rgb, BlendFunc src_alpha, BlendFunc dst_alpha) -> void; - - /** - * @brief Set blend equation - * @param equation The blend equation to use - */ - auto set_blend_equation(BlendEquation equation) -> void; - - /** - * @brief Enable or disable face culling - * @param enable True to enable culling, false to disable - */ - auto set_cull_face(bool enable) -> void; - - /** - * @brief Set cull face mode - * @param mode The cull face mode - */ - auto set_cull_face_mode(CullFaceMode mode) -> void; - - /** - * @brief Set front face winding order - * @param front_face The front face winding order - */ - auto set_front_face(FrontFace front_face) -> void; - - /** - * @brief Enable or disable stencil testing - * @param enable True to enable stencil testing, false to disable - */ - auto set_stencil_test(bool enable) -> void; - - /** - * @brief Set stencil state - * @param func Stencil comparison function - * @param ref Stencil reference value - * @param mask Stencil read mask - */ - auto set_stencil(CompareFunc func, uint32_t ref, uint32_t mask) -> void; - - /** - * @brief Set stencil write mask - * @param mask Stencil write mask - */ - auto set_stencil_mask(uint32_t mask) -> void; - - /** - * @brief Set stencil operations - * @param fail Operation when stencil test fails - * @param z_fail Operation when stencil passes but depth fails - * @param z_pass Operation when both stencil and depth pass - */ - auto set_stencil_op(StencilOp fail, StencilOp z_fail, StencilOp z_pass) -> void; + StateManager() = default; /** - * @brief Apply all dirty states to OpenGL - */ - auto apply() -> void; + * @brief Apply a render state snapshot incrementally + * + * Compares against the previously applied state and only applies the + * changed components (depth/blend/cull/stencil). + * @param state Frontend render state snapshot + */ + auto apply(const graphic::RenderState& state) -> void; /** - * @brief Force apply all states (ignore dirty flags) - */ + * @brief Force apply all states (ignore dirty flags) + */ auto force_apply_all() -> void; - /** - * @brief Get current depth state - * @return Reference to the current depth state - */ - [[nodiscard]] auto get_depth_state() const -> const DepthState&; - - /** - * @brief Get current blend state - * @return Reference to the current blend state - */ - [[nodiscard]] auto get_blend_state() const -> const BlendState&; - - /** - * @brief Get current cull face state - * @return Reference to the current cull face state - */ - [[nodiscard]] auto get_cull_face_state() const -> const CullFaceState&; - - /** - * @brief Get current stencil state - * @return Reference to the current stencil state - */ - [[nodiscard]] auto get_stencil_state() const -> const StencilState&; - private: - /** - * @brief Apply depth state if dirty - */ + /// @brief Apply depth state if changed auto apply_depth_state() -> void; - /** - * @brief Apply blend state if dirty - */ + /// @brief Apply blend state if changed auto apply_blend_state() -> void; - /** - * @brief Apply cull face state if dirty - */ + /// @brief Apply cull face state if changed auto apply_cull_face_state() -> void; - /** - * @brief Apply stencil state if dirty - */ + /// @brief Apply stencil state if changed auto apply_stencil_state() -> void; - /// @brief Current shadow states - DepthState depth_state; // Current depth state - BlendState blend_state; // Current blend state - CullFaceState cull_face_state; // Current cull face state - StencilState stencil_state; // Current stencil state - - /// @brief Dirty flags bitmask - uint8_t dirty_flags = 0; // Bitmask indicating which states need updating - - static constexpr uint8_t DIRTY_DEPTH = 1 << 0; // Dirty flag for depth state - static constexpr uint8_t DIRTY_BLEND = 1 << 1; // Dirty flag for blend state - static constexpr uint8_t DIRTY_CULL = 1 << 2; // Dirty flag for cull face state - static constexpr uint8_t DIRTY_STENCIL = 1 << 3; // Dirty flag for stencil state + /// @brief Currently applied states (shadow state) + graphic::DepthState depth_state; // Current depth state + graphic::BlendState blend_state; // Current blend state + graphic::CullFaceState cull_face_state; // Current cull face state + graphic::StencilState stencil_state; // Current stencil state }; } // namespace gkit::graphic::opengl diff --git a/src/graphic/backend/opengl/Texture.cpp b/src/graphic/backend/opengl/Texture.cpp index 438940b..4c226a4 100644 --- a/src/graphic/backend/opengl/Texture.cpp +++ b/src/graphic/backend/opengl/Texture.cpp @@ -24,6 +24,22 @@ gkit::graphic::opengl::Texture::Texture(const std::string& path, gkit::graphic:: } } +auto gkit::graphic::opengl::Texture::set_size(int width, int height) -> void { + // Only framebuffer textures are (re)allocated on attach; loaded images must + // keep their pixel data. + if (this->type != gkit::graphic::TextureType::TextureFramebuffer) { + return; + } + if (this->width == width && this->height == height) { + return; + } + this->width = width; + this->height = height; + glBindTexture(GL_TEXTURE_2D, this->renderer_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, this->width, this->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_2D, 0); +} + gkit::graphic::opengl::Texture::~Texture() { delete[] this->local_buffer; if (this->renderer_id != 0) { diff --git a/src/graphic/backend/opengl/Texture.hpp b/src/graphic/backend/opengl/Texture.hpp index 2fa098e..4fc0bd2 100644 --- a/src/graphic/backend/opengl/Texture.hpp +++ b/src/graphic/backend/opengl/Texture.hpp @@ -1,7 +1,7 @@ #pragma once -#include "gkit/graphic/Texture.hpp" #include "gkit/graphic/config.hpp" +#include "gkit/graphic/resource/Texture.hpp" #include #include @@ -48,6 +48,17 @@ namespace gkit::graphic::opengl { */ [[nodiscard]] inline auto get_renderer_id() const -> uint32_t { return this->renderer_id; } + /** + * @brief Resize the texture storage (framebuffer textures only) + * + * A framebuffer texture is allocated empty at construction; attaching it + * to a FrameBuffer reallocates storage to the FBO size so the texture is + * never bound to the global SCR_WIDTH/SCR_HEIGHT constants. + * @param width new width in pixels + * @param height new height in pixels + */ + auto set_size(int width, int height) -> void; + private: inline static const std::vector FACES = { "right.jpg", "left.jpg", "top.jpg", "bottom.jpg", "front.jpg", "back.jpg"}; diff --git a/src/graphic/backend/opengl/VertexArray.cpp b/src/graphic/backend/opengl/VertexArray.cpp index 4b146b8..f7777df 100644 --- a/src/graphic/backend/opengl/VertexArray.cpp +++ b/src/graphic/backend/opengl/VertexArray.cpp @@ -1,8 +1,8 @@ #include "graphic/backend/opengl/VertexArray.hpp" -#include "graphic/backend/opengl/VertexBuffer.hpp" #include "gkit/math/matrix4.hpp" #include "gkit/math/vector4.hpp" +#include "graphic/backend/opengl/VertexBuffer.hpp" #include diff --git a/src/graphic/backend/opengl/VertexArray.hpp b/src/graphic/backend/opengl/VertexArray.hpp index 4bcd4d4..505d489 100644 --- a/src/graphic/backend/opengl/VertexArray.hpp +++ b/src/graphic/backend/opengl/VertexArray.hpp @@ -1,8 +1,8 @@ #pragma once -#include "gkit/graphic/VertexArray.hpp" -#include "gkit/graphic/VertexBuffer.hpp" #include "gkit/graphic/VertexBufferLayout.hpp" +#include "gkit/graphic/resource/VertexArray.hpp" +#include "gkit/graphic/resource/VertexBuffer.hpp" #include diff --git a/src/graphic/backend/opengl/VertexBuffer.hpp b/src/graphic/backend/opengl/VertexBuffer.hpp index aedb94c..9e94b32 100644 --- a/src/graphic/backend/opengl/VertexBuffer.hpp +++ b/src/graphic/backend/opengl/VertexBuffer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "gkit/graphic/VertexBuffer.hpp" +#include "gkit/graphic/resource/VertexBuffer.hpp" #include diff --git a/src/graphic/create_device.cpp b/src/graphic/create_device.cpp index c10edab..da431f9 100644 --- a/src/graphic/create_device.cpp +++ b/src/graphic/create_device.cpp @@ -1,4 +1,4 @@ -#include "gkit/graphic/RenderDevice.hpp" +#include "gkit/graphic/render/RenderDevice.hpp" #include "graphic/backend/opengl/Device.hpp" namespace gkit::graphic { diff --git a/test/graphic/alpha_triangle.shader b/test/graphic/alpha_triangle.shader new file mode 100644 index 0000000..be732d8 --- /dev/null +++ b/test/graphic/alpha_triangle.shader @@ -0,0 +1,27 @@ +#shader vertex +#version 450 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aColor; + +out vec3 v_Color; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + v_Color = aColor; +} + +#shader fragment +#version 450 core +out vec4 FragColor; + +in vec3 v_Color; + +// Alpha factor for the blend test; defaults to opaque so an unset uniform +// does not silently make the object invisible. +uniform float u_alpha = 1.0; + +void main() +{ + FragColor = vec4(v_Color, u_alpha); +} diff --git a/test/graphic/test_render.cpp b/test/graphic/test_render.cpp new file mode 100644 index 0000000..1433c29 --- /dev/null +++ b/test/graphic/test_render.cpp @@ -0,0 +1,277 @@ +#include "gkit/graphic/VertexBufferLayout.hpp" +#include "gkit/graphic/config.hpp" +#include "gkit/graphic/render/Renderer.hpp" +#include "graphic/backend/opengl/Texture.hpp" +#include "test_utils.hpp" + +#include +#include + +#include "SDL3/SDL.h" +#include + +namespace fs = std::filesystem; + +auto test_render_loop() -> bool { + // Resource files live in /test/graphic/, same folder as this source file + fs::path resource_base = fs::path(__FILE__).parent_path(); + +#pragma region Init + // Initialize SDL + if (!SDL_Init(SDL_INIT_VIDEO)) { + std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << '\n'; + return false; + } + + // Request OpenGL 4.6 Core Profile + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + // Create window + int screen_width = gkit::graphic::SCR_WIDTH; + int screen_height = gkit::graphic::SCR_HEIGHT; + + SDL_Window* window = SDL_CreateWindow("OpenGL Window", screen_width, screen_height, SDL_WINDOW_OPENGL); + + if (window == nullptr) { + std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << '\n'; + SDL_Quit(); + return false; + } + + // Create OpenGL context + SDL_GLContext gl_context = SDL_GL_CreateContext(window); + if (gl_context == nullptr) { + std::cerr << "OpenGL context could not be created! SDL_Error: " << SDL_GetError() << '\n'; + SDL_DestroyWindow(window); + SDL_Quit(); + return false; + } + + // Initialize GLAD + if (!gladLoadGL(SDL_GL_GetProcAddress)) { + std::cerr << "Failed to initialize GLAD!" << '\n'; + SDL_GL_DestroyContext(gl_context); + SDL_DestroyWindow(window); + SDL_Quit(); + return false; + } + + // Print OpenGL version + std::cout << "OpenGL Version: " << glGetString(GL_VERSION) << '\n'; + std::cout << "GLSL Version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << '\n'; + std::cout << "Renderer: " << glGetString(GL_RENDERER) << '\n'; + +#pragma endregion + + { + auto& renderer = gkit::graphic::Renderer::instance(); + renderer.init(); // default OpenGL backend + + auto& device = renderer.get_device(); + +#pragma region triangle + // Colored triangle vertex data (position + color) + std::vector tri_vertices = {// positions // colors + 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top: red + -0.4f, -0.25f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom-left: green + 0.4f, -0.25f, 0.0f, 0.0f, 0.0f, 1.0f}; // bottom-right: blue + + std::vector tri_indices = {0, 1, 2}; + + gkit::graphic::VertexBufferLayout tri_layout; + tri_layout.push(3); // position + tri_layout.push(3); // color + + // load shader source + auto tri_shader = device.create_shader((resource_base / "color_triangle.shader").string()); + + // Full-screen quad vertex data (post-processing). + // z = 0.2 (farthest): it depth-tests first and writes the deepest value, + // so the later triangles (z = 0.1 and z = 0) depth-test against it. + std::vector quad_vertices = {// positions // tex coords + -1.0f, -1.0f, 0.2f, 0.0f, 0.0f, + 1.0f, -1.0f, 0.2f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.2f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.2f, 0.0f, 1.0f}; + + std::vector quad_indices = {0, 1, 2, 2, 3, 0}; + + gkit::graphic::VertexBufferLayout quad_layout; + quad_layout.push(3); + quad_layout.push(2); + + // load post-processing shader + auto post_shader = device.create_shader((resource_base / "post_process.shader").string()); + + // load alpha-blended triangle shader (u_alpha uniform controls opacity) + auto alpha_shader = device.create_shader((resource_base / "alpha_triangle.shader").string()); +#pragma endregion + +#pragma region framebuffer + // FBO is half the window size. + const int fbo_width = screen_width; + const int fbo_height = screen_height; + auto fbo = device.create_frame_buffer(fbo_width, fbo_height); + gkit::graphic::opengl::Texture fbo_texture(" ", gkit::graphic::TextureType::TextureFramebuffer); + auto rbo = device.create_render_buffer(fbo_width, fbo_height); + fbo->attach_color_texture(fbo_texture, 0); + fbo->attach_depth_stencil(*rbo); + fbo->check(); +#pragma endregion + +#pragma region render_objects + // Reusable draw units: user provides data arrays + material; VAO/VBO/IBO hidden. + + // Triangle material + gkit::graphic::Material tri_material; + tri_material.shader = tri_shader.get(); + + // Post-processing quad material (samples the FBO texture) + gkit::graphic::Material post_material; + post_material.shader = post_shader.get(); + post_material.textures[0] = &fbo_texture; + post_material.texture_count = 1; + post_material.uniforms.values.push_back({"screenTexture", 0}); + + // Opaque objects enable depth testing so they write their depth and the + // translucent triangle is really depth-tested against them (a disabled + // depth test does not write the depth buffer, which would leave it empty). + gkit::graphic::RenderObject triangle_obj(tri_vertices, tri_indices, tri_layout, tri_material); + triangle_obj.state.depth.enabled = true; + + gkit::graphic::RenderObject quad_obj(quad_vertices, quad_indices, quad_layout, post_material); + quad_obj.state.depth.enabled = true; + + // Transparent triangle: reuses the triangle's shape (positions + colors), + // offset 50 px toward the bottom-left (NDC: 500 px window → 1 px = 0.004, + // 50 px = 0.2). z = 0.1 sits between the quad (z = 0.2, farthest) and the + // comparison triangle (z = 0, nearest), so depth testing decides the layering + // among the three. It blends with what is already on screen + // (SrcAlpha / OneMinusSrcAlpha, u_alpha = 0.8 → 0.8·src + 0.2·dst). + const float px_to_ndc = 2.0f / static_cast(screen_width); // 1 px in NDC + const float offset_x = -50.0f * px_to_ndc; + const float offset_y = -50.0f * px_to_ndc; + std::vector alpha_vertices; + for (std::size_t v = 0; v < tri_vertices.size(); v += 6) { + alpha_vertices.push_back(tri_vertices[v] + offset_x); // position x + alpha_vertices.push_back(tri_vertices[v + 1] + offset_y); // position y + alpha_vertices.push_back(0.1f); // position z: between quad (0.2) and comparison triangle (0) + // reuse the original colors (indices 3..5) + alpha_vertices.insert(alpha_vertices.end(), tri_vertices.begin() + v + 3, tri_vertices.begin() + v + 6); + } + + gkit::graphic::Material alpha_material; + alpha_material.shader = alpha_shader.get(); + alpha_material.uniforms.values.push_back({"u_alpha", 0.8f}); + + gkit::graphic::RenderObject alpha_triangle_obj(alpha_vertices, tri_indices, tri_layout, alpha_material); + alpha_triangle_obj.state.depth.enabled = true; // depth-tests against quad + comparison triangle + alpha_triangle_obj.state.blend.enabled = true; + alpha_triangle_obj.state.blend.src_rgb = gkit::graphic::BlendFunc::SrcAlpha; + alpha_triangle_obj.state.blend.dst_rgb = gkit::graphic::BlendFunc::OneMinusSrcAlpha; + alpha_triangle_obj.state.blend.src_alpha = gkit::graphic::BlendFunc::SrcAlpha; + alpha_triangle_obj.state.blend.dst_alpha = gkit::graphic::BlendFunc::OneMinusSrcAlpha; + alpha_triangle_obj.transparent = true; // sorted after opaque, back-to-front + alpha_triangle_obj.depth_key = 0.0f; // nearest transparent → drawn last + + // Stencil-mask triangle: offset 50 px right and 50 px up, written into the + // FBO as a stencil=1 region (color is irrelevant — the FBO color is cleared + // right after). Depth test stays disabled so it writes no depth and can't + // reject the later masked triangle. + const float right_offset = 50.0f * px_to_ndc; + const float up_offset = 35.0f * px_to_ndc; + std::vector stencil_vertices; + for (std::size_t v = 0; v < tri_vertices.size(); v += 6) { + stencil_vertices.push_back(tri_vertices[v] + right_offset); // position x + 50 px + stencil_vertices.push_back(tri_vertices[v + 1] + up_offset); // position y + 50 px + stencil_vertices.push_back(tri_vertices[v + 2]); // position z unchanged + // reuse the original colors (indices 3..5) + stencil_vertices.insert(stencil_vertices.end(), tri_vertices.begin() + v + 3, tri_vertices.begin() + v + 6); + } + + gkit::graphic::RenderObject stencil_triangle_obj(stencil_vertices, tri_indices, tri_layout, tri_material); + stencil_triangle_obj.state.stencil.enabled = true; + stencil_triangle_obj.state.stencil.compare_func = gkit::graphic::CompareFunc::Always; // always pass, just write + stencil_triangle_obj.state.stencil.ref = 1; + stencil_triangle_obj.state.stencil.write_mask = 0xFF; + stencil_triangle_obj.state.stencil.fail = gkit::graphic::StencilOp::Keep; + stencil_triangle_obj.state.stencil.z_fail = gkit::graphic::StencilOp::Keep; + stencil_triangle_obj.state.stencil.z_pass = gkit::graphic::StencilOp::Replace; // write stencil=ref=1 + + // Triangle drawn after the stencil mask: NotEqual(1) rejects fragments + // inside the mask region, leaving a hole there and drawing elsewhere. + gkit::graphic::RenderObject masked_triangle_obj(tri_vertices, tri_indices, tri_layout, tri_material); + masked_triangle_obj.state.depth.enabled = true; + masked_triangle_obj.state.stencil.enabled = true; + masked_triangle_obj.state.stencil.compare_func = gkit::graphic::CompareFunc::Notequal; // stencil==1 fails + masked_triangle_obj.state.stencil.ref = 1; + masked_triangle_obj.state.stencil.fail = gkit::graphic::StencilOp::Keep; + masked_triangle_obj.state.stencil.z_fail = gkit::graphic::StencilOp::Keep; + masked_triangle_obj.state.stencil.z_pass = gkit::graphic::StencilOp::Keep; +#pragma endregion + + // Main loop + bool quit = false; + SDL_Event event; + while (!quit) { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_QUIT) { + quit = true; + } + if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.key.key == SDLK_ESCAPE) { + quit = true; + } + } + } + + // Clear the default framebuffer (screen) every frame. Depth is cleared + // too: the translucent triangle depth-tests, so a stale depth buffer + // would reject it on later frames. + renderer.clear(gkit::graphic::ClearFlags::ColorDepth); + + // Submit reusable render objects; Renderer enqueues them and flush() executes. + // FBO stencil-mask pass: + // Draw 1: clear the FBO (color+depth+stencil) then write stencil=1 in the + // mask triangle's region (offset 50 px up; color is irrelevant). + stencil_triangle_obj.clear = true; // clears color+depth+stencil (All) + renderer.draw(stencil_triangle_obj, fbo.get()); + // Draw 2: clear only the FBO color (keeps the stencil marks), then draw the + // triangle with stencil NotEqual(1): fragments inside the stencil + // region are rejected, leaving a hole. + masked_triangle_obj.clear = true; + masked_triangle_obj.clear_flags = gkit::graphic::ClearFlags::Color; + renderer.draw(masked_triangle_obj, fbo.get()); + // Draw 3: post-processing quad to screen (samples fbo texture) + renderer.draw(quad_obj); + // Draw 4:triangle to screen (no post-processing, just for comparison) + triangle_obj.clear = false; + renderer.draw(triangle_obj, nullptr, gkit::graphic::Viewport{.x=0, .y=0, .width=screen_width / 2, .height=screen_height / 2}); + // Draw 5: translucent triangle blended over the screen (depth-tested on + // top of the FBO quad; transparent flag sorts it last, so it's the top layer). + renderer.draw(alpha_triangle_obj); + + renderer.flush(); + + // Swap buffers + SDL_GL_SwapWindow(window); + } + } + + // Cleanup + SDL_GL_DestroyContext(gl_context); + SDL_DestroyWindow(window); + SDL_Quit(); + + gkit::test::logln("window closed successfully"); + return true; +} + +auto main() -> int { + auto test_runner = gkit::test::TestRunner().add_test_func(test_render_loop); + + test_runner.run(); + return 0; +} diff --git a/test/graphic/test_window.cpp b/test/graphic/test_window.cpp deleted file mode 100644 index 363ac6e..0000000 --- a/test/graphic/test_window.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "gkit/graphic/Renderer.hpp" -#include "gkit/graphic/VertexBufferLayout.hpp" -#include "graphic/backend/opengl/Texture.hpp" -#include "graphic/backend/opengl/config.hpp" -#include "test_utils.hpp" - -#include -#include - -#include "SDL3/SDL.h" -#include - -namespace fs = std::filesystem; - -auto test_window_render_loop() -> bool { - // Resource files live in /test/graphic/, same folder as this source file - fs::path resource_base = fs::path(__FILE__).parent_path(); - -#pragma region Init - // Initialize SDL - if (!SDL_Init(SDL_INIT_VIDEO)) { - std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << '\n'; - return false; - } - - // Request OpenGL 4.6 Core Profile - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - - // Create window - int screen_width = gkit::graphic::SCR_WIDTH; - int screen_height = gkit::graphic::SCR_HEIGHT; - - SDL_Window* window = SDL_CreateWindow("OpenGL Window", screen_width, screen_height, SDL_WINDOW_OPENGL); - - if (window == nullptr) { - std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << '\n'; - SDL_Quit(); - return false; - } - - // Create OpenGL context - SDL_GLContext gl_context = SDL_GL_CreateContext(window); - if (gl_context == nullptr) { - std::cerr << "OpenGL context could not be created! SDL_Error: " << SDL_GetError() << '\n'; - SDL_DestroyWindow(window); - SDL_Quit(); - return false; - } - - // Initialize GLAD - if (!gladLoadGL(SDL_GL_GetProcAddress)) { - std::cerr << "Failed to initialize GLAD!" << '\n'; - SDL_GL_DestroyContext(gl_context); - SDL_DestroyWindow(window); - SDL_Quit(); - return false; - } - - // Print OpenGL version - std::cout << "OpenGL Version: " << glGetString(GL_VERSION) << '\n'; - std::cout << "GLSL Version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << '\n'; - std::cout << "Renderer: " << glGetString(GL_RENDERER) << '\n'; - -#pragma endregion - - { - auto& renderer = gkit::graphic::Renderer::instance(); - renderer.init(); // default OpenGL backend - - auto& device = renderer.get_device(); - -#pragma region triangle - // Colored triangle vertex data (position + color) - float tri_vertices[] = {// positions // colors - 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top: red - -0.4f, -0.25f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom-left: green - 0.4f, -0.25f, 0.0f, 0.0f, 0.0f, 1.0f}; // bottom-right: blue - - // index data - unsigned int tri_indices[] = {0, 1, 2}; - - auto tri_vao = device.create_vertex_array(); - auto tri_vbo = device.create_vertex_buffer(tri_vertices, sizeof(tri_vertices), false); - auto tri_ibo = device.create_index_buffer(tri_indices, 3); - - gkit::graphic::VertexBufferLayout tri_layout; - tri_layout.push(3); // position - tri_layout.push(3); // color - tri_vao->add_buffer(*tri_vbo, tri_layout); - - // load shader source - auto tri_shader = device.create_shader((resource_base / "graphic" / "color_triangle.shader").string()); - - // Full-screen quad vertex data (post-processing) - float quad_vertices[] = {// positions // tex coords - -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, - 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f}; - - unsigned int quad_indices[] = {0, 1, 2, 2, 3, 0}; - - auto quad_vao = device.create_vertex_array(); - auto quad_vb = device.create_vertex_buffer(quad_vertices, sizeof(quad_vertices), false); - auto quad_ib = device.create_index_buffer(quad_indices, 6); - - gkit::graphic::VertexBufferLayout quad_layout; - quad_layout.push(3); - quad_layout.push(2); - quad_vao->add_buffer(*quad_vb, quad_layout); - - // load post-processing shader - auto post_shader = device.create_shader((resource_base / "graphic" / "post_process.shader").string()); -#pragma endregion - -#pragma region framebuffer - auto fbo = device.create_frame_buffer(gkit::graphic::SCR_WIDTH, gkit::graphic::SCR_HEIGHT); - gkit::graphic::opengl::Texture fbo_texture(" ", gkit::graphic::TextureType::TextureFramebuffer); - auto rbo = device.create_render_buffer(gkit::graphic::SCR_WIDTH, gkit::graphic::SCR_HEIGHT); - fbo->attach_color_texture(fbo_texture, 0); - fbo->attach_depth_stencil(*rbo); - fbo->check(); -#pragma endregion - - // Main loop - bool quit = false; - SDL_Event event; - while (!quit) { - while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_QUIT) { - quit = true; - } - if (event.type == SDL_EVENT_KEY_DOWN) { - if (event.key.key == SDLK_ESCAPE) { - quit = true; - } - } - } - - fbo->bind(); - fbo->set_viewport(0, 0, screen_width, screen_height); - renderer.clear(gkit::graphic::ClearFlags::All); - // 1. Render to framebuffer - tri_shader->bind(); - renderer.draw(*tri_vao, *tri_ibo, *tri_shader); - - // 2. Render to screen (post-processing) - fbo->unbind(); - gkit::graphic::opengl::viewport::set_viewport(0, 0, screen_width, screen_height); - post_shader->bind(); - fbo_texture.bind(0); - post_shader->set_uniform_1i("screenTexture", 0); - renderer.draw(*quad_vao, *quad_ib, *post_shader); - - gkit::graphic::opengl::viewport::set_viewport(0, 0, screen_width / 2, screen_height / 2); - tri_shader->bind(); - renderer.draw(*tri_vao, *tri_ibo, *tri_shader); - - gkit::graphic::opengl::viewport::set_viewport(0, 0, screen_width / 4, screen_height / 4); - post_shader->bind(); - fbo_texture.bind(0); - post_shader->set_uniform_1i("screenTexture", 0); - renderer.draw(*quad_vao, *quad_ib, *post_shader); - - // Swap buffers - SDL_GL_SwapWindow(window); - } - } - - // Cleanup - SDL_GL_DestroyContext(gl_context); - SDL_DestroyWindow(window); - SDL_Quit(); - - gkit::test::logln("window closed successfully"); - return true; -} - -auto main() -> int { - auto test_runner = gkit::test::TestRunner().add_test_func(test_window_render_loop); - - test_runner.run(); - return 0; -}