@@ -2180,6 +2180,7 @@ static int recomputeDownstreamProfiles(TP_STRUCT *tp,
21802180
21812181 recomputed++;
21822182 }
2183+
21832184 return recomputed;
21842185}
21852186
@@ -2265,6 +2266,88 @@ static double estimateExitAtFeed(TP_STRUCT const *tp, TC_STRUCT const *tc,
22652266 tc->maxaccel , default_jerk, max_vel, target);
22662267}
22672268
2269+ /* *
2270+ * @brief Write an alternate entry profile for the next segment.
2271+ *
2272+ * When a brake branch on the current segment changes its exit velocity,
2273+ * the next segment's pre-computed profile has a stale entry velocity.
2274+ * This writes an alternate profile with v0 = brake's exit velocity.
2275+ * RT picks whichever profile (main or alt_entry) has v0 closer to the
2276+ * actual junction velocity — smooth regardless of whether brake was taken.
2277+ *
2278+ * Called BEFORE commitBranch so the cushion is in place before the jump.
2279+ */
2280+ static void writeAltEntry (TP_STRUCT *tp, TC_STRUCT *tc,
2281+ double branch_exit_vel, double new_feed_scale)
2282+ {
2283+ if (tc->term_cond != TC_TERM_COND_TANGENT) return ;
2284+
2285+ TC_STRUCT *next = tcqItem_user (&tp->queue , 1 );
2286+ if (!next) return ;
2287+
2288+ // Skip if alt_entry wouldn't differ meaningfully from main profile
2289+ if (__atomic_load_n (&next->shared_9d .profile .valid , __ATOMIC_ACQUIRE)) {
2290+ double main_v0 = next->shared_9d .profile .v [0 ];
2291+ if (fabs (branch_exit_vel - main_v0) < 0.1 ) return ;
2292+ }
2293+
2294+ double next_vel_limit = getEffectiveVelLimit (tp, next);
2295+ double next_max_vel = next_vel_limit * new_feed_scale;
2296+ next_max_vel = applyVLimit (tp, next, next_max_vel);
2297+ double next_jerk = next->maxjerk > 0 ? next->maxjerk : g_handoff_config.default_max_jerk ;
2298+
2299+ // Determine target exit velocity for next segment
2300+ double unscaled_fv = atomicLoadDouble (&next->shared_9d .final_vel );
2301+ double target_exit_vel = (next->term_cond == TC_TERM_COND_TANGENT)
2302+ ? fmin (unscaled_fv * new_feed_scale, next_max_vel)
2303+ : 0.0 ;
2304+ target_exit_vel = applyKinkVelCap (target_exit_vel, unscaled_fv, next_max_vel, next->kink_vel );
2305+
2306+ double achievable_exit = findAchievableExitVelocity (
2307+ branch_exit_vel, 0.0 ,
2308+ next->target , next->maxaccel , next_jerk,
2309+ next_max_vel, target_exit_vel);
2310+
2311+ // Compute Ruckig profile for next segment with adjusted entry velocity
2312+ try {
2313+ ruckig::Ruckig<1 > otg (g_handoff_config.servo_cycle_time_sec );
2314+ ruckig::InputParameter<1 > input;
2315+ ruckig::Trajectory<1 > traj;
2316+
2317+ input.current_position = {0.0 };
2318+ input.current_velocity = {branch_exit_vel};
2319+ input.current_acceleration = {0.0 };
2320+ input.target_position = {next->target };
2321+ input.target_velocity = {achievable_exit};
2322+ input.target_acceleration = {0.0 };
2323+ input.max_velocity = {next_max_vel};
2324+ input.max_acceleration = {next->maxaccel };
2325+ input.max_jerk = {next_jerk};
2326+
2327+ auto result = otg.calculate (input, traj);
2328+ if (result != ruckig::Result::Working && result != ruckig::Result::Finished)
2329+ return ;
2330+
2331+ ruckig_profile_t alt_profile;
2332+ memset (&alt_profile, 0 , sizeof (alt_profile));
2333+ copyRuckigProfile (traj, &alt_profile);
2334+ if (!alt_profile.valid ) return ;
2335+ if (profileHasNegativeVelocity (&alt_profile)) return ;
2336+
2337+ alt_profile.computed_feed_scale = new_feed_scale;
2338+ alt_profile.computed_vel_limit = next_vel_limit;
2339+ alt_profile.computed_vLimit = tp->vLimit ;
2340+
2341+ // Write alt_entry — cushion in place before the jump
2342+ next->shared_9d .alt_entry .profile = alt_profile;
2343+ next->shared_9d .alt_entry .v0 = branch_exit_vel;
2344+ __atomic_thread_fence (__ATOMIC_RELEASE);
2345+ __atomic_store_n (&next->shared_9d .alt_entry .valid , 1 , __ATOMIC_RELEASE);
2346+ } catch (...) {
2347+ // Alt-entry is best-effort; failure falls back to v0 correction in RT
2348+ }
2349+ }
2350+
22682351/* *
22692352 * @brief Compute a branch trajectory for feed override change
22702353 *
@@ -2289,6 +2372,11 @@ static double estimateExitAtFeed(TP_STRUCT const *tp, TC_STRUCT const *tc,
22892372 *
22902373 * @return true if branch was successfully computed and committed
22912374 */
2375+ static void computeSpillOver (const ruckig_profile_t *profile,
2376+ double remaining_dist,
2377+ double *spill_vel,
2378+ double *spill_acc);
2379+
22922380bool computeBranch (TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
22932381{
22942382 if (!tp || !tc) return false ;
@@ -2532,6 +2620,75 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
25322620
25332621 bool committed = commitBranch (&tc->shared_9d , &main_profile, NULL , 0.0 , 0.0 ,
25342622 handoff_time, state.position , 0.0 , window_end_time);
2623+
2624+ // Spill-over: if stop distance exceeds remaining segment, the
2625+ // machine will cross into downstream segments mid-deceleration.
2626+ // Write alt-entry stop profiles so RT has correct v0 at each
2627+ // boundary (same pattern as tpRequestAbortBranch_9D).
2628+ if (committed && tc->term_cond == TC_TERM_COND_TANGENT) {
2629+ double spill_vel, spill_acc;
2630+ computeSpillOver (&main_profile, remaining, &spill_vel, &spill_acc);
2631+
2632+ if (spill_vel > TP_VEL_EPSILON) {
2633+ TC_QUEUE_STRUCT *queue = &tp->queue ;
2634+ int queue_len = tcqLen_user (queue);
2635+ double entry_vel = spill_vel;
2636+ double entry_acc = spill_acc;
2637+
2638+ for (int i = 1 ; i < queue_len && entry_vel > TP_VEL_EPSILON; i++) {
2639+ TC_STRUCT *next = tcqItem_user (queue, i);
2640+ if (!next || next->target < 1e-9 ) break ;
2641+
2642+ double this_entry_vel = entry_vel;
2643+ double next_vel_limit = getEffectiveVelLimit (tp, next);
2644+ double next_jerk = next->maxjerk > 0 ? next->maxjerk : default_jerk;
2645+
2646+ try {
2647+ ruckig::Ruckig<1 > otg_spill (g_handoff_config.servo_cycle_time_sec );
2648+ ruckig::InputParameter<1 > in_spill;
2649+ ruckig::Trajectory<1 > traj_spill;
2650+
2651+ in_spill.control_interface = ruckig::ControlInterface::Velocity;
2652+ in_spill.current_position = {0.0 };
2653+ in_spill.current_velocity = {entry_vel};
2654+ in_spill.current_acceleration = {entry_acc};
2655+ in_spill.target_velocity = {0.0 };
2656+ in_spill.target_acceleration = {0.0 };
2657+ in_spill.max_velocity = {next_vel_limit};
2658+ in_spill.max_acceleration = {next->maxaccel };
2659+ in_spill.max_jerk = {next_jerk};
2660+
2661+ auto result = otg_spill.calculate (in_spill, traj_spill);
2662+ if (result != ruckig::Result::Working &&
2663+ result != ruckig::Result::Finished)
2664+ break ;
2665+
2666+ ruckig_profile_t stop_prof;
2667+ memset (&stop_prof, 0 , sizeof (stop_prof));
2668+ copyRuckigProfile (traj_spill, &stop_prof);
2669+ if (!stop_prof.valid || profileHasNegativeVelocity (&stop_prof))
2670+ break ;
2671+ stop_prof.computed_feed_scale = 0.0 ;
2672+ stop_prof.computed_vel_limit = next_vel_limit;
2673+ stop_prof.computed_vLimit = tp->vLimit ;
2674+
2675+ // Check if this segment also spills over
2676+ computeSpillOver (&stop_prof, next->target ,
2677+ &entry_vel, &entry_acc);
2678+
2679+ // Write as alt-entry so RT picks it at the junction
2680+ next->shared_9d .alt_entry .profile = stop_prof;
2681+ next->shared_9d .alt_entry .v0 = this_entry_vel;
2682+ __atomic_thread_fence (__ATOMIC_RELEASE);
2683+ __atomic_store_n (&next->shared_9d .alt_entry .valid , 1 ,
2684+ __ATOMIC_RELEASE);
2685+ } catch (...) {
2686+ break ;
2687+ }
2688+ }
2689+ }
2690+ }
2691+
25352692 return committed;
25362693 }
25372694
@@ -2672,6 +2829,8 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
26722829 brake_profile.computed_vLimit = tp->vLimit ;
26732830 }
26742831
2832+ bool use_single_stage = false ; // set true if sub-cycle gap detected
2833+
26752834 // Stage 2: Main profile (position control)
26762835 // Start from stage1_target_vel (after brake/stabilize), go to segment end
26772836 {
@@ -2753,11 +2912,73 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
27532912 main_profile.computed_feed_scale = effective_feed_scale;
27542913 main_profile.computed_vel_limit = vel_limit;
27552914 main_profile.computed_vLimit = tp->vLimit ;
2915+
2916+ // Sub-cycle gap fix: when the main profile is shorter than
2917+ // one servo cycle, the brake→main transition causes a
2918+ // displacement dip (jerk spike). Replace with a single-stage
2919+ // position-control profile from current state to segment end.
2920+ {
2921+ double cycle_time = g_handoff_config.servo_cycle_time_sec ;
2922+ if (main_profile.duration < cycle_time) {
2923+ ruckig::InputParameter<1 > alt_input;
2924+ ruckig::Trajectory<1 > alt_traj;
2925+
2926+ double alt_target_exit = (tc->term_cond == TC_TERM_COND_TANGENT)
2927+ ? fmin (atomicLoadDouble (&tc->shared_9d .final_vel ) * new_feed_scale, new_max_vel)
2928+ : 0.0 ;
2929+ alt_target_exit = applyKinkVelCap (alt_target_exit,
2930+ atomicLoadDouble (&tc->shared_9d .final_vel ), new_max_vel, tc->kink_vel );
2931+ alt_target_exit = fmin (alt_target_exit, downstream_exit_cap);
2932+ double alt_achievable = findAchievableExitVelocity (
2933+ state.velocity , state.acceleration ,
2934+ remaining, tc->maxaccel , default_jerk,
2935+ fmax (state.velocity , new_max_vel), alt_target_exit);
2936+
2937+ alt_input.control_interface = ruckig::ControlInterface::Position;
2938+ alt_input.current_position = {0.0 };
2939+ alt_input.current_velocity = {state.velocity };
2940+ alt_input.current_acceleration = {state.acceleration };
2941+ alt_input.target_position = {remaining};
2942+ alt_input.target_velocity = {alt_achievable};
2943+ alt_input.target_acceleration = {0.0 };
2944+ alt_input.max_velocity = {fmax (state.velocity , new_max_vel)};
2945+ alt_input.max_acceleration = {tc->maxaccel };
2946+ alt_input.max_jerk = {default_jerk};
2947+
2948+ auto alt_result = otg.calculate (alt_input, alt_traj);
2949+ bool alt_ok = (alt_result == ruckig::Result::Working ||
2950+ alt_result == ruckig::Result::Finished);
2951+
2952+ ruckig_profile_t alt_profile = {};
2953+ if (alt_ok) {
2954+ copyRuckigProfile (alt_traj, &alt_profile);
2955+ alt_ok = alt_profile.valid &&
2956+ !profileHasNegativeVelocity (&alt_profile);
2957+ }
2958+
2959+ if (alt_ok) {
2960+ // Replace two-stage with single-stage
2961+ main_profile = alt_profile;
2962+ main_profile.computed_feed_scale = effective_feed_scale;
2963+ main_profile.computed_vel_limit = vel_limit;
2964+ main_profile.computed_vLimit = tp->vLimit ;
2965+ achievable_exit = alt_achievable;
2966+ if (alt_achievable > alt_target_exit + 0.01 ) {
2967+ effective_feed_scale = alt_achievable / vel_limit;
2968+ if (effective_feed_scale > 1.0 ) effective_feed_scale = 1.0 ;
2969+ }
2970+ tc->shared_9d .achieved_exit_vel = alt_achievable;
2971+ use_single_stage = true ;
2972+ }
2973+ }
2974+ }
27562975 }
27572976
27582977 // Fix A: Reject if brake or main profile contains backward motion
2759- if (profileHasNegativeVelocity (&brake_profile) ||
2760- profileHasNegativeVelocity (&main_profile)) {
2978+ // (skip brake check when using single-stage — no brake profile)
2979+ if (!use_single_stage &&
2980+ (profileHasNegativeVelocity (&brake_profile) ||
2981+ profileHasNegativeVelocity (&main_profile))) {
27612982 rtapi_print_msg (RTAPI_MSG_DBG,
27622983 " Branch: REJECT seg=%d reason=negvel(2stg) brake_neg=%d main_neg=%d "
27632984 " brake_dist=%.4f seg_remaining=%.4f seg_target=%.4f progress=%.4f "
@@ -2777,13 +2998,22 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
27772998 g_ruckig_timing.count ++;
27782999 if (calc_us > g_ruckig_timing.max_us ) g_ruckig_timing.max_us = calc_us;
27793000
2780- // Commit with both profiles
2781- // brake_target_vel = stage1_target_vel (what brake/stabilize brings velocity to)
2782- bool committed = commitBranch (&tc->shared_9d , &main_profile, &brake_profile,
2783- stage1_target_vel, brake_end_position,
2784- handoff_time, state.position ,
2785- effective_feed_scale, window_end_time,
2786- tc->shared_9d .achieved_exit_vel );
3001+ // Cushion before jump: write alt-entry on N+1 before committing brake on N
3002+ writeAltEntry (tp, tc, tc->shared_9d .achieved_exit_vel , effective_feed_scale);
3003+
3004+ // Commit profile(s)
3005+ // When use_single_stage, commit as single-stage (no brake).
3006+ // Otherwise commit two-stage with brake_target_vel = stage1_target_vel.
3007+ bool committed = use_single_stage
3008+ ? commitBranch (&tc->shared_9d , &main_profile, NULL , 0.0 , 0.0 ,
3009+ handoff_time, state.position ,
3010+ effective_feed_scale, window_end_time,
3011+ tc->shared_9d .achieved_exit_vel )
3012+ : commitBranch (&tc->shared_9d , &main_profile, &brake_profile,
3013+ stage1_target_vel, brake_end_position,
3014+ handoff_time, state.position ,
3015+ effective_feed_scale, window_end_time,
3016+ tc->shared_9d .achieved_exit_vel );
27873017
27883018 return committed;
27893019
@@ -2900,6 +3130,9 @@ bool computeBranch(TP_STRUCT *tp, TC_STRUCT *tc, double new_feed_scale)
29003130 g_ruckig_timing.count ++;
29013131 if (calc_us > g_ruckig_timing.max_us ) g_ruckig_timing.max_us = calc_us;
29023132
3133+ // Cushion before jump: write alt-entry on N+1 before committing branch on N
3134+ writeAltEntry (tp, tc, tc->shared_9d .achieved_exit_vel , effective_feed_scale);
3135+
29033136 bool committed = commitBranch (&tc->shared_9d , &main_profile, NULL , 0.0 , 0.0 ,
29043137 handoff_time, state.position ,
29053138 effective_feed_scale, window_end_time,
@@ -3165,9 +3398,23 @@ extern "C" void manageBranches(TP_STRUCT *tp)
31653398 // Feed change detected.
31663399 // If cursor walk is active, queue this change (next-in-line, freely overwritable).
31673400 if (g_recompute_cursor > 0 ) {
3168- g_next_feed_scale = current_feed;
3169- g_next_rapid_scale = emcmotStatus->rapid_scale ;
3170- return ;
3401+ // Recovery from feed-hold: abort the hold cursor and restart fresh,
3402+ // just like program start. The hold cursor is walking at feed≈0,
3403+ // skipping segments with valid profiles. Those profiles are stale
3404+ // (computed at the pre-hold feed) and RT will hit them with wrong v0.
3405+ // By falling through to the normal feed-change path, we get
3406+ // invalidateNextNSegments + synchronous recomputeDownstreamProfiles
3407+ // before RT can advance into the stale segments.
3408+ if (g_recompute_feed_scale < 0.001 && current_feed > 0.001 ) {
3409+ g_recompute_cursor = 0 ;
3410+ g_next_feed_scale = -1.0 ;
3411+ g_next_rapid_scale = -1.0 ;
3412+ // Fall through to process recovery feed immediately
3413+ } else {
3414+ g_next_feed_scale = current_feed;
3415+ g_next_rapid_scale = emcmotStatus->rapid_scale ;
3416+ return ;
3417+ }
31713418 }
31723419
31733420 // Cursor idle — start processing this feed change.
@@ -3182,30 +3429,20 @@ extern "C" void manageBranches(TP_STRUCT *tp)
31823429
31833430 // Two-phase feed hold: when user requests 0%, first decelerate to 0.1%
31843431 // using normal Ruckig (Phase 1), then engage real 0% to stop completely
3185- // (Phase 2). Phase 1 writes 0.001 profiles downstream so that Phase 2's
3186- // createFeedHoldProfile passes the DONT_CLOBBER check (0.001 > 0.001 is false).
3187- // Use profile_feed (what's actually running) not canonical_feed (per-segment, can be stale).
3432+ // (Phase 2). Phase 1 is a normal Ruckig branch — no special treatment.
3433+ // Phase 2 only fires once the machine has actually decelerated to near the
3434+ // 0.1% target velocity, checked via predictStateAtTime (not profile feed
3435+ // label, which drops to 0.001 immediately on commit).
31883436 static const double MINIMUM_RUCKIG_FEED = 0.001 ; // 0.1%
3189- static int last_phase = 0 ; // Track state changes for logging
3190- if (snap_feed < 0.001 && profile_feed > MINIMUM_RUCKIG_FEED + 0.005 ) {
3191- // Phase 1: profile still at high feed, decelerate to 0.1% first
3192- snap_feed = MINIMUM_RUCKIG_FEED;
3193- if (last_phase != 1 ) {
3194- rtapi_print_msg (RTAPI_MSG_DBG,
3195- " PHASE1 seg %d: profile_feed=%.3f > 0.006, override to 0.1%%\n " ,
3196- tc->id , profile_feed);
3197- last_phase = 1 ;
3198- }
3199- } else if (snap_feed < 0.001 ) {
3200- // Phase 2: profile at 0.1% or lower, allow real 0%
3201- if (last_phase != 2 ) {
3202- rtapi_print_msg (RTAPI_MSG_DBG,
3203- " PHASE2 seg %d: profile_feed=%.3f <= 0.006, allowing real 0%%\n " ,
3204- tc->id , profile_feed);
3205- last_phase = 2 ;
3437+ if (snap_feed < 0.001 ) {
3438+ double elapsed = atomicLoadDouble (&tc->elapsed_time );
3439+ PredictedState probe = predictStateAtTime (tc, elapsed);
3440+ double vel_at_min_feed = tc->reqvel * MINIMUM_RUCKIG_FEED;
3441+ if (!probe.valid || probe.velocity > vel_at_min_feed) {
3442+ // Phase 1: still decelerating, use normal Ruckig at 0.1%
3443+ snap_feed = MINIMUM_RUCKIG_FEED;
32063444 }
3207- } else {
3208- last_phase = 0 ;
3445+ // else: Phase 2 — deceleration complete, allow real 0%
32093446 }
32103447
32113448 int commit_seg;
@@ -3811,6 +4048,7 @@ extern "C" void checkFeedOverride(TP_STRUCT *tp)
38114048 g_recompute_first_batch_done = true ;
38124049 // Update throughput estimate (EMA, alpha=0.3)
38134050 g_segments_per_tick = 0.7 * g_segments_per_tick + 0.3 * recomputed;
4051+
38144052 }
38154053
38164054 // Advance or finish
0 commit comments