From 8cdac20033fcbbb505d2e39cefcb7f506e854737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pe=C5=82ka?= Date: Thu, 24 Sep 2026 13:57:47 +0200 Subject: [PATCH 1/2] Step 3: per-edge delete, Ctrl+M loop closure, Save session poses - Manual loop closure: add a Delete button to each edge row, replacing "remove active edge". Deletion happens after the list is drawn and keeps index_active_edge pointing at the same edge. - Entering loop closure mode drops the session gizmo selection and disables the session Gizmo radio buttons, since session gizmos are not drawn there and would block the edge gizmo. - Bind Ctrl+M to toggle Manual Loop Closure (the menu wrongly showed Ctrl+L, which is Load sessions) and list it in the shortcuts table. - Add File > "Save session poses". save_results() now reports poses files that failed to write instead of ignoring the result. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../multi_session_registration.cpp | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/apps/multi_session_registration/multi_session_registration.cpp b/apps/multi_session_registration/multi_session_registration.cpp index dcb78e46..9139353e 100644 --- a/apps/multi_session_registration/multi_session_registration.cpp +++ b/apps/multi_session_registration/multi_session_registration.cpp @@ -79,7 +79,7 @@ static const std::vector appShortcuts = { { "Normal keys", "A", " { "", "L", "" }, { "", "Ctrl+L", "Load sessions" }, { "", "M", "" }, - { "", "Ctrl+M", "" }, + { "", "Ctrl+M", "Manual loop closure" }, { "", "N", "" }, { "", "Ctrl+N", "" }, { "", "O", "" }, @@ -404,6 +404,15 @@ void setActiveEdge(int i) void loop_closure_gui() { + // Session gizmos are not drawn in this mode and would block the edge gizmo, so drop the selection. + if (index_gizmo != -1) + { + index_gizmo = -1; + old_index_gizmo = -1; + for (auto& s : sessions) + s.is_gizmo = false; + } + if (ImGui::Begin("Manual Pose Graph Loop Closure Mode", &is_loop_closure_gui, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::Button("Optimize GRAPH")) @@ -507,6 +516,7 @@ void loop_closure_gui() is_session_gizmo |= s.is_gizmo; ImGui::BeginDisabled(is_session_gizmo); + int delete_edge_index = -1; for (int i = 0; i < (int)edges.size(); i++) { const auto& e = edges[i]; @@ -527,21 +537,36 @@ void loop_closure_gui() setActiveEdge(i); manipulate_active_edge = true; } + ImGui::SameLine(); + snprintf(buttonLabel, sizeof(buttonLabel), "Delete##%d", i); + if (ImGui::Button(buttonLabel)) + delete_edge_index = i; } ImGui::EndDisabled(); + + // Deleted after the loop so the list is not modified while it is drawn. The active edge keeps pointing at + // the same edge; deleting the active edge itself leaves manipulate mode, as "remove active edge" does. + if (delete_edge_index != -1) + { + edges.erase(edges.begin() + delete_edge_index); + + if (delete_edge_index == index_active_edge) + { + edge_gizmo = false; + manipulate_active_edge = false; + index_active_edge = edges.empty() ? -1 : std::clamp(delete_edge_index - 1, 0, (int)edges.size() - 1); + } + else if (delete_edge_index < index_active_edge) + { + index_active_edge--; + } + } if (edges.size() > 0) { if (ImGui::Checkbox("manipulate_active_edge", &manipulate_active_edge) && manipulate_active_edge) setActiveEdge(std::clamp(index_active_edge, 0, (int)edges.size() - 1)); if (manipulate_active_edge) { - int remove_edge_index = -1; - if (ImGui::Button("remove active edge")) - { - edge_gizmo = false; - remove_edge_index = index_active_edge; - } - int prev_index_active_edge = index_active_edge; if (!edge_gizmo) @@ -576,20 +601,6 @@ void loop_closure_gui() txt = "index_to: " + std::to_string(edges[index_active_edge].index_to); ImGui::Text(txt.c_str()); - if (remove_edge_index != -1) - { - std::vector new_edges; - for (size_t i = 0; i < edges.size(); i++) - { - if (remove_edge_index != i) - new_edges.push_back(edges[i]); - } - edges = new_edges; - - index_active_edge = edges.empty() ? -1 : std::clamp(remove_edge_index - 1, 0, (int)edges.size() - 1); - manipulate_active_edge = false; - } - const bool prev_gizmo = edge_gizmo; ImGui::Checkbox("gizmo", &edge_gizmo); @@ -1750,14 +1761,22 @@ bool revert_to_initial(std::vector& sessions) bool save_results(std::vector& sessions) { + std::string failed; for (auto& session : sessions) { if (!session.is_ground_truth) { std::cout << "saving result to: " << session.point_clouds_container.poses_file_name << std::endl; - session.point_clouds_container.save_poses(fs::path(session.point_clouds_container.poses_file_name).string(), false); + if (!session.point_clouds_container.save_poses(fs::path(session.point_clouds_container.poses_file_name).string(), false)) + failed += "'" + session.point_clouds_container.poses_file_name + "'\n"; } } + + if (!failed.empty()) + { + pfd::message("Saving poses failed", "Could not write poses file(s):\n" + failed, pfd::choice::ok, pfd::icon::error); + return false; + } return true; } @@ -2495,7 +2514,7 @@ void settings_gui() ImGui::BeginDisabled(!sessions[i].visible); { - ImGui::BeginDisabled(sessions[i].is_ground_truth); + ImGui::BeginDisabled(sessions[i].is_ground_truth || is_loop_closure_gui); { ImGui::SameLine(); if (ImGui::RadioButton(("Gizmo##" + std::to_string(i)).c_str(), &index_gizmo, i)) @@ -3614,6 +3633,16 @@ pose_tait_bryan_from_affine_matrix(m_src.inverse() * m_g); io.AddKeyEvent(ImGuiKey_L, false); io.AddKeyEvent(ImGuiMod_Ctrl, false); } + // Same enable rule as the "Manual Loop Closure" menu item; closing is always allowed. + if (is_loop_closure_gui || number_visible_sessions == 1 || number_visible_sessions == 2) + if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_M, false)) + { + is_loop_closure_gui = !is_loop_closure_gui; + + // workaround + io.AddKeyEvent(ImGuiKey_M, false); + io.AddKeyEvent(ImGuiMod_Ctrl, false); + } if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_O, false)) { openProject(); @@ -3671,7 +3700,12 @@ pose_tait_bryan_from_affine_matrix(m_src.inverse() * m_g); loadSessions(); ImGui::Separator(); + if (ImGui::MenuItem("Save session poses", nullptr, nullptr, loaded_sessions && !sessions.empty())) + save_results(sessions); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("Overwrite the poses file of every session except ground truth"); + ImGui::Separator(); if (ImGui::BeginMenu("Save all marked trajectories", sessions.size() > 0)) { if (ImGui::MenuItem("Save all as las/laz files")) @@ -4197,7 +4231,7 @@ pose_tait_bryan_from_affine_matrix(m_src.inverse() * m_g); // bool prev_is_loop_closure_gui ImGui::MenuItem( - "Manual Loop Closure", "Ctrl+L", &is_loop_closure_gui, (number_visible_sessions == 1 || number_visible_sessions == 2)); + "Manual Loop Closure", "Ctrl+M", &is_loop_closure_gui, (number_visible_sessions == 1 || number_visible_sessions == 2)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Manually connect overlapping scan sections"); From a1575c0d7d220ecd219f4e0f5f709d0314807eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pe=C5=82ka?= Date: Thu, 24 Sep 2026 13:57:47 +0200 Subject: [PATCH 2/2] Split large point clouds across GPU buffers to fix glDrawArrays crash rlLoadVertexBuffer() takes the byte size as int. TrajectoryViewer stores 28 bytes per point, so a 105M-point cloud (2.95 GB) overflowed it, the buffer got no storage and glDrawArrays segfaulted. ScanRenderer (steps 2 and 3) and the camera_lidar_calibration Renderer have the same limit at 16 bytes per point (~134M points). Add RaylibWidgets/PointBufferParts.h, which uploads interleaved vertices as VAO/VBO parts of at most 16M points and draws them one glDrawArrays per part, and use it in all three renderers. The part struct lives in its own GL-free header so raylib_render.hpp and Renderer.h don't pull in glad.h. gl_VertexID-based sparse drawing now restarts at each part. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/camera_lidar_calibration/Renderer.cpp | 33 ++----- apps/camera_lidar_calibration/Renderer.h | 6 +- .../TrajectoryViewer.cpp | 42 +++------ core/include/Core/raylib_render.hpp | 7 +- core/src/raylib_render.cpp | 84 ++++++++---------- .../include/RaylibWidgets/PointBufferPart.h | 15 ++++ .../include/RaylibWidgets/PointBufferParts.h | 85 +++++++++++++++++++ 7 files changed, 161 insertions(+), 111 deletions(-) create mode 100644 raylib_widgets/include/RaylibWidgets/PointBufferPart.h create mode 100644 raylib_widgets/include/RaylibWidgets/PointBufferParts.h diff --git a/apps/camera_lidar_calibration/Renderer.cpp b/apps/camera_lidar_calibration/Renderer.cpp index 1d16e3d3..431f9e78 100644 --- a/apps/camera_lidar_calibration/Renderer.cpp +++ b/apps/camera_lidar_calibration/Renderer.cpp @@ -4,6 +4,7 @@ // glad function pointers are compiled into raylib; the header only declares them #include "RendererShaders.h" #include "external/glad.h" +#include #include #include #include @@ -134,32 +135,14 @@ void Renderer::uploadCloud(const PointCloud& cloud) data.push_back(p.intensity); } - cloudVAO = rlLoadVertexArray(); - rlEnableVertexArray(cloudVAO); - cloudVBO = rlLoadVertexBuffer(data.data(), static_cast(data.size() * sizeof(float)), false); - const int stride = 4 * sizeof(float); // locations fixed by layout() qualifiers in both shaders - rlSetVertexAttribute(0, 3, RL_FLOAT, false, stride, 0); - rlEnableVertexAttribute(0); - rlSetVertexAttribute(1, 1, RL_FLOAT, false, stride, 3 * sizeof(float)); - rlEnableVertexAttribute(1); - rlDisableVertexArray(); - - cloudCount = static_cast(cloud.points.size()); + cloudCount = cloud.points.size(); + cloudParts = raylib_widgets::uploadPointBufferParts(data.data(), cloudCount, { 3, 1 }); } void Renderer::unloadCloudGPU() { - if (cloudVAO) - { - rlUnloadVertexArray(cloudVAO); - cloudVAO = 0; - } - if (cloudVBO) - { - rlUnloadVertexBuffer(cloudVBO); - cloudVBO = 0; - } + raylib_widgets::unloadPointBufferParts(cloudParts); cloudCount = 0; } @@ -219,9 +202,7 @@ void Renderer::renderImageOverlay( rlSetUniform(locPrjColorMode, &vp.colorMode, RL_SHADER_UNIFORM_INT, 1); rlSetUniform(locPrjDecim, &vp.drawDecim, RL_SHADER_UNIFORM_INT, 1); - rlEnableVertexArray(cloudVAO); - glDrawArrays(GL_POINTS, 0, cloudCount); - rlDisableVertexArray(); + raylib_widgets::drawPointBufferParts(cloudParts); rlDisableShader(); } @@ -283,9 +264,7 @@ void Renderer::draw3DCloud( rlSetUniform(locCamTex, &slot, RL_SHADER_UNIFORM_INT, 1); } - rlEnableVertexArray(cloudVAO); - glDrawArrays(GL_POINTS, 0, cloudCount); - rlDisableVertexArray(); + raylib_widgets::drawPointBufferParts(cloudParts); rlDisableShader(); } diff --git a/apps/camera_lidar_calibration/Renderer.h b/apps/camera_lidar_calibration/Renderer.h index 36990ced..4d0e4fe1 100644 --- a/apps/camera_lidar_calibration/Renderer.h +++ b/apps/camera_lidar_calibration/Renderer.h @@ -3,6 +3,7 @@ #include #include #include +#include #include using namespace calib; @@ -78,9 +79,8 @@ class Renderer // GPU point cloud (VAO shared by both shaders via fixed attrib locations) Shader pointShader = {}; bool shaderValid = false; - unsigned int cloudVAO = 0; - unsigned int cloudVBO = 0; - int cloudCount = 0; + std::vector cloudParts; + size_t cloudCount = 0; // 3D view shader uniforms int locMVP = -1, locColorMode = -1, locHeightRange = -1; int locMaxDist = -1, locOpacity = -1, locPointSize = -1, locDecim = -1; diff --git a/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp b/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp index 6b9fe3ba..e80ee8b8 100644 --- a/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp +++ b/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -163,8 +164,8 @@ using trajectory_viewer_shaders::kVS; struct GpuCloud { - unsigned int vao = 0, vbo = 0; - int count = 0; + std::vector parts; + size_t count = 0; float maxDist = 50.f; void upload(const std::vector& data, float mx) @@ -173,35 +174,16 @@ struct GpuCloud if (data.empty()) return; maxDist = mx; - vao = rlLoadVertexArray(); - rlEnableVertexArray(vao); - vbo = rlLoadVertexBuffer(data.data(), (int)(data.size() * sizeof(float)), false); - const int stride = 7 * sizeof(float); - rlSetVertexAttribute(0, 3, RL_FLOAT, false, stride, 0); - rlEnableVertexAttribute(0); - rlSetVertexAttribute(1, 1, RL_FLOAT, false, stride, 3 * sizeof(float)); - rlEnableVertexAttribute(1); - rlSetVertexAttribute(2, 1, RL_FLOAT, false, stride, 4 * sizeof(float)); - rlEnableVertexAttribute(2); - rlSetVertexAttribute(3, 1, RL_FLOAT, false, stride, 5 * sizeof(float)); - rlEnableVertexAttribute(3); - rlSetVertexAttribute(4, 1, RL_FLOAT, false, stride, 6 * sizeof(float)); - rlEnableVertexAttribute(4); - rlDisableVertexArray(); - count = (int)(data.size() / 7); + count = data.size() / 7; + parts = raylib_widgets::uploadPointBufferParts(data.data(), count, { 3, 1, 1, 1, 1 }); + } + void draw() const + { + raylib_widgets::drawPointBufferParts(parts); } void unload() { - if (vao) - { - rlUnloadVertexArray(vao); - vao = 0; - } - if (vbo) - { - rlUnloadVertexBuffer(vbo); - vbo = 0; - } + raylib_widgets::unloadPointBufferParts(parts); count = 0; } }; @@ -1621,9 +1603,7 @@ static void drawScene(AppState& s) rlSetUniform(s.locDecim, &s.drawDecim, RL_SHADER_UNIFORM_INT, 1); int sel = (s.isolateCamera && s.imgViewIdx >= 0 && s.imgViewIdx < (int)s.imageTsNs.size()) ? s.imgViewIdx : -1; rlSetUniform(s.locSel, &sel, RL_SHADER_UNIFORM_INT, 1); - rlEnableVertexArray(s.cloud.vao); - glDrawArrays(GL_POINTS, 0, s.cloud.count); - rlDisableVertexArray(); + s.cloud.draw(); rlDisableShader(); } } diff --git a/core/include/Core/raylib_render.hpp b/core/include/Core/raylib_render.hpp index 959db519..5bead031 100644 --- a/core/include/Core/raylib_render.hpp +++ b/core/include/Core/raylib_render.hpp @@ -25,6 +25,8 @@ #include "raylib.h" +#include + #include #include @@ -238,9 +240,8 @@ class ScanRenderer private: struct CloudGPU { - unsigned int vao = 0; - unsigned int vbo = 0; - int count = 0; + std::vector parts; + size_t count = 0; Eigen::Affine3d lastPose = Eigen::Affine3d::Identity(); bool hasPose = false; bool hasMarkColor = false; diff --git a/core/src/raylib_render.cpp b/core/src/raylib_render.cpp index 3413a7a7..a8b9af1a 100644 --- a/core/src/raylib_render.cpp +++ b/core/src/raylib_render.cpp @@ -7,6 +7,8 @@ #include "raylib_render_shaders.hpp" +#include + #include #include #include @@ -92,16 +94,7 @@ void ScanRenderer::shutdown() void ScanRenderer::unload(CloudGPU& cloud) { - if (cloud.vao) - { - rlUnloadVertexArray(cloud.vao); - cloud.vao = 0; - } - if (cloud.vbo) - { - rlUnloadVertexBuffer(cloud.vbo); - cloud.vbo = 0; - } + raylib_widgets::unloadPointBufferParts(cloud.parts); cloud.count = 0; } @@ -172,16 +165,8 @@ void ScanRenderer::rebuild(size_t index, const PointCloud& pc) return; } - gpu.vao = rlLoadVertexArray(); - rlEnableVertexArray(gpu.vao); - gpu.vbo = rlLoadVertexBuffer(data.data(), static_cast(data.size() * sizeof(float)), false); - rlSetVertexAttribute(0, 3, RL_FLOAT, false, kVertexStride, 0); - rlEnableVertexAttribute(0); - rlSetVertexAttribute(1, 1, RL_FLOAT, false, kVertexStride, 3 * sizeof(float)); - rlEnableVertexAttribute(1); - rlDisableVertexArray(); - - gpu.count = static_cast(data.size() / 4); + gpu.count = data.size() / 4; + gpu.parts = raylib_widgets::uploadPointBufferParts(data.data(), gpu.count, { 3, 1 }); gpu.lastPose = pc.m_pose; gpu.hasPose = true; } @@ -324,21 +309,24 @@ void ScanRenderer::draw( rlSetUniform(locColor_, color, RL_SHADER_UNIFORM_VEC4, 1); rlSetUniform(locColorMode_, &colorModeInt, RL_SHADER_UNIFORM_INT, 1); - rlEnableVertexArray(gpu.vao); - // Re-specifies both attributes' stride against the VAO's cached VBO - // binding every draw (cheap: a handful of GL calls per visible - // scan), since the VAO otherwise keeps whatever stride rebuild() - // baked in and there's no persistent "decimated" VAO to switch to. - rlEnableVertexBuffer(gpu.vbo); - rlSetVertexAttribute(0, 3, RL_FLOAT, false, byteStride, 0); - rlEnableVertexAttribute(0); - rlSetVertexAttribute(1, 1, RL_FLOAT, false, byteStride, 3 * sizeof(float)); - rlEnableVertexAttribute(1); - - const int drawCount = (gpu.count + stride - 1) / stride; - glDrawArrays(GL_POINTS, 0, drawCount); - ++lastDrawCallCount_; - lastVertexCount_ += drawCount; + for (const auto& part : gpu.parts) + { + rlEnableVertexArray(part.vao); + // Re-specifies both attributes' stride against the VAO's cached VBO + // binding every draw (cheap: a handful of GL calls per visible + // part), since the VAO otherwise keeps whatever stride rebuild() + // baked in and there's no persistent "decimated" VAO to switch to. + rlEnableVertexBuffer(part.vbo); + rlSetVertexAttribute(0, 3, RL_FLOAT, false, byteStride, 0); + rlEnableVertexAttribute(0); + rlSetVertexAttribute(1, 1, RL_FLOAT, false, byteStride, 3 * sizeof(float)); + rlEnableVertexAttribute(1); + + const int drawCount = (part.count + stride - 1) / stride; + glDrawArrays(GL_POINTS, 0, drawCount); + ++lastDrawCallCount_; + lastVertexCount_ += drawCount; + } rlDisableVertexArray(); } @@ -445,18 +433,20 @@ void ScanRenderer::drawCachedWithTransform( rlSetUniform(locYzOn_, &intersectionOff, RL_SHADER_UNIFORM_INT, 1); rlSetUniform(locXyOn_, &intersectionOff, RL_SHADER_UNIFORM_INT, 1); - rlEnableVertexArray(gpu.vao); - // Explicitly re-specified (not just inherited from the VAO's last - // binding) since draw() rebinds this same VAO's attributes with a - // decimation-widened stride every frame -- this draw always wants every - // cached point, independent of the current "sparse drawing" setting. - rlEnableVertexBuffer(gpu.vbo); - rlSetVertexAttribute(0, 3, RL_FLOAT, false, kVertexStride, 0); - rlEnableVertexAttribute(0); - rlSetVertexAttribute(1, 1, RL_FLOAT, false, kVertexStride, 3 * sizeof(float)); - rlEnableVertexAttribute(1); - - glDrawArrays(GL_POINTS, 0, gpu.count); + for (const auto& part : gpu.parts) + { + rlEnableVertexArray(part.vao); + // Explicitly re-specified (not just inherited from the VAO's last + // binding) since draw() rebinds this same VAO's attributes with a + // decimation-widened stride every frame -- this draw always wants every + // cached point, independent of the current "sparse drawing" setting. + rlEnableVertexBuffer(part.vbo); + rlSetVertexAttribute(0, 3, RL_FLOAT, false, kVertexStride, 0); + rlEnableVertexAttribute(0); + rlSetVertexAttribute(1, 1, RL_FLOAT, false, kVertexStride, 3 * sizeof(float)); + rlEnableVertexAttribute(1); + glDrawArrays(GL_POINTS, 0, part.count); + } rlDisableVertexArray(); rlDisableShader(); } diff --git a/raylib_widgets/include/RaylibWidgets/PointBufferPart.h b/raylib_widgets/include/RaylibWidgets/PointBufferPart.h new file mode 100644 index 00000000..11c46702 --- /dev/null +++ b/raylib_widgets/include/RaylibWidgets/PointBufferPart.h @@ -0,0 +1,15 @@ +#pragma once + +// Kept apart from PointBufferParts.h so headers can hold parts without pulling in glad.h. +namespace raylib_widgets +{ + + //! One VAO/VBO pair holding a contiguous slice of a point cloud. + struct PointBufferPart + { + unsigned int vao = 0; + unsigned int vbo = 0; + int count = 0; + }; + +} // namespace raylib_widgets diff --git a/raylib_widgets/include/RaylibWidgets/PointBufferParts.h b/raylib_widgets/include/RaylibWidgets/PointBufferParts.h new file mode 100644 index 00000000..c030e578 --- /dev/null +++ b/raylib_widgets/include/RaylibWidgets/PointBufferParts.h @@ -0,0 +1,85 @@ +#pragma once + +// Uploads a point cloud as several VAO/VBO pairs instead of one. raylib's +// rlLoadVertexBuffer() takes the byte size as int, so a single buffer +// overflows past 2 GiB (~76M points at 7 floats, ~134M at 4) and the +// following glDrawArrays reads from a buffer with no storage and crashes. +// Header-only (like Shaders.h) so core_raylib can use it without linking +// raylib_widgets. + +#include "external/glad.h" +#include "rlgl.h" + +#include + +#include +#include +#include +#include + +namespace raylib_widgets +{ + + //! 16M vertices -- at most 448 MB per buffer at 7 floats per vertex. + constexpr size_t kMaxVerticesPerPart = size_t(1) << 24; + + //! Uploads interleaved float vertices, kMaxVerticesPerPart per part. + //! @param data interleaved vertices, sum(attribSizes) floats each + //! @param vertexCount number of vertices in data + //! @param attribSizes float components per attribute, bound to locations 0, 1, ... in order + //! @return the uploaded parts, empty when vertexCount is 0 + //! @note gl_VertexID restarts at 0 in every part. + inline std::vector uploadPointBufferParts( + const float* data, size_t vertexCount, std::initializer_list attribSizes) + { + int floatsPerVertex = 0; + for (int s : attribSizes) + floatsPerVertex += s; + const int stride = floatsPerVertex * static_cast(sizeof(float)); + + std::vector parts; + for (size_t first = 0; first < vertexCount; first += kMaxVerticesPerPart) + { + PointBufferPart p; + p.count = static_cast(std::min(kMaxVerticesPerPart, vertexCount - first)); + p.vao = rlLoadVertexArray(); + rlEnableVertexArray(p.vao); + p.vbo = rlLoadVertexBuffer(data + first * floatsPerVertex, p.count * stride, false); + int location = 0; + int offset = 0; + for (int s : attribSizes) + { + rlSetVertexAttribute(location, s, RL_FLOAT, false, stride, offset * static_cast(sizeof(float))); + rlEnableVertexAttribute(location); + ++location; + offset += s; + } + rlDisableVertexArray(); + parts.push_back(p); + } + return parts; + } + + //! Issues one glDrawArrays(GL_POINTS) per part; the caller binds the shader and sets its uniforms. + inline void drawPointBufferParts(const std::vector& parts) + { + for (const PointBufferPart& p : parts) + { + rlEnableVertexArray(p.vao); + glDrawArrays(GL_POINTS, 0, p.count); + } + rlDisableVertexArray(); + } + + //! Releases every part's VAO/VBO and clears parts. + inline void unloadPointBufferParts(std::vector& parts) + { + for (const PointBufferPart& p : parts) + { + rlUnloadVertexArray(p.vao); + rlUnloadVertexBuffer(p.vbo); + } + parts.clear(); + } + +} // namespace raylib_widgets