Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7f3287d
add UboManager
nschimme Feb 26, 2026
57bb3e9
Improve UboManager safety and integration
nschimme Mar 16, 2026
c8693c2
Improve UboManager safety, container support, and API consistency
nschimme Mar 16, 2026
b80041b
Refine UBO updates and FullScreenMesh rendering
nschimme Mar 16, 2026
071906b
Refine MVP handling and UBO type safety
nschimme Mar 16, 2026
c97c9ac
Eliminate raw pointer for UboManager and unify FullScreenMesh MVP beh…
nschimme Mar 16, 2026
89c6a59
Improve UboManager safety, transformation logic, and dependency manag…
nschimme Mar 16, 2026
76a48a1
Address UboManager and Functions initialization code review comments.
nschimme Mar 16, 2026
0200436
Harden UboManager safety and type-correctness.
nschimme Mar 16, 2026
282ebca
Harden UboManager and refactor Functions initialization.
nschimme Mar 16, 2026
7fd6dd6
Harden UboManager and refactor Functions initialization.
nschimme Mar 16, 2026
3468dfc
Simplify and harden buffer uploads.
nschimme Mar 16, 2026
7a5c882
Address UboManager review: consolidate API and harden safety.
nschimme Mar 16, 2026
967c874
Consolidate buffer upload API and harden UboManager safety.
nschimme Mar 16, 2026
3671db7
Consolidate buffer upload API and harden UboManager safety.
nschimme Mar 16, 2026
a587bee
Consolidate buffer upload API and harden UboManager safety.
nschimme Mar 16, 2026
44a92f6
Refine UboManager safety and consolidate buffer upload API.
nschimme Mar 16, 2026
fba6b2a
Refine UboManager safety, efficiency, and consolidate buffer upload API.
nschimme Mar 17, 2026
70192f4
Refine UboManager safety, efficiency, and consolidate buffer upload API.
nschimme Mar 17, 2026
e827fe4
fix style
nschimme Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -415,12 +415,15 @@ set(mmapper_SRCS
opengl/LineRendering.h
opengl/OpenGL.cpp
opengl/OpenGL.h
opengl/UboManager.h
opengl/OpenGLConfig.cpp
opengl/OpenGLConfig.h
opengl/OpenGLTypes.cpp
opengl/OpenGLTypes.h
opengl/legacy/AbstractShaderProgram.cpp
opengl/legacy/AbstractShaderProgram.h
opengl/legacy/AttributeLessMeshes.cpp
opengl/legacy/AttributeLessMeshes.h
opengl/legacy/Binders.cpp
opengl/legacy/Binders.h
opengl/legacy/FBO.cpp
Expand Down
14 changes: 13 additions & 1 deletion src/display/mapcanvas_gl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,16 @@ void MapCanvas::initializeGL()
initLogger();

gl.initializeRenderer(static_cast<float>(QPaintDevice::devicePixelRatioF()));

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

updateMultisampling();

// REVISIT: should the font texture have the lowest ID?
Expand Down Expand Up @@ -481,7 +491,9 @@ void MapCanvas::actuallyPaintGL()
setViewportAndMvp(width(), height());

auto &gl = getOpenGL();
gl.bindNamedColorsBuffer();
auto &funcs = deref(gl.getSharedFunctions(Badge<MapCanvas>{}));

gl.getUboManager().bind(funcs, Legacy::SharedVboEnum::NamedColorsBlock);

gl.bindFbo();
gl.clear(Color{getConfig().canvas.backgroundColor});
Expand Down
28 changes: 10 additions & 18 deletions src/opengl/OpenGL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ OpenGL::OpenGL()
{
switch (OpenGLConfig::getBackendType()) {
case OpenGLProber::BackendType::GL:
m_opengl = Legacy::Functions::alloc<Legacy::FunctionsGL33>();
m_opengl = Legacy::Functions::alloc<Legacy::FunctionsGL33>(m_uboManager);
break;
case OpenGLProber::BackendType::GLES:
m_opengl = Legacy::Functions::alloc<Legacy::FunctionsES30>();
m_opengl = Legacy::Functions::alloc<Legacy::FunctionsES30>(m_uboManager);
break;
case OpenGLProber::BackendType::None:
default:
Expand Down Expand Up @@ -211,35 +211,27 @@ GLRenderState OpenGL::getDefaultRenderState()
return GLRenderState{};
}

void OpenGL::bindNamedColorsBuffer()
{
auto &gl = getFunctions();
const auto buffer = Legacy::SharedVboEnum::NamedColorsBlock;
const auto shared = gl.getSharedVbos().get(buffer);
Legacy::VBO &vbo = deref(shared);
if (!vbo) {
vbo.emplace(gl.shared_from_this());
// the shader is declared vec4, so the data has to be 4 floats per entry.
const auto &vec4_colors = XNamedColor::getAllColorsAsVec4();
std::ignore = gl.setUbo(vbo.get(), vec4_colors, BufferUsageEnum::DYNAMIC_DRAW);
}
gl.glBindBufferBase(GL_UNIFORM_BUFFER, buffer, vbo.get());
}

void OpenGL::resetNamedColorsBuffer()
{
getFunctions().getSharedVbos().reset(Legacy::SharedVboEnum::NamedColorsBlock);
m_uboManager.invalidate(Legacy::SharedVboEnum::NamedColorsBlock);
}

void OpenGL::initializeRenderer(const float devicePixelRatio)
{
setDevicePixelRatio(devicePixelRatio);

auto &funcs = getFunctions();

// REVISIT: Move this somewhere else?
GLint maxSamples = 0;
getFunctions().glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
funcs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
OpenGLConfig::setMaxSamples(maxSamples);

GLint maxUboBindings = 0;
funcs.glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUboBindings);
assert(static_cast<GLint>(Legacy::NUM_SHARED_VBOS) <= maxUboBindings);

m_rendererInitialized = true;
}

Expand Down
5 changes: 4 additions & 1 deletion src/opengl/OpenGL.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "../global/Badge.h"
#include "../global/utils.h"
#include "OpenGLTypes.h"
#include "UboManager.h"

#include <memory>
#include <vector>
Expand All @@ -27,6 +28,7 @@ class NODISCARD OpenGL final
{
private:
std::shared_ptr<Legacy::Functions> m_opengl;
Legacy::UboManager m_uboManager;
bool m_rendererInitialized = false;

private:
Expand Down Expand Up @@ -167,10 +169,11 @@ class NODISCARD OpenGL final
public:
void cleanup();
NODISCARD GLRenderState getDefaultRenderState();
void bindNamedColorsBuffer();
void resetNamedColorsBuffer();
void setTextureLookup(MMTextureId, SharedMMTexture);

NODISCARD Legacy::UboManager &getUboManager() { return m_uboManager; }

public:
void uploadArrayLayer(const SharedMMTexture &array,
int layer,
Expand Down
8 changes: 8 additions & 0 deletions src/opengl/OpenGLTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ struct NODISCARD GLRenderState final
};

Uniforms uniforms;
std::optional<glm::mat4> mvp;

NODISCARD GLRenderState withBlend(const BlendModeEnum new_blend) const
{
Expand Down Expand Up @@ -282,6 +283,13 @@ struct NODISCARD GLRenderState final
copy.uniforms.textures = Textures{new_texture, INVALID_MM_TEXTURE_ID};
return copy;
}

NODISCARD GLRenderState withMvp(std::optional<glm::mat4> new_mvp) const
{
GLRenderState copy = *this;
copy.mvp = new_mvp;
return copy;
}
};

struct NODISCARD IRenderable
Expand Down
188 changes: 188 additions & 0 deletions src/opengl/UboManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#pragma once
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 The MMapper Authors

#include "../global/EnumIndexedArray.h"
#include "../global/RuleOf5.h"
#include "../global/logging.h"
#include "../global/utils.h"
#include "legacy/Legacy.h"
#include "legacy/VBO.h"

#include <cassert>
#include <cstddef>
#include <functional>
#include <optional>
#include <type_traits>
#include <vector>

namespace Legacy {

/**
* @brief Central manager for Uniform Buffer Objects (UBOs).
*
* Tracks which UBOs are currently valid on the GPU and coordinates their updates.
* Follows a lazy rebuild pattern: UBOs are only updated when a bind() is requested
* and the block is marked as dirty (represented by std::nullopt in the bound buffer tracker).
*
* Note: This class is not thread-safe.
*/
class UboManager final
{
public:
using RebuildFunction = std::function<void(Legacy::Functions &gl)>;

UboManager() { invalidateAll(); }
DELETE_CTORS_AND_ASSIGN_OPS(UboManager);

public:
/**
* @brief Marks a UBO block as dirty by resetting its bound state.
*/
void invalidate(Legacy::SharedVboEnum block) { m_boundBuffers[block] = std::nullopt; }

/**
* @brief Marks all UBO blocks as dirty.
*/
void invalidateAll()
{
m_boundBuffers.for_each([](std::optional<GLuint> &v) { v = std::nullopt; });
}

/**
* @brief Registers a function that can rebuild the UBO data.
*/
void registerRebuildFunction(Legacy::SharedVboEnum block, RebuildFunction func)
{
if (m_rebuildFunctions[block]) {
MMLOG_WARNING() << "UboManager::registerRebuildFunction: overwriting existing "
"rebuild function for UBO block "
<< static_cast<int>(block);
}
m_rebuildFunctions[block] = std::move(func);
}

/**
* @brief Checks if a UBO block is currently dirty/invalid.
*/
bool isInvalid(Legacy::SharedVboEnum block) const { return !m_boundBuffers[block].has_value(); }

/**
* @brief Rebuilds the UBO if it's invalid using the registered rebuild function.
* @return The bound buffer ID, or 0 if failed.
*/
GLuint updateIfInvalid(Legacy::Functions &gl, Legacy::SharedVboEnum block)
{
if (const auto bound = m_boundBuffers[block]) {
return *bound;
}

const auto &func = m_rebuildFunctions[block];
assert(func && "UBO block is invalid and no rebuild function is registered");
if (func) {
func(gl);

if (const auto bound = m_boundBuffers[block]) {
return *bound;
}

MMLOG_ERROR() << "UboManager::updateIfInvalid: rebuild function failed to call "
"update() for block "
<< static_cast<int>(block);
assert(false && "Rebuild function must call update()");
}
return 0;
}

/**
* @brief Uploads data to the UBO and marks it as valid.
* Also binds it to its assigned point.
* @return The bound buffer ID.
*
* Overload for bulk vector data.
*/
template<typename T, typename A>
GLuint update(Legacy::Functions &gl, Legacy::SharedVboEnum block, const std::vector<T, A> &data)
{
Legacy::VBO &vbo = getOrCreateVbo(gl, block);
static_cast<void>(
gl.setVbo(GL_UNIFORM_BUFFER, vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW));
return bind_internal(gl, block, vbo.get());
}

/**
* @brief Uploads data to the UBO and marks it as valid.
* Also binds it to its assigned point.
* @return The bound buffer ID.
*
* Overload for single trivially-copyable objects.
*/
template<typename T>
GLuint update(Legacy::Functions &gl, Legacy::SharedVboEnum block, const T &data)
{
Legacy::VBO &vbo = getOrCreateVbo(gl, block);
gl.setVbo(GL_UNIFORM_BUFFER, vbo.get(), data, BufferUsageEnum::DYNAMIC_DRAW);
return bind_internal(gl, block, vbo.get());
}

/**
* @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?)");
Comment on lines +132 to +141
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.

return;
}

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

private:
Legacy::VBO &getOrCreateVbo(Legacy::Functions &gl, Legacy::SharedVboEnum block)
{
const auto sharedVbo = gl.getSharedVbos().get(block);
Legacy::VBO &vbo = deref(sharedVbo);

if (!vbo) {
vbo.emplace(gl.shared_from_this());
}
return vbo;
}

private:
/**
* @brief Binds the UBO to its assigned point.
* @return The bound buffer ID.
*
* Note: This implementation explicitly assumes that Legacy::SharedVboEnum values
* are 0-based, contiguous, and directly correspond to UBO binding indices in
* shader blocks.
*/
GLuint bind_internal(Legacy::Functions &gl, Legacy::SharedVboEnum block, GLuint buffer)
{
const auto bindingIndex = Legacy::getUboBindingIndex(block);
assert(static_cast<std::size_t>(bindingIndex) < m_boundBuffers.size());

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

private:
EnumIndexedArray<RebuildFunction, Legacy::SharedVboEnum> m_rebuildFunctions;
EnumIndexedArray<std::optional<GLuint>, Legacy::SharedVboEnum> m_boundBuffers;
};

} // namespace Legacy
12 changes: 12 additions & 0 deletions src/opengl/legacy/AttributeLessMeshes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 The MMapper Authors

#include "AttributeLessMeshes.h"

namespace Legacy {

BlitMesh::~BlitMesh() = default;

PlainFullScreenMesh::~PlainFullScreenMesh() = default;

} // namespace Legacy
Loading
Loading