Skip to content

Port mm-viewer and icp-log-viewer from nanogui to Dear ImGui - #79

Open
jlblancoc wants to merge 10 commits into
developfrom
feat/imgui-viewers-port
Open

Port mm-viewer and icp-log-viewer from nanogui to Dear ImGui#79
jlblancoc wants to merge 10 commits into
developfrom
feat/imgui-viewers-port

Conversation

@jlblancoc

@jlblancoc jlblancoc commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Migrates both GUI apps in mp2p_icp_viz (mm-viewer, icp-log-viewer) from mrpt::gui::CDisplayWindowGUI + nanogui to Dear ImGui (docking branch), on top of mrpt::imgui::CImGuiSceneView (ships with mrpt::gui >= 2.15.19).

  • Dear ImGui vendored once as a git submodule (mp2p_icp_viz/3rdparty/imgui, docking branch), built as a private static lib that is never installed/exported.
  • Shared infrastructure in mp2p_icp_viz/apps/imgui_app_common/: ImGuiAppShell (GLFW/GL/docking boilerplate + a setupDefaultLayout hook so each app defines its own default panel layout) and SimpleFileDialog (self-contained ImGui file browser, no native/system dialog dependency).
  • Both apps split into independent dockable panels (Map viewer/View/Maps/Travelling for mm-viewer; Control/Summary/Variables/Maps/Pairings/View/Manual for icp-log-viewer), with a default docking layout applied only on first run.
  • Feature parity kept with the old nanogui apps: layer visibility/recolorization, clip/FOV/ortho/2D view options, camera travelling keyframe animation, mouse/camera coordinate readout, keyboard navigation, pairing visualization, prior/uncertainty ellipsoids, and persisted UI/camera state (now under ~/.config/<app>/).
  • The small axis-corner gizmo mini-viewports are restored (kept in sync every frame) but currently inert under MRPT 2.x (CImGuiSceneView only renders the "main" viewport) — documented in agents.md since MRPT 3.x is expected to support this and the code should start working again unmodified.
  • Fixed an ImGui duplicate-ID bug found during testing (icp-log-viewer's global/local layer checkboxes collided when both maps share a layer name).

See ~/plans/mp2p_icp_imgui_port.md (not part of this repo) for the full phased plan/status.

Test plan

  • colcon build --packages-select mp2p_icp_viz builds clean
  • find install -iname '*imgui*' returns nothing (imgui never installed/exported)
  • mm-viewer manually run against mp2p_icp_core/demos/global_001.mm: load, layer toggles, recolorization, camera, default docking layout all verified visually
  • icp-log-viewer manually run against a real .icplog generated via icp-run -d: record load, stats, dynamic variables, pairings, default docking layout all verified visually
  • clang-format-14 --dry-run --Werror clean via mp2p_icp_core/scripts/formatter.sh --check
  • CI (colcon build/tests) — pending

Summary by CodeRabbit

  • New Features
    • Replaced the legacy map viewer and ICP log viewer UIs with dockable Dear ImGui-based interfaces (mm-viewer and icp-log-viewer).
    • Added persistent layouts/settings, richer dockable panels, interactive 3D viewing, and in-app export.
    • Introduced an ImGui-based open/save file dialog, plus georeferenced overlay support and optional extra 3D scene loading.
  • Documentation
    • Documented the ongoing ImGui migration and a known MRPT 2.x mini-viewport rendering limitation.
  • Build
    • Vendored Dear ImGui (docking) via a shared app-shell/common components and updated build/manifest to pull required graphics headers.

Adds mp2p_icp_viz/3rdparty/imgui as a git submodule (docking branch),
built once as a private static library (imgui::imgui, never installed)
via mp2p_icp_viz/3rdparty/imgui_static, shared by mm-viewer and
icp-log-viewer. Also adds a small ImGuiAppShell helper wrapping the
GLFW window/context/docking boilerplate common to both apps.

First step of the ongoing port from nanogui to Dear ImGui; see
~/plans/mp2p_icp_imgui_port.md.
Rewrites mm-viewer/main.cpp on top of the shared ImGuiAppShell and
mrpt::imgui::CImGuiSceneView, replacing the old CDisplayWindowGUI +
nanogui sidebar with dockable ImGui panels: "Map viewer" (file
load/export + coordinate readout), "View", "Maps", and "Travelling",
plus a "3D View" window hosting the scene. Feature set kept at parity
with the old app: layer visibility/recolorization, clip/FOV/ortho/2D
view options, camera travelling keyframe animation, mouse/camera
coordinate readout with map/ENU/lat-lon units, keyboard navigation,
and persisted UI/camera state across sessions.

Adds a reusable SimpleFileDialog (self-contained ImGui file browser,
no native/system dialog dependency) to imgui_app_common for the
Open/Export prompts, and a setupDefaultLayout hook on ImGuiAppShell so
each app can define its own default docking arrangement on first run
(Map viewer docked left, View/Maps/Travelling tabbed below it, 3D View
filling the remaining background area) without hardcoding app-specific
window names into the shared shell.

Part of the ongoing port from nanogui to Dear ImGui; see
~/plans/mp2p_icp_imgui_port.md.
Previously written next to the process's current working directory.
Now resolved via the same cfgpath.h helper already used for each
app's own settings file, landing at e.g. ~/.config/mm-viewer/mm-viewer.imgui.ini
instead of polluting whatever directory the app happens to be launched from.
Re-adds the "Map frame"/"ENU frame" axis-corner mini-viewports
(ensureMiniCornerViewports/updateMiniCornerView) that were dropped
during the initial ImGui port. mrpt::imgui::CImGuiSceneView only
renders the scene's "main" viewport under MRPT 2.x, so these stay
invisible for now, but are kept and updated every frame since MRPT 3.x
is expected to support rendering named viewports the same way. Noted
in agents.md so this isn't mistaken for dead code later.

Also splits the default docking layout so "Maps" gets its own docked
panel instead of being tabbed with View/Travelling.
Rewrites icp-log-viewer/main.cpp on the same ImGuiAppShell +
mrpt::imgui::CImGuiSceneView infrastructure used by mm-viewer, split
into dockable panels: Control (record selector, navigation, iteration
slider), Summary, Variables, Maps (layer visibility + export), View,
Pairings, and Manual (6-DoF pose nudge), plus a 3D View window. Feature
set kept at parity with the old app, including autoplay, keyboard
navigation, pairing visualization, prior/uncertainty ellipsoids, and
persisted UI/camera state. Point-cloud regeneration caching is now
driven by comparing mp2p_icp::render_params_t (like mm-viewer) instead
of an explicit regenerateMaps bool parameter, forced on record change.

Also fixes an ImGui duplicate-ID bug surfaced while testing: the
global/local layer checkboxes in the "Maps" panel used the bare layer
name as their ID, colliding whenever both maps share a layer name
(e.g. "2d_lidar" in the demo dataset) -- scoped with PushID/PopID.

Restores the small axis-corner gizmo mini-viewport (kept inert under
MRPT 2.x, same rationale as mm-viewer; see agents.md).

Part of the ongoing port from nanogui to Dear ImGui; see
~/plans/mp2p_icp_imgui_port.md.
Run via mp2p_icp_core/scripts/formatter.sh; no functional changes.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR migrates mm-viewer and icp-log-viewer from nanogui to Dear ImGui, adds a vendored docking build, introduces shared shell, asynchronous task, and file-dialog components, and updates map/log rendering, controls, persistence, and export flows.

Changes

Dear ImGui migration

Layer / File(s) Summary
Dear ImGui build integration
.gitignore, .gitmodules, mp2p_icp_viz/3rdparty/..., mp2p_icp_viz/CMakeLists.txt, mp2p_icp_viz/package.xml, .github/workflows/ros-build.yml
Adds the docking submodule, internal static-library target, GLFW/OpenGL linkage, and build dependencies.
Shared ImGui shell and dialogs
mp2p_icp_viz/apps/CMakeLists.txt, mp2p_icp_viz/apps/imgui_app_common/..., mp2p_icp_viz/apps/*/CMakeLists.txt
Adds ImGuiAppShell, AsyncTask, and SimpleFileDialog for shared application lifecycle, background work, and frame-driven file selection.
ICP log viewer migration
mp2p_icp_viz/apps/icp-log-viewer/main.cpp
Replaces the nanogui interface with state-driven ImGui panels, lazy log loading, ICP visualization, statistics, export, docking, and persistent settings.
Metric-map viewer migration
mp2p_icp_viz/apps/mm-viewer/main.cpp
Replaces the nanogui interface with ImGui windows, asynchronous map loading, MRPT scene rendering, cached visualization, camera controls, overlays, trajectories, and extra scene support.
Migration documentation
agents.md
Documents the in-progress Dear ImGui migration, MRPT 2.x limitation, and MRPT3 cleanup items.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Viewer
  participant ImGuiAppShell
  participant CImGuiSceneView
  participant ConfigFile
  User->>Viewer: launch viewer
  Viewer->>ConfigFile: load UI and camera settings
  Viewer->>ImGuiAppShell: initialize and run
  ImGuiAppShell->>Viewer: invoke renderFrame
  Viewer->>CImGuiSceneView: update scene and camera
  Viewer->>User: render dockable panels and 3D view
  User->>Viewer: change controls or select file
  Viewer->>CImGuiSceneView: rebuild visualization
  Viewer->>ConfigFile: save UI and camera settings
Loading

Possibly related PRs

  • MOLAorg/mp2p_icp#28: Adds related mm-viewer extra 3D-scene loading and rendering through ExtraVizLayer.
  • MOLAorg/mp2p_icp#73: Adds the related georeferenced polygon overlay functionality in mm-viewer.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: both viewers were ported from nanogui to Dear ImGui.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/imgui-viewers-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (4)
mp2p_icp_viz/apps/mm-viewer/main.cpp (4)

889-904: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

app.layerVisible[lyName] silently inserts false for unseen layers.

Using operator[] in this read path means any layer name not already present (e.g. if updateGuiAfterLoadingNewMap() was skipped after a partial load) defaults to hidden rather than visible, which is the opposite of the initialization at Line 638. Prefer an explicit lookup with a visible-by-default fallback.

♻️ Proposed change
-        if (!app.layerVisible[lyName])
+        const auto itV      = app.layerVisible.find(lyName);
+        const bool isVisible = (itV == app.layerVisible.end()) ? true : itV->second;
+        if (!isVisible)
         {
             continue;  // hidden
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp` around lines 889 - 904, Update the
visibility check in the layer-rendering loop around app.layerVisible to use an
explicit lookup instead of operator[]. Treat missing layer entries as visible by
default, while preserving the existing hidden-layer continue behavior for
entries explicitly set to false.

259-271: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Quadratic iteration over keyframes.

Restarting from begin() and std::advance()-ing on every iteration makes this O(n²). A single pass over the interpolator is equivalent and simpler.

♻️ Proposed refactor
     app.camTravellingLabels.clear();
-    for (size_t i = 0; i < app.camTravelling.size(); i++)
-    {
-        auto it = app.camTravelling.begin();
-        std::advance(it, static_cast<std::ptrdiff_t>(i));
-
-        app.camTravellingLabels.push_back(mrpt::format(
-            "[%02u] t=%.02fs pose=%s", static_cast<unsigned int>(i),
-            mrpt::Clock::toDouble(it->first), it->second.asString().c_str()));
-    }
+    size_t i = 0;
+    for (const auto& [t, p] : app.camTravelling)
+    {
+        app.camTravellingLabels.push_back(mrpt::format(
+            "[%02u] t=%.02fs pose=%s", static_cast<unsigned int>(i++),
+            mrpt::Clock::toDouble(t), p.asString().c_str()));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp` around lines 259 - 271, Update
rebuildCamTravellingLabels to iterate directly over app.camTravelling with a
single iterator-based pass, while maintaining a separate incrementing index for
the label number; remove the repeated begin() and std::advance() calls so label
generation is linear.

92-159: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Persisted UI state omits some panel fields.

colorIntensityIdx, recolorizeByFieldIdx, mouseUnitsIdx, animFPS, and travellingInterpIdx are part of the panel state but are not round-tripped through appCfg in mainShowGui() (Lines 1468-1522), unlike the neighbouring booleans/floats. Consider persisting at least the colormap selection for parity with the rest of the View/Maps options.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp` around lines 92 - 159, Update
mainShowGui() appCfg persistence to round-trip the panel fields
colorIntensityIdx, recolorizeByFieldIdx, mouseUnitsIdx, animFPS, and
travellingInterpIdx, including at minimum colorIntensityIdx. Keep their existing
AppState defaults and ensure loading and saving use the corresponding appCfg
entries alongside the neighboring view and animation settings.

1350-1363: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Coordinate footer lags one frame behind.

app.mouseCoordText/cameraLookText are updated by renderSceneOverlay(), which runs inside renderSceneWindow() — after renderMapViewerPanel() has already drawn them. Rendering the scene window first (or updating the strings before the panels) removes the one-frame lag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp` around lines 1350 - 1363, Update
renderFrame so renderSceneWindow runs before renderMapViewerPanel, ensuring
renderSceneOverlay refreshes app.mouseCoordText and cameraLookText before the
coordinate footer is drawn; preserve the remaining panel rendering order unless
required.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agents.md`:
- Around line 145-146: Remove the machine-specific
`~/plans/mp2p_icp_imgui_port.md` reference from the migration note in
`agents.md`, or replace it with a repository-relative plan location only if that
plan is added to the repository. Keep the Dear ImGui migration context intact.
- Around line 156-157: Align the documented and declared MRPT dependency for
CImGuiSceneView with its actual provider: update the agents.md requirement and
mp2p_icp_viz/package.xml to require MRPT 2.15.11 or newer and include
mrpt_imgui. Keep the unconditional includes in mm-viewer and icp-log-viewer
valid, or instead guard those features consistently and document the guarded
behavior.

In `@mp2p_icp_viz/apps/icp-log-viewer/main.cpp`:
- Around line 632-642: Update onExport and the export-related logic in
renderManualPanel to catch exceptions from DelayedLoadLog::get() instead of
allowing them to escape the ImGui frame. Report load or export failures through
the existing UI error-display mechanism, while preserving the current successful
export behavior.
- Around line 838-861: Update the index-change handling in rebuild_3d_view() to
reseed app.gtPose from the newly selected record’s lr.icpResult.optimal_tf.mean
pose whenever selectorIdx changes. Populate all six components before
renderManualPanel() uses the sliders, preserving the selected record’s current
pose and preventing stale values across records.
- Around line 626-630: Update rebuild_3d_view and its per-frame call path to
latch the failed record index when DelayedLoadLog::get() throws, then skip
rebuilding while the current index matches that latched failure. Reset the
failure latch whenever navigation changes the record index, while preserving
rebuild attempts for new indices.
- Around line 943-950: Update the min-quality guard to check
filteredFiles.empty() instead of files.empty() before moving filteredFiles into
files. Preserve the existing exception message and threshold handling so the
specific error is raised when all logs are filtered out.
- Around line 160-163: Change the default value of pairingsCov2CovDecim to
approximately 2.7 so it is valid log10-space for the existing std::pow(10.0,
...) consumption and [0, 3] slider range. Keep the persisted and re-read
configuration default aligned with this corrected value.

In `@mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp`:
- Around line 127-134: Update the double-click handling in SimpleFileDialog’s
selectable-entry logic so it assigns result only when the dialog is in Open
mode; in Save mode, double-clicking should update typedName_ but require the
Save button’s existing validation and confirmation flow.
- Around line 89-102: Update the directory iteration in the file-listing routine
to use error-code-based status checks: call entry.exists(ec), skip the entry
when ec is set or it does not exist, then call entry.is_directory(ec) and skip
on any resulting error before classifying directories or filtered files.
Preserve the existing dirs/files population behavior for entries whose status
resolves successfully.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp`:
- Around line 941-955: Invalidate the cached visualization whenever a new map is
loaded, not only when render_params_t changes. Update loadMapFile() or
updateGuiAfterLoadingNewMap() to mark the cache stale, then make the rebuild
condition around prevRenderParams also consume that invalidation before calling
app.theMap.get_visualization().
- Around line 273-278: Update loadMapFile so it loads into a temporary
metric_map_t and commits it to app.theMap only after all loading steps succeed.
Keep the existing app.layerNames, layerVisible, and theMapFileName bookkeeping
unchanged on failure, while preserving the current success-path updates and GUI
refresh behavior.
- Around line 1285-1297: Update the keyframe UI around app.camTravellingLabels
and the "##travellingKeys" combo so it is not presented as an inert interactive
selector: either add AppState selection tracking and handle ImGui::Selectable
results with corresponding goto/delete actions, or replace the combo with a
plain scrolling list. Ensure the chosen approach accurately reflects the
available keyframe behavior instead of hard-wiring the preview to the last
entry.
- Around line 1463-1465: Validate appCfgFile immediately after
get_user_config_file() and handle an empty path before constructing CConfigFile.
When the path is empty, avoid persistence by skipping the associated config
read/write flow or use an in-memory configuration; ensure later appCfg.write
calls cannot target an empty filename while preserving normal behavior for valid
paths.
- Around line 419-441: Update transformAndFormatSelectedPoint so case 1 and case
2 convert map-frame points to ENU using
app.theMap.georeferencing->T_enu_to_map.mean.inverseComposePoint when
georeferencing exists and applyGeoRef is false, matching the conversion used by
case 0. Format the converted point in the ENU and lat-lon modes while preserving
existing behavior for already-ENU points.
- Around line 871-880: Update onSaveLayers() to export only layers marked in
app.layerVisible, matching each selected layer name from app.layerNames. Derive
a distinct output filename for every selected layer before calling
saveMetricMapRepresentationToFile(), so exports do not overwrite one another;
leave unselected layers untouched.
- Around line 1056-1066: Move the app.openDialog.render() and
app.exportDialog.render() calls outside the “Map viewer” ID stack so dialog
visibility is independent of that window’s collapse or hiding. Update each
SimpleFileDialog instance to use a distinct popup ID, preserving the existing
loadMapFile/updateGuiAfterLoadingNewMap and onSaveLayers handling for their
respective dialogs.
- Around line 754-755: Update the playback timing around t advancement to use
real elapsed time between rendered frames rather than 1/animFPS, while
preserving the requested animation speed. Also clamp animFPS immediately after
the InputFloat handling to a valid positive range (for example 1–240) so
negative values cannot prevent playback from reaching t1; update any remaining
animFPS uses consistently.

---

Nitpick comments:
In `@mp2p_icp_viz/apps/mm-viewer/main.cpp`:
- Around line 889-904: Update the visibility check in the layer-rendering loop
around app.layerVisible to use an explicit lookup instead of operator[]. Treat
missing layer entries as visible by default, while preserving the existing
hidden-layer continue behavior for entries explicitly set to false.
- Around line 259-271: Update rebuildCamTravellingLabels to iterate directly
over app.camTravelling with a single iterator-based pass, while maintaining a
separate incrementing index for the label number; remove the repeated begin()
and std::advance() calls so label generation is linear.
- Around line 92-159: Update mainShowGui() appCfg persistence to round-trip the
panel fields colorIntensityIdx, recolorizeByFieldIdx, mouseUnitsIdx, animFPS,
and travellingInterpIdx, including at minimum colorIntensityIdx. Keep their
existing AppState defaults and ensure loading and saving use the corresponding
appCfg entries alongside the neighboring view and animation settings.
- Around line 1350-1363: Update renderFrame so renderSceneWindow runs before
renderMapViewerPanel, ensuring renderSceneOverlay refreshes app.mouseCoordText
and cameraLookText before the coordinate footer is drawn; preserve the remaining
panel rendering order unless required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6708ba0b-fff6-4aab-801e-4acdd35192a0

📥 Commits

Reviewing files that changed from the base of the PR and between a3b72bc and 109652f.

📒 Files selected for processing (17)
  • .gitignore
  • .gitmodules
  • agents.md
  • mp2p_icp_viz/3rdparty/imgui
  • mp2p_icp_viz/3rdparty/imgui_static/CMakeLists.txt
  • mp2p_icp_viz/CMakeLists.txt
  • mp2p_icp_viz/apps/CMakeLists.txt
  • mp2p_icp_viz/apps/icp-log-viewer/CMakeLists.txt
  • mp2p_icp_viz/apps/icp-log-viewer/main.cpp
  • mp2p_icp_viz/apps/imgui_app_common/CMakeLists.txt
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/ImGuiAppShell.h
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/SimpleFileDialog.h
  • mp2p_icp_viz/apps/imgui_app_common/src/ImGuiAppShell.cpp
  • mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp
  • mp2p_icp_viz/apps/mm-viewer/CMakeLists.txt
  • mp2p_icp_viz/apps/mm-viewer/main.cpp
  • mp2p_icp_viz/package.xml

Comment thread agents.md Outdated
Comment thread agents.md Outdated
Comment thread mp2p_icp_viz/apps/icp-log-viewer/main.cpp
Comment thread mp2p_icp_viz/apps/icp-log-viewer/main.cpp
Comment thread mp2p_icp_viz/apps/icp-log-viewer/main.cpp
Comment thread mp2p_icp_viz/apps/mm-viewer/main.cpp
Comment thread mp2p_icp_viz/apps/mm-viewer/main.cpp Outdated
Comment thread mp2p_icp_viz/apps/mm-viewer/main.cpp Outdated
Comment thread mp2p_icp_viz/apps/mm-viewer/main.cpp Outdated
Comment on lines +1463 to +1465
char appCfgFile[1024];
::get_user_config_file(appCfgFile, sizeof(appCfgFile), APP_NAME);
mrpt::config::CConfigFile appCfg(appCfgFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unchecked config path from get_user_config_file().

get_user_config_file() writes an empty string when HOME/XDG_CONFIG_HOME are unset or the buffer is too small (see mp2p_icp_viz/apps/libcfgpath/cfgpath.h). CConfigFile("") then operates on an empty filename, and the later appCfg.write(...) calls at Lines 1499-1522 will attempt to save it. Guard for the empty path and skip persistence (or fall back to an in-memory config).

🛡️ Proposed guard
     char appCfgFile[1024];
     ::get_user_config_file(appCfgFile, sizeof(appCfgFile), APP_NAME);
+    if (appCfgFile[0] == '\0')
+    {
+        std::cerr << "Warning: could not determine a user config file path; "
+                     "UI settings will not be persisted.\n";
+    }
     mrpt::config::CConfigFile appCfg(appCfgFile);

(and wrap the read/write blocks accordingly).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/mm-viewer/main.cpp` around lines 1463 - 1465, Validate
appCfgFile immediately after get_user_config_file() and handle an empty path
before constructing CConfigFile. When the path is empty, avoid persistence by
skipping the associated config read/write flow or use an in-memory
configuration; ensure later appCfg.write calls cannot target an empty filename
while preserving normal behavior for valid paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp (2)

194-203: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not silently overwrite Save targets.

Save mode returns success for an existing path without confirmation. The downstream mm-viewer::onSaveLayers() writes derived per-layer files, so existing exports can be replaced without an explicit user decision. Add overwrite confirmation or reject existing output targets, including the derived files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp` around lines 194
- 203, Update the path-selection logic around candidate and the mode_ checks so
Save mode does not return success when the target already exists. Require
explicit overwrite confirmation before accepting existing targets, and validate
the derived per-layer output files used by mm-viewer::onSaveLayers() rather than
checking only the base path.

194-198: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a regular file and avoid throwing filesystem checks.

exists(candidate) will also succeed for directories and the throwless exists overload is not being used, so permission/status problems can still bubble out of the GUI. Use the error_code overload and is_regular_file for Open mode, and report the filesystem error explicitly.

Proposed fix
+                std::error_code candidateEc;
-                if (mode_ == Mode::Open && !std::filesystem::exists(candidate))
+                if (mode_ == Mode::Open &&
+                    !std::filesystem::is_regular_file(candidate, candidateEc))
                 {
-                    errorMsg_ = "File does not exist.";
+                    errorMsg_ = candidateEc
+                                    ? "Unable to inspect file."
+                                    : "File does not exist or is not a regular file.";
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp` around lines 194
- 198, Update the Open-mode validation in the typedName_ candidate handling to
use non-throwing filesystem status checks with a std::error_code, require
candidate to be a regular file via is_regular_file, and set errorMsg_ to
explicitly report any filesystem error returned by the checks. Preserve the
existing success path for valid regular files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp`:
- Around line 194-203: Update the path-selection logic around candidate and the
mode_ checks so Save mode does not return success when the target already
exists. Require explicit overwrite confirmation before accepting existing
targets, and validate the derived per-layer output files used by
mm-viewer::onSaveLayers() rather than checking only the base path.
- Around line 194-198: Update the Open-mode validation in the typedName_
candidate handling to use non-throwing filesystem status checks with a
std::error_code, require candidate to be a regular file via is_regular_file, and
set errorMsg_ to explicitly report any filesystem error returned by the checks.
Preserve the existing success path for valid regular files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2f8459c-c61d-43cb-8e7a-64fb013308de

📥 Commits

Reviewing files that changed from the base of the PR and between 109652f and b9c091b.

📒 Files selected for processing (6)
  • .github/workflows/ros-build.yml
  • agents.md
  • mp2p_icp_viz/apps/icp-log-viewer/main.cpp
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/SimpleFileDialog.h
  • mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp
  • mp2p_icp_viz/apps/mm-viewer/main.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/SimpleFileDialog.h
  • agents.md
  • mp2p_icp_viz/apps/icp-log-viewer/main.cpp
  • mp2p_icp_viz/apps/mm-viewer/main.cpp

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.07%. Comparing base (a3b72bc) to head (d597e00).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop      #79   +/-   ##
========================================
  Coverage    72.07%   72.07%           
========================================
  Files          155      155           
  Lines         6790     6790           
  Branches       861      861           
========================================
  Hits          4894     4894           
  Misses        1896     1896           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

mrpt::imgui::CImGuiSceneView.h (now used directly by mp2p_icp_viz)
pulls in mrpt/opengl/opengl_api.h, which unconditionally includes
<GL/glut.h> on Linux. There is no rosdep key for freeglut3-dev, so
rosdep install silently skips it and the build fails at the first
translation unit that touches the header. Install it directly via
apt, matching the existing pattern for other non-rosdep packages in
this workflow.

missing glut dep for mrpt2
SimpleFileDialog:
- Give each instance a unique popup ID (derived from `this`); mm-viewer's
  openDialog/exportDialog previously shared the same hardcoded ID.
- Move dialog rendering to top level in both apps, outside any other
  window's Begin/End -- nesting it meant collapsing that window made
  BeginPopupModal() fail to start, which was treated as a dismiss.
- Use the error-code overload of is_directory() during listing instead
  of the throwing one.
- Save mode no longer confirms on double-click (only Open does); Save
  always goes through the Save button and its checks.

icp-log-viewer:
- Fix pairingsCov2CovDecim default (500.0f instead of its log10, ~2.7):
  consumed as pow(10, x) with a slider range of [0,3], so until the
  slider was touched this overflowed to inf and then to UB in the
  static_cast to size_t.
- Fix the min-quality filter checking the wrong (unfiltered) list, so
  the "no logs passed" error never actually fired.
- Guard against repeatedly retrying (and re-logging) a record that
  failed to load, and stop it from being able to throw out of the
  ImGui frame from onExport()/renderManualPanel().
- Seed the manual pose-nudge sliders from each record's own pose
  instead of leaving stale values from whatever record was open before.

mm-viewer:
- onSaveLayers() now honors layer visibility (matching its "marked
  layers" label) and writes one file per layer instead of clobbering
  the same path for every layer.
- Fix enu/lat-lon mouse-coordinate readout when a georeferenced map is
  loaded but "Apply georeferenced pose" is unchecked.
- Force point-cloud regeneration when a new map is loaded, instead of
  only on render_params_t changes (avoids showing stale geometry if a
  different file happens to produce an equal render_params_t).
- Clear layer bookkeeping alongside the map on a failed load.
- Render the keyframe list as plain read-only text instead of a combo
  that looked interactive but ignored selection.
- Clamp the animation FPS input; warn (instead of silently no-op-ing)
  if no user config directory could be resolved.

agents.md: correct the MRPT minimum for CImGuiSceneView (2.15.11, not
2.15.19) and drop the machine-local plan file path.
…operly

Save mode returned success for an existing path with no confirmation --
unlike a native OS save dialog (which the old nanogui app got automatically
via nanogui::file_dialog()), this in-app dialog had no overwrite protection
of its own. Add a small nested confirmation popup, only reached when the
target already exists.

Also switch the Open-mode existence check to the non-throwing
is_regular_file() overload (instead of exists(), which also passes for
directories and has a throwing default overload).
…hread

Loading a large .mm file (real-world example: a 938MB / 35M-point map)
blocked the main/GL thread for the whole duration of both the file I/O
and the subsequent theMap.get_visualization() call. Since that thread
also owns the GLFW event loop, it stopped calling glfwPollEvents() for
long enough that the window manager concluded the process was hung and
offered to kill it, even though the app would have kept responding once
given the chance.

Adds a small reusable AsyncTask<T> helper (imgui_app_common) that runs
a computation via std::async and is polled once per frame instead of
blocked on. loadMapFile() is split into a self-contained worker (safe
to run off-thread, touches no AppState) plus a cheap main-thread commit
step; get_visualization() is similarly dispatched on a background
thread, working off a shallow copy of the map (cheap: shares the
underlying layer pointers) and tagged with a generation counter so a
stale result (superseded by a newer map load while it was still
building) is discarded instead of overwriting the current view.

The CLI-arg initial load stays synchronous: no GUI window exists yet
at that point, so there is nothing for the window manager to watch.

Verified against a real 938MB/35M-point map (loads, builds its full
visualization, and stays interactive throughout) and against the small
demo map (no regression).
@jlblancoc
jlblancoc force-pushed the feat/imgui-viewers-port branch from 2e8da2a to d597e00 Compare July 28, 2026 05:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/AsyncTask.h`:
- Around line 25-28: Update AsyncTask::start to reject calls while a task is
already running, returning a success/failure result without replacing the active
future; only launch and mark running for accepted starts. Adjust the
startMapLoad callers for mapLoadTask and vizBuildTask to handle the rejected
result and preserve the existing load flow without blocking or overwriting an
in-progress task.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1fe20e7-2df3-4340-833c-593efbec800e

📥 Commits

Reviewing files that changed from the base of the PR and between b9c091b and d597e00.

📒 Files selected for processing (9)
  • .github/workflows/ros-build.yml
  • agents.md
  • mp2p_icp_viz/apps/icp-log-viewer/main.cpp
  • mp2p_icp_viz/apps/imgui_app_common/CMakeLists.txt
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/AsyncTask.h
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/SimpleFileDialog.h
  • mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp
  • mp2p_icp_viz/apps/mm-viewer/main.cpp
  • mp2p_icp_viz/package.xml
🚧 Files skipped from review as they are similar to previous changes (7)
  • mp2p_icp_viz/package.xml
  • mp2p_icp_viz/apps/imgui_app_common/CMakeLists.txt
  • mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/SimpleFileDialog.h
  • agents.md
  • mp2p_icp_viz/apps/imgui_app_common/src/SimpleFileDialog.cpp
  • mp2p_icp_viz/apps/icp-log-viewer/main.cpp
  • mp2p_icp_viz/apps/mm-viewer/main.cpp

Comment on lines +25 to +28
void start(std::function<T()> fn)
{
future_ = std::async(std::launch::async, std::move(fn));
running_ = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify every AsyncTask start site is protected against overlapping work.
rg -n -C 5 '\.(mapLoadTask|vizBuildTask)\.start\(' mp2p_icp_viz/apps
rg -n -C 5 'startMapLoad|isRunning\(\)' mp2p_icp_viz/apps

Repository: MOLAorg/mp2p_icp

Length of output: 5740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AsyncTask.h =="
cat -n mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/AsyncTask.h

echo
echo "== mapLoadTask declarations/usages =="
rg -n -C 4 'mapLoadTask|vizBuildTask|isLoadingMap|isBuildingViz|startMapLoad|pollMapLoad|pollBuildTask' mp2p_icp_viz/apps/mm-viewer/main.cpp

echo
echo "== behavioral probe: std::future assignment under std::launch::async task running =="
cat > /tmp/async_reassign_probe.cpp <<'CPP'
`#include` <future>
`#include` <mutex>
`#include` <condition_variable>
`#include` <chrono>
`#include` <iostream>

void no_move_test()
{
    bool running = false;
    std::mutex m;
    std::condition_variable wake_for_release, done;

    {
        std::future<void> future = std::async(std::launch::async, [&](){
            {
                std::lock_guard<std::mutex> lock(m);
                running = true;
                wake_for_release.wait(lock);
            }
            done.notify_all();
        });

        {
            std::unique_lock<std::mutex> lock(m);
            done.wait(lock, [&]{ return running; });
        }

        std::cerr << "waiting_before_assignment:";
        auto start = std::chrono::steady_clock::now();
        std::future<void> replacement;
        replacement = std::async(std::launch::async, [](){ std::cerr << "not_reached\n"; });
        auto end = std::chrono::steady_clock::now();
        std::cerr << " duration_ms="
                  << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
                  << "\n";
        replacement = std::move(future);
    }
}

int main()
{
    no_move_test();
}
CPP

if command -v c++ >/dev/null 2>&1; then
  c++ -std=c++17 -pthread /tmp/async_reassign_probe.cpp -o /tmp/async_reassign_probe
  echo "running /tmp/async_reassign_probe"
  timeout 10 /tmp/async_reassign_probe || true
else
  echo "c++ unavailable"
fi

Repository: MOLAorg/mp2p_icp

Length of output: 8621


🌐 Web query:

C++ std::future destructor wait for ready state std::launch::async documentation

💡 Result:

In C++, the destructor of a std::future object typically does not block, with one specific exception: when the future is associated with a shared state created by a call to std::async using the std::launch::async policy [1][2][3]. If a std::future is obtained from std::async and satisfies the following three conditions, its destructor will block until the asynchronous task completes [1][3]: 1. The shared state was created by a call to std::async [1][3]. 2. The shared state is not yet ready (the task has not completed) [1][3]. 3. The std::future object is the last reference to that shared state (i.e., it has not been moved from) [1][3]. This behavior essentially makes the asynchronous operation synchronous for temporary std::future objects [2][4]. For example, the following code will execute sequentially because the destructor of the first future blocks until the task completes before the second line is executed: std::async(std::launch::async, []{ /* task 1 / }); // Destructor blocks here until task 1 finishes std::async(std::launch::async, []{ / task 2 */ }); // Task 2 does not start until task 1 is done This blocking mechanism exists to ensure that the associated thread, which may depend on local variables in the scope where std::async was called, is properly joined and does not access invalid memory after the scope ends [5]. Destructors of std::futures obtained through other means, such as std::promise or std::packaged_task, do not block [2][4].

Citations:


Guard AsyncTask against replacing an active future.

start() assigns to future_ before clearing running_; when that task is still pending and its shared state has no other references, std::future destructor assignment can block on the async thread completion. If startMapLoad() is called twice before pollMapLoad() completes, the UI load path can stall for the previous map load. Reject overlapping start() calls explicitly and update mapLoadTask.start() / vizBuildTask.start() callers to handle the result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mp2p_icp_viz/apps/imgui_app_common/include/imgui_app_common/AsyncTask.h`
around lines 25 - 28, Update AsyncTask::start to reject calls while a task is
already running, returning a success/failure result without replacing the active
future; only launch and mark running for accepted starts. Adjust the
startMapLoad callers for mapLoadTask and vizBuildTask to handle the rejected
result and preserve the existing load flow without blocking or overwriting an
in-progress task.

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