-
Notifications
You must be signed in to change notification settings - Fork 29
introduce UboManager for lazy UBO updates and binding #472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7f3287d
57bb3e9
c8693c2
b80041b
071906b
c97c9ac
89c6a59
76a48a1
0200436
282ebca
7fd6dd6
3468dfc
7a5c882
967c874
3671db7
a587bee
44a92f6
fba6b2a
70192f4
e827fe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): This relies on 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, ¤tlyBound);
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:
If
|
||
| 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 | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.