Skip to content

Add CpuLidar system plugin with optimized physics raycasting#3343

Open
apojomovsky wants to merge 17 commits intogazebosim:mainfrom
apojomovsky:feature/cpu-lidar
Open

Add CpuLidar system plugin with optimized physics raycasting#3343
apojomovsky wants to merge 17 commits intogazebosim:mainfrom
apojomovsky:feature/cpu-lidar

Conversation

@apojomovsky
Copy link

@apojomovsky apojomovsky commented Feb 19, 2026

🎉 New feature

This PR is 3 out of 3 that implement: gazebosim/gz-sensors#26

Depends on:

Summary

Add the CpuLidar system plugin that bridges CpuLidarSensor (gz-sensors) with the physics raycasting pipeline. The end result is a close match in performance to that found on Gazebo Classic, while tested with a 128-beam × 80-ring lidar scene).

CpuLidar system plugin

A new system plugin following the Imu system pattern (ISystemConfigure + ISystemPreUpdate + ISystemPostUpdate):

  • Configure: stores the SensorFactory reference.
  • PreUpdate: discovers new <sensor type="lidar"> entities via EachNew<CpuLidar>, creates CpuLidarSensor instances, calls GenerateRays() to populate the RaycastData ECM component, and sets the needsRaycast flag when a scan is due.
  • PostUpdate: reads RaycastData results (populated by the Physics system), feeds them to CpuLidarSensor::SetRaycastResults(), and calls sensor->Update() to trigger publishing.

Performance optimization 1: Batch raycasting in Physics.cc

UpdateRayIntersections() now tries the batch API first. When available (dartsim + Bullet), all rays are submitted in a single GetBatchRayIntersectionFromLastStep() call. The single-ray loop is retained as a fallback.

Performance optimization 2: Frequency-gated raycasting

UpdateRayIntersections() was called every physics step (1 kHz default), but the sensor only needs results at its own rate (e.g. 10 Hz). A needsRaycast flag in RaycastDataInfo is set by CpuLidar::PreUpdate only when a scan is due and
there are active subscribers. The Physics system skips entities where the flag is false. Both the batch and fallback paths are gated.

Stage RTF
Baseline (DART raycast() at physics rate) ~7%
+ Batch API with btWorld::rayTest() ~13%
+ needsRaycast frequency gating ~95%

Test it

colcon build --merge-install --packages-up-to gz-sim \
  --cmake-args '-DBUILD_TESTING=ON'
colcon test --packages-select gz-sim \
  --ctest-args -R INTEGRATION_cpu_lidar

Manual test:

gz sim examples/worlds/cpu_lidar_sensor.sdf

# In another terminal:
gz topic -e -t /lidar
gz topic -e -t /lidar/points

Checklist

  • Signed all commits for DCO
  • Added a screen capture or video to the PR description that demonstrates the feature
  • Added tests
  • Added example and/or tutorial
  • Updated documentation (as needed)
  • Updated migration guide (as needed)
  • Consider updating Python bindings (if the library has them)
  • codecheck passed (See contributing)
  • All tests passed (See test coverage)
  • Updated Bazel files (if adding new files). Created an issue otherwise.
  • While waiting for a review on your PR, please help review another open pull request to support the maintainers
  • Was GenAI used to generate this PR? If so, make sure to add "Generated-by" to your commits. (See this policy for more info.)

Generated-by: Claude Opus 4.6

Disclaimer: The code here was reviewed, tested, and profiled by hand by the author.

Note to maintainers: Remember to use Squash-Merge and edit the commit message to match the pull request summary while retaining Signed-off-by and Generated-by messages.

output.mp4

@apojomovsky apojomovsky requested a review from Zyrin February 20, 2026 15:55
@apojomovsky apojomovsky changed the base branch from gz-sim10 to main February 20, 2026 20:22
…ulation, result feeding, and integration tests

Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Physics was raycasting every simulation step (e.g. 1 kHz) even though
a CPU lidar only needs fresh results at its own update rate (e.g. 10 Hz).
This caused ~100× wasted raycast work per real scan.

Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Opus 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Claude Sonnet 4.6

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
This reverts commit 4de207b.

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
Generated-by: Copilot

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
…ocessed

- Default RaycastDataInfo::needsRaycast to true so that manually-created
  RaycastData components are processed by Physics without needing to know
  about the optimization flag. The CpuLidar system already explicitly
  manages the flag each step.
- Add a topic subscriber in the RaycastResultsProcessed test so that
  HasConnections() returns true and the CpuLidar system requests raycasts.

Generated-by: Amp <amp@ampcode.com>

Signed-off-by: Alexis Pojomovsky <apojomovsky@gmail.com>
@azeey
Copy link
Contributor

azeey commented Feb 27, 2026

As mentioned in gazebosim/gz-sensors#26, here's the manually triggered CI job that builds all of your branches on gz-sensors, gz-physics and gz-sim.

Build Status

Comment on lines +200 to +202
std::function<void(const msgs::LaserScan &)> cb =
[](const msgs::LaserScan &){};
node.Subscribe("/test/cpu_lidar", cb);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
std::function<void(const msgs::LaserScan &)> cb =
[](const msgs::LaserScan &){};
node.Subscribe("/test/cpu_lidar", cb);
node.Subscribe("/test/cpu_lidar", [](const msgs::LaserScan &){});

To make it consistent with e.g. line 205.
And there are some more lambdas further down where this can be applied, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

3 participants