Conversation
|
Is there a way to add scripts in tests/ to demonstrate that this new code is working? |
|
|
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? |
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... |
|
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... |
|
Preserving the generic plugable nature of kinematics across trajectory planners is a very nice feature and should be preserved if possible. |
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. |
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.
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).
Have a look at A similar strategy will also work for userspace kinematics. |
|
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 . |
|
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. |
|
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. |
|
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) |
|
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:
Or is the *_math.h header a remnant from the previous code iteration? |
|
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 |
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.
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. |
|
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. |
|
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 |
|
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. |
|
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. |
|
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. |
|
Afaik, only when you run 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, 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. |
|
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 Math stays in the .c file, nonrt_* functions exported alongside RT functions |
That is completely optional. You are allowed to export the non-RT functions and they will simply go unused and fill a marginal amount of space. No problem with that. As long as there are no dyn-link refs, but that is a naming question. You only need to make sure that it links, which could mean the requirement of a few stubs. Although, the code can be designed that no or only few stubs are required.
That I see as an advantage because the actual kinematics is in one file. You can reuse code more effectively. You do have to choose carefully what the interface does. You do not want to replicate code from higher layers in the modules.
That is a general issue in all of the components already because of the kernel/userspace boundary. The RT/non-RT boundary is easier to handle. Just make your code run as RT, then it should also run as non-RT. I can't imagine that your use of the kinematics calculations changes its actual behaviour in any meaningful way. Or does it? If not, then it should be a moot issue. The biggest advantage here is that the changeset should be easier to understand and people with their own kinematics component can add/change their code to work with the new way a bit easier. I guess a "how to migrate kinematics components" document would be required in any circumstance. |
|
Kernel modules are pretty restricted in what they can do, whereas in the userspace realtime thread nearly everything is allowed, including C++, exceptions, etc... Things with non-constant upper limit of runtime should be avoided though, and code should only access data and code that is locked and can't be evicted or paged out. Shared memory segment and stack is OK, dynamic memory probably not. The rtapi glue code memlocks all code, but probably not stuff that you dlopen somewhere in a module, so that should be checked. |
|
Agreed, I will go ahead with refactoring, thank you for the guidance.
The dlopen() of the RT .so happens in milltask (non-RT), not in the servo thread. |
|
refactored the Kinematics, much cleaner approach, thank you @BsAtHome for the guidance |
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.
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
Replace the direction-vector decomposition (which assumed joints = XYZ) with proper finite-difference computation of per-joint velocity, acceleration, and jerk from IK output. Correct for all kinematics.
G20/G21 from a previous run persists in canon.lengthUnits, causing FROM_PROG_LEN() to convert differently on re-run for G-code commands that appear before the G20/G21 line.
bezier9Init() accepts curvature parameters (kappa, normal) at both endpoints. For arc-adjacent blends, P2/P3 are offset in the curvature normal direction by delta = 5*alpha^2*kappa/4 to match adjacent segment curvature, eliminating centripetal acceleration discontinuity at junctions. For line segments (kappa=0) the offset is zero and behavior is unchanged. Add bezier9PathDeviation() which measures maximum distance to the two-segment programmed path (P_start -> corner -> P_end), correct for both symmetric and asymmetric blends. Add bezier9MaxDeviation() for multi-sample diagnostic verification.
Pass curvature (kappa) and normal direction from adjacent segments into the Bezier blend optimizer for C2 continuity at arc-blend junctions. For lines (kappa=0) behavior is unchanged. Replace bezier9Deviation (corner-point distance) with bezier9PathDeviation (distance to two-segment polyline) for correct tolerance checking on both symmetric and asymmetric blends. Cap blend maxvel at curvature-rate jerk limit so the backward pass prevents high-override entry velocities from exceeding curvature bounds.
- Don't blend between rapid (G0) and feed (G1) moves to prevent unreachable junction velocities (matches planner 0/1 behavior). - Take min of per-joint Jacobian v_jerk_cap and INI-based aggregate kink_vel from createBlendSegment9, instead of unconditionally overwriting. Prevents high-override entry into blends. - Compute blend maxjerk from Jacobian-projected tangential jerk budget (BLEND_ACC_RATIO_TANGENTIAL * jlim[j] / proj[j]) instead of simple min(prev, tc). Accounts for non-trivial kinematics where joint projections differ from 1.0.
When a new LINE segment is collinear with the previous LINE in the queue (< 1 degree angle, same feed rate, same motion type), extend the previous segment instead of creating a new segment boundary. This reduces segment count, eliminates split cycles at collinear junctions, and produces smoother velocity profiles for CAM-generated toolpaths that approximate curves as chains of tiny line segments. Guarded out in G61.1 (exact stop) mode where every junction must decelerate to zero.
Extend the RT queue depth gate to all non-EXACT segments, not just id==0. When a segment is alone in the queue, its profile is computed with v_exit=0 (no successor yet). Previously, RT would activate it immediately, and the optimizer's active-segment skip prevented correction when the successor arrived — causing a velocity spike at the junction. The gate holds activation until queue_len >= 2 or 20ms timeout, giving the optimizer time to recompute with the correct exit velocity while the segment is still inactive. EXACT segments (last of program) skip the gate since their v_exit=0 is always correct. Also removes investigation debug probes (FIX4_DBG, STUCK_FORCE).
… and backtrack passes
…egments In Phase B of a split cycle, cycle_time was still set to remain_time when tpUpdateCycle advanced elapsed_time. This made elapsed = 2*remain instead of remain+cycleTime, causing tpCheckEndCondition to compute a negative last_sample_time when remain < 0.5*cycleTime. Short segments that should have been detected for split-cycle handling were missed, completing mid-cycle on the next regular cycle with progress clamped to target — producing a 20-33% velocity dip and ~30M mm/s³ jerk spike. Reset cycle_time to cycleTime after saving remain_time but before tpUpdateCycle, so the elapsed advance gives the correct value and tpCheckEndCondition can properly detect short segments during Phase B.
… by delta magnitude Added normalization of blend projections by the magnitude of delta to ensure consistent scaling in non-identity kinematics. This change improves the accuracy of the projection calculations, particularly for trivial kinematics where the Jacobian is identity.
…ter P tolerance compliance Removed the bezier9MaxDeviation function and updated bezier9PathDeviation to directly use bezier9Deviation for distance measurement to the corner. This change ensures that the path deviation adheres more closely to the specified tolerance, enhancing the accuracy of motion planning.
…seeding Three fixes for velocity discontinuities at segment junctions during feed override changes: 1. Feed ramp deferral (Fix A): recomputeDownstreamProfiles now seeds successor entry from active segment's actual profile exit via v0_override, eliminating stale-feed deferral on micro-segments. 2. Backtrack v0 preservation (Fix B): one-step backtrack only lowers predecessor exit velocity, never clamps successor v0. Falls back to reachability-clamped entry when Ruckig can't reach the requested exit or produces negative velocities. 3. Stale merged_exit_vel (Fix C): all three callers of recomputeDownstreamProfiles now pass explicit v0_override — merge path uses achieved_exit_vel, branch creation and DOWNSTREAM_FIX use new getActiveProfileExit() helper. Removes the stale merged_exit_vel fallback that persisted absolute velocities across feed changes. Adds reachability_exit_cap and merged_exit_vel fields to TC_STRUCT shared_9d.
…unction spikes Queue consumption shifts segments forward, giving them a new predecessor whose exit velocity differs from what originally seeded their profile. At steady feed, no existing mechanism re-seeds profiles near the active segment, causing junction velocity mismatches at RT handoff. Step 4 walks positions 1-8 from the active segment (outside the optimizer window) and recomputes any profile whose v0 doesn't match its predecessor's exit. Uses a physics-derived dynamic threshold (jerk * dt^2) instead of a magic number. Includes one-step backtrack for reachability-limited segments and sticky reachability_exit_cap to prevent the optimizer backward pass from undoing the backtrack on subsequent cycles.
…nup debug probes - Add multi-depth writeAltEntry in Path B (commit_seg>=2): two calls after recompute seeded from old_q1_exit (depth 2+) and active_exit_vel (depth 1+) - Add ALT_STOPSKIP guard: skip alt profiles on non-tangent segments when achievable_exit > 0.5 to prevent hard stops from v0_correction overflow - Add Phase 2 alt-entry detection: recompute downstream from alt exit velocity when RT sets alt_entry.taken - Add near-active v0 reconciliation (HEAD_FIX) for stale-predecessor junctions - Remove all investigation debug prints (HANDOFF2, ALT_WRITE, ALT_SKIP, ALT_ENTER, ALT_NEGVEL, HEAD_FIX, CHAIN_GAP, CW_TICK, DS_CHAIN_CAP)
…mping, and alt-entry improvements Three complementary fixes to eliminate junction velocity spikes during feed override transitions: 1. Cap v0_correction at jerk budget (maxjerk * dt²) instead of acceleration budget (maxaccel * dt). A velocity correction applied in one servo cycle produces effective jerk = correction / dt², so the cap must be jerk-limited to avoid jerk spikes. 2. Clamp junction_vel to _pre_split_vel ± maxaccel * dt so stale or swapped profiles can't produce physically impossible velocity jumps at the crossing point. 3. Alt-entry chain improvements: feed-ratio scaling when comparing main profile v0 across feed changes, flythrough retry for short segments where Ruckig can't compute a valid profile, and reachability_exit_cap enforcement in the backward pass to prevent geometric smoothing from undoing forward-pass reachability constraints. Also removes debug probes (SPIKE_INFO, V0CORR_TRACK, DS_RECOMP, ALT_SEED, ALT_RESULT, FW_REACH, BW_ACTIVE).
…override When feed override changes while the machine is at high velocity on short segments, computeBranch fails (feed_limit_locked) and falls through to commit_seg=2, which calls writeAltEntry to provide RT with correct junction velocities. The flythrough bail check (entry_vel > capped_exit + tol) was blocking alt-entry writes at the junction segment itself, guaranteeing a velocity spike since the main profile v0 doesn't match the actual junction velocity. Fix: exempt the junction segment (start_depth) and any subsequent segments that inherited a flythrough from flythrough bail. At exempt depths, use a layered fallback: 1. Max-decel with uncapped (physical) exit velocity 2. Constant-velocity flythrough as last resort The prev_was_flythrough flag propagates the exemption through consecutive short segments that can't decelerate, ensuring the alt-entry chain reaches a segment long enough for actual deceleration. Also removes debug fprintf diagnostics (FEED_REJECT, COMMIT_REJECT, COMMIT_SEG2, ALT_WRITE, ALT_CONVERGE, PAIR_STATE, BT_SKIP_ACTIVE, FEED_PATH, FEED_BRANCH) and pre-existing rtapi_print_msg debug messages from computeBranch.
When the alt-entry chain converges (entry_vel ≈ main_v0 < 0.1 mm/s), any pre-existing alt-entry from a previous feed change is now cleared. Previously, the convergence break left stale alts intact with v0 values from a completely different feed era (gaps up to 40+ mm/s observed). RT could pick these stale alts at junction time, causing velocity spikes.
…are snap path Three fixes for feed override junction velocity mismatches: 1. Exempt feed hold from debounce: when feed drops below 0.1%, bypass the 50ms feed_override_debounce so computeBranch fires immediately. This writes correct spill-over alt-entries reflecting actual deceleration dynamics instead of stale profile exits from the previous feed. Previously a rapid feed drop (e.g. 194% → 1% → 0%) left stale alts from the first snap, causing 6+ mm/s junction spikes. 2. Branch-aware snap path: when the active segment has a taken brake branch, use computeSpillOver on the branch profile to estimate the actual junction velocity instead of getActiveProfileExit (stale from old feed). Covers the case where RT is already decelerating on a brake when the snap fires. 3. Preserve deceleration alts at convergence: the writeAltEntry convergence break now only invalidates "acceleration" alts (old_v0 >= entry_vel) while preserving deceleration alts that cover feed hold recovery where the machine arrives slower than expected. Also cleans up all debug instrumentation (HANDOFF_DBG, SPILL_FIX, ALT_WRITE, ALT_CALL, ALT_PRESERVE, OPT_ACTIVE, OPT_FWD).
…n clamp Spill-over stop profiles could produce negative velocity when the inherited deceleration exceeded sqrt(2*v*jerk), causing Ruckig to overshoot past zero. Clamp entry acceleration to the physics limit (no magic safety factor needed — discrete time steps are always v_min + j*dt²/8 > 0). Retry with a=0 as fallback. Also adds split-pending guard in tpHandleAbort and binary-search crossing velocity for abort spill-over boundary transitions.
…G1 transitions Three fixes for 5-axis blend quality: 1. Rate-weighted tangent vectors for Bezier control points: Scale per-subspace tangent vectors by their progress rates (tmag_sub/target) before passing to bezier9Init. This ensures the Bezier's 9D tangent direction at endpoints matches the adjacent line segment's actual 9D tangent. Previously, unit normalization per subspace gave equal weight to all subspaces regardless of their displacement rates, causing j0 velocity discontinuities at blend↔line boundaries (e.g., 2-sample steps on sawjet B-turn). 2. Primary-subspace arc-length parameterization for Bezier curves: Changed bezier9_deriv_mag and build_arc_length_table to use only the primary subspace's derivative magnitude (matching pmLine9Target priority: xyz > uvw > abc). With rate-weighted tangents, the old 9D arc length would balloon (abc control points ~15.9× farther apart for sawjet), making velocity units incompatible at line↔bezier junctions. Now both lines and Beziers measure progress in the same primary-subspace units. 3. Force TC_TERM_COND_STOP at G0↔G1 boundaries: The TANGENT promotion in the blend fallback path fired unconditionally, allowing non-zero exit velocity from a G1 into a G0 (or vice versa) with no blend to catch it. Now matches planner 0/1 handleModeChange() behavior. Both tpAddLine_9D and tpAddCircle_9D are fixed. Also removes all temporary diagnostic prints (_DBG).
No description provided.