Skip to content

[POC] Replace GLUT with Raylib - #467

Merged
JanuszBedkowski merged 17 commits into
mainfrom
mp/rl_step2
Jul 30, 2026
Merged

[POC] Replace GLUT with Raylib #467
JanuszBedkowski merged 17 commits into
mainfrom
mp/rl_step2

Conversation

@michalpelka

@michalpelka michalpelka commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

To say that HDMapping use legacy method to draw is to say nothing.

multi_view_tls_registration (step2) was still running on GLUT plus immediate-mode OpenGL straight out of the 90s — glBegin/glVertex, gluPerspective, gluUnProject, raster-position bitmap fonts, the lot. This PR moves it to raylib, in place, keeping the app's existing structure (globals, function
names, control flow) as close to the original as swapping the rendering backend allows.

#What changed

  • multi_view_tls_registration_gui.cpp now runs on raylib instead of GLUT + legacy GL. The camera/picking/mini-compass/misc-widget code it used to pull from the shared core/src/utils.cpp (still used by several other GLUT apps, so it couldn't be touched) is now a local reimplementation in the app's
    own rl_utils.cpp/rl_utils.h, built on rlgl's legacy-GL-emulation API (a software matrix stack + immediate-mode layer) instead of real gl*/glu*/glut* calls.
  • Point-cloud and loop-closure rendering — previously core's shared legacy-GL PointCloud::render()/PointClouds::render()/ManualPoseGraphLoopClosure::Render() — now goes through a new core_raylib target (Core/raylib_render.hpp's ScanRenderer): a real GPU shader-based point renderer.
  • New, not something the legacy renderer could do: per-point jet-colormap coloring by intensity, height, or distance from the rotation center (View > Point cloud).
  • Pose/edge index labels are back in the loop-closure overlay, projected to screen space by hand since there's no glutBitmapString equivalent under a core GL profile.

Porting was done with Sonnet 5 with Claude Code.

New dependencies and licensing

Two new third-party deps come in via cmake/raylib.cmake (FetchContent): raylib and rlImGui, both zlib-licensed — same permissive family as this project's MIT license, no new licensing friction. Dear ImGui itself isn't duplicated: imgui_raylib builds from the same already-vendored 3rdparty/imgui tree
the GLUT apps' imgui target uses (it's on the docking branch, which ShowMainDockSpace() needs), just without the GLUT/OpenGL2 backend files.

Strucutre

Raylib has thin abstraction for OpenGL, that is very handy in this project. It introduce intermediate mode, emulates old GL2 mechanism making conversion quite simple.

The split deliberately mirrors the project's existing architecture rather than inventing a new one:

  • rl_utils.cpp/rl_utils.h plays the same role for this app that core/src/utils.cpp/Core/utils.hpp plays for the GLUT apps — same function names, same "shared camera/picking/widget glue, separate from the app's own main file" split.
  • core_raylib sits alongside core/core_no_gui as a third target, the same way the project already splits GUI/no-GUI builds — gated behind whether raylib is actually needed, not linked into anything that doesn't use it.

Anyone already familiar with the GLUT apps' structure should find this one recognizable, not foreign.

Pictures

Screenshot from 2026-07-25 14-44-39 Screenshot from 2026-07-25 14-45-33

What to do next

It is not ready to be merged - the code is AI slop. But working prototype with managable diff.
I would like to continue, but I need to:

  • create test scenarios for manual testing (based on Janusz's videos on yt)
  • check current main for regression
  • check / fix all bugs introduced by this refactor
  • fix CI errors / deployments on all platforms

michalpelka and others added 2 commits July 25, 2026 03:10
Replaces GLUT + legacy immediate-mode OpenGL with raylib in place, in
multi_view_tls_registration_gui.cpp: same globals, function names, and
structure as before, but windowing/input/camera/picking/mini-compass
(previously from the shared core/src/utils.cpp, which other GLUT apps
still depend on and so can't be changed) are now local re-implementations
using rlgl's rl*() legacy-GL-emulation API. Point-cloud and loop-closure
rendering (previously core's shared legacy-GL .render() methods) go
through a new core_raylib target's ScanRenderer instead, which also adds
per-point jet-colormap gradient coloring by intensity, height, or distance
from the rotation center (view menu), and pose/edge index number labels in
the loop closure overlay -- none of which the GLUT app had.

GNSS/ControlPoints/GroundControlPoints 3D rendering and the observation-
picking Intersection wireframes are not yet ported (their ImGui panels
still work); noted with comments at each call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
Moves the ~1300 lines of camera/picking/mini-compass/misc-ImGui-widget
code (this app's raylib-based replacement for the shared core/src/utils.cpp
API) out of multi_view_tls_registration_gui.cpp into their own
rl_utils.cpp/rl_utils.h files, mirroring how core/src/utils.cpp /
Core/utils.hpp were structured for the GLUT apps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
@michalpelka

Copy link
Copy Markdown
Contributor Author

Here is good example why Raylib is great choice:
image

michalpelka and others added 5 commits July 25, 2026 14:57
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
raylib (new dependency, fetched via cmake/raylib.cmake for the
multi_view_tls_registration raylib port) builds GLFW from source, which
needs libxrandr-dev/libxinerama-dev/libxcursor-dev/libxext-dev to
configure its X11 backend -- CI's minimal images don't have these by
default, unlike this dev machine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
raylib.h's DrawText/CloseWindow/ShowCursor collide with windows.h's
identically-named GDI macro and WinUser functions once both headers land
in the same translation unit (something this app never did before, since
the original never included raylib.h). Fixed with NOGDI/NOUSER before
<windows.h> in rl_utils.cpp (the only file that still needs windows.h, for
ShellExecuteA), pinned with clang-format off/on since alphabetical
reordering silently reintroduces the windows.h-before-shellapi.h ordering
bug this also needs to avoid. gui.cpp's own <windows.h> include is dropped
entirely -- nothing in it actually used anything from windows.h.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
The previous fix only covered rl_utils.cpp. gui.cpp itself pulls in real
windows.h transitively via portable-file-dialogs.h (needed for
SendMessage/DispatchMessage/MessageBoxW/GetActiveWindow, so NOGDI/NOUSER
isn't an option here like it was in rl_utils.cpp), which collides with
raylib.h's CloseWindow(void)/ShowCursor(void) declarations -- a hard
extern-"C"-redeclaration error regardless of include order, not just a
macro issue. Renamed raylib's versions via macro just around its own
#include, and #undef DrawText after the last include (windows.h #defines
it to DrawTextA) so plain DrawText/CloseWindow calls later in the file
keep meaning raylib's functions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
The previous attempt renamed raylib.h's CloseWindow/ShowCursor around its
own #include, which avoided the compile-time redeclaration clash but broke
the link: the compiled raylib library still only exports the symbol under
its real name, so the renamed *declaration* just became an unresolved
external symbol. Flipped it around: rename windows.h's versions instead
(scoped to just the portable-file-dialogs.h include, which is what
actually pulls windows.h in) -- safe since portable-file-dialogs.h itself
never calls CloseWindow/ShowCursor, so nothing there breaks, and raylib's
real CloseWindow/ShowCursor stay callable normally everywhere else in the
file, matching what's actually in the library.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
@mwlasiuk

Copy link
Copy Markdown
Collaborator

Nice. I would suggest to display GL_RENDERER string in the app bar. Although RAYLib prints it some people might not look at the console output at all and this string is helpfull especially on systems with multiple GPU as OpenGL uses some default GPU and might choose iGPU instead of dGPU.

Just debug thing to let the user know that they shoul probably set their OpenGL default renderer to high performace card in system settings.

michalpelka and others added 4 commits July 25, 2026 23:57
Stakeholder request: apps/multi_view_tls_registration_legacy is a full,
independent copy of step2 as it was before the GLUT-to-raylib port
(restored from the pre-port commit) -- new target
multi_view_tls_registration_step_2_legacy, built side by side with the
raylib-based multi_view_tls_registration_step_2. Not sharing translation
units between the two, so they can diverge or be retired independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
On some GPU/driver combinations (seen with an NVIDIA PRIME-offloaded
context) GLFW's GLX request for a 4x-multisample framebuffer fails
outright ("GLX: Failed to create context: BadValue"), and raylib/GLFW
then segfaults using the broken context instead of degrading
gracefully. Not worth the crash risk for a cosmetic antialiasing hint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
Surfaces GPU renderer info (raylib has no wrapper for it) plus
per-frame draw-call/vertex counts from ScanRenderer::draw(), replacing
the plain FPS counter. ScanRenderer tracks its own counts since neither
raylib nor rlgl expose draw-call/vertex stats for custom (non-batched)
glDrawArrays calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
Windows: force GLFW_USE_HYBRID_HPG ON so the raylib build exports
NvOptimusEnablement/AmdPowerXpressRequestHighPerformance, the standard
symbol hint NVIDIA/AMD drivers read from an EXE to pick the discrete
GPU on Optimus/PowerXpress laptops. No build-time equivalent exists on
Linux (GPU selection there is a runtime PRIME choice), so add a README
note pointing Linux users at prime-run instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
@michalpelka

michalpelka commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

good idea @mwlasiuk added:
image

Also I've tested Windows build.
I also added legacy version

michalpelka and others added 2 commits July 26, 2026 00:36
prime-run isn't always installed even when prime-select is, and
prime-select nvidia alone doesn't force offload mode per-launch --
give the underlying env vars as the primary instructions, prime-run
as a shorthand when available.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgxCVLnEEAftKKRvZHPUB9
@michalpelka

Copy link
Copy Markdown
Contributor Author

Janusz tested macOS. It runs but draws incorectly. We need to address it

michalpelka and others added 2 commits July 27, 2026 02:11
GetLaserBeam() queried rlgl's matrix stack live, but mouse() runs
before display() each frame, so it was reading the previous frame's
post-end3DMatrixStack() state (identity modelview, 2D ortho
projection) instead of the 3D camera -- every pick ray was garbage,
so Ctrl+click picking (GCP, loop closure, translate tool) would grab
an arbitrary point and the camera-transition would fly there, looking
like the scene exploding. display() now caches the 3D view/projection
into frame_view_3d/frame_proj_3d right before end3DMatrixStack() resets
the stack, and GetLaserBeam() reads those instead.

Also ports the three remaining legacy-GL render() functions (shared
with the GLUT apps in `core`, so they can't be touched) to raylib:
GroundControlPoints, GNSS, and ControlPoints -- markers/lines via
DrawLine3D/rlBegin(RL_LINES), text labels via 2D screen-space DrawText
projected through frame_mvp_3d. ControlPoints additionally restricts
scan_renderer.draw() to the active scan while editing, since the bulk
multi-scan draw is skipped entirely in that mode.

Fixes a label-overlap bug found along the way (GCP/Control Points
labels sitting centimeters apart in world space collapsed to the same
screen pixels) by giving drawOutlinedText() a `line` param so nearby
labels stack instead of overlapping, and restores full scan visibility
when Manual Loop Closure closes (nothing previously undid the
source/target-only filtering it applies while open).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michalpelka

Copy link
Copy Markdown
Contributor Author

Fixed control points, gnss and ground control points.
Screenshot from 2026-07-27 02-08-35

Michal and others added 2 commits July 30, 2026 00:02
Signed-off-by: Michal <michal@Michals-MacBook-Air.local>
Signed-off-by: Michał Pełka <michalpelka@gmail.com>
@michalpelka
michalpelka marked this pull request as ready for review July 30, 2026 12:38
@JanuszBedkowski
JanuszBedkowski merged commit f012d01 into main Jul 30, 2026
7 checks passed
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.

3 participants