From e9899a156896597832b18dc3312bdbbf87c99224 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Fri, 3 Oct 2025 01:20:31 +0100 Subject: [PATCH 1/3] tractogram: Implement element buffer objects for track rendering Instead of using `glMultiDrawArrays`, which doesn't guarantee that draw calls will be executed in a batched fashion, we use `glDrawElements` with an index buffer. This fixes the performance issues seen on MacOS when visualising tractograms. See https://programming4.us/multimedia/8302.aspx --- .../mrview/tool/tractography/tractogram.cpp | 42 +++++++++++++++++-- src/gui/mrview/tool/tractography/tractogram.h | 3 ++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/gui/mrview/tool/tractography/tractogram.cpp b/src/gui/mrview/tool/tractography/tractogram.cpp index a8f6905364..5eec94b1cb 100644 --- a/src/gui/mrview/tool/tractography/tractogram.cpp +++ b/src/gui/mrview/tool/tractography/tractogram.cpp @@ -24,9 +24,10 @@ #include "gui/opengl/lighting.h" #include "gui/mrview/mode/base.h" - +#include const size_t MAX_BUFFER_SIZE = 2796200; // number of points to fill 32MB +constexpr uint32_t PRIMITIVE_RESTART_SENTINEL = 0xFFFFFFFFu; // Primitive restart index for UNSIGNED_INT namespace MR { @@ -399,6 +400,8 @@ namespace MR gl::DeleteBuffers (intensity_scalar_buffers.size(), &intensity_scalar_buffers[0]); if (threshold_scalar_buffers.size()) gl::DeleteBuffers (threshold_scalar_buffers.size(), &threshold_scalar_buffers[0]); + if (!element_buffers.empty()) + gl::DeleteBuffers(element_buffers.size(), &element_buffers[0]); GL::assert_context_is_current(); } @@ -566,8 +569,15 @@ namespace MR auto mode = geometry_type == TrackGeometryType::Points ? gl::POINTS : gl::LINE_STRIP; - gl::MultiDrawArrays (mode, &track_starts[buf][0], &track_sizes[buf][0], num_tracks_per_buffer[buf]); - + if(mode == gl::POINTS) { + gl::MultiDrawArrays(mode, &track_starts[buf][0], &track_sizes[buf][0], num_tracks_per_buffer[buf]); + } else if (element_counts[buf] > 0 && element_buffers[buf] != 0) { + // Use the EBO stored with this VAO to render all tracks in this chunk + gl::Enable(gl::PRIMITIVE_RESTART); + gl::PrimitiveRestartIndex(PRIMITIVE_RESTART_SENTINEL); + gl::DrawElements(gl::LINE_STRIP, element_counts[buf], gl::UNSIGNED_INT, nullptr); + gl::Disable(gl::PRIMITIVE_RESTART); + } } vao_dirty = false; @@ -987,6 +997,32 @@ namespace MR original_track_sizes.push_back (sizes); num_tracks_per_buffer.push_back (tck_count); + // Build an index buffer for this chunk of tracks + std::vector chunk_indices; + for (size_t t = 0; t < sizes.size(); ++t) { + const GLint start = starts[t]; + const GLint n = sizes[t]; + if (n < 2) continue; + for (GLint k = 0; k < n; ++k) + chunk_indices.push_back(static_cast(start + k)); + chunk_indices.push_back(PRIMITIVE_RESTART_SENTINEL); + } + + if (!chunk_indices.empty()) { + GLuint ebo = 0; + gl::GenBuffers(1, &ebo); + // bind to the VAO so that ELEMENT_ARRAY_BUFFER is part of VAO state + gl::BindVertexArray(vertex_array_object); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData(gl::ELEMENT_ARRAY_BUFFER, chunk_indices.size() * sizeof(uint32_t), chunk_indices.data(), gl::STATIC_DRAW); + element_buffers.push_back(ebo); + element_counts.push_back(static_cast(chunk_indices.size())); + } else { + // keep arrays aligned with other per-chunk vectors + element_buffers.push_back(0); + element_counts.push_back(0); + } + buffer.clear(); starts.clear(); sizes.clear(); diff --git a/src/gui/mrview/tool/tractography/tractogram.h b/src/gui/mrview/tool/tractography/tractogram.h index b6ecd9e002..86115a1c11 100644 --- a/src/gui/mrview/tool/tractography/tractogram.h +++ b/src/gui/mrview/tool/tractography/tractogram.h @@ -144,6 +144,9 @@ namespace MR vector > original_track_sizes; vector > original_track_starts; vector num_tracks_per_buffer; + // EBOs and indices for chunks of tracks + std::vector element_buffers; + std::vector element_counts; GLint sample_stride; bool vao_dirty; From ab43e2e81db2e34a25f70d7b99da41526caf60b6 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Fri, 3 Oct 2025 07:57:53 +0100 Subject: [PATCH 2/3] Use MR::vector instead of std::vector --- src/gui/mrview/tool/tractography/tractogram.cpp | 2 +- src/gui/mrview/tool/tractography/tractogram.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/mrview/tool/tractography/tractogram.cpp b/src/gui/mrview/tool/tractography/tractogram.cpp index 5eec94b1cb..a694c31aea 100644 --- a/src/gui/mrview/tool/tractography/tractogram.cpp +++ b/src/gui/mrview/tool/tractography/tractogram.cpp @@ -998,7 +998,7 @@ namespace MR num_tracks_per_buffer.push_back (tck_count); // Build an index buffer for this chunk of tracks - std::vector chunk_indices; + vector chunk_indices; for (size_t t = 0; t < sizes.size(); ++t) { const GLint start = starts[t]; const GLint n = sizes[t]; diff --git a/src/gui/mrview/tool/tractography/tractogram.h b/src/gui/mrview/tool/tractography/tractogram.h index 86115a1c11..535067a7a5 100644 --- a/src/gui/mrview/tool/tractography/tractogram.h +++ b/src/gui/mrview/tool/tractography/tractogram.h @@ -145,8 +145,8 @@ namespace MR vector > original_track_starts; vector num_tracks_per_buffer; // EBOs and indices for chunks of tracks - std::vector element_buffers; - std::vector element_counts; + vector element_buffers; + vector element_counts; GLint sample_stride; bool vao_dirty; From 22e94fcf7c16d77f9932290a37dc5cd775773be9 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Mon, 6 Oct 2025 13:02:04 +0100 Subject: [PATCH 3/3] Replace drawMultiArrays in base fixel class Use index buffer + glDrawElements instead of drawMultiArrays to ensure that the rendering calls are hardware batched. Motivation is the same as in e9899a156896597832b18dc3312bdbbf87c99224. --- src/gui/mrview/tool/fixel/base_fixel.cpp | 50 ++++++++++++++++++++++-- src/gui/mrview/tool/fixel/base_fixel.h | 8 ++++ src/gui/mrview/tool/fixel/image4D.cpp | 3 +- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/gui/mrview/tool/fixel/base_fixel.cpp b/src/gui/mrview/tool/fixel/base_fixel.cpp index 415548f517..45658eb720 100644 --- a/src/gui/mrview/tool/fixel/base_fixel.cpp +++ b/src/gui/mrview/tool/fixel/base_fixel.cpp @@ -16,6 +16,7 @@ #include "gui/mrview/tool/fixel/base_fixel.h" +#include namespace MR { @@ -63,6 +64,7 @@ namespace MR regular_grid_dir_buffer.clear (); regular_grid_colour_buffer.clear (); regular_grid_val_buffer.clear (); + element_index_buffer.clear (); } std::string BaseFixel::Shader::vertex_shader_source (const Displayable&) @@ -263,9 +265,11 @@ namespace MR if (!fixel_tool.is_cropped_to_slab()) { vertex_array_object.bind(); - for (size_t x = 0, N = slice_fixel_indices[0].size(); x < N; ++x) { - if (slice_fixel_counts[0][x]) - gl::MultiDrawArrays (gl::POINTS, &slice_fixel_indices[0][x][0], &slice_fixel_sizes[0][x][0], slice_fixel_counts[0][x]); + if (element_indices_dirty) + rebuild_element_index_buffer(); + element_index_buffer.bind(gl::ELEMENT_ARRAY_BUFFER); + if (!element_indices.empty()) { + gl::DrawElements(gl::POINTS, static_cast(element_indices.size()), gl::UNSIGNED_INT, (void*)0); } } else { request_update_interp_image_buffer (projection); @@ -471,6 +475,7 @@ namespace MR value_buffer.gen (); colour_buffer.gen (); threshold_buffer.gen (); + element_index_buffer.gen (); // voxel centres vertex_buffer.bind (gl::ARRAY_BUFFER); @@ -478,6 +483,9 @@ namespace MR gl::EnableVertexAttribArray (0); gl::VertexAttribPointer (0, 3, gl::FLOAT, gl::FALSE_, 0, (void*)0); + rebuild_element_index_buffer(); + element_indices_dirty = false; + GL::assert_context_is_current(); dir_buffer_dirty = true; @@ -487,6 +495,42 @@ namespace MR } + void BaseFixel::rebuild_element_index_buffer() + { + GL::Context::Grab context; + GL::assert_context_is_current(); + + element_indices.clear(); + if (!slice_fixel_indices.empty() && !slice_fixel_sizes.empty() && !slice_fixel_counts.empty()) { + const auto &starts_by_slice = slice_fixel_indices[0]; + const auto &sizes_by_slice = slice_fixel_sizes[0]; + const auto &counts_by_slice = slice_fixel_counts[0]; + const size_t S = starts_by_slice.size(); + for (size_t s = 0; s < S; ++s) { + const auto &starts = starts_by_slice[s]; + const auto &sizes = sizes_by_slice[s]; + const GLsizei draw_count = counts_by_slice[s]; + if (!draw_count) continue; + assert(starts.size() == sizes.size()); + assert(static_cast(draw_count) <= starts.size()); + for (GLsizei d = 0; d < draw_count; ++d) { + const uint32_t start = static_cast(starts[d]); + const uint32_t len = static_cast(sizes[d]); + for (uint32_t i = 0; i < len; ++i) + element_indices.push_back(start + i); + } + } + } + + vertex_array_object.bind(); + element_index_buffer.bind(gl::ELEMENT_ARRAY_BUFFER); + if (!element_indices.empty()) + gl::BufferData(gl::ELEMENT_ARRAY_BUFFER, element_indices.size()*sizeof(uint32_t), element_indices.data(), gl::STATIC_DRAW); + + element_indices_dirty = false; + } + + void BaseFixel::reload_directions_buffer () { GL::Context::Grab context; diff --git a/src/gui/mrview/tool/fixel/base_fixel.h b/src/gui/mrview/tool/fixel/base_fixel.h index 2c2a3e5566..7188595175 100644 --- a/src/gui/mrview/tool/fixel/base_fixel.h +++ b/src/gui/mrview/tool/fixel/base_fixel.h @@ -18,6 +18,7 @@ #define __gui_mrview_tool_fixel_fixelimage_h__ #include +#include #include "header.h" #include "image.h" @@ -264,6 +265,7 @@ namespace MR vector regular_grid_buffer_val; vector regular_grid_buffer_threshold; + // If slice_fixel* are modified, rebuild_element_index_buffer() must be called vector > > slice_fixel_indices; vector > > slice_fixel_sizes; vector > slice_fixel_counts; @@ -282,6 +284,8 @@ namespace MR bool value_buffer_dirty; bool threshold_buffer_dirty; bool dir_buffer_dirty; + bool element_indices_dirty = false; + void rebuild_element_index_buffer(); private: Fixel& fixel_tool; @@ -290,6 +294,7 @@ namespace MR GL::VertexBuffer colour_buffer; GL::VertexBuffer value_buffer; GL::VertexBuffer threshold_buffer; + GL::VertexBuffer element_index_buffer; GL::VertexArrayObject vertex_array_object; GL::VertexArrayObject regular_grid_vao; @@ -299,6 +304,9 @@ namespace MR GL::VertexBuffer regular_grid_val_buffer; GL::VertexBuffer regular_grid_threshold_buffer; + // Index buffer for rendering slabs + vector element_indices; + float voxel_size_length_multipler; float user_line_length_multiplier; float line_thickness; diff --git a/src/gui/mrview/tool/fixel/image4D.cpp b/src/gui/mrview/tool/fixel/image4D.cpp index 29527367f3..418687359c 100644 --- a/src/gui/mrview/tool/fixel/image4D.cpp +++ b/src/gui/mrview/tool/fixel/image4D.cpp @@ -64,7 +64,8 @@ namespace MR pos_buffer_store.clear(); dir_buffer_store.clear(); fixel_val_store.clear(); - + element_indices_dirty = true; + for (size_t axis = 0; axis < 3; ++axis) { std::fill (slice_fixel_indices[axis].begin(), slice_fixel_indices[axis].end(), vector()); std::fill (slice_fixel_sizes[axis].begin(), slice_fixel_sizes[axis].end(), vector());