Skip to content

Conversation

@nschimme
Copy link
Contributor

@nschimme nschimme commented Jan 31, 2026

Quad meshes are now 75% (as GL_QUADS) to 83% (as GL_TRIANGLES) smaller.

  • read named colors from a uniform buffer objects (UBO) for named colors
  • consolidate plain and textured rendering into a single shader path
  • use lazy mesh generation for room tints and layer boosts

Summary by Sourcery

Switch room rendering to an instanced quad path with a new shader and UBO-driven named colors to reduce VRAM usage and simplify draw calls.

New Features:

  • Introduce instanced room quad textured meshes and a dedicated shader that derive per-instance color and texture from packed vertex data and a named-colors uniform buffer.
  • Add a white-pixel texture and array entry to support tinting and layer boost rendering via the unified instanced path.

Enhancements:

  • Replace per-vertex quad expansion with instanced rendering for room tiles, consolidating plain and textured room rendering into a single mesh type.
  • Move named room colors into a uniform buffer object and propagate its binding through the render state so multiple draw paths share the same color source.
  • Refactor map layer batching to lazily generate meshes for room tints and layer boosts, deferring GPU upload until needed and reducing memory usage.
  • Initialize and manage legacy shader programs through a new early_init path and extend GL function wrappers to support UBOs, element buffers, and instanced draws.
  • Clean up various include paths and minor OpenGL plumbing, including new draw modes and utility helpers.

Quad meshes are now 75% (as GL_QUADS) to 83% (as GL_TRIANGLES) smaller.

* read named colors from a uniform buffer objects (UBO) for named colors
* consolidate plain and textured rendering into a single shader path
* use lazy mesh generation for room tints and layer boosts
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 31, 2026

Reviewer's Guide

Refactors room rendering to use an instanced quad shader backed by a named-colors UBO and a 1x1 white texture, consolidating tint/highlight/layer-boost paths while extending the GL helpers for UBOs, instancing, and named-color upload.

Sequence diagram for instanced room quad rendering with named-colors UBO

sequenceDiagram
    actor User
    participant MC as MapCanvas
    participant GL as OpenGL
    participant LF as Legacy_Functions
    participant SP as ShaderPrograms
    participant LM as LayerMeshes
    participant GPU as GPU/Driver

    User->>MC: paintGL / renderMapBatches
    MC->>MC: updateNamedColorsUBO()
    MC->>GL: getSharedFunctions()
    GL-->>MC: SharedFunctions
    MC->>LF: setUbo(ubo, XNamedColor::getAllColorsAsVec4)
    LF->>GPU: glBindBuffer(GL_UNIFORM_BUFFER)
    LF->>GPU: glBufferData(GL_UNIFORM_BUFFER, vec4_colors)
    LF->>GPU: glBindBufferBase(binding=0, ubo)

    MC->>LM: render(thisLayer, focusedLayer, named_colors_buffer_id)
    LM->>LM: resolve(Fn/FnVec) -> UniqueMesh
    LM->>MC: getDefaultRenderState()
    MC-->>LM: GLRenderState(namedColorBufferObject=ubo)

    loop For each room batch
        LM->>GL: UniqueMesh::render(GLRenderState)
        GL->>LF: createRoomQuadTexBatch(verts, texture)
        LF->>SP: getRoomQuadTexShader()
        SP-->>LF: RoomQuadTexShader
        LF->>GPU: glUseProgram(RoomQuadTexShader)
        LF->>GPU: glBindTexture(texture_array)
        LF->>GPU: glBindBuffer(GL_ARRAY_BUFFER, instance_VBO)
        LF->>GPU: glVertexAttribIPointer(aVertTexCol)
        LF->>GPU: glVertexAttribDivisor(aVertTexCol, 1)
        LF->>GPU: glDrawElementsInstanced(TRIANGLE_FAN, indexCount=4, instanceCount=numRooms)
    end
Loading

File-Level Changes

Change Details Files
Adopt instanced room quad rendering for room textures, tints, layer boosts, and diff highlights.
  • Introduce RoomQuadTexVert and DrawModeEnum::INSTANCED_QUADS, encoding room coords, texture layer, and named-color id into a single ivec4 per instance.
  • Replace per-quad vertex expansion with compact per-room vertices and createRoomQuadTexBatch in MapCanvasRoomDrawer and MapCanvas diff highlighting.
  • Add RoomQuadTexMesh and RoomQuadTexShader that use an integer vertex attribute, instance divisor, and a triangle-fan index buffer shared via drawRoomQuad().
  • Refactor LayerMeshesIntermediate and LayerMeshes to store mesh-creation lambdas for tints/layer boosts and resolve them lazily into UniqueMesh objects.
src/opengl/OpenGLTypes.h
src/opengl/OpenGL.cpp
src/opengl/OpenGL.h
src/display/MapCanvasRoomDrawer.cpp
src/display/MapBatches.h
src/display/mapcanvas.h
src/display/mapcanvas_gl.cpp
src/display/mapcanvas.cpp
src/opengl/legacy/SimpleMesh.h
src/opengl/legacy/SimpleMesh.cpp
src/opengl/legacy/Meshes.h
src/opengl/legacy/Legacy.cpp
Introduce a named-colors uniform buffer object (UBO) and plumb it through render state and shaders.
  • Define MAX_NAMED_COLORS, add storage of colors as vec4 in GlobalData, and expose XNamedColor::getAllColorsAsVec4().
  • Add GLRenderState::Uniforms.namedColorBufferObject and use it in LayerMeshes::render and MapCanvas::getDefaultRenderState().
  • Implement MapCanvas::updateNamedColorsUBO() to lazily allocate a VBO-backed UBO, upload vec4 colors via Functions::setUbo(), and track dirtiness.
  • Extend AbstractShaderProgram with setUBO() and have RoomQuadTexShader bind a NamedColorsBlock uniform block; add MAX_NAMED_COLORS define injection in shader compilation.
src/global/NamedColors.h
src/global/NamedColors.cpp
src/opengl/OpenGLTypes.h
src/display/mapcanvas.h
src/display/mapcanvas_gl.cpp
src/display/mapcanvas.cpp
src/opengl/legacy/AbstractShaderProgram.h
src/opengl/legacy/AbstractShaderProgram.cpp
src/opengl/legacy/ShaderUtils.cpp
src/opengl/legacy/Shaders.h
src/opengl/legacy/Shaders.cpp
src/resources/shaders/legacy/room/tex/acolor/vert.glsl
src/resources/shaders/legacy/room/tex/acolor/frag.glsl
Extend GL helper APIs to support instancing, UBO uploads, and integer vertex attributes.
  • Generalize buffer upload via setBufferData_internal and add Functions::setIbo() and Functions::setUbo().
  • Expose glBindBufferBase, glDrawElementsInstanced, glGetUniformBlockIndex, glUniformBlockBinding, and glVertexAttribDivisor, plus an enableAttribI() helper.
  • Teach FunctionsGL33/ES30::virt_toGLenum about INSTANCED_QUADS and ensure non-quad modes still map correctly.
  • Add Legacy::drawRoomQuad() to manage a shared index buffer and perform instanced drawElements for quads.
src/opengl/legacy/Legacy.h
src/opengl/legacy/Legacy.cpp
src/opengl/legacy/FunctionsGL33.cpp
src/opengl/legacy/FunctionsES30.cpp
src/opengl/legacy/SimpleMesh.cpp
src/opengl/legacy/SimpleMesh.h
Refactor texture/texture-array setup and introduce a white 1x1 texture for generic quad effects.
  • Load a single-pixel white texture (white_pixel) and create a corresponding texture array (white_pixel_Array) alongside other MapCanvas textures.
  • Pass textures.white_pixel into LayerBatchData to build tint and layer-boost meshes using the shared instanced quad shader instead of plain quads.
  • Update MapCanvas diff rendering to use the same instanced room quad path with room_highlight texture array and named-colors UBO.
src/display/Textures.cpp
src/display/Textures.h
src/display/MapCanvasRoomDrawer.cpp
src/display/mapcanvas_gl.cpp
src/display/mapcanvas.h
Tidy and centralize shader initialization and misc includes.
  • Add ShaderPrograms::early_init() and call it from MapCanvas::initializeGL() instead of touching each shader getter manually; ensure resetAll() also resets the new RoomQuadTexShader.
  • Fix a few include paths to consistently use ../global/… and remove outdated comments about texture array support.
  • Mark named-colors as dirty on graphics settings changes so UBO updates reflect configuration.
src/opengl/legacy/Shaders.h
src/opengl/legacy/Shaders.cpp
src/display/mapcanvas_gl.cpp
src/display/mapcanvas.cpp
src/viewers/TopLevelWindows.cpp
src/global/AsyncTasks.cpp
src/map/ExitDirection.cpp
src/parser/SendToUserSourceEnum.h
src/viewers/AnsiViewWindow.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

@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:

  • updateNamedColorsUBO() and the lambda in renderMapBatches() both create and upload the named-colors UBO using a static weak VBO; consider consolidating this into a single helper (or just reusing updateNamedColorsUBO()) to avoid duplicated logic and potential divergence.
  • createMeshFn/Resolver::resolve contain constexpr bool flags and null-function handling paths that are effectively dead code; cleaning these up or replacing them with a clearer optional/empty-fn convention would make the lazy mesh generation easier to reason about.
  • The getTintColor(RoomTintEnum) and UBO index packing in RoomQuadTexVert both assume small, bounded enum ranges; adding explicit static_asserts or comments tying the enum ranges to MAX_NAMED_COLORS would help prevent subtle issues if new tint or named color values are introduced later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- `updateNamedColorsUBO()` and the lambda in `renderMapBatches()` both create and upload the named-colors UBO using a static weak VBO; consider consolidating this into a single helper (or just reusing `updateNamedColorsUBO()`) to avoid duplicated logic and potential divergence.
- `createMeshFn`/`Resolver::resolve` contain `constexpr bool` flags and null-function handling paths that are effectively dead code; cleaning these up or replacing them with a clearer optional/empty-fn convention would make the lazy mesh generation easier to reason about.
- The `getTintColor(RoomTintEnum)` and UBO index packing in `RoomQuadTexVert` both assume small, bounded enum ranges; adding explicit static_asserts or comments tying the enum ranges to `MAX_NAMED_COLORS` would help prevent subtle issues if new tint or named color values are introduced later.

## Individual Comments

### Comment 1
<location> `src/display/mapcanvas_gl.cpp:1013-1022` </location>
<code_context>
+void MapCanvas::updateNamedColorsUBO()
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid duplicating named-colors UBO setup logic by reusing `updateNamedColorsUBO()`

The static `Legacy::VBO` allocation, upload of `XNamedColor::getAllColorsAsVec4()`, and assignment of `m_named_colors_buffer_id` now live both in `updateNamedColorsUBO()` and inline in `renderMapBatches()`. This duplication risks the two paths diverging (e.g., if `MAX_NAMED_COLORS` or buffer usage changes) and can create two separate static `weak_ptr<VBO>`s instead of a single shared buffer.

To avoid this, have `renderMapBatches()` call `updateNamedColorsUBO()` instead, and make that function idempotent/cheap when no data changes. That keeps all named-color UBO handling in one place and reduces maintenance risk.

Suggested implementation:

```cpp
    // Ensure named colors UBO is allocated and up to date
    updateNamedColorsUBO();

```

I assumed the duplicated block in `renderMapBatches()` matches the inline named-colors UBO logic you moved into `updateNamedColorsUBO()` (static `g_weak_vbo`, `XNamedColor::getAllColorsAsVec4()`, and `m_named_colors_buffer_id` assignment). If the actual code differs slightly (e.g., variable names, comments, or `bufferStatic` call signature), adjust the `SEARCH` section to match your current `renderMapBatches()` implementation exactly, keeping the `REPLACE` section as a simple `updateNamedColorsUBO();` call.
</issue_to_address>

### Comment 2
<location> `src/display/mapcanvas_gl.cpp:1026-1035` </location>
<code_context>
+        vbo.emplace(fns);
+    }
+
+    m_named_colors_buffer_id = std::invoke([&gl, &shared_vbo]() {
+        auto &fns = gl.getSharedFunctions(Badge<MapCanvas>{});
+        Legacy::VBO &vbo = deref(shared_vbo);
+
+        // the shader is declared vec4, so the data has to be 4 floats per entry.
+        const auto &vec4_colors = XNamedColor::getAllColorsAsVec4();
+
+        // Can we avoid the upload if it's not necessary?
+        MAYBE_UNUSED const auto result = fns->setUbo(vbo.get(),
+                                                     vec4_colors,
+                                                     BufferUsageEnum::DYNAMIC_DRAW);
+        assert(result == static_cast<int>(vec4_colors.size()));
+        return vbo.get();
+    });
+}
</code_context>

<issue_to_address>
**issue (performance):** `renderMapBatches()` re-uploads the named-colors UBO even though `updateNamedColorsUBO()` already does it

`actuallyPaintGL()` already calls `updateNamedColorsUBO()` every frame, but `renderMapBatches()` allocates/uploads the same UBO again and overwrites `m_named_colors_buffer_id`. This causes a redundant upload each frame and makes the earlier call pointless.

Consider making `updateNamedColorsUBO()` the single place that creates/updates `m_named_colors_buffer_id`, and have `renderMapBatches()` just use it. If this path must upload, guard it with a cheap check (e.g., only when colors change or the buffer ID is zero).
</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.

@nschimme
Copy link
Contributor Author

@sourcery-ai review

Copy link

@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:

  • AbstractShaderProgram::setUBO does a glGetUniformBlockIndex and glUniformBlockBinding on every call; consider caching the block index (and binding point) per program to avoid repeated GL queries in the hot render path.
  • The static std::weak_ptr<VBO> usage in both drawRoomQuad and MapCanvas::updateNamedColorsUBO assumes the GL context remains valid for the lifetime of the shared VBO; consider tying these to the existing shader/VBO reset lifecycle (e.g., ShaderPrograms::resetAll or a similar hook) to handle context loss or reinitialization more robustly.
  • Named-color UBO binding is now set up in multiple places (LayerMeshes::render and MapCanvas::getDefaultRenderState); it may be worth centralizing the construction of a "default render state" with the named-colors buffer to avoid divergence if the uniforms layout evolves.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- `AbstractShaderProgram::setUBO` does a `glGetUniformBlockIndex` and `glUniformBlockBinding` on every call; consider caching the block index (and binding point) per program to avoid repeated GL queries in the hot render path.
- The static `std::weak_ptr<VBO>` usage in both `drawRoomQuad` and `MapCanvas::updateNamedColorsUBO` assumes the GL context remains valid for the lifetime of the shared VBO; consider tying these to the existing shader/VBO reset lifecycle (e.g., `ShaderPrograms::resetAll` or a similar hook) to handle context loss or reinitialization more robustly.
- Named-color UBO binding is now set up in multiple places (`LayerMeshes::render` and `MapCanvas::getDefaultRenderState`); it may be worth centralizing the construction of a "default render state" with the named-colors buffer to avoid divergence if the uniforms layout evolves.

## Individual Comments

### Comment 1
<location> `src/display/mapcanvas_gl.cpp:725-727` </location>
<code_context>
                 DECL_TIMER(t3, "[async] actuallyPaintGL: compute highlights");
-                ColoredTexVertVector highlights;
-                auto drawQuad = [&highlights](const RawRoom &room, const Color color) {
+                DiffQuadVector highlights;
+                auto drawQuad = [&highlights](const RawRoom &room, const NamedColorEnum color) {
                     const auto &pos = room.getPosition().to_vec3();
-                    for (auto &corner : corners) {
-                        highlights.emplace_back(color, corner, pos + corner);
</code_context>

<issue_to_address>
**issue (bug_risk):** Using to_vec3() when constructing RoomQuadTexVert is inconsistent with other call sites and may rely on an unintended implicit conversion.

Here `drawQuad` passes `room.getPosition().to_vec3()` while the other new call sites use `to_ivec3()`, and `RoomQuadTexVert` expects a `glm::ivec3`. This relies on an implicit float→int conversion (and may even fail to compile, depending on GLM). Please switch to `to_ivec3()` here for consistency and to avoid the implicit conversion.
</issue_to_address>

### Comment 2
<location> `src/opengl/legacy/AbstractShaderProgram.cpp:165-172` </location>
<code_context>
     setUniform1iv(uFontTextureLoc, 1, &textureUnit);
 }

+void AbstractShaderProgram::setUBO(const char *const block_name, const GLuint uboId)
+{
+    assert(uboId != 0);
+    auto functions = m_functions.lock();
+    const GLuint program = m_program.get();
+    auto block_index = functions->glGetUniformBlockIndex(program, block_name);
+    functions->glUniformBlockBinding(program, block_index, 0);
+    deref(functions).glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboId);
+}
+
</code_context>

<issue_to_address>
**issue (bug_risk):** setUBO does not handle the case where the uniform block is not found in the program.

If `glGetUniformBlockIndex` returns `GL_INVALID_INDEX` (e.g., from a typo in `block_name` or shader changes), calling `glUniformBlockBinding`/`glBindBufferBase` with that index is undefined. Please guard against `GL_INVALID_INDEX` and early-return (optionally asserting/logging in debug builds) to make such failures visible.
</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 +725 to 727
DiffQuadVector highlights;
auto drawQuad = [&highlights](const RawRoom &room, const NamedColorEnum color) {
const auto &pos = room.getPosition().to_vec3();
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Using to_vec3() when constructing RoomQuadTexVert is inconsistent with other call sites and may rely on an unintended implicit conversion.

Here drawQuad passes room.getPosition().to_vec3() while the other new call sites use to_ivec3(), and RoomQuadTexVert expects a glm::ivec3. This relies on an implicit float→int conversion (and may even fail to compile, depending on GLM). Please switch to to_ivec3() here for consistency and to avoid the implicit conversion.

Comment on lines +165 to +172
void AbstractShaderProgram::setUBO(const char *const block_name, const GLuint uboId)
{
assert(uboId != 0);
auto functions = m_functions.lock();
const GLuint program = m_program.get();
auto block_index = functions->glGetUniformBlockIndex(program, block_name);
functions->glUniformBlockBinding(program, block_index, 0);
deref(functions).glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboId);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): setUBO does not handle the case where the uniform block is not found in the program.

If glGetUniformBlockIndex returns GL_INVALID_INDEX (e.g., from a typo in block_name or shader changes), calling glUniformBlockBinding/glBindBufferBase with that index is undefined. Please guard against GL_INVALID_INDEX and early-return (optionally asserting/logging in debug builds) to make such failures visible.

@codecov
Copy link

codecov bot commented Jan 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.72%. Comparing base (26d1a9d) to head (424e891).
⚠️ Report is 182 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #446      +/-   ##
==========================================
+ Coverage   66.48%   75.72%   +9.24%     
==========================================
  Files          85       94       +9     
  Lines        4186     4355     +169     
  Branches      255      295      +40     
==========================================
+ Hits         2783     3298     +515     
+ Misses       1403     1057     -346     

☔ 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.

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.

1 participant