Skip to content

9d planner#3807

Draft
grandixximo wants to merge 18 commits intoLinuxCNC:masterfrom
grandixximo:9d
Draft

9d planner#3807
grandixximo wants to merge 18 commits intoLinuxCNC:masterfrom
grandixximo:9d

Conversation

@grandixximo
Copy link

No description provided.

Luca Toniolo and others added 14 commits February 13, 2026 20:53
COMPLETED
=========

Architecture:
  * Dual-layer: userspace planning, RT execution
  * Lock-free SPSC queue with atomic operations
  * 9D vector abstractions (lines work, arcs TODO)
  * Backward velocity pass optimizer
  * Peak smoothing algorithm
  * Atomic state sharing between layers

Critical Fixes:
  * Optimizer now updates `tc->finalvel` (prevents velocity discontinuities)
  * Force exact stop mode (`TC_TERM_COND_STOP`) - no blending yet
  * RT loop calls `tpRunCycle()` every cycle (fixes 92% done bug)
  * Error handling uses proper `rtapi_print_msg()` instead of `printf()`

Verified Working:
  * Simple linear G-code completes (squares, rectangles)
  * Acceleration stays within INI limits during normal motion
  * No blend spikes (fixed)

KNOWN LIMITATIONS
=================

E-stop: 3x acceleration spike
  - Tormach has identical behavior (checked their code)
  - Industry standard for emergency stops
  - Safety requirement: immediate response
  - Acceptable for Phase 0

No Blending: Exact stop at every corner
  - Expected - Phase 4 feature
  - Prevents acceleration spikes without blend geometry

No Arcs: G2/G3 not implemented
  - Not needed for Phase 0 validation
  - `tpAddCircle_9D()` stub exists

Feed Override: Abrupt changes
  - Predictive handoff needed (Phase 3)
  - Works, just not smooth

FUTURE PHASES
=============

Phase 1: Kinematics in userspace
Phase 2: Ruckig S-curve integration
Phase 3: Predictive handoff, time-based buffering
Phase 4: Bezier blend geometry
Phase 5: Hardening, edge cases
Phase 6: Cleanup

FILES MODIFIED
==============

Core Planning:
  src/emc/motion_planning/motion_planning_9d.cc - Optimizer
  src/emc/motion_planning/motion_planning_9d_userspace.cc - Segment queueing
  src/emc/motion_planning/motion_planning_9d.hh - Interface

RT Layer:
  src/emc/motion/control.c - RT control loop fix
  src/emc/motion/command.c - Mode transitions
  src/emc/tp/tp.c - Apply optimized velocities
  src/emc/tp/tcq.c - Lock-free queue operations

Infrastructure:
  src/emc/motion/atomic_9d.h - SPSC atomics
  src/emc/tp/tc_9d.c/h - 9D vector math
  src/emc/tp/tc_types.h - Shared data structures

TEST
====

G21 G90 F1000
G1 X10 Y0
G1 X10 Y10
G1 X0 Y10
G1 X0 Y0
M2

Expected: Jerky motion (exact stop), completes without errors.
Extract kinematics math into HAL-independent headers and create userspace
kinematics infrastructure for trajectory planning.

Key changes:
- Extract pure math functions from all kinematics modules into *_math.h
  headers (5axiskins, corexykins, genhexkins, genserkins, lineardeltakins,
  maxkins, pentakins, pumakins, rosekins, rotarydeltakins, rotatekins,
  scarakins, scorbotkins, tripodkins, trtfuncs)
- Add kinematics_userspace/ with C interface for userspace kinematics
- Add HAL pin reader for accessing kinematics parameters from userspace
- Add motion_planning modules: Jacobian calculation, joint limits
  enforcement, path sampling, and userspace kinematics integration
- Add kinematics_params.h for shared parameter definitions
- Update trajectory planner (tp.c) to support userspace kinematics path
- Update INI parsing to load userspace kinematics configuration

This enables the 9D planner to compute inverse kinematics and perform
singularity-aware velocity limiting without RT kernel calls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 (Ruckig Integration) was merged from master.

This phase adds:
- Feed override handling with proper velocity/acceleration management
- 9-phase profile optimization for trajectory smoothing
- Downstream exit velocity capping for segment boundaries
- Tangent-aware Jacobian limits for per-axis joint constraints
- Backward pass optimization for profile consistency
- Jerk-aware motion planning refinements
…tation

Replace 1-segment downstream lookahead with multi-segment chain walk
(computeChainExitCap) that cascades reachability limits backward through
up to 16 downstream segments. Prevents exit velocities that create
impossible junctions on short segments or sharp kinks further ahead.

Add feed-limiting binary search in computeBranch: instead of rejecting
branches that exceed junction constraints, find the maximum feed that
produces a feasible exit velocity. Reduces branch rejections during
rapid feed override changes.

Add kink and chain exit cap checks in manageBranches that force branch
computation when the active segment's profile exits above constraints.
The backward fixup pass skips the active segment, so these checks are
the only runtime enforcement point.

Remove SPLIT_MISMATCH detection and velocity clamp from RT handoff
(tp.c) — upstream constraints now prevent the mismatch at the source.

Downgrade diagnostic messages (BRAKE_MAIN, PROFILE_GAP, FEED_LIMIT)
from ERR to DBG level.
@petterreinholdtsen
Copy link
Collaborator

Is there a way to add scripts in tests/ to demonstrate that this new code is working?

@BsAtHome
Copy link
Contributor

src/emc/motion/kinematics_params.h has fixed kinematics enum. The problem is that kinematics modules are loadable from anything the user want to make. Having them enumerated creates a problem for those kinematics not know in the default distro that the user adds himself. The loss of generic user-defined kinematics would be a great loss for us all. Or can you still load new modules with any and all TP?

@grandixximo
Copy link
Author

grandixximo commented Feb 16, 2026

src/emc/motion/kinematics_params.h has fixed kinematics enum. The problem is that kinematics modules are loadable from anything the user want to make. Having them enumerated creates a problem for those kinematics not know in the default distro that the user adds himself. The loss of generic user-defined kinematics would be a great loss for us all. Or can you still load new modules with any and all TP?

Planner 0/1 are unaffected, the TP works in Cartesian space and the RT module's kinematicsInverse/kinematicsForward symbols are resolved at load time as usual. Any user-written kinematics module works fine.

Planner 2 has a problem: it needs a userspace reimplementation of each kinematics module's math (for Jacobian computation, joint-space limit enforcement, path sampling). Currently kinematics_params.h enumerates known modules, and kinematicsUserInit() hard-fails for anything not in the list, aborting trajectory init entirely.

Three options to preserve compatibility with custom kinematics in Planner 2:

Fallback to identity kins in userspace, Treat unknown modules as trivkins for the userspace layer. RT still uses the real module. Joint limit enforcement would be approximate but conservative.

Downgrade to planner 0/1, If userspace kins init fails for an unknown module, automatically fall back to planner 1 (or 0) with a warning. Simplest fix, preserves "any kins works with any TP".

Generic RT-userspace bridge, Add a KINS_TYPE_GENERIC that calls the RT module's forward/inverse via shared memory. Correct but slower, and requires a new communication channel.

Which approach would you prefer?

@grandixximo
Copy link
Author

Is there a way to add scripts in tests/ to demonstrate that this new code is working?

The new code is not really done yet, I'll work on tests after things are stable enough, G64 is still not implemented, rigid tapping is missing, adaptive feed not tested, a few more things...

@grandixximo
Copy link
Author

at the moment still hardening the Feed Override system, it is quite complex still have not squashed all possible ways things could go wrong, but getting closer each day...

@BsAtHome
Copy link
Contributor

Preserving the generic plugable nature of kinematics across trajectory planners is a very nice feature and should be preserved if possible.
What is the structural (core) difference between the realtime kinematics and userspace kinematics? Isn't it possible to make the same base code plugable for both realtime and userspace use with the appropriate wrappers? Say, one source compiles both into a realtime module for TP=[0,1] and a userspace module for TP=2.

@grandixximo
Copy link
Author

grandixximo commented Feb 16, 2026

Preserving the generic plugable nature of kinematics across trajectory planners is a very nice feature and should be preserved if possible.
What is the structural (core) difference between the realtime kinematics and userspace kinematics? Isn't it possible to make the same base code plugable for both realtime and userspace use with the appropriate wrappers? Say, one source compiles both into a realtime module for TP=[0,1] and a userspace module for TP=2.

Good point. The math is actually already shared, each kinematics module has a *_math.h header (e.g. 5axiskins_math.h, trtfuncs_math.h) with pure static inline forward/inverse functions and no RT dependencies. Both the RT module and the userspace lib call into these same headers.

What differs is the glue code around the math. The RT side creates HAL pins with hal_pin_float_newf() and reads them by direct pointer dereference, while userspace walks HAL shmem by pin name string through hal_pin_reader. Init is hal_malloc() + switchkinsSetup() + EXPORT_SYMBOL() on the RT side vs calloc() + function pointer dispatch in userspace. Logging is rtapi_print() vs fprintf. Trying to unify these into a single .c would mean heavy #ifdef RTAPI scaffolding around everything except the math, which is already shared.

The real obstacle for custom kinematics in planner 2 isn't math duplication, it's that kinematics_user.c needs to know the module exists at compile time (enum entry, function pointers, HAL pin names for refresh()). A user-written RT module has no matching userspace entry.

A possible path: let custom modules optionally ship a mykins_userspace.so implementing a standard kins_userspace_init() API, which the planner dlopen()s at runtime. That would make planner 2 pluggable the same way RT kins already are, without enumerating every module. If that's too heavy, falling back to planner 1 for unknown modules is the simplest safe option.

If you are able to conjure up a method to make this work, I'd be glad to implement it.

@BsAtHome
Copy link
Contributor

The real obstacle for custom kinematics in planner 2 isn't math duplication, it's that kinematics_user.c needs to know the module exists at compile time (enum entry, function pointers, HAL pin names for refresh()). A user-written RT module has no matching userspace entry.

That is the real problem. You have static enumeration instead of a dynamic plugable system. Why do you need enumeration? Your interface into the kinematics should be generic.

The real question is, when the math is shared, what glue is required for userspace to be usable with the new planner and what is the glue code for realtime.

The glue-code should be the same for each and every kinematics for the interface. Therefore, you only need to devise a way to compile the kinematics modules so they give you two resulting loadable modules, one for realtime and one for userspace.

A possible path: let custom modules optionally ship a mykins_userspace.so implementing a standard kins_userspace_init() API, which the planner dlopen()s at runtime. That would make planner 2 pluggable the same way RT kins already are, without enumerating every module. If that's too heavy, falling back to planner 1 for unknown modules is the simplest safe option.

That is how I think it is supposed to be, yes. Just like the realtime kinematics. You probably only need to be able to load and not to unload modules (the kinematics is set in a configuration file and cannot be changed at run-time).

If you are able to conjure up a method to make this work, I'd be glad to implement it.

Have a look at rtapi/uspace_rtapi_app.cc, which uses dlopen() to load RT modules. The same procedure can be used for userspace modules. It might even be possible to reuse some of the current infrastructure. The system works by the loaded module registering/announcing itself. Example: RT modules have rtapi_app_main()/rtapi_app_exit() functions that are called on load/unload which will registers/unregister with HAL.

A similar strategy will also work for userspace kinematics.

@rmu75
Copy link
Collaborator

rmu75 commented Feb 16, 2026

Would it be possible to pass an ID value to the kinematics modules and use that instead of the enum? Then users could configure / customize it .

@grandixximo
Copy link
Author

Deep into hardening feed override system, I'll have a better look at the kins probably tomorrow, thank you for the input, I'll try my best to make it work, I'm sure there is a possible approach.

- Alt-entry: pre-compute alternate profile for N+1 with v0 matching
  brake exit velocity, RT picks closer match at junction
- Sub-cycle gap: replace two-stage with single-stage position-control
  when main profile < 1 servo cycle (eliminates displacement dip)
- Spill-over: write stop profiles on downstream segments when
  deceleration crosses segment boundaries
- Feed-hold recovery: abort stale cursor walk on resume, use
  predictStateAtTime for phase 2 transition
@grandixximo
Copy link
Author

grandixximo commented Feb 16, 2026

The last PR addresses all jerk spikes I was able to find, my testing method was running gcodes with small segments, at high feed rate, and having a script wildly swing the feed override, the feed override hand-off branching system is now basically bullet proof as far as I've tested, and I have tested it a lot.
There was a lot of thought and multiple refinements put into it, last two or three weeks its all I've been working on basically, pretty proud of the result, it is basically 4 - 5k lines of code, but covers proactively every single way you could make the system fail, the only draw back is, there is a delay 50 - 100ms before the feed rate change takes actual effect, but it is inherent to the architecture I've envisioned for planner type 2, and it is something that an operator would barely feel, the only weakness is probably adaptive feed override, but will have to be tested, I think I'll take a look again at the kins before moving to blending.

Luca Toniolo added 3 commits February 17, 2026 06:04
kinematics modules from working with planner type 2. Each kinematics
module now ships a <name>_userspace.so plugin loaded via dlopen at
init time, so built-in and custom modules are treated identically.

- Add kins_plugin.h defining the plugin API (kins_userspace_setup)
- Extract 17 built-in kinematics into self-contained plugin .so files
- Rewrite kinematics_user.c as a dlopen loader (~280 lines, was ~1500)
- Remove kinematics_type_id_t enum, type_id field, and dispatch switch
- Remove kins_type_id from emcmot_config_t (shared memory layout change)
- Fall back to planner type 0 if plugin .so not found

Requires make clean (shared memory struct changed).
@grandixximo
Copy link
Author

grandixximo commented Feb 17, 2026

Implemented the dlopen plugin approach. Here's what changed:

The kinematics_type_id_t enum and map_kinsname_to_type_id() are gone entirely. No IDs, no dispatch switch. The module name string is the identity — it maps directly to _userspace.so.

Each kinematics module now ships a small plugin (50-150 lines) that exports one symbol: kins_userspace_setup(). The loader does dlopen(EMC2_HOME "/lib/kinematics/" name "_userspace.so"), calls setup, and the plugin sets its forward/inverse/refresh function pointers. Built-in and custom modules are loaded identically.

The 17 built-in kinematics were extracted into self-contained .c files under plugins/. They reuse the existing *_math.h headers (pure math, no HAL deps) — same shared code that RT uses. The glue is minimal: read params from ctx->params, call the math function, done.

If planner 2 is requested but the plugin .so doesn't exist (custom kins without a userspace plugin), it warns and falls back to planner 0 instead of aborting. Custom kins still work fine on planners 0/1 as before — they just won't get planner 2 until they add a _userspace.so.

kinematics_user.c went from ~1500 lines to ~280. The shared memory struct changed (removed type_id field)

@BsAtHome
Copy link
Contributor

Good to hear you implemented dlopen().

But I'm still not sure why you moved the actual forward/reverse kinematics calculation into a *_math.h header file. That seems to defeat the one source file and two glues. Header files are usually a very bad place for code. Header files are there as an interface layer. Sure, using "static inline" qualifiers makes them local, but that is, IMO, a very bad habit.

What I had expected was:

  • sources:
    • some_kinematics.c (with actual calculation) --> compile to some_kinematics.o
    • hal_kins_glue.c --> compile to hal_kins_glue.o
    • nonrt_kins_glue.c --> compile to nonrt_kins_glue.o
  • link some_kinematics.o + hal_kins_glue.o --> some_kins_hal.so (hal kinematics component)
  • link some_kinematics.o + nonrt_kins_glue.o --> some_kins_nonrt.so (your non-RT userspace kinematics module)

Or is the *_math.h header a remnant from the previous code iteration?

@rmu75
Copy link
Collaborator

rmu75 commented Feb 17, 2026

In C, code in header files is indeed not a common practice.

If we don't want to abandon RTAI just yet, I think it is usually not possible to link one object file to a kernel module and to a normal program. IMO #inlcude-ing the code is not a bad idea in this case, also keeps the build system out of the loop.

The sources to be included could be renamed to a different ending like .inc.

@BsAtHome
Copy link
Contributor

In C, code in header files is indeed not a common practice.
If we don't want to abandon RTAI just yet, I think it is usually not possible to link one object file to a kernel module and to a normal program. IMO #inlcude-ing the code is not a bad idea in this case, also keeps the build system out of the loop.

Creating a .ko can be done from multiple .o objects, just like creating an .so can be created from multiple .o objects. There is no difference afaics. Only the userspace/kernelspace interface/glue layer is different, which is done in rtapi. I only propose to add some glue to differentiate between linking RT and non-RT kinematics modules.

I don't think the non-RT kinematics can run in kernel space. @grandixximo must pitch in here to make that assessment whether the non-RT kinematics could ever be a kernel module.

The sources to be included could be renamed to a different ending like .inc.

I don't think we should be using this type of code inclusion at all.

And for RTAI, it seems that development has stopped completely. I'm not sure it is worth the effort to keep it in very much longer. There is already a lot that does not work with RTAI anyway.

@grandixximo
Copy link
Author

I don't think they can be a kernel module, because they run on a userspace thread, but I'm no expert, and have not really explored this deeply yet.
The separation was because kinematics modules were deeply linked with their variable parameters like pivot lenghts which they can get from hal pins.
I did not find another clean method to reuse the same code, without making a bunch of ifndef everywhere, in my opinion complicating things. So I opted for extracting the math, and having userspace and RT both include the same math, and from uspace get the pin values according to what pins are created by the kinematics component in RT, it's just what I thought would work the best maintenance wise, trying not to duplicate too much code. About RTAI I don't have hardware that I can reliably test RTAI on, I could never get it to run reliably enough, and I'm using ethercat heavily and RTAI does not support ethercat as far as I know, so RTAI is something I would need someone to test it for me on real hardware.
I'll look into it a bit more, if you guys prefer ifndef and a single file, I can do that.

@rmu75
Copy link
Collaborator

rmu75 commented Feb 17, 2026

I have no problem if kernel-mode stuff is abandoned, but then it should be stated, and all that C / C++ schisma could be resolved / isn't needed in new code, so the whole split would be pointless.

If we want to keep kernel support for now, linking one object into userspace and kernel objects is of course possible in theory, but it is asking for trouble.

math stuff is handled differently for one, you can't just include <math.h> in kernel code, rtapi_math.h has conditional compilation depending on KERNEL or not. It may work to link stuff compiled against the "wrong" prototypes, but that is a hack at best. There may be other problems like LTO, autovectorization, calling conventions, frame pointer and in worst case it would break on "wrong" kernel configs. I don't think it's worth it just to get rid of a #include that would be unclean under normal circumstances.

@grandixximo
Copy link
Author

I had a better look at this. I considered the single .o approach, but the *_math.h pattern has advantages, for example in BUILD_SYS=normal (kernel), RT objects are compiled with -nostdinc and kernel includes, so the same .o can't serve both contexts. The math headers work for both build systems with zero #ifdef. They could be renamed to .inc if the .h extension bothers, but static inline functions in headers is the same pattern the Linux kernel uses extensively (list.h, rbtree.h, etc.), so unless the kernel also has bad habits, I think it's fine.

@grandixximo
Copy link
Author

I might be wrong about this, but I explored this for a while before settling on the shared header approach. The alternative would be splitting each *_math.h into a .h (prototypes) and .c (implementation), then compiling the .c twice with different flags and updating the link rules for every module. It's doable but adds significant Makefile complexity for the same result. If you'd prefer that approach I can implement it.

@BsAtHome
Copy link
Contributor

If I understand it correctly, the loadable kinematics module is not going into the same process space for RT (rtai_app) and non-RT (milltask?). When your new TP cannot load into the kernel (RTAI) then we do not need to consider that option too seriously, just enough to bypass in compilation.

In the case of uspace, why can't the same kinematics .so be loaded into two different processes and perform their specific function in the process' context?

The motion controller links directly into the kinematics{Forward,Reverse} functions, which means that the kinematics .so must be loaded before the controller's .so to satisfy the dynamic linking process. If you also export appropriate functions for your non-RT process hook, then you could, in principle, load the same .so in both processes and have it perform the kinematics there too.

Or am I missing something here?

@grandixximo
Copy link
Author

If I understand it correctly, the loadable kinematics module is not going into the same process space for RT (rtai_app) and non-RT (milltask?). When your new TP cannot load into the kernel (RTAI) then we do not need to consider that option too seriously, just enough to bypass in compilation.

In the case of uspace, why can't the same kinematics .so be loaded into two different processes and perform their specific function in the process' context?

The motion controller links directly into the kinematics{Forward,Reverse} functions, which means that the kinematics .so must be loaded before the controller's .so to satisfy the dynamic linking process. If you also export appropriate functions for your non-RT process hook, then you could, in principle, load the same .so in both processes and have it perform the kinematics there too.

Or am I missing something here?

The RT .so (e.g., maxkins.so) does hal_init() + hal_pin_new() in rtapi_app_main(), and kinematicsForward() reads params directly from HAL pin pointers. Loading the same .so in a second process would either conflict on hal_init() or need runtime detection to skip it and read parameters differently. The separate userspace plugin avoids that, it reads HAL pin values through a read-only interface without registering as a HAL component.

@BsAtHome
Copy link
Contributor

BsAtHome commented Feb 17, 2026

Afaik, only when you run halcmd loadrt <module> (or call the equivalent internally) it will call rtapi_app_main() (see rtapi/uspace_rtapi_app.c:301 where it calls the start function, which was acquired in line 285).

But, you don't need to call rtapi_app_main() at all when you yourself do the dlopen(). A call to dlopen() will do nothing more than resolve the dynamic link dependencies. Adding RTLD_LOCAL will prevent exporting any symbols from the loaded .so and the only way to get to them is to use dlsym(). You don't even need worry or care about the kinematicsForward and kinematicsReverse symbols (functions).

You can simply split the mathematics inside the kinematics source and implement and export, lets say, as an example, nonrt_kinematicsForward and nonrt_kinematicsReverse from the kinematics file and then find these symbols using dlsym(). Your functions don't need to hook into HAL if you don't want them to.

You can also prevent your functions from being exported in a kernel build simply by placing the definition and EXPORT_SYMBOL() invocations in a #ifndef __KERNEL__ conditional. More should not be required.

@grandixximo
Copy link
Author

grandixximo commented Feb 17, 2026

You're right that dlopen() alone won't call rtapi_app_main(), confirmed. Both approaches work, so here's a comparison from a maintenance perspective:

Current approach (math headers + separate plugins):

Math extracted into *_math.h, RT modules and userspace plugins both include it
RT modules are thin wrappers: read HAL pins -> fill params struct -> call math
Plugins are thin wrappers: read params from shared memory -> call math
(+) Each file has a single responsibility, easy to review in isolation
(+) Custom modules just ship an extra _userspace.so,no special exports required
(+) No #ifdef conditionals
(-) More files (math header + plugin per module)
(-) Param struct defined twice (math header + kinematics_params.h)
(-) static inline in headers is unconventional for C
Your proposal (nonrt exports from the RT .so):

Math stays in the .c file, nonrt_* functions exported alongside RT functions
Userspace planner does dlopen() on the RT .so + dlsym() for nonrt_*
(+) Fewer files, everything for one module in one place
(+) No param struct duplication
(+) Conventional C (code in .c files, not headers)
(-) Every RT module needs #ifndef KERNEL blocks for the nonrt_* exports
(-) Custom modules must implement both interfaces in one file
(-) RT and userspace concerns mixed in the same source file
I'm fine with either approach. The math separation was the hard part and that's done regardless of which way the glue is structured. What's your preference?

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.

4 participants