Skip to content

introduce UboManager for lazy UBO updates and binding#472

Merged
nschimme merged 20 commits intoMUME:masterfrom
nschimme:ubo
Mar 17, 2026
Merged

introduce UboManager for lazy UBO updates and binding#472
nschimme merged 20 commits intoMUME:masterfrom
nschimme:ubo

Conversation

@nschimme
Copy link
Copy Markdown
Contributor

@nschimme nschimme commented Mar 16, 2026

Summary by Sourcery

Introduce a centralized UBO management system and attribute-less mesh utilities, refactoring OpenGL legacy code to use them for shared uniform buffers and full-screen rendering.

New Features:

  • Add a UboManager to centrally track, lazily rebuild, and bind shared uniform buffer objects based on SharedVboEnum blocks.
  • Introduce attribute-less mesh helpers for full-screen and blit rendering using gl_VertexID instead of explicit vertex attributes.
  • Extend GLRenderState to optionally carry an MVP matrix override used by meshes and shaders.

Enhancements:

  • Refactor Legacy::Functions to depend on UboManager, enforce trivially copyable buffer uploads, and derive UBO binding indices from SharedVboEnum.
  • Simplify instanced room quad rendering to use glDrawArraysInstanced with gl_VertexID-generated vertices instead of an index buffer.
  • Tighten SharedVBO/VAO enums and counts with static assertions and helper to compute UBO binding indices, and assert UBO binding capacity at initialization.
  • Update shaders and render paths to rely on std140 uniform blocks and the new UBO manager instead of ad-hoc buffer binding logic.

Build:

  • Register new UboManager and attribute-less mesh sources/headers in the CMake build.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 16, 2026

Reviewer's Guide

Introduces a central UboManager for shared uniform buffer management, refactors legacy GL functions and OpenGL initialization to route UBO setup/binding through it, adds attribute-less fullscreen mesh utilities, and simplifies quad rendering and buffer upload helpers while wiring everything into the build and shader pipeline.

Sequence diagram for NamedColorsBlock UBO rebuild and binding via UboManager

sequenceDiagram
    actor User
    participant MapCanvas
    participant OpenGL
    participant UboManager as Legacy_UboManager
    participant Funcs as Legacy_Functions

    %% Registration during initialization
    User->>MapCanvas: initializeGL()
    MapCanvas->>OpenGL: initializeRenderer(dpr)
    OpenGL->>Funcs: getFunctions()
    OpenGL->>Funcs: glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS)
    OpenGL-->>MapCanvas: renderer initialized

    MapCanvas->>OpenGL: getUboManager()
    MapCanvas->>UboManager: registerRebuildFunction(NamedColorsBlock, lambda)
    UboManager->>UboManager: invalidateAll()

    %% Later, during rendering
    User->>MapCanvas: actuallyPaintGL()
    MapCanvas->>OpenGL: getSharedFunctions(Badge)
    MapCanvas->>OpenGL: getUboManager()
    MapCanvas->>UboManager: bind(Funcs, NamedColorsBlock)

    UboManager->>UboManager: updateIfInvalid(Funcs, NamedColorsBlock)
    alt block is invalid
        UboManager->>UboManager: lookup rebuild function
        UboManager->>Funcs: call lambda(Funcs)
        note right of Funcs: lambda body
        Funcs->>UboManager: update(Funcs, NamedColorsBlock, XNamedColor::getAllColorsAsVec4())
        UboManager->>Funcs: getOrCreateVbo(Funcs, NamedColorsBlock)
        UboManager->>Funcs: setVbo(GL_UNIFORM_BUFFER, vbo, data)
        UboManager->>Funcs: glBindBufferBase(GL_UNIFORM_BUFFER, getUboBindingIndex(block), vbo)
        UboManager->>UboManager: record bound buffer in m_boundBuffers
    else block already valid
        UboManager->>UboManager: reuse cached buffer id
    end

    UboManager-->>MapCanvas: UBO bound for NamedColorsBlock
    MapCanvas->>OpenGL: bindFbo(), clear(), draw...
Loading

Sequence diagram for rendering with FullScreenMesh using attribute-less VAO

sequenceDiagram
    participant Renderer as Caller
    participant Mesh as BlitMesh~FullScreenMesh~
    participant Funcs as Legacy_Functions
    participant Program as BlitShader
    participant VAOs as SharedVaos

    Renderer->>Mesh: virt_render(renderState)
    Mesh->>Program: bind()
    Program-->>Mesh: program bound

    Mesh->>Funcs: getProjectionMatrix()
    alt renderState.mvp has value
        Mesh->>Mesh: mvp = renderState.mvp
    else no MVP provided
        Mesh->>Mesh: mvp = projectionMatrix
    end

    Mesh->>Program: setUniforms(mvp, renderState.uniforms)
    Mesh->>Funcs: getTexLookup()
    Mesh->>Funcs: construct RenderStateBinder(Funcs, texLookup, renderState)

    Mesh->>Funcs: getSharedVaos()
    Funcs->>VAOs: get(EmptyVao)
    VAOs-->>Mesh: SharedVao (possibly empty)
    alt VAO not yet created
        Mesh->>Mesh: create VAO with SharedFunctions
    end

    Mesh->>Funcs: glBindVertexArray(vao)
    Mesh->>Funcs: glDrawArrays(m_mode, 0, m_numVerts)
    Mesh->>Funcs: glBindVertexArray(0)

    Mesh-->>Renderer: render complete
Loading

Class diagram for UboManager integration with OpenGL and Legacy::Functions

classDiagram
    direction LR

    class OpenGL {
        - shared_ptr~Legacy::Functions~ m_opengl
        - Legacy::UboManager m_uboManager
        - bool m_rendererInitialized
        + OpenGL()
        + ~OpenGL()
        + void initializeRenderer(float devicePixelRatio)
        + GLRenderState getDefaultRenderState()
        + void resetNamedColorsBuffer()
        + void setTextureLookup(MMTextureId id, SharedMMTexture tex)
        + Legacy::UboManager &getUboManager()
    }

    class Legacy_Functions {
        - unique_ptr~ShaderPrograms~ m_shaderPrograms
        - unique_ptr~StaticVbos~ m_staticVbos
        - unique_ptr~SharedVbos~ m_sharedVbos
        - unique_ptr~SharedVaos~ m_sharedVaos
        - unique_ptr~TexLookup~ m_texLookup
        - unique_ptr~FBO~ m_fbo
        - Legacy::UboManager &m_uboManager
        + Functions(Badge~Functions~, Legacy::UboManager &uboManager)
        + ~Functions()
        + static shared_ptr~Functions~ alloc(T, Legacy::UboManager &uboManager)
        + SharedVbos &getSharedVbos()
        + SharedVaos &getSharedVaos()
        + FBO &getFBO()
        + Legacy::UboManager &getUboManager()
        + void glBindBufferBase(GLenum target, SharedVboEnum block, GLuint buffer)
        + template~T,A~ GLsizei setVbo(GLenum target, GLuint buffer, vector~T,A~ &batch, BufferUsageEnum usage)
        + template~T~ void setVbo(GLenum target, GLuint buffer, T &data, BufferUsageEnum usage)
        + pair~DrawModeEnum,GLsizei~ setVbo(DrawModeEnum mode, GLuint vbo, vector~T,A~ &batch, BufferUsageEnum usage)
        + static const char *getUniformBlockName(SharedVboEnum block)
        + static void applyDefaultUniformBlockBindings(GLuint program)
    }

    class Legacy_FunctionsGL33 {
        + FunctionsGL33(Badge~Functions~, UboManager &uboManager)
        + ~FunctionsGL33()
    }

    class Legacy_FunctionsES30 {
        + FunctionsES30(Badge~Functions~, UboManager &uboManager)
        + ~FunctionsES30()
    }

    class Legacy_UboManager {
        + typedef RebuildFunction
        - EnumIndexedArray~RebuildFunction,SharedVboEnum~ m_rebuildFunctions
        - EnumIndexedArray~optional~GLuint~~ m_boundBuffers
        + UboManager()
        + void invalidate(SharedVboEnum block)
        + void invalidateAll()
        + void registerRebuildFunction(SharedVboEnum block, RebuildFunction func)
        + bool isInvalid(SharedVboEnum block) const
        + GLuint updateIfInvalid(Legacy::Functions &gl, SharedVboEnum block)
        + template~T,A~ GLuint update(Legacy::Functions &gl, SharedVboEnum block, vector~T,A~ &data)
        + template~T~ GLuint update(Legacy::Functions &gl, SharedVboEnum block, T &data)
        + void bind(Legacy::Functions &gl, SharedVboEnum block)
        - VBO &getOrCreateVbo(Legacy::Functions &gl, SharedVboEnum block)
        - GLuint bind_internal(Legacy::Functions &gl, SharedVboEnum block, GLuint buffer)
    }

    class Legacy_SharedVboEnum {
        <<enumeration>>
        NamedColorsBlock
        NUM_BLOCKS
    }

    class GLRenderState {
        + Uniforms uniforms
        + optional~glm::mat4~ mvp
        + GLRenderState withBlend(BlendModeEnum new_blend) const
        + GLRenderState withAlpha(float alpha) const
        + GLRenderState withTexture(MMTextureId new_texture) const
        + GLRenderState withMvp(optional~glm::mat4~ new_mvp) const
    }

    class MapCanvas {
        + void initializeGL()
        + void actuallyPaintGL()
    }

    OpenGL --> Legacy_UboManager : owns
    OpenGL --> Legacy_Functions : uses via m_opengl

    Legacy_FunctionsGL33 --|> Legacy_Functions
    Legacy_FunctionsES30 --|> Legacy_Functions

    Legacy_Functions --> Legacy_UboManager : holds reference
    Legacy_UboManager --> Legacy_Functions : uses in update/bind

    Legacy_UboManager --> Legacy_SharedVboEnum : indexes by
    Legacy_Functions --> Legacy_SharedVboEnum : glBindBufferBase, getUniformBlockName

    MapCanvas --> OpenGL : uses
    MapCanvas --> Legacy_UboManager : calls registerRebuildFunction, bind

    GLRenderState ..> Legacy_Functions : used in rendering
Loading

File-Level Changes

Change Details Files
Introduce UboManager and wire it through OpenGL and Legacy::Functions for centralized UBO lifecycle and binding management.
  • Add Legacy::UboManager class to track per-SharedVboEnum rebuild functions, lazily rebuild and bind UBOs, and cache bound buffer IDs with validation against contiguous enum indices.
  • Store a UboManager instance in OpenGL, pass it into Legacy::Functions instances via updated constructors, and expose it via accessors on both OpenGL and Functions.
  • Replace the previous NamedColors UBO setup with a MapCanvas-level registration of a rebuild function and use UboManager::bind/update/reset instead of manual buffer management.
src/opengl/UboManager.h
src/opengl/OpenGL.h
src/opengl/OpenGL.cpp
src/opengl/legacy/Legacy.cpp
src/opengl/legacy/Legacy.h
src/display/mapcanvas_gl.cpp
Tighten SharedVboEnum/SharedVaoEnum definitions to be contiguous, 0-based, and usable as UBO binding indices with runtime validation.
  • Simplify XFOREACH_SHARED_VBO to only uniform blocks, derive NUM_SHARED_VBOS and NUM_SHARED_VAOS via macro counting, and add static_asserts that enums are contiguous and match counts.
  • Introduce getUboBindingIndex helper that maps SharedVboEnum directly to GLuint binding indices and use it in glBindBufferBase wrapper.
  • At renderer initialization, query GL_MAX_UNIFORM_BUFFER_BINDINGS and assert that NUM_SHARED_VBOS does not exceed the runtime capability.
src/opengl/legacy/Legacy.h
src/opengl/OpenGL.cpp
Refactor Legacy::Functions buffer upload helpers to be type-safe and reusable for arbitrary trivially-copyable data, not just vectors.
  • Remove the old setVbo_internal/setIbo/setUbo helpers and replace them with templated setVbo overloads supporting both std::vector (with allocator template) and single objects.
  • Add a static enforceTriviallyCopyable helper to constrain buffer upload types at compile time.
  • Update call sites in UboManager and VBO upload paths to use the new setVbo signatures, including automatic quad-to-triangle conversion paths.
src/opengl/legacy/Legacy.h
src/opengl/UboManager.h
Introduce attribute-less fullscreen meshes that rely on gl_VertexID with a shared empty VAO, and generalize room quad rendering to use instanced draws without an index buffer.
  • Add FullScreenMesh template plus BlitMesh and PlainFullScreenMesh concrete types that render using glDrawArrays with an EmptyVao and MVP from render state or default projection.
  • Wire AttributeLessMeshes sources into the build system.
  • Simplify drawRoomQuad to use glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, numInstances) without creating or binding an index buffer VBO, relying on shader-side gl_VertexID.
src/opengl/legacy/AttributeLessMeshes.h
src/opengl/legacy/AttributeLessMeshes.cpp
src/opengl/legacy/SimpleMesh.cpp
src/CMakeLists.txt
Extend GLRenderState and shader interface to carry an optional MVP matrix and standardize uniform block layout for named colors.
  • Add an optional glm::mat4 mvp field to GLRenderState and a withMvp helper to construct modified states.
  • Update SimpleMesh rendering to use renderState.mvp when present, falling back to the Functions projection matrix otherwise.
  • Change the NamedColorsBlock uniform block in the room textured vertex shader to use layout(std140) for better UBO compatibility.
  • Ensure Functions exposes glDrawArraysInstanced and related wrappers used by new rendering paths.
src/opengl/OpenGLTypes.h
src/opengl/legacy/SimpleMesh.h
src/resources/shaders/legacy/room/tex/acolor/vert.glsl
src/opengl/legacy/Legacy.h
Adjust Legacy::Functions subclasses and shared-enum utilities to align with the new UBO and mesh infrastructure.
  • Update FunctionsGL33 and FunctionsES30 constructors to accept a UboManager reference and forward it to the base Functions constructor.
  • Expose UboManager from Functions via getUboManager, and add DEFINE_ENUM_COUNT for SharedVboEnum based on NUM_SHARED_VBOS.
  • Clean up uniform-block binding helpers to reflect that all SharedVboEnum entries are uniform blocks and handle the NUM_BLOCKS sentinel in getUniformBlockName/applyDefaultUniformBlockBindings.
src/opengl/legacy/FunctionsGL33.h
src/opengl/legacy/FunctionsGL33.cpp
src/opengl/legacy/FunctionsES30.h
src/opengl/legacy/FunctionsES30.cpp
src/opengl/legacy/Legacy.cpp
src/opengl/legacy/Legacy.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In UboManager::bind, if a block is invalid and no rebuild function is registered, a VBO is lazily created and bound without any data ever being uploaded; consider either asserting that a rebuild function exists for any block you try to bind or skipping the bind in that case to avoid using an uninitialized UBO.
  • Functions::getUboManager() dereferences a raw pointer set via setUboManager; it might be safer to assert or guard against m_uboManager == nullptr to catch accidental use before OpenGL::initializeRenderer wires it up.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::bind`, if a block is invalid and no rebuild function is registered, a VBO is lazily created and bound without any data ever being uploaded; consider either asserting that a rebuild function exists for any block you try to bind or skipping the bind in that case to avoid using an uninitialized UBO.
- `Functions::getUboManager()` dereferences a raw pointer set via `setUboManager`; it might be safer to assert or guard against `m_uboManager == nullptr` to catch accidental use before `OpenGL::initializeRenderer` wires it up.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/Legacy.h" line_range="137-140" />
<code_context>
     std::unique_ptr<SharedVaos> m_sharedVaos;
     std::unique_ptr<TexLookup> m_texLookup;
     std::unique_ptr<FBO> m_fbo;
+    UboManager *m_uboManager = nullptr;

 protected:
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against use of Functions::getUboManager() before the pointer is initialized.

`m_uboManager` is a raw pointer defaulted to `nullptr`, and `getUboManager()` always dereferences it. It’s only set via `setUboManager()` from `OpenGL::initializeRenderer()`, but nothing guarantees callers won’t use `Functions::getUboManager()` earlier. Consider either asserting `m_uboManager != nullptr` in `getUboManager()`, or requiring it via construction/factory wiring so the object can’t exist without a valid UBO manager. Otherwise, early access leads to UB instead of a clear failure.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 0% with 112 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.52%. Comparing base (26d1a9d) to head (e827fe4).
⚠️ Report is 215 commits behind head on master.

Files with missing lines Patch % Lines
src/opengl/UboManager.h 0.00% 49 Missing ⚠️
src/opengl/legacy/AttributeLessMeshes.h 0.00% 16 Missing ⚠️
src/opengl/legacy/Legacy.h 0.00% 15 Missing ⚠️
src/display/mapcanvas_gl.cpp 0.00% 8 Missing ⚠️
src/opengl/OpenGL.cpp 0.00% 8 Missing ⚠️
src/opengl/legacy/Legacy.cpp 0.00% 6 Missing ⚠️
src/opengl/legacy/AttributeLessMeshes.cpp 0.00% 2 Missing ⚠️
src/opengl/legacy/FunctionsES30.cpp 0.00% 2 Missing ⚠️
src/opengl/legacy/FunctionsGL33.cpp 0.00% 2 Missing ⚠️
src/opengl/legacy/SimpleMesh.cpp 0.00% 2 Missing ⚠️
... and 2 more
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #472       +/-   ##
===========================================
- Coverage   66.48%   25.52%   -40.96%     
===========================================
  Files          85      506      +421     
  Lines        4186    41525    +37339     
  Branches      255     4529     +4274     
===========================================
+ Hits         2783    10600     +7817     
- Misses       1403    30925    +29522     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Update UboManager::bind to skip binding if invalid and no rebuild function exists.
- Add assertions in UboManager::updateIfInvalid and Functions::getUboManager to catch uninitialized usage.
- Change Functions::setUboManager to accept a reference instead of a raw pointer.
- Update OpenGL::initializeRenderer to pass the UBO manager by reference.
@nschimme
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In UboManager::bind, a missing rebuild function only triggers an assert in debug builds and otherwise results in a silent no-op (invalid block stays invalid and nothing is bound); consider either enforcing registration at compile-time/initialization, or logging/failing loudly in release to avoid hard-to-track rendering issues.
  • The UboManager::update template currently special-cases only std::vector via utils::is_vector_v; if you anticipate passing other contiguous containers (e.g. std::array, std::span), it may be cleaner to add overloads or a more general trait rather than relying on this single-type detection.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::bind`, a missing rebuild function only triggers an `assert` in debug builds and otherwise results in a silent no-op (invalid block stays invalid and nothing is bound); consider either enforcing registration at compile-time/initialization, or logging/failing loudly in release to avoid hard-to-track rendering issues.
- The `UboManager::update` template currently special-cases only `std::vector` via `utils::is_vector_v`; if you anticipate passing other contiguous containers (e.g. `std::array`, `std::span`), it may be cleaner to add overloads or a more general trait rather than relying on this single-type detection.

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="104-66" />
<code_context>
+     * @brief Binds the UBO to its assigned point.
+     * If invalid and a rebuild function is registered, it will be updated first.
+     */
+    void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
+    {
+        updateIfInvalid(gl, block);
+
+        if (isInvalid(block)) {
+            return;
+        }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** bind() silently becomes a no-op when a block is invalid and no rebuild function is registered, which can hide configuration mistakes.

Right now, if the block is still invalid after `updateIfInvalid()`, `bind()` just returns and nothing is bound, so missing configuration (e.g. not calling `registerRebuildFunction`) fails silently. Consider surfacing this more explicitly—e.g., assert in debug builds when `isInvalid(block)` remains true after `updateIfInvalid()`, or log an error—to make misconfigured UBOs easier to catch.

Suggested implementation:

```c
    /**
     * @brief Binds the UBO to its assigned point.
     * If invalid and a rebuild function is registered, it will be updated first.
     * In debug builds, an assertion will fire if the block remains invalid after the update.
     */
    void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
    {
        updateIfInvalid(gl, block);

        if (isInvalid(block)) {
#ifndef NDEBUG
            // If we get here, either the block is misconfigured (e.g. no rebuild function
            // registered) or the rebuild function failed to produce a valid UBO.
            assert(false && "UboManager::bind: attempted to bind invalid UBO block after updateIfInvalid (missing or failing rebuild function?)");
#endif
            return;
        }

        bind_internal(gl, block);
    }

```

1. Ensure `<cassert>` is included in `src/opengl/UboManager.h` (or a common header) so that `assert` is available. For example:
   - Add `#include <cassert>` alongside the other standard-library includes at the top of this header.
2. If the actual `bind_internal` signature requires additional parameters (e.g., a VBO handle), adapt the `bind_internal(gl, block);` call to match the existing declaration/usage in the rest of the file.
3. If the project uses its own logging or assertion macros (e.g., `LOG_ERROR`, `DBG_ASSERT`), you may want to replace the raw `assert` with the project-standard mechanism to stay consistent with the rest of the codebase.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- UboManager::bind: Log error and assert in debug if block remains invalid after update.
- Generalized container support in UboManager::update using a new is_contiguous_container trait.
- Added Functions::setUboGeneric for uploading data from any contiguous container.
- Restored consistent (ubo, data) parameter order across all setUbo methods.
- Added missing <cassert> include in Legacy.h.
- Re-verified all changes with a successful build and test run.
@nschimme
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In UboManager::update / Functions::setUboGeneric, consider adding a static_assert(utils::is_contiguous_container_v<T>) (or similar constraint) on the generic path so misuse with non-contiguous containers is caught at compile time rather than relying solely on the if constexpr in update.
  • The FullScreenMesh::virt_render unconditionally uses an identity MVP instead of the renderState's matrix; if some full-screen shaders expect the caller-provided MVP, it may be safer to either use renderState.mvp by default or provide a hook/override to customize the matrix without reimplementing the whole render path.
  • The new utils::is_vector trait is currently unused; if it’s not part of an imminent follow-up change, consider removing it to avoid dead code and keep the utility header focused.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::update` / `Functions::setUboGeneric`, consider adding a `static_assert(utils::is_contiguous_container_v<T>)` (or similar constraint) on the generic path so misuse with non-contiguous containers is caught at compile time rather than relying solely on the `if constexpr` in `update`.
- The `FullScreenMesh::virt_render` unconditionally uses an identity MVP instead of the `renderState`'s matrix; if some full-screen shaders expect the caller-provided MVP, it may be safer to either use `renderState.mvp` by default or provide a hook/override to customize the matrix without reimplementing the whole render path.
- The new `utils::is_vector` trait is currently unused; if it’s not part of an imminent follow-up change, consider removing it to avoid dead code and keep the utility header focused.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/Legacy.h" line_range="414-423" />
<code_context>
 template<typename... Args>
 static inline constexpr bool are_distinct_v = are_distinct<Args...>::value;

+template<typename T>
+struct is_vector : std::false_type
+{};
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Constrain `setUboGeneric` to contiguous containers to avoid unsafe usage.

`setUboGeneric` currently relies on `T` having `data()`/`size()` and being stored contiguously, which isn’t enforced. To catch misuse at compile time, add a `static_assert(utils::is_contiguous_container_v<T>, ...)` (and optionally `std::is_trivially_copyable_v<ValueType>`) in the template so non‑uploadable containers with `data`/`size` are rejected during compilation.
</issue_to_address>

### Comment 2
<location path="src/global/utils.h" line_range="467-476" />
<code_context>
 template<typename... Args>
 static inline constexpr bool are_distinct_v = are_distinct<Args...>::value;

+template<typename T>
+struct is_vector : std::false_type
+{};
+
+template<typename T, typename A>
+struct is_vector<std::vector<T, A>> : std::true_type
+{};
+
+template<typename T>
+static inline constexpr bool is_vector_v = is_vector<T>::value;
+
+template<typename T, typename = void>
</code_context>
<issue_to_address>
**nitpick:** Consider removing or using the new `is_vector` trait to avoid dead code.

This trait is defined but unused in these changes, unlike `is_contiguous_container`. If you don’t have an imminent use for it, consider removing it for now to keep the header minimal, and reintroduce it when you actually need it.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- src/global/utils.h: Remove unused is_vector trait.
- src/opengl/legacy/Legacy.h: Add static_assert constraints to setUboGeneric to ensure contiguous and trivially copyable data.
- src/opengl/OpenGLTypes.h: Add mvp matrix to GLRenderState with withMvp helper.
- src/opengl/legacy/AttributeLessMeshes.h: Use renderState.mvp in FullScreenMesh::virt_render.
- src/opengl/legacy/SimpleMesh.h: Respect renderState.mvp in SimpleMesh::virt_render, falling back to global projection matrix if mvp is identity.
@nschimme
Copy link
Copy Markdown
Contributor Author

nschimme commented Mar 16, 2026

@sourcery-ai review

Copy link
Copy Markdown

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Using glm::mat4(1.f) as a sentinel in GLRenderState::mvp (and equality checks in SimpleMesh) is fragile; consider an explicit flag/optional to indicate whether an override MVP is set instead of relying on a specific matrix value.
  • The generic utils::is_contiguous_container trait will treat any type exposing data()/size() as a contiguous container; if you only intend to support a narrow set (e.g. std::vector, std::array, std::span), consider constraining it or adding documentation to avoid accidental use with unexpected types.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `glm::mat4(1.f)` as a sentinel in `GLRenderState::mvp` (and equality checks in `SimpleMesh`) is fragile; consider an explicit flag/optional to indicate whether an override MVP is set instead of relying on a specific matrix value.
- The generic `utils::is_contiguous_container` trait will treat any type exposing `data()`/`size()` as a contiguous container; if you only intend to support a narrow set (e.g. `std::vector`, `std::array`, `std::span`), consider constraining it or adding documentation to avoid accidental use with unexpected types.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/SimpleMesh.h" line_range="177-179" />
<code_context>
         m_functions.checkError();

-        const glm::mat4 mvp = m_functions.getProjectionMatrix();
+        const glm::mat4 mvp = renderState.mvp == glm::mat4(1.f)
+                                  ? m_functions.getProjectionMatrix()
+                                  : renderState.mvp;
         auto programUnbinder = m_program.bind();
         m_program.setUniforms(mvp, renderState.uniforms);
</code_context>
<issue_to_address>
**issue (bug_risk):** Using an identity matrix as a sentinel for `renderState.mvp` can break explicit identity use-cases.

Because `glm::mat4(1.f)` is treated as "no MVP provided", callers cannot intentionally pass an identity MVP—`withMvp(glm::mat4(1.f))` will still use the projection matrix. This also couples default initialization to behavior. Prefer an explicit indicator (e.g., a flag or `std::optional<glm::mat4>` in `GLRenderState`) to distinguish "no custom MVP" from a legitimate identity matrix.
</issue_to_address>

### Comment 2
<location path="src/opengl/legacy/Legacy.h" line_range="414-423" />
<code_context>
+struct is_contiguous_container : std::false_type
+{};
+
+template<typename T>
+struct is_contiguous_container<
+    T,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `setUboSingle` should enforce that `T` is trivially copyable, mirroring `setUboGeneric`.

`setUboGeneric` uses a `static_assert` to ensure its element type is trivially copyable before calling `glBufferData`, but `setUboSingle` does not. Since `setUboSingle` also uploads from `&data`, it should add the same `static_assert(std::is_trivially_copyable_v<T>, ...)` to prevent undefined behavior with non-trivially-copyable types and keep both helpers’ guarantees consistent.
</issue_to_address>

Hi @nschimme! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- OpenGLTypes.h: Change GLRenderState::mvp to std::optional<glm::mat4> to distinguish between no override and explicit identity matrix.
- AttributeLessMeshes.h / SimpleMesh.h: Update virt_render to use value_or() for MVP selection.
- Legacy.h: Add static_assert(std::is_trivially_copyable_v<T>) to setUboSingle for consistent type safety.
- utils.h: Remove unused is_vector trait.
@nschimme
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The FunctionsUboManager coupling currently relies on a raw pointer and an external setUboManager call; consider passing a reference into Functions at construction or wrapping the pointer in something that guarantees initialization so getUboManager() can’t be called before it’s valid.
  • FullScreenMesh::virt_render defaults the MVP to identity while other paths (e.g. SimpleMesh) default to getProjectionMatrix(), which may lead to subtle inconsistencies in how full-screen passes are transformed; it could be worth unifying the default behavior or documenting the intentional difference where these meshes are constructed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Functions``UboManager` coupling currently relies on a raw pointer and an external `setUboManager` call; consider passing a reference into `Functions` at construction or wrapping the pointer in something that guarantees initialization so `getUboManager()` can’t be called before it’s valid.
- `FullScreenMesh::virt_render` defaults the MVP to identity while other paths (e.g. `SimpleMesh`) default to `getProjectionMatrix()`, which may lead to subtle inconsistencies in how full-screen passes are transformed; it could be worth unifying the default behavior or documenting the intentional difference where these meshes are constructed.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…avior

- src/opengl/legacy/Legacy.h/.cpp: Refactor Functions to take UboManager & at construction. Remove setUboManager.
- src/opengl/legacy/FunctionsGL33.h/.cpp, FunctionsES30.h/.cpp: Pass UboManager & to base class.
- src/opengl/OpenGL.cpp: Pass m_uboManager during Functions allocation.
- src/opengl/legacy/AttributeLessMeshes.h: Unified FullScreenMesh::virt_render to default to getProjectionMatrix() when no custom MVP is provided, matching SimpleMesh.
- src/opengl/legacy/SimpleMesh.h: Replaced sentinel-based MVP selection with std::optional's value_or().
- src/opengl/OpenGLTypes.h: Replaced mvp sentinel with std::optional<glm::mat4>.
- src/opengl/legacy/Legacy.h: Added static_assert for trivially copyable types in setUboSingle.
- src/global/utils.h: Removed unused is_vector trait.
@nschimme
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The new utils::is_contiguous_container trait will treat types like std::string as valid contiguous containers; if that’s not intended for UBO uploads, consider tightening the constraint (e.g., excluding char-like element types or requiring a user-facing opt‑in) to avoid accidental misuse.
  • In UboManager::update() and bind(), the logic to ensure the shared VBO exists and is emplaced is duplicated; factoring this into a small helper (e.g., getOrCreateSharedUboVbo(Functions&, SharedVboEnum)) would reduce repetition and keep future UBO-related changes localized.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new utils::is_contiguous_container trait will treat types like std::string as valid contiguous containers; if that’s not intended for UBO uploads, consider tightening the constraint (e.g., excluding char-like element types or requiring a user-facing opt‑in) to avoid accidental misuse.
- In UboManager::update() and bind(), the logic to ensure the shared VBO exists and is emplaced is duplicated; factoring this into a small helper (e.g., getOrCreateSharedUboVbo(Functions&, SharedVboEnum)) would reduce repetition and keep future UBO-related changes localized.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…ement

- Refactor Legacy::Functions to take UboManager & at construction, eliminating raw pointer risk.
- Unified FullScreenMesh and SimpleMesh MVP defaults to use the projection matrix.
- Replaced fragile identity sentinel for MVP overrides with std::optional<glm::mat4>.
- Hardened all UBO and IBO upload paths with trivially copyable static assertions.
- Added getOrCreateVbo helper to UboManager to deduplicate logic.
- Improved error visibility in UboManager::bind with logging and debug assertions.
- Added is_vector and is_vector_v traits to utils.h.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In UboManager::update, the setUboSingle call uses the arguments in the wrong order (gl.setUboSingle(data, vbo.get(), ...)), which does not match the setUboSingle(const GLuint ubo, const T &data, ...) signature and will either fail to compile or silently misbehave; swap the arguments so the buffer ID comes first.
  • The UboManager::update template falls back to setUboSingle for any type that is not exactly std::vector<T, A>; consider constraining this (e.g., via static_assert or additional traits) so that unsupported container types are rejected at compile time rather than being treated as a single POD block.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::update`, the `setUboSingle` call uses the arguments in the wrong order (`gl.setUboSingle(data, vbo.get(), ...)`), which does not match the `setUboSingle(const GLuint ubo, const T &data, ...)` signature and will either fail to compile or silently misbehave; swap the arguments so the buffer ID comes first.
- The `UboManager::update` template falls back to `setUboSingle` for any type that is not exactly `std::vector<T, A>`; consider constraining this (e.g., via `static_assert` or additional traits) so that unsupported container types are rejected at compile time rather than being treated as a single POD block.

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="87" />
<code_context>
+    {
+        Legacy::VBO &vbo = getOrCreateVbo(gl, block);
+
+        if constexpr (utils::is_vector_v<T>) {
+            static_cast<void>(gl.setUbo(vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW));
+        } else {
</code_context>
<issue_to_address>
**issue (bug_risk):** The `utils::is_vector_v` trait reference likely won’t compile due to namespace mismatch.

`is_vector`/`is_vector_v` is declared in the global namespace in `global/utils.h`, so `utils::is_vector_v<T>` will not resolve unless there is an actual `namespace utils` wrapper elsewhere. If that namespace doesn’t exist, use `::is_vector_v<T>` (or move the trait into a real `utils` namespace) so this compiles.
</issue_to_address>

### Comment 2
<location path="src/opengl/UboManager.h" line_range="89-90" />
<code_context>
+
+        if constexpr (utils::is_vector_v<T>) {
+            static_cast<void>(gl.setUbo(vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW));
+        } else {
+            gl.setUboSingle(data, vbo.get(), BufferUsageEnum::DYNAMIC_DRAW);
+        }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The `setUboSingle` call uses parameters in the opposite order of the declared function signature.

In `Legacy::Functions`, the template is `setUboSingle(const GLuint ubo, const T &data, ...)`, so this call will either not compile or bind the wrong buffer. Update it to `gl.setUboSingle(vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW);` to match the signature and the semantics of `setUbo`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Refactor Legacy::Functions to require UboManager& at construction.
- Implement safety checks and lazy rebuild logic in UboManager.
- Add type safety assertions for UBO/IBO uploads.
- Unify MVP fallback behavior using std::optional in GLRenderState.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In UboManager::update, consider adding a static_assert to enforce that non-std::vector data types passed to the templated update are trivially copyable (mirroring setUbo/setUboSingle) to prevent accidental use with non-POD types.
  • When registering rebuild functions in UboManager::registerRebuildFunction, you might want to assert or log if a rebuild function is already present for a given SharedVboEnum to catch accidental double registration or overwriting of handlers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::update`, consider adding a `static_assert` to enforce that non-`std::vector` data types passed to the templated `update` are trivially copyable (mirroring `setUbo`/`setUboSingle`) to prevent accidental use with non-POD types.
- When registering rebuild functions in `UboManager::registerRebuildFunction`, you might want to assert or log if a rebuild function is already present for a given `SharedVboEnum` to catch accidental double registration or overwriting of handlers.

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="130-134" />
<code_context>
+    }
+
+private:
+    void bind_internal(Legacy::Functions &gl, Legacy::SharedVboEnum block, GLuint buffer)
+    {
+        auto &bound = m_boundBuffers[block];
+        if (!bound.has_value() || bound.value() != buffer) {
+            gl.glBindBufferBase(GL_UNIFORM_BUFFER, static_cast<GLuint>(block), buffer);
+            bound = buffer;
+        }
</code_context>
<issue_to_address>
**suggestion:** Relying on enum values as UBO binding indices could benefit from an explicit check or assertion.

`static_cast<GLuint>(block)` as the binding index assumes `SharedVboEnum` is 0-based, contiguous, and within `GL_MAX_UNIFORM_BUFFER_BINDINGS`. That can be broken by future enum changes. Please add a `static_assert` or runtime assert that ties `SharedVboEnum`/`NUM_SHARED_VBOS` to the available binding range (at least checking the casted value is `< NUM_SHARED_VBOS`).

Suggested implementation:

```c
    void bind_internal(Legacy::Functions &gl, Legacy::SharedVboEnum block, GLuint buffer)
    {
        const auto bindingIndex = static_cast<std::size_t>(block);
        assert(bindingIndex < m_boundBuffers.size());

        auto &bound = m_boundBuffers[block];
        if (!bound.has_value() || bound.value() != buffer) {
            gl.glBindBufferBase(GL_UNIFORM_BUFFER, static_cast<GLuint>(bindingIndex), buffer);
            bound = buffer;
        }

```

- Ensure this header (or a common precompiled header it uses) includes `<cassert>` and `<cstddef>` if they are not already included.
- If `m_boundBuffers` is not sized to the number of valid `SharedVboEnum` values, adjust its construction so that `m_boundBuffers.size()` equals the number of valid shared VBO bindings, preserving the invariant enforced by the `assert`.
- If you want a stronger tie to the GL limits, in the constructor or initialization logic of `UboManager`, query `GL_MAX_UNIFORM_BUFFER_BINDINGS` via `glGetIntegerv` and add a runtime check that `m_boundBuffers.size() <= maxBindings`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Add static_assert for POD types in UboManager::update.
- Prevent duplicate rebuild function registration.
- Validate UBO binding indices against OpenGL limits.
- Refactor bind_internal for explicit index handling.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In GLRenderState, consider changing withMvp(const std::optional<glm::mat4>&) to take the optional by value (withMvp(std::optional<glm::mat4> mvp)) to avoid surprising reference semantics and make passing temporaries more ergonomic.
  • In UboManager::bind_internal, the static bool limitsChecked pattern is not thread-safe; if there is any chance of UBO binding from multiple threads/contexts, consider guarding this with std::once_flag or doing the limit check once at initialization.
  • The UboManager::update template currently special-cases only std::vector via utils::is_vector_v; if you expect to upload data from other contiguous containers (e.g. std::array or custom spans), consider adding overloads or a more general trait (like accepting any range of trivially copyable elements) to avoid forcing callers into std::vector.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `GLRenderState`, consider changing `withMvp(const std::optional<glm::mat4>&)` to take the optional by value (`withMvp(std::optional<glm::mat4> mvp)`) to avoid surprising reference semantics and make passing temporaries more ergonomic.
- In `UboManager::bind_internal`, the `static bool limitsChecked` pattern is not thread-safe; if there is any chance of UBO binding from multiple threads/contexts, consider guarding this with `std::once_flag` or doing the limit check once at initialization.
- The `UboManager::update` template currently special-cases only `std::vector` via `utils::is_vector_v`; if you expect to upload data from other contiguous containers (e.g. `std::array` or custom spans), consider adding overloads or a more general trait (like accepting any range of trivially copyable elements) to avoid forcing callers into `std::vector`.

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="89" />
<code_context>
+    {
+        Legacy::VBO &vbo = getOrCreateVbo(gl, block);
+
+        if constexpr (utils::is_vector_v<T>) {
+            static_cast<void>(gl.setUbo(vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW));
+        } else {
</code_context>
<issue_to_address>
**issue (bug_risk):** Namespace mismatch for is_vector_v causes a compile error

`is_vector` / `is_vector_v` are declared in the global namespace in `utils.h`, but this code uses `utils::is_vector_v<T>`, which will fail to compile. Please either move the trait into the `utils` namespace, or update this usage to `is_vector_v<T>` (possibly via a `using` alias).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Refactor Legacy::Functions to require UboManager& at construction.
- Implement lazy rebuild and safety checks in UboManager.
- Improve thread-safety of UboManager limits check using std::call_once.
- enforce type safety (POD) for GPU buffer uploads.
- Unify MVP transform logic in GLRenderState using std::optional.
@nschimme
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In UboManager::bind_internal you implicitly rely on the underlying SharedVboEnum values as UBO binding indices; consider making that contract explicit (e.g., a comment and/or static_assert on the enum range/order) so future enum changes don’t silently break the binding layout.
  • The utils::is_vector_v trait is only used to distinguish std::vector uploads in UboManager::update; you could simplify this by adding a dedicated overload of update for std::vector instead of a custom type trait, which would keep the generic path cleaner and easier to reason about.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In UboManager::bind_internal you implicitly rely on the underlying SharedVboEnum values as UBO binding indices; consider making that contract explicit (e.g., a comment and/or static_assert on the enum range/order) so future enum changes don’t silently break the binding layout.
- The utils::is_vector_v trait is only used to distinguish std::vector uploads in UboManager::update; you could simplify this by adding a dedicated overload of update for std::vector<T> instead of a custom type trait, which would keep the generic path cleaner and easier to reason about.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Refactor Legacy::Functions to require UboManager& at construction.
- Implement lazy rebuild and safety checks in UboManager.
- Improve thread-safety of UboManager limits check using std::call_once.
- Enforce type safety (POD) for GPU buffer uploads.
- Unify MVP transform logic in GLRenderState using std::optional.
- Replace generic update trait with specialized overloads for bulk/single data.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The hardcoded static_assert(static_cast<int>(Legacy::SharedVboEnum::NamedColorsBlock) == 0) and the assumption that enum values map directly to binding indices in UboManager::bind_internal is quite brittle; consider centralizing the binding index mapping (e.g., via a small helper or metadata table) so new enum entries or reordering cannot silently break UBO bindings.
  • In MapCanvas::initializeGL, the rebuild lambda for NamedColorsBlock captures gl by reference and is stored in UboManager; it may be safer to capture by value (or store only lightweight data needed to reacquire Functions) to avoid subtle lifetime issues if the OpenGL instance is ever replaced or outlives the canvas.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The hardcoded `static_assert(static_cast<int>(Legacy::SharedVboEnum::NamedColorsBlock) == 0)` and the assumption that enum values map directly to binding indices in `UboManager::bind_internal` is quite brittle; consider centralizing the binding index mapping (e.g., via a small helper or metadata table) so new enum entries or reordering cannot silently break UBO bindings.
- In `MapCanvas::initializeGL`, the rebuild lambda for `NamedColorsBlock` captures `gl` by reference and is stored in `UboManager`; it may be safer to capture by value (or store only lightweight data needed to reacquire `Functions`) to avoid subtle lifetime issues if the `OpenGL` instance is ever replaced or outlives the canvas.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/Legacy.h" line_range="140" />
<code_context>

 protected:
-    explicit Functions(Badge<Functions>);
+    explicit Functions(Badge<Functions>, UboManager &uboManager);

 public:
</code_context>
<issue_to_address>
**issue (bug_risk):** Functions now holds a UboManager reference; ensure lifetime guarantees to avoid dangling references.

Because `Functions` now stores `UboManager&`, `UboManager` must strictly outlive every `Functions` instance, including any `SharedFunctions` kept by meshes or other subsystems. Today `OpenGL` owns both, but callers can retain `std::shared_ptr<Functions>` after `OpenGL` is destroyed.

Either ensure `UboManager`’s ownership/lifetime is above all `Functions` (e.g., shared/central owner), or enforce that `Functions` instances cannot outlive `OpenGL` (e.g., by constraining where `SharedFunctions` can be stored). Otherwise, `getUboManager()` on a `Functions` that outlives `OpenGL` will be undefined behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


protected:
explicit Functions(Badge<Functions>);
explicit Functions(Badge<Functions>, UboManager &uboManager);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Functions now holds a UboManager reference; ensure lifetime guarantees to avoid dangling references.

Because Functions now stores UboManager&, UboManager must strictly outlive every Functions instance, including any SharedFunctions kept by meshes or other subsystems. Today OpenGL owns both, but callers can retain std::shared_ptr<Functions> after OpenGL is destroyed.

Either ensure UboManager’s ownership/lifetime is above all Functions (e.g., shared/central owner), or enforce that Functions instances cannot outlive OpenGL (e.g., by constraining where SharedFunctions can be stored). Otherwise, getUboManager() on a Functions that outlives OpenGL will be undefined behavior.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo methods in Legacy::Functions.
- Update UboManager to use the new setVbo methods.
- Enforce type safety (POD) for all buffer uploads.
- Remove redundant is_vector traits from utils.h.
- Fix thread-safety and validation in UboManager::bind_internal.
- Transition Legacy::Functions to use UboManager by reference.
- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Use dedicated overloads in UboManager::update instead of traits.
- Harden bind_internal with explicit contract documentation and static_assert.
- Improve thread-safety of limit checks using std::call_once.
- Enforce POD requirement for all buffer uploads.
- Require UboManager& in Functions constructor.
- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Refactor UboManager::update to use overloads instead of type traits.
- Harden UboManager::bind_internal with explicit contract documentation and static_assert.
- Implement thread-safe limit validation in UboManager using std::call_once.
- Transition Legacy::Functions to require UboManager& at construction.
- Enforce POD type safety for all GPU buffer uploads.
- Unify rendering state transformations using std::optional in GLRenderState.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • UboManager::bind_internal hard-codes the assumption that Legacy::SharedVboEnum is 0-based and contiguous (with a static_assert on NamedColorsBlock == 0); consider centralizing the enum→binding-index mapping or enforcing this invariant at the enum definition to avoid subtle breakage when adding/reordering SharedVboEnum values.
  • There is some duplication around trivially-copyable checks for buffer uploads (Functions::setVbo and UboManager::update(T)), which could be consolidated so that the invariant is enforced in a single place and error messages remain consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- UboManager::bind_internal hard-codes the assumption that Legacy::SharedVboEnum is 0-based and contiguous (with a static_assert on NamedColorsBlock == 0); consider centralizing the enum→binding-index mapping or enforcing this invariant at the enum definition to avoid subtle breakage when adding/reordering SharedVboEnum values.
- There is some duplication around trivially-copyable checks for buffer uploads (Functions::setVbo and UboManager::update(T)), which could be consolidated so that the invariant is enforced in a single place and error messages remain consistent.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/AttributeLessMeshes.h" line_range="59" />
<code_context>
+
+        RenderStateBinder rsBinder(m_functions, m_functions.getTexLookup(), renderState);
+
+        SharedVao shared = m_functions.getSharedVaos().get(SharedVaoEnum::EmptyVao);
+        VAO &vao = deref(shared);
+        if (!vao) {
</code_context>
<issue_to_address>
**issue (bug_risk):** SharedVaoEnum::EmptyVao is used but not defined by the current XFOREACH_SHARED_VAO macro.

With `XFOREACH_SHARED_VAO` currently empty, `SharedVaoEnum` has no enumerators, so `SharedVaoEnum::EmptyVao` cannot compile unless the macro is updated elsewhere to define it. If `EmptyVao` is meant to be a shared VAO slot, add it through `XFOREACH_SHARED_VAO` (and include it in `NUM_SHARED_VAOS`) to keep the enum and shared VAO storage in sync.
</issue_to_address>

### Comment 2
<location path="src/display/mapcanvas_gl.cpp" line_range="240-245" />
<code_context>

     gl.initializeRenderer(static_cast<float>(QPaintDevice::devicePixelRatioF()));
+
+    gl.getUboManager().registerRebuildFunction(
+        Legacy::SharedVboEnum::NamedColorsBlock, [&gl](Legacy::Functions &funcs) {
+            gl.getUboManager().update(funcs,
+                                      Legacy::SharedVboEnum::NamedColorsBlock,
+                                      XNamedColor::getAllColorsAsVec4());
+        });
+
</code_context>
<issue_to_address>
**suggestion:** The UBO rebuild lambda ignores its Functions parameter and instead captures OpenGL by reference.

The lambda takes `Legacy::Functions &funcs` but ignores it and instead captures `gl` to call `gl.getUboManager()`. This adds unnecessary coupling to a specific `OpenGL` instance. Prefer using the `funcs` parameter directly, e.g. `[](Legacy::Functions &funcs) { auto &manager = funcs.getUboManager(); manager.update(funcs, Legacy::SharedVboEnum::NamedColorsBlock, XNamedColor::getAllColorsAsVec4()); }`, so the callback remains self-contained and its dependencies are explicit.

```suggestion
    gl.getUboManager().registerRebuildFunction(
        Legacy::SharedVboEnum::NamedColorsBlock, [](Legacy::Functions &funcs) {
            auto &uboManager = funcs.getUboManager();
            uboManager.update(funcs,
                              Legacy::SharedVboEnum::NamedColorsBlock,
                              XNamedColor::getAllColorsAsVec4());
        });
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Refactor UboManager::update to use overloads instead of type traits.
- Harden UboManager::bind_internal with explicit contract documentation and static_assert.
- Implement thread-safe limit validation in UboManager using std::call_once.
- Transition Legacy::Functions to require UboManager& at construction.
- Enforce POD type safety for all GPU buffer uploads.
- Unify rendering state transformations using std::optional in GLRenderState.
- Fix EmptyVao definition and lambda capturing in MapCanvas.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The tight coupling between Legacy::SharedVboEnum and UBO binding indices is enforced in multiple places (static_asserts in Legacy::Legacy.h and UboManager::bind_internal); consider centralizing this constraint (e.g., a single helper or trait) so that future enum changes only need to be validated in one place.
  • UboManager::registerRebuildFunction currently hard-asserts if called twice for the same block; if there are scenarios where the renderer may need to reconfigure or reinitialize UBO sources (e.g., after device/context loss), consider either allowing re-registration or providing an explicit unregister/reset API instead of crashing in debug builds.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The tight coupling between Legacy::SharedVboEnum and UBO binding indices is enforced in multiple places (static_asserts in Legacy::Legacy.h and UboManager::bind_internal); consider centralizing this constraint (e.g., a single helper or trait) so that future enum changes only need to be validated in one place.
- UboManager::registerRebuildFunction currently hard-asserts if called twice for the same block; if there are scenarios where the renderer may need to reconfigure or reinitialize UBO sources (e.g., after device/context loss), consider either allowing re-registration or providing an explicit unregister/reset API instead of crashing in debug builds.

## Individual Comments

### Comment 1
<location path="src/opengl/legacy/AttributeLessMeshes.h" line_range="45-47" />
<code_context>
+protected:
+    void virt_clear() final {}
+    void virt_reset() final {}
+    NODISCARD bool virt_isEmpty() const final { return false; }
+
+    void virt_render(const GLRenderState &renderState) override
</code_context>
<issue_to_address>
**suggestion (bug_risk):** virt_isEmpty() always returning false might hide configuration errors where m_numVerts is zero.

Since rendering uses m_numVerts as the draw count, a FullScreenMesh constructed with numVerts == 0 would still go through the render path but only issue a no-op draw, masking the misconfiguration. Consider either making virt_isEmpty() return m_numVerts == 0 or adding a constructor assert that m_numVerts > 0 to enforce the invariant explicitly.

```suggestion
    void virt_clear() final {}
    void virt_reset() final {}
    NODISCARD bool virt_isEmpty() const final { return m_numVerts == 0; }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Centralize UBO binding index mapping and SharedVboEnum contract in Legacy.h.
- Allow UboManager rebuild function re-registration with a warning.
- Implement thread-safe limit validation in UboManager using std::call_once.
- Transition Legacy::Functions to require UboManager& at construction.
- Enforce POD type safety for all GPU buffer uploads via a unified private helper.
- Unify rendering state transformations using std::optional in GLRenderState.
- Fix EmptyVao definition and lambda capturing in MapCanvas.
- Improve FullScreenMesh::virt_isEmpty() logic.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In UboManager::bind_internal you compute a bindingIndex via getUboBindingIndex but then call gl.glBindBufferBase(GL_UNIFORM_BUFFER, block, buffer) rather than using bindingIndex; consider passing the computed index directly for consistency with the earlier assertion and to avoid relying on the SharedVboEnum overload at this low level.
  • UboManager.h includes <mutex> but only uses std::once_flag/std::call_once in a #ifndef NDEBUG block; if you don’t intend to make the manager generally thread-safe, you can either remove the unused synchronization member/commented intent or document that only the debug binding-limit check is guarded and the rest of the class is not thread-safe.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::bind_internal` you compute a `bindingIndex` via `getUboBindingIndex` but then call `gl.glBindBufferBase(GL_UNIFORM_BUFFER, block, buffer)` rather than using `bindingIndex`; consider passing the computed index directly for consistency with the earlier assertion and to avoid relying on the `SharedVboEnum` overload at this low level.
- `UboManager.h` includes `<mutex>` but only uses `std::once_flag`/`std::call_once` in a `#ifndef NDEBUG` block; if you don’t intend to make the manager generally thread-safe, you can either remove the unused synchronization member/commented intent or document that only the debug binding-limit check is guarded and the rest of the class is not thread-safe.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Centralize UBO binding index mapping and SharedVboEnum contract in Legacy.h.
- Improve thread-safety of limit checks using std::call_once.
- Enforce POD type safety for GPU buffer uploads via a unified private helper.
- Unify rendering state transformations using std::optional in GLRenderState.
- Improve FullScreenMesh safety and virt_isEmpty implementation.
- Clean up utils.h includes and improve UboManager documentation.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In UboManager::bind_internal and Legacy::getUboBindingIndex, the assumption that SharedVboEnum is 0-based and contiguous is duplicated; consider centralizing this invariant and performing the GL_MAX_UNIFORM_BUFFER_BINDINGS sanity check once during GL initialization rather than lazily in a debug call to make the constraints clearer and avoid scattered assumptions.
  • The UboManager::bind path always calls getOrCreateVbo after updateIfInvalid, even though update() already does this and binds; you could avoid the redundant lookup by having updateIfInvalid return the buffer (or cache it) so bind() can skip re-fetching and re-binding when nothing changed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UboManager::bind_internal` and `Legacy::getUboBindingIndex`, the assumption that `SharedVboEnum` is 0-based and contiguous is duplicated; consider centralizing this invariant and performing the `GL_MAX_UNIFORM_BUFFER_BINDINGS` sanity check once during GL initialization rather than lazily in a debug call to make the constraints clearer and avoid scattered assumptions.
- The `UboManager::bind` path always calls `getOrCreateVbo` after `updateIfInvalid`, even though `update()` already does this and binds; you could avoid the redundant lookup by having `updateIfInvalid` return the buffer (or cache it) so `bind()` can skip re-fetching and re-binding when nothing changed.

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="76-53" />
<code_context>
+    /**
+     * @brief Rebuilds the UBO if it's invalid using the registered rebuild function.
+     */
+    void updateIfInvalid(Legacy::Functions &gl, Legacy::SharedVboEnum block)
+    {
+        if (isInvalid(block)) {
+            const auto &func = m_rebuildFunctions[block];
+            assert(func && "UBO block is invalid and no rebuild function is registered");
+            if (func) {
+                func(gl);
+                // The rebuild function is expected to call update() which marks it valid.
+                assert(!isInvalid(block));
+            }
+        }
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** updateIfInvalid relies on the rebuild function calling update(), which is easy to forget and only enforced by an assert.

Because `updateIfInvalid` assumes the rebuild callback will call an `update()` overload, a missed call leaves the block invalid: `bind()` will then only log and assert (which is compiled out in release builds), resulting in a silently unbound UBO.

To make this safer, you could either:
- Have the rebuild callback return the data/descriptor and let `UboManager` call `update()` itself, or
- Track whether `update()` was invoked during `updateIfInvalid` (e.g., guard/flag) and emit a clear error if not.

This would reduce the chance that new rebuild functions accidentally skip `update()` and cause subtle rendering issues.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Centralize UBO binding index mapping and SharedVboEnum contract in Legacy.h.
- Move UBO limit validation to OpenGL::initializeRenderer.
- Optimize UboManager by returning buffer IDs from update/updateIfInvalid.
- Implement safety checks to ensure rebuild functions call update().
- Enforce POD type safety for GPU buffer uploads via a unified private helper.
- Unify rendering state transformations using std::optional in GLRenderState.
- Improve FullScreenMesh safety and virt_isEmpty implementation.
- Clean up utils.h includes and document UboManager thread-safety.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Since SharedVboEnum values are used as UBO binding indices, consider adding a stronger compile‑time check (e.g., asserting that the last enum value + 1 == NUM_SHARED_VBOS) instead of only asserting that NamedColorsBlock == 0, so future additions can’t accidentally break the 0‑based contiguous invariant.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since SharedVboEnum values are used as UBO binding indices, consider adding a stronger compile‑time check (e.g., asserting that the last enum value + 1 == NUM_SHARED_VBOS) instead of only asserting that NamedColorsBlock == 0, so future additions can’t accidentally break the 0‑based contiguous invariant.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Consolidate setUbo, setIbo, and setUboSingle into overloaded setVbo in Legacy::Functions.
- Centralize UBO binding index mapping and SharedVboEnum/SharedVaoEnum contract in Legacy.h.
- Move UBO limit validation to OpenGL::initializeRenderer.
- Optimize UboManager by returning buffer IDs from update/updateIfInvalid.
- Enforce POD type safety for GPU buffer uploads via a unified private helper.
- Unify rendering state transformations using std::optional in GLRenderState.
- Improve FullScreenMesh safety and virt_isEmpty implementation.
- Clean up utils.h includes and document UboManager thread-safety.
@nschimme
Copy link
Copy Markdown
Contributor Author

@SourceryAI review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/opengl/UboManager.h" line_range="131-140" />
<code_context>
+     * @brief Binds the UBO to its assigned point.
+     * If invalid and a rebuild function is registered, it will be updated first.
+     */
+    void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
+    {
+        const GLuint buffer = updateIfInvalid(gl, block);
+
+        if (buffer == 0) {
+            MMLOG_ERROR() << "UboManager::bind: attempted to bind invalid UBO block "
+                          << static_cast<int>(block);
+            assert(false
+                   && "UboManager::bind: attempted to bind invalid UBO block after "
+                      "updateIfInvalid (missing or failing rebuild function?)");
+            return;
+        }
+
+        // updateIfInvalid already ensured it is bound.
+        assert(m_boundBuffers[block] == buffer);
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `bind` assumes no one else mutates UBO bindings, which can desynchronize internal tracking from actual GL state.

This relies on `m_boundBuffers` as the sole source of truth and assumes no external `glBindBufferBase` calls for these binding points. If legacy code bypasses `UboManager`, the cached state can become stale and `bind` will early-return without actually re-binding. If this invariant is intentional, consider enforcing it in debug builds (e.g., compare `glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, bindingIndex)` with `m_boundBuffers[block]`) or documenting that all UBO binds for these blocks must go through `UboManager`.

Suggested implementation:

```c
    /**
     * @brief Binds the UBO to its assigned point.
     *
     * If invalid and a rebuild function is registered, it will be updated first.
     *
     * All UBO binds for managed blocks are expected to go through UboManager.
     * In debug builds, this is enforced by validating the actual GL binding
     * against UboManager's cached state and asserting on desynchronization.
     */
    void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
    {
        const GLuint buffer = updateIfInvalid(gl, block);

        if (buffer == 0) {
            MMLOG_ERROR() << "UboManager::bind: attempted to bind invalid UBO block "
                          << static_cast<int>(block);
            assert(false
                   && "UboManager::bind: attempted to bind invalid UBO block after "
                      "updateIfInvalid (missing or failing rebuild function?)");
            return;
        }

#if !defined(NDEBUG)
        // Verify that external code did not mutate the GL UBO binding state
        // behind UboManager's back for this block.
        const GLuint bindingIndex = m_bindingPoints[block];
        GLint currentlyBound = 0;
        gl.getIntegeri_v(GL_UNIFORM_BUFFER_BINDING, bindingIndex, &currentlyBound);

        if (static_cast<GLuint>(currentlyBound) != buffer) {
            MMLOG_ERROR() << "UboManager::bind: GL state desynchronized for block "
                          << static_cast<int>(block)
                          << ", expected buffer " << buffer
                          << " but GL has " << currentlyBound
                          << ". All UBO binds for this block must go through UboManager.";
            assert(false && "UboManager::bind: GL UBO binding state desynchronized from UboManager cache");
        }
#endif

        // updateIfInvalid already ensured it is (and remains) bound.
        assert(m_boundBuffers[block] == buffer);
    }

#include "../global/EnumIndexedArray.h"

```

These edits assume:
1. `m_bindingPoints` is an indexable container mapping `Legacy::SharedVboEnum` to the corresponding UBO binding index.
2. `Legacy::Functions` exposes `getIntegeri_v(GLenum, GLuint, GLint*)` (or a compatible signature). If the actual name/signature differs, adapt the `gl.getIntegeri_v(...)` call accordingly.
3. `GL_UNIFORM_BUFFER_BINDING` is available in this translation unit (it likely already is, since UBOs are used elsewhere).

If `m_bindingPoints` or the `getIntegeri_v` wrapper do not exist or are named differently, you will need to:
- Replace `m_bindingPoints[block]` with the correct way to look up the binding index for `block`.
- Replace `gl.getIntegeri_v(...)` with the correct Legacy wrapper for `glGetIntegeri_v`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +131 to +140
void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
{
const GLuint buffer = updateIfInvalid(gl, block);

if (buffer == 0) {
MMLOG_ERROR() << "UboManager::bind: attempted to bind invalid UBO block "
<< static_cast<int>(block);
assert(false
&& "UboManager::bind: attempted to bind invalid UBO block after "
"updateIfInvalid (missing or failing rebuild function?)");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): bind assumes no one else mutates UBO bindings, which can desynchronize internal tracking from actual GL state.

This relies on m_boundBuffers as the sole source of truth and assumes no external glBindBufferBase calls for these binding points. If legacy code bypasses UboManager, the cached state can become stale and bind will early-return without actually re-binding. If this invariant is intentional, consider enforcing it in debug builds (e.g., compare glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, bindingIndex) with m_boundBuffers[block]) or documenting that all UBO binds for these blocks must go through UboManager.

Suggested implementation:

    /**
     * @brief Binds the UBO to its assigned point.
     *
     * If invalid and a rebuild function is registered, it will be updated first.
     *
     * All UBO binds for managed blocks are expected to go through UboManager.
     * In debug builds, this is enforced by validating the actual GL binding
     * against UboManager's cached state and asserting on desynchronization.
     */
    void bind(Legacy::Functions &gl, Legacy::SharedVboEnum block)
    {
        const GLuint buffer = updateIfInvalid(gl, block);

        if (buffer == 0) {
            MMLOG_ERROR() << "UboManager::bind: attempted to bind invalid UBO block "
                          << static_cast<int>(block);
            assert(false
                   && "UboManager::bind: attempted to bind invalid UBO block after "
                      "updateIfInvalid (missing or failing rebuild function?)");
            return;
        }

#if !defined(NDEBUG)
        // Verify that external code did not mutate the GL UBO binding state
        // behind UboManager's back for this block.
        const GLuint bindingIndex = m_bindingPoints[block];
        GLint currentlyBound = 0;
        gl.getIntegeri_v(GL_UNIFORM_BUFFER_BINDING, bindingIndex, &currentlyBound);

        if (static_cast<GLuint>(currentlyBound) != buffer) {
            MMLOG_ERROR() << "UboManager::bind: GL state desynchronized for block "
                          << static_cast<int>(block)
                          << ", expected buffer " << buffer
                          << " but GL has " << currentlyBound
                          << ". All UBO binds for this block must go through UboManager.";
            assert(false && "UboManager::bind: GL UBO binding state desynchronized from UboManager cache");
        }
#endif

        // updateIfInvalid already ensured it is (and remains) bound.
        assert(m_boundBuffers[block] == buffer);
    }

#include "../global/EnumIndexedArray.h"

These edits assume:

  1. m_bindingPoints is an indexable container mapping Legacy::SharedVboEnum to the corresponding UBO binding index.
  2. Legacy::Functions exposes getIntegeri_v(GLenum, GLuint, GLint*) (or a compatible signature). If the actual name/signature differs, adapt the gl.getIntegeri_v(...) call accordingly.
  3. GL_UNIFORM_BUFFER_BINDING is available in this translation unit (it likely already is, since UBOs are used elsewhere).

If m_bindingPoints or the getIntegeri_v wrapper do not exist or are named differently, you will need to:

  • Replace m_bindingPoints[block] with the correct way to look up the binding index for block.
  • Replace gl.getIntegeri_v(...) with the correct Legacy wrapper for glGetIntegeri_v.

@nschimme nschimme changed the title add UboManager introduce UboManager for lazy UBO updates and binding Mar 17, 2026
@nschimme nschimme merged commit 4c70cbe into MUME:master Mar 17, 2026
18 checks passed
@nschimme nschimme deleted the ubo branch March 17, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants