This document captures architectural decisions and design principles for cuVSLAM. Follow these when making code changes or designing new features.
Rule: A low-level parameter that must vary for a single frame is passed explicitly to
Odometry::Track() through cuvslam::internal::Internals. Do not mutate a long-lived
object through a setter to change one frame.
Internals is an unstable expert/development interface declared in
libs/cuvslam/cuvslam2_internal.h. It is not part of the stable user-facing API. Normal
applications should omit it and use the built-in defaults.
Why: Setters create implicit shared state between frames. A parameter change made
during one Track() call can silently bleed into the next frame if the setter mutates an
object that is reused across calls. This makes behavior hard to reason about, test, and
reproduce.
How it works in cuVSLAM:
Odometry::Track() accepts an optional pointer to Internals. All fields have concrete
defaults. BuildTrackFrameSettings() converts the selected values to
odom::TrackPerFrameSettings, which is threaded down the call stack without modifying
stored settings.
// Expert/development use: override feature count for one frame only.
cuvslam::internal::Internals internals;
internals.num_desired_tracks = 200;
odometry.Track(images, {}, {}, &internals);
// The next call uses built-in defaults.
odometry.Track(images);Construction-time settings stored by Odometry::Impl are not changed by the per-frame
override.
What to avoid:
// BAD — setter mutates shared state and can bleed across frames.
odometry.SetNumDesiredTracks(200);
odometry.Track(images);Where to add a new per-frame internal parameter:
- Confirm that the parameter is for expert/development tuning rather than a normal user
feature. Stable user-facing behavior belongs in
Odometry::Configor another public API. - Add the field and its default to
cuvslam::internal::Internalsinlibs/cuvslam/cuvslam2_internal.h. - Map it into
odom::TrackPerFrameSettingsinBuildTrackFrameSettings()inlibs/cuvslam/cuvslam2.cpp. - Add it to the appropriate
TrackPerFrameSettingssub-struct (sof,kf,pnp,icp, and so on), then thread it through the call chain without storing it. - Update the Python binding and YAML loader only when the parameter must be available to the corresponding development tools.
Rule: Optional input is appropriate at an API boundary where a caller may genuinely omit a value. Internal APIs below that boundary should receive concrete settings whenever possible.
Why: Optionals at every layer of the call stack force every internal function to check
has_value() before use. This is noise. Once the public API has resolved an optional to a
concrete value (using a default), the rest of the system should not need to know the value
was ever absent.
How it works in cuVSLAM:
Odometry::Track() accepts a nullable Internals pointer. A null pointer selects
Internals{}. BuildTrackFrameSettings() converts the result to a concrete
TrackPerFrameSettings; lower layers do not need to know whether the caller supplied an
override.
Internals::kf_override_frame_selection is an intentional tri-state exception: unset uses
automatic keyframe selection, true forces a keyframe, and false forces a non-keyframe.
Resolve such values at the first layer that has enough context, rather than propagating
optionality farther down the call stack.
Internals* (null or expert/development overrides)
└─► Internals{} when null
└─► BuildTrackFrameSettings() produces TrackPerFrameSettings
└─► IVisualOdometry::track(TrackPerFrameSettings&) // no optional
└─► IMultiSOF::trackNextFrame(TrackPerFrameSettings&) // no optional
└─► IMonoSOF::track(Settings&) // no optional
What to avoid:
// BAD — optional leaks into internal API
void trackNextFrame(..., std::optional<Settings> sof_settings = std::nullopt);
// BAD — internal function must check presence
if (sof_settings.has_value()) { ... }Corollary — no default arguments on internal functions:
Internal functions should not have = {} default arguments. That is just a hidden optional.
Every call site should pass the struct explicitly, making the data flow visible in the code.
// BAD — hides that data is being passed; caller can silently get wrong defaults
void track(const Settings& sof_settings = {});
// GOOD — caller always states what settings it is using
void track(const Settings& sof_settings);Rule: When a group of parameters is always used together or represents a coherent configuration unit, wrap them in a named struct. Do not add individual parameters to function signatures.
Why: Long argument lists are fragile (easy to reorder), hard to extend, and obscure what a function actually needs. A named struct documents intent, can be forwarded as a single argument through multiple layers, and makes adding new fields backwards-compatible at the struct level.
How it works in cuVSLAM:
sof::Settings— all feature tracking parameters.odom::KeyFrameSettings— keyframe selection thresholds.odom::TrackPerFrameSettings— bundles the above two for passing through the VO layer.sba::Settings,pnp::PNPSettings, etc. — each subsystem owns its config struct.
When a new per-frame parameter category is needed (e.g. ICP overrides), add a new
sub-struct to TrackPerFrameSettings rather than adding individual fields or new function
parameters:
struct TrackPerFrameSettings {
sof::Settings sof;
KeyFrameSettings kf;
// Add new categories here, not as additional function parameters
};Settings fall into three categories:
| Category | Example | Where it lives | Stability |
|---|---|---|---|
| Construction-time configuration | GPU on/off, odometry mode, data export | Odometry::Config, passed to the constructor |
Public API |
| Per-frame internal tuning | Feature count, border sizes, keyframe threshold | internal::Internals, passed to Track(), never stored |
Unstable expert/development API |
| Persistent internal tuning | SBA window and solver parameters | internal::InternalParameter, passed to ApplyPersistentInternalParameters() |
Internal use only |
If a normal user must choose a value at startup, it belongs in Config. If a low-level
development tool must vary a solver value per frame, it may belong in Internals. Values
that intentionally persist after tracker construction use
ApplyPersistentInternalParameters().
Do not expose a user-facing feature through Internals merely because it is convenient.
Design a stable public API for that feature instead.