Step 3: stop re-uploading GPU buffers while dragging a session's gizmo - #553
Draft
michalpelka wants to merge 11 commits into
Draft
michalpelka wants to merge 11 commits into
michalpelka wants to merge 11 commits into
Conversation
Dragging a session's gizmo mutated every one of its scans' m_pose every frame, which made ScanRenderer::syncPoses() tear down and re-upload each scan's full point buffer every frame for as long as the drag lasted. Merely having a gizmo selected (not dragging) also triggered this: a double -> float -> double round trip through the gizmo matrix was enough float noise to look like a pose change every frame. While ImGuizmo::IsUsing() is true, the session's candidate pose chain is now stashed in session_drag_preview_poses instead of being written into the session, and the render loop draws it via ScanRenderer::drawCachedWithTransform() (pose folded into the MVP, no GPU re-upload) until the drag ends, at which point the pose is committed for real -- the only frame that triggers a rebuild. drawCachedWithTransform() took a flat/intensity bool, so the drag preview rendered flat-colored instead of matching the live per-point shading (View > Points color, elevation/distance bounds) the rest of the scene uses. It now takes the real ScanColorMode (plus the elevation/distance bounds Elevation/Distance need), defaulting to Flat so the existing loop-closure edge preview callers (step 2 and step 3) are unaffected; the drag preview opts in via renderScanAtPose()'s new useSceneColorMode flag. Also removes ~200 lines of dead, commented-out pre-raylib-port code in multi_session_registration.cpp's display() that referenced a `session` variable and GLUT/legacy-GL calls no longer present in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The +/- InputInt was the only way to change the active loop-closure edge, which is painfully slow with many edges on a large project. Adds a SliderInt next to it for quick jumps. Fixes MapsHD#549. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same InputInt-only problem as index_active_edge (MapsHD#549): no fast way to scrub to a far-away scan index when setting up a loop closure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SameLine() between slider and input, matching num_edge_extended_before/after's existing layout, instead of each on its own row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ported from step 2's per-session "Intersections" menu. Step 3 has no single active session, so (same pattern as View > Points size) the width and three toggles are global and get pushed to every loaded session's point_clouds_container when changed. The renderer already read these fields (ScanRenderer's xzOn/yzOn/xyOn shader uniforms); step 3 just had no UI for them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
In every "ICP" button's is_with_ground_truth branch, `source` was built from edges[index_active_edge]'s raw index_session_to/index_to instead of the already-reordered index_session_to/index_to locals. Those locals get swapped when the *target* (not source) side of the edge is the ground truth session; source wasn't tracking that swap, so ICP silently aligned the ground-truth scan against a filtered copy of itself whenever ground truth was on the edge's "to" side, and the registration error grew with every iteration. Pre-existing bug, not introduced by this branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Step 2's save_intersection() (per-session xz/yz/xy slab export) only existed in multi_view_tls_registration.cpp, so step 3 couldn't reuse it without copy-pasting the filter loop. Move it into Core/export_laz.h (already included by both apps) as the one shared implementation. Also fixes a latent array-desync bug while moving it: a point past the end of p.timestamps was pushed to `intensity` and `pointcloud` but not to `timestamps`, leaving the three parallel arrays different lengths. Now defaults to 0.0, matching the existing save_all_to_las() pattern in the same header. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ys + export - drawCachedWithTransform() now sets the xz/yz/xy slab-filter uniforms from the caller instead of hardcoding them off, so the loop-closure edge view and the gizmo-drag preview respect View > Intersections like the main render does. - Consolidated the loop-closure panel's separate "gui_point_size" (which unconditionally overwrote every session's point_size every frame, fighting View > Points size) into the one point_size control. - Added step 2's remaining Intersections-menu features: 10m/1m/0.1m grid overlays per xz/yz/xy plane (drawIntersectionGrids(), ported to rlgl) and "Export xz/yz/xy intersection" (one laz per session, auto-named -- step 3 has no single active session to pick a file dialog for). Moved the menu to the top level to match step 2's ordering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The legacy GLUT step 2 app (forked in 39ffca5) kept its own save_intersection() in multi_view_tls_registration.{h,cpp}, left over from before 3e626aa moved the shared implementation into Core/export_laz.h. Since the legacy gui.cpp already includes that header too, both overloads were visible and MSVC failed to pick one (C2668), breaking the Windows CI build. Drop the legacy duplicate so it shares the same Core/export_laz.h implementation as the raylib-based step 2 app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Unlike assert(), it is never compiled out by a release build, so a failed invariant is never silently swallowed. Debug builds abort immediately; release builds log and continue by default, since aborting a shipped app on an invariant violation is worse for users than limping on in a degraded state. The new HDMAPPING_ASSERT_ALWAYS_ABORT CMake option forces the abort even with NDEBUG defined, for debugging a RelWithDebInfo/Release build without flipping NDEBUG (and everything else gated on it) project-wide. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
michalpelka
marked this pull request as draft
October 5, 2026 15:12
michalpelka
force-pushed
the
mp/step3-fix-gizmo-drag-gpu-churn
branch
from
October 5, 2026 15:24
d3aa9e1 to
3fa8825
Compare
…ycle Harden the manual loop-closure edge list against the crashes hit while selecting edges with only some sessions visible: - canEdgeBeActivated() checked the global index_active_edge instead of its own parameter, and its bounds guard was inverted, so it fell through to edges.at(index_active_edge) precisely when that index was invalid (-1 on the common "no edge selected yet" path) -- guaranteed std::out_of_range. It also checked index_from/index_to (scan indices) against visible_sessions, which holds session indices. Now it checks the passed-in edge index's own index_session_from/to, bounds-safely. - setActiveEdge() asserted visible_sessions.contains(first_session_index) / second_session_index -- the previously active edge's sessions, still stale at that point -- instead of the sessions of the edge actually being activated. Reordered to read the edge once and validate everything against it before any assignment. - settings_gui()'s per-frame visibility sync inserted the stale first_session_index into visible_sessions instead of the loop's own session index, so visible_sessions (a set) collapsed to at most one lagging entry instead of the real set of visible sessions -- undermining every check above that gates on it. - display()'s manipulate_active_edge rendering block mixed the same first_session_index/second_session_index globals (resynced every frame by settings_gui() for the unrelated "pick two sessions for a new edge" UI) with edges[index_active_edge]'s own session fields: an index range bounds-checked against one session's point-cloud count was then used to index a *different* session via .at(), e.g. walking a range sized for a 576-scan session while indexing a 177-scan one. Introduced active_session_from/active_session_to read once from the active edge and used consistently, matching dead commented-out code right above it that already did this correctly. Also fixed an adjacent bounds check comparing first_session_index itself (a session index) against a point-cloud count instead of the scan index (index_loop_closure_source) actually used. - The "Set Active" button's BeginDisabled/EndDisabled wrapped the already-executed click body instead of the button itself, so it never actually blocked anything; moved it to wrap the button. Also fix on_loop_closure_gui_open/close() never firing when the loop-closure window is closed via its own title-bar X: that flips is_loop_closure_gui from inside the very call the old code checked before, so the transition was always missed. The close check now runs after loop_closure_gui() so it observes the post-click value. Factor the five duplicated "two sessions ground truth -> skip, else trim ground truth cloud to bbox" ICP button bodies into shared icp_active_edge()/ground_truth_icp_source_target() helpers, and link spdlog::spdlog explicitly (it was only reaching this target transitively via core's PRIVATE link, without the fmt dependency spdlog's compiled logger needs). main()'s outer catch (const std::exception&)/catch (...) around the whole app silently swallowed any uncaught exception -- including exactly the bugs above -- printed e.what() with no trailing newline, and returned 0, so a real invariant violation looked like a clean exit. Removed both, keeping only the bad_alloc handler (a deliberate, distinct OOM dialog); any other uncaught exception now reaches std::terminate() and aborts loudly, matching HDMAPPING_ASSERT's "never silently continue" design. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
michalpelka
force-pushed
the
mp/step3-fix-gizmo-drag-gpu-churn
branch
from
October 5, 2026 15:24
3fa8825 to
6432f6d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
m_poseevery frame, which forcedScanRenderer::syncPoses()to tear down and re-upload each scan's full GPU point buffer every single frame for as long as the drag lasted (visible as a stream ofVAO: [ID ...] Unloaded...log lines while dragging). Merely selecting a gizmo (not even dragging) triggered the same rebuild, because adouble -> float -> doubleround trip through the gizmo matrix was enough float noise to look like a pose change every frame.ImGuizmo::IsUsing()is true, the candidate pose chain is now stashed insession_drag_preview_posesinstead of being written into the session; the render loop draws it viaScanRenderer::drawCachedWithTransform()(pose folded into the MVP, no GPU re-upload) until the drag ends, at which point the pose is committed for real — the only frame that triggers an actual rebuild.drawCachedWithTransform()only supported a flat/intensity bool, so the drag preview rendered flat-colored instead of matching the scene's live per-point shading (View > Points color, elevation/distance bounds). It now takes the realScanColorModeplus the elevation/distance bounds those modes need, defaulting toFlatso the existing loop-closure edge preview callers (step 2 and step 3) keep their current look; the drag preview opts in viarenderScanAtPose()'s newuseSceneColorModeflag.multi_session_registration.cpp'sdisplay()(referenced asessionvariable and GLUT/legacy-GL calls no longer present in this file).Test plan
multi_session_registration_step_3andmulti_view_tls_registration_step_2both build cleanly (RelWithDebInfo, clang-18).VAO: [ID ...] Unloaded...per frame, and the dragged session now shades the same way as the rest of the scene instead of going flat.gizmo_all_sessions(move-everything) on a multi-session project before merging.🤖 Generated with Claude Code