LCOV - code coverage report
Current view: top level - src - FFBEngine.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 99.5 % 623 620
Test Date: 2026-03-01 21:30:38 Functions: 100.0 % 15 15
Branches: 80.3 % 476 382

             Branch data     Line data    Source code
       1                 :             : #include "FFBEngine.h"
       2                 :             : #include "Config.h"
       3                 :             : #include <iostream>
       4                 :             : #include <mutex>
       5                 :             : 
       6                 :             : extern std::recursive_mutex g_engine_mutex;
       7                 :             : #include <algorithm>
       8                 :             : #include <cmath>
       9                 :             : 
      10                 :             : using namespace ffb_math;
      11                 :             : 
      12         [ +  - ]:         676 : FFBEngine::FFBEngine() {
      13                 :         338 :     last_log_time = std::chrono::steady_clock::now();
      14         [ +  - ]:         338 :     Preset::ApplyDefaultsToEngine(*this);
      15                 :         338 : }
      16                 :             : 
      17                 :             : // v0.7.34: Safety Check for Issue #79
      18                 :             : // Determines if FFB should be active based on vehicle scoring state.
      19                 :             : // v0.7.49: Modified for #126 to allow cool-down laps after finish.
      20                 :          16 : bool FFBEngine::IsFFBAllowed(const VehicleScoringInfoV01& scoring, unsigned char gamePhase) const {
      21                 :             :     // mControl: 0 = local player, 1 = AI, 2 = Remote, -1 = None
      22                 :             :     // mFinishStatus: 0 = none, 1 = finished, 2 = DNF, 3 = DQ
      23                 :             : 
      24                 :             :     // 1. Core control check
      25   [ +  +  +  + ]:          16 :     if (!scoring.mIsPlayer || scoring.mControl != 0) return false;
      26                 :             : 
      27                 :             :     // 2. DO NOT use gamePhase to determine if IsFFBAllowed (eg. session over would cause no FFB after finish line, or time up)
      28                 :             :     // (gamePhase, Game Phases: 7=Stopped, 8=Session Over)
      29                 :             : 
      30                 :             :     // 3. Individual status safety
      31                 :             :     // Allow Finished (1) and DNF (2) as long as player is in control.
      32                 :             :     // Mute for Disqualified (3).
      33         [ +  + ]:           9 :     if (scoring.mFinishStatus == 3) return false;
      34                 :             : 
      35                 :             :     // 4. Garage Safety: Mute FFB when in garage stall
      36         [ +  + ]:           7 :     if (scoring.mInGarageStall) return false;
      37                 :             : 
      38                 :           5 :     return true;
      39                 :             : }
      40                 :             : 
      41                 :             : // v0.7.49: Slew rate limiter for safety (#79)
      42                 :             : // Clamps the rate of change of the output force to prevent violent jolts.
      43                 :             : // If restricted is true (e.g. after finish or lost control), limit is tighter.
      44                 :        4770 : double FFBEngine::ApplySafetySlew(double target_force, double dt, bool restricted) {
      45         [ +  + ]:        4770 :     if (!std::isfinite(target_force)) return 0.0;
      46         [ +  + ]:        4767 :     double max_slew = restricted ? (double)SAFETY_SLEW_RESTRICTED : (double)SAFETY_SLEW_NORMAL;
      47                 :        4767 :     double max_change = max_slew * dt;
      48                 :        4767 :     double delta = target_force - m_last_output_force;
      49                 :        4767 :     delta = std::clamp(delta, -max_change, max_change);
      50                 :        4767 :     m_last_output_force += delta;
      51                 :        4767 :     return m_last_output_force;
      52                 :             : }
      53                 :             : 
      54                 :             : // Helper to retrieve data (Consumer)
      55                 :         104 : std::vector<FFBSnapshot> FFBEngine::GetDebugBatch() {
      56                 :         104 :     std::vector<FFBSnapshot> batch;
      57                 :             :     {
      58         [ +  - ]:         104 :         std::lock_guard<std::mutex> lock(m_debug_mutex);
      59         [ +  + ]:         104 :         if (!m_debug_buffer.empty()) {
      60                 :          41 :             batch.swap(m_debug_buffer);
      61                 :             :         }
      62                 :         104 :     }
      63                 :         104 :     return batch;
      64                 :           0 : }
      65                 :             : 
      66                 :             : // ---------------------------------------------------------------------------
      67                 :             : // Grip & Load Estimation methods have been moved to GripLoadEstimation.cpp.
      68                 :             : // See docs/dev_docs/reports/FFBEngine_refactoring_analysis.md for rationale.
      69                 :             : // Functions moved:
      70                 :             : //   update_static_load_reference, InitializeLoadReference,
      71                 :             : //   calculate_raw_slip_angle_pair, calculate_slip_angle,
      72                 :             : //   calculate_grip, approximate_load, approximate_rear_load,
      73                 :             : //   calculate_kinematic_load, calculate_manual_slip_ratio,
      74                 :             : //   calculate_slope_grip, calculate_slope_confidence,
      75                 :             : //   calculate_wheel_slip_ratio
      76                 :             : // ---------------------------------------------------------------------------
      77                 :             : 
      78                 :             : 
      79                 :             : // Signal Conditioning: Applies idle smoothing and notch filters to raw torque
      80                 :             : // Returns the conditioned force value ready for effect processing
      81                 :       12359 : double FFBEngine::apply_signal_conditioning(double raw_torque, const TelemInfoV01* data, FFBCalculationContext& ctx) {
      82                 :       12359 :     double game_force_proc = raw_torque;
      83                 :             : 
      84                 :             :     // Idle Smoothing
      85                 :       12359 :     double effective_shaft_smoothing = (double)m_steering_shaft_smoothing;
      86                 :       12359 :     double idle_speed_threshold = (double)m_speed_gate_upper;
      87         [ +  + ]:       12359 :     if (idle_speed_threshold < (double)IDLE_SPEED_MIN_M_S) idle_speed_threshold = (double)IDLE_SPEED_MIN_M_S;
      88         [ +  + ]:       12359 :     if (ctx.car_speed < idle_speed_threshold) {
      89                 :        1018 :         double idle_blend = (idle_speed_threshold - ctx.car_speed) / idle_speed_threshold;
      90                 :        1018 :         double dynamic_smooth = (double)IDLE_BLEND_FACTOR * idle_blend; 
      91                 :        1018 :         effective_shaft_smoothing = (std::max)(effective_shaft_smoothing, dynamic_smooth);
      92                 :             :     }
      93                 :             :     
      94         [ +  + ]:       12359 :     if (effective_shaft_smoothing > MIN_TAU_S) {
      95                 :        1039 :         double alpha_shaft = ctx.dt / (effective_shaft_smoothing + ctx.dt);
      96                 :        1039 :         alpha_shaft = (std::min)(ALPHA_MAX, (std::max)(ALPHA_MIN, alpha_shaft));
      97                 :        1039 :         m_steering_shaft_torque_smoothed += alpha_shaft * (game_force_proc - m_steering_shaft_torque_smoothed);
      98                 :        1039 :         game_force_proc = m_steering_shaft_torque_smoothed;
      99                 :             :     } else {
     100                 :       11320 :         m_steering_shaft_torque_smoothed = game_force_proc;
     101                 :             :     }
     102                 :             : 
     103                 :             :     // Frequency Estimator Logic
     104                 :       12359 :     double alpha_hpf = ctx.dt / (HPF_TIME_CONSTANT_S + ctx.dt);
     105                 :       12359 :     m_torque_ac_smoothed += alpha_hpf * (game_force_proc - m_torque_ac_smoothed);
     106                 :       12359 :     double ac_torque = game_force_proc - m_torque_ac_smoothed;
     107                 :             : 
     108   [ +  +  +  + ]:       12359 :     if ((m_prev_ac_torque < -ZERO_CROSSING_EPSILON && ac_torque > ZERO_CROSSING_EPSILON) ||
     109   [ +  +  +  + ]:       11992 :         (m_prev_ac_torque > ZERO_CROSSING_EPSILON && ac_torque < -ZERO_CROSSING_EPSILON)) {
     110                 :             : 
     111                 :         731 :         double now = data->mElapsedTime;
     112                 :         731 :         double period = now - m_last_crossing_time;
     113                 :             : 
     114   [ +  +  +  - ]:         731 :         if (period > MIN_FREQ_PERIOD && period < MAX_FREQ_PERIOD) {
     115                 :          41 :             double inst_freq = 1.0 / (period * DUAL_DIVISOR);
     116                 :          41 :             m_debug_freq = m_debug_freq * DEBUG_FREQ_SMOOTHING + inst_freq * (1.0 - DEBUG_FREQ_SMOOTHING);
     117                 :             :         }
     118                 :         731 :         m_last_crossing_time = now;
     119                 :             :     }
     120                 :       12359 :     m_prev_ac_torque = ac_torque;
     121                 :             : 
     122                 :       12359 :     const TelemWheelV01& fl_ref = data->mWheel[0];
     123                 :       12359 :     double radius = (double)fl_ref.mStaticUndeflectedRadius / UNIT_CM_TO_M;
     124         [ +  + ]:       12359 :     if (radius < RADIUS_FALLBACK_MIN_M) radius = RADIUS_FALLBACK_DEFAULT_M;
     125                 :       12359 :     double circumference = TWO_PI * radius;
     126         [ +  - ]:       12359 :     double wheel_freq = (circumference > 0.0) ? (ctx.car_speed / circumference) : 0.0;
     127                 :       12359 :     m_theoretical_freq = wheel_freq;
     128                 :             :     
     129                 :             :     // Dynamic Notch Filter
     130         [ +  + ]:       12359 :     if (m_flatspot_suppression) {
     131         [ +  + ]:           3 :         if (wheel_freq > 1.0) {
     132         [ +  - ]:           1 :             m_notch_filter.Update(wheel_freq, 1.0/ctx.dt, (double)m_notch_q);
     133                 :           1 :             double input_force = game_force_proc;
     134                 :           1 :             double filtered_force = m_notch_filter.Process(input_force);
     135                 :           1 :             game_force_proc = input_force * (1.0f - m_flatspot_strength) + filtered_force * m_flatspot_strength;
     136                 :             :         } else {
     137                 :           2 :             m_notch_filter.Reset();
     138                 :             :         }
     139                 :             :     }
     140                 :             :     
     141                 :             :     // Static Notch Filter
     142         [ +  + ]:       12359 :     if (m_static_notch_enabled) {
     143                 :        1500 :          double bw = (double)m_static_notch_width;
     144         [ +  + ]:        1500 :          if (bw < MIN_NOTCH_WIDTH_HZ) bw = MIN_NOTCH_WIDTH_HZ;
     145                 :        1500 :          double q = (double)m_static_notch_freq / bw;
     146         [ +  - ]:        1500 :          m_static_notch_filter.Update((double)m_static_notch_freq, 1.0/ctx.dt, q);
     147                 :        1500 :          game_force_proc = m_static_notch_filter.Process(game_force_proc);
     148                 :             :     } else {
     149                 :       10859 :          m_static_notch_filter.Reset();
     150                 :             :     }
     151                 :             : 
     152                 :       12359 :     return game_force_proc;
     153                 :             : }
     154                 :             : 
     155                 :             : // Refactored calculate_force
     156                 :       12352 : double FFBEngine::calculate_force(const TelemInfoV01* data, const char* vehicleClass, const char* vehicleName, float genFFBTorque, bool allowed) {
     157         [ +  + ]:       12352 :     if (!data) return 0.0;
     158         [ +  - ]:       12350 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     159                 :             : 
     160                 :             :     // Select Torque Source
     161                 :             :     // v0.7.63 Fix: genFFBTorque (Direct Torque 400Hz) is normalized [-1.0, 1.0].
     162                 :             :     // It must be scaled by m_wheelbase_max_nm to match the engine's internal Nm-based pipeline.
     163         [ +  + ]:       12350 :     double raw_torque_input = (m_torque_source == 1) ? (double)genFFBTorque * (double)m_wheelbase_max_nm : data->mSteeringShaftTorque;
     164                 :             : 
     165                 :             :     // RELIABILITY FIX: Sanitize input torque
     166         [ +  + ]:       12350 :     if (!std::isfinite(raw_torque_input)) return 0.0;
     167                 :             : 
     168                 :             :     // --- 0. DYNAMIC NORMALIZATION (Issue #152) ---
     169                 :             :     // 1. Contextual Spike Rejection (Lightweight MAD alternative)
     170                 :       12349 :     double current_abs_torque = std::abs(raw_torque_input);
     171                 :       12349 :     double alpha_slow = data->mDeltaTime / (TORQUE_ROLL_AVG_TAU + data->mDeltaTime); // 1-second rolling average
     172                 :       12349 :     m_rolling_average_torque += alpha_slow * (current_abs_torque - m_rolling_average_torque);
     173                 :             : 
     174                 :       12349 :     double lat_g_abs = std::abs(data->mLocalAccel.x / GRAVITY_MS2);
     175                 :       12349 :     double torque_slew = std::abs(raw_torque_input - m_last_raw_torque) / (data->mDeltaTime + EPSILON_DIV);
     176                 :       12349 :     m_last_raw_torque = raw_torque_input;
     177                 :             : 
     178                 :             :     // Flag as spike if torque jumps > 3x the rolling average (with a 15Nm floor to prevent low-speed false positives)
     179   [ +  +  +  + ]:       12349 :     bool is_contextual_spike = (current_abs_torque > (m_rolling_average_torque * TORQUE_SPIKE_RATIO)) && (current_abs_torque > TORQUE_SPIKE_MIN_NM);
     180                 :             : 
     181                 :             :     // Safety check for clean state
     182   [ +  +  +  +  :       12349 :     bool is_clean_state = (lat_g_abs < LAT_G_CLEAN_LIMIT) && (torque_slew < TORQUE_SLEW_CLEAN_LIMIT) && !is_contextual_spike;
                   +  - ]
     183                 :             : 
     184                 :             :     // 2. Leaky Integrator (Exponential Decay + Floor)
     185   [ +  +  +  +  :       12349 :     if (is_clean_state && m_torque_source == 0 && m_dynamic_normalization_enabled) {
                   +  + ]
     186         [ +  + ]:         802 :         if (current_abs_torque > m_session_peak_torque) {
     187                 :         401 :             m_session_peak_torque = current_abs_torque; // Fast attack
     188                 :             :         } else {
     189                 :             :             // Exponential decay (0.5% reduction per second)
     190                 :         401 :             double decay_factor = 1.0 - (SESSION_PEAK_DECAY_RATE * data->mDeltaTime);
     191                 :         401 :             m_session_peak_torque *= decay_factor;
     192                 :             :         }
     193                 :             :         // Absolute safety floor and ceiling
     194                 :         802 :         m_session_peak_torque = std::clamp(m_session_peak_torque, (double)PEAK_TORQUE_FLOOR, (double)PEAK_TORQUE_CEILING);
     195                 :             :     }
     196                 :             : 
     197                 :             :     // 3. EMA Filtering on the Gain Multiplier (Zero-latency physics)
     198                 :             :     // v0.7.71: For In-Game FFB (1), we normalize against the wheelbase max since the signal is already normalized [-1, 1].
     199                 :             :     double target_structural_mult;
     200         [ +  + ]:       12349 :     if (m_torque_source == 1) {
     201                 :          19 :         target_structural_mult = 1.0 / (m_wheelbase_max_nm + EPSILON_DIV);
     202         [ +  + ]:       12330 :     } else if (m_dynamic_normalization_enabled) {
     203                 :         803 :         target_structural_mult = 1.0 / (m_session_peak_torque + EPSILON_DIV);
     204                 :             :     } else {
     205                 :       11527 :         target_structural_mult = 1.0 / (m_target_rim_nm + EPSILON_DIV);
     206                 :             :     }
     207                 :       12349 :     double alpha_gain = data->mDeltaTime / (STRUCT_MULT_SMOOTHING_TAU + data->mDeltaTime); // 250ms smoothing
     208                 :       12349 :     m_smoothed_structural_mult += alpha_gain * (target_structural_mult - m_smoothed_structural_mult);
     209                 :             : 
     210                 :             :     // Class Seeding
     211                 :       12349 :     bool seeded = false;
     212   [ +  +  +  -  :       12349 :     if (vehicleClass && m_current_class_name != vehicleClass) {
             +  +  +  + ]
     213         [ +  - ]:          48 :         m_current_class_name = vehicleClass;
     214         [ +  - ]:          48 :         InitializeLoadReference(vehicleClass, vehicleName);
     215                 :          48 :         seeded = true;
     216                 :             :     }
     217                 :             :     
     218                 :             :     // --- 1. INITIALIZE CONTEXT ---
     219                 :       12349 :     FFBCalculationContext ctx;
     220                 :       12349 :     ctx.dt = data->mDeltaTime;
     221                 :             : 
     222                 :             :     // Sanity Check: Delta Time
     223         [ +  + ]:       12349 :     if (ctx.dt <= DT_EPSILON) {
     224                 :         208 :         ctx.dt = DEFAULT_DT; // Default to 400Hz
     225         [ +  + ]:         208 :         if (!m_warned_dt) {
     226   [ +  -  +  -  :          18 :             std::cout << "[WARNING] Invalid DeltaTime (<=0). Using default " << DEFAULT_DT << "s." << std::endl;
             +  -  +  - ]
     227                 :          18 :             m_warned_dt = true;
     228                 :             :         }
     229                 :         208 :         ctx.frame_warn_dt = true;
     230                 :             :     }
     231                 :             :     
     232                 :       12349 :     ctx.car_speed_long = data->mLocalVel.z;
     233                 :       12349 :     ctx.car_speed = std::abs(ctx.car_speed_long);
     234                 :             :     
     235                 :             :     // Update Context strings (for UI/Logging)
     236                 :             :     // Only update if first char differs to avoid redundant copies
     237   [ +  +  -  + ]:       12349 :     if (m_vehicle_name[0] != data->mVehicleName[0] || m_vehicle_name[VEHICLE_NAME_CHECK_IDX] != data->mVehicleName[VEHICLE_NAME_CHECK_IDX]) {
     238                 :             : #ifdef _WIN32
     239                 :             :          strncpy_s(m_vehicle_name, sizeof(m_vehicle_name), data->mVehicleName, _TRUNCATE);
     240                 :             :          strncpy_s(m_track_name, sizeof(m_track_name), data->mTrackName, _TRUNCATE);
     241                 :             : #else
     242                 :         183 :          strncpy(m_vehicle_name, data->mVehicleName, STR_MAX_64);
     243                 :         183 :          m_vehicle_name[STR_MAX_64] = '\0';
     244                 :         183 :          strncpy(m_track_name, data->mTrackName, STR_MAX_64);
     245                 :         183 :          m_track_name[STR_MAX_64] = '\0';
     246                 :             : #endif
     247                 :             :     }
     248                 :             : 
     249                 :             :     // --- 2. SIGNAL CONDITIONING (STATE UPDATES) ---
     250                 :             :     
     251                 :             :     // Chassis Inertia Simulation
     252                 :       12349 :     double chassis_tau = (double)m_chassis_inertia_smoothing;
     253         [ +  - ]:       12349 :     if (chassis_tau < MIN_TAU_S) chassis_tau = MIN_TAU_S;
     254                 :       12349 :     double alpha_chassis = ctx.dt / (chassis_tau + ctx.dt);
     255                 :       12349 :     m_accel_x_smoothed += alpha_chassis * (data->mLocalAccel.x - m_accel_x_smoothed);
     256                 :       12349 :     m_accel_z_smoothed += alpha_chassis * (data->mLocalAccel.z - m_accel_z_smoothed);
     257                 :             : 
     258                 :             :     // --- 3. TELEMETRY PROCESSING ---
     259                 :             :     // Front Wheels
     260                 :       12349 :     const TelemWheelV01& fl = data->mWheel[0];
     261                 :       12349 :     const TelemWheelV01& fr = data->mWheel[1];
     262                 :             : 
     263                 :             :     // Raw Inputs
     264                 :       12349 :     double raw_torque = raw_torque_input;
     265                 :       12349 :     double raw_load = (fl.mTireLoad + fr.mTireLoad) / DUAL_DIVISOR;
     266                 :       12349 :     double raw_grip = (fl.mGripFract + fr.mGripFract) / DUAL_DIVISOR;
     267                 :             : 
     268                 :             :     // Update Stats
     269                 :       12349 :     s_torque.Update(raw_torque);
     270                 :       12349 :     s_load.Update(raw_load);
     271                 :       12349 :     s_grip.Update(raw_grip);
     272                 :       12349 :     s_lat_g.Update(data->mLocalAccel.x);
     273                 :             :     
     274                 :             :     // Stats Latching
     275                 :       12349 :     auto now = std::chrono::steady_clock::now();
     276   [ +  -  +  -  :       12349 :     if (std::chrono::duration_cast<std::chrono::seconds>(now - last_log_time).count() >= 1) {
                   +  + ]
     277                 :           1 :         s_torque.ResetInterval(); 
     278                 :           1 :         s_load.ResetInterval(); 
     279                 :           1 :         s_grip.ResetInterval(); 
     280                 :           1 :         s_lat_g.ResetInterval();
     281                 :           1 :         last_log_time = now;
     282                 :             :     }
     283                 :             : 
     284                 :             :     // --- 4. PRE-CALCULATIONS ---
     285                 :             : 
     286                 :             :     // Average Load & Fallback Logic
     287                 :       12349 :     ctx.avg_load = raw_load;
     288                 :             : 
     289                 :             :     // Hysteresis for missing load
     290   [ +  +  +  + ]:       12349 :     if (ctx.avg_load < 1.0 && ctx.car_speed > SPEED_EPSILON) {
     291                 :        2041 :         m_missing_load_frames++;
     292                 :             :     } else {
     293                 :       10308 :         m_missing_load_frames = (std::max)(0, m_missing_load_frames - 1);
     294                 :             :     }
     295                 :             : 
     296         [ +  + ]:       12349 :     if (m_missing_load_frames > MISSING_LOAD_WARN_THRESHOLD) {
     297                 :             :         // Fallback Logic
     298         [ +  + ]:        1307 :         if (fl.mSuspForce > MIN_VALID_SUSP_FORCE) {
     299         [ +  - ]:         129 :             double calc_load_fl = approximate_load(fl);
     300         [ +  - ]:         129 :             double calc_load_fr = approximate_load(fr);
     301                 :         129 :             ctx.avg_load = (calc_load_fl + calc_load_fr) / DUAL_DIVISOR;
     302                 :             :         } else {
     303         [ +  - ]:        1178 :             double kin_load_fl = calculate_kinematic_load(data, 0);
     304         [ +  - ]:        1178 :             double kin_load_fr = calculate_kinematic_load(data, 1);
     305                 :        1178 :             ctx.avg_load = (kin_load_fl + kin_load_fr) / DUAL_DIVISOR;
     306                 :             :         }
     307         [ +  + ]:        1307 :         if (!m_warned_load) {
     308   [ +  -  +  -  :          18 :             std::cout << "Warning: Data for mTireLoad from the game seems to be missing for this car (" << data->mVehicleName << "). (Likely Encrypted/DLC Content). Using Kinematic Fallback." << std::endl;
             +  -  +  - ]
     309                 :          18 :             m_warned_load = true;
     310                 :             :         }
     311                 :        1307 :         ctx.frame_warn_load = true;
     312                 :             :     }
     313                 :             : 
     314                 :             :     // Sanity Checks (Missing Data)
     315                 :             :     
     316                 :             :     // 1. Suspension Force (mSuspForce)
     317                 :       12349 :     double avg_susp_f = (fl.mSuspForce + fr.mSuspForce) / DUAL_DIVISOR;
     318   [ +  +  +  +  :       12349 :     if (avg_susp_f < MIN_VALID_SUSP_FORCE && std::abs(data->mLocalVel.z) > SPEED_EPSILON) {
                   +  + ]
     319                 :        2890 :         m_missing_susp_force_frames++;
     320                 :             :     } else {
     321                 :        9459 :          m_missing_susp_force_frames = (std::max)(0, m_missing_susp_force_frames - 1);
     322                 :             :     }
     323   [ +  +  +  + ]:       12349 :     if (m_missing_susp_force_frames > MISSING_TELEMETRY_WARN_THRESHOLD && !m_warned_susp_force) {
     324   [ +  -  +  -  :          11 :          std::cout << "Warning: Data for mSuspForce from the game seems to be missing for this car (" << data->mVehicleName << "). (Likely Encrypted/DLC Content). A fallback estimation will be used." << std::endl;
             +  -  +  - ]
     325                 :          11 :          m_warned_susp_force = true;
     326                 :             :     }
     327                 :             : 
     328                 :             :     // 2. Suspension Deflection (mSuspensionDeflection)
     329                 :       12349 :     double avg_susp_def = (std::abs(fl.mSuspensionDeflection) + std::abs(fr.mSuspensionDeflection)) / DUAL_DIVISOR;
     330   [ +  +  +  +  :       12349 :     if (avg_susp_def < DEFLECTION_NEAR_ZERO_M && std::abs(data->mLocalVel.z) > SPEED_HIGH_THRESHOLD) {
                   +  + ]
     331                 :       10514 :         m_missing_susp_deflection_frames++;
     332                 :             :     } else {
     333                 :        1835 :         m_missing_susp_deflection_frames = (std::max)(0, m_missing_susp_deflection_frames - 1);
     334                 :             :     }
     335   [ +  +  +  + ]:       12349 :     if (m_missing_susp_deflection_frames > MISSING_TELEMETRY_WARN_THRESHOLD && !m_warned_susp_deflection) {
     336   [ +  -  +  -  :          30 :         std::cout << "Warning: Data for mSuspensionDeflection from the game seems to be missing for this car (" << data->mVehicleName << "). (Likely Encrypted/DLC Content). A fallback estimation will be used." << std::endl;
             +  -  +  - ]
     337                 :          30 :         m_warned_susp_deflection = true;
     338                 :             :     }
     339                 :             : 
     340                 :             :     // 3. Front Lateral Force (mLateralForce)
     341                 :       12349 :     double avg_lat_force_front = (std::abs(fl.mLateralForce) + std::abs(fr.mLateralForce)) / DUAL_DIVISOR;
     342   [ +  -  +  +  :       12349 :     if (avg_lat_force_front < MIN_VALID_LAT_FORCE_N && std::abs(data->mLocalAccel.x) > G_FORCE_THRESHOLD) {
                   +  + ]
     343                 :        4547 :         m_missing_lat_force_front_frames++;
     344                 :             :     } else {
     345                 :        7802 :         m_missing_lat_force_front_frames = (std::max)(0, m_missing_lat_force_front_frames - 1);
     346                 :             :     }
     347   [ +  +  +  + ]:       12349 :     if (m_missing_lat_force_front_frames > MISSING_TELEMETRY_WARN_THRESHOLD && !m_warned_lat_force_front) {
     348   [ +  -  +  -  :          13 :          std::cout << "Warning: Data for mLateralForce (Front) from the game seems to be missing for this car (" << data->mVehicleName << "). (Likely Encrypted/DLC Content). A fallback estimation will be used." << std::endl;
             +  -  +  - ]
     349                 :          13 :          m_warned_lat_force_front = true;
     350                 :             :     }
     351                 :             : 
     352                 :             :     // 4. Rear Lateral Force (mLateralForce)
     353                 :       12349 :     double avg_lat_force_rear = (std::abs(data->mWheel[2].mLateralForce) + std::abs(data->mWheel[3].mLateralForce)) / DUAL_DIVISOR;
     354   [ +  +  +  +  :       12349 :     if (avg_lat_force_rear < MIN_VALID_LAT_FORCE_N && std::abs(data->mLocalAccel.x) > G_FORCE_THRESHOLD) {
                   +  + ]
     355                 :        4487 :         m_missing_lat_force_rear_frames++;
     356                 :             :     } else {
     357                 :        7862 :         m_missing_lat_force_rear_frames = (std::max)(0, m_missing_lat_force_rear_frames - 1);
     358                 :             :     }
     359   [ +  +  +  + ]:       12349 :     if (m_missing_lat_force_rear_frames > MISSING_TELEMETRY_WARN_THRESHOLD && !m_warned_lat_force_rear) {
     360   [ +  -  +  -  :          12 :          std::cout << "Warning: Data for mLateralForce (Rear) from the game seems to be missing for this car (" << data->mVehicleName << "). (Likely Encrypted/DLC Content). A fallback estimation will be used." << std::endl;
             +  -  +  - ]
     361                 :          12 :          m_warned_lat_force_rear = true;
     362                 :             :     }
     363                 :             : 
     364                 :             :     // 5. Vertical Tire Deflection (mVerticalTireDeflection)
     365                 :       12349 :     double avg_vert_def = (std::abs(fl.mVerticalTireDeflection) + std::abs(fr.mVerticalTireDeflection)) / DUAL_DIVISOR;
     366   [ +  +  +  +  :       12349 :     if (avg_vert_def < DEFLECTION_NEAR_ZERO_M && std::abs(data->mLocalVel.z) > SPEED_HIGH_THRESHOLD) {
                   +  + ]
     367                 :        1855 :         m_missing_vert_deflection_frames++;
     368                 :             :     } else {
     369                 :       10494 :         m_missing_vert_deflection_frames = (std::max)(0, m_missing_vert_deflection_frames - 1);
     370                 :             :     }
     371   [ +  +  +  + ]:       12349 :     if (m_missing_vert_deflection_frames > MISSING_TELEMETRY_WARN_THRESHOLD && !m_warned_vert_deflection) {
     372                 :           8 :         std::cout << "[WARNING] mVerticalTireDeflection is missing for car: " << data->mVehicleName 
     373   [ +  -  +  -  :           8 :                   << ". (Likely Encrypted/DLC Content). Road Texture fallback active." << std::endl;
             +  -  +  - ]
     374                 :           8 :         m_warned_vert_deflection = true;
     375                 :             :     }
     376                 :             :     
     377                 :             :     // Peak Hold Logic
     378   [ +  +  +  + ]:       12349 :     if (m_auto_load_normalization_enabled && !seeded) {
     379         [ +  + ]:         204 :         if (ctx.avg_load > m_auto_peak_load) {
     380                 :          53 :             m_auto_peak_load = ctx.avg_load; // Fast Attack
     381                 :             :         } else {
     382                 :         151 :             m_auto_peak_load -= (LOAD_DECAY_RATE * ctx.dt); // Slow Decay (~100N/s)
     383                 :             :         }
     384                 :             :     }
     385                 :       12349 :     m_auto_peak_load = (std::max)(LOAD_SAFETY_FLOOR, m_auto_peak_load); // Safety Floor
     386                 :             : 
     387                 :             :     // Load Factors (Stage 3: Giannoulis Soft-Knee Compression)
     388                 :             :     // 1. Calculate raw load multiplier based on static weight
     389                 :             :     // Safety clamp: Ensure load factor is non-negative even with telemetry noise
     390                 :       12349 :     double x = (std::max)(0.0, ctx.avg_load / m_static_front_load);
     391                 :             : 
     392                 :             :     // 2. Giannoulis Soft-Knee Parameters
     393                 :       12349 :     double T = COMPRESSION_KNEE_THRESHOLD;  // Threshold (Start compressing at 1.5x static weight)
     394                 :       12349 :     double W = COMPRESSION_KNEE_WIDTH;  // Knee Width (Transition from 1.25x to 1.75x)
     395                 :       12349 :     double R = COMPRESSION_RATIO;  // Compression Ratio (4:1 above the knee)
     396                 :             : 
     397                 :       12349 :     double lower_bound = T - (W / DUAL_DIVISOR);
     398                 :       12349 :     double upper_bound = T + (W / DUAL_DIVISOR);
     399                 :       12349 :     double compressed_load_factor = x;
     400                 :             : 
     401                 :             :     // 3. Apply Compression
     402         [ +  + ]:       12349 :     if (x > upper_bound) {
     403                 :             :         // Linear compressed region
     404                 :        9562 :         compressed_load_factor = T + ((x - T) / R);
     405         [ +  + ]:        2787 :     } else if (x > lower_bound) {
     406                 :             :         // Quadratic soft-knee transition
     407                 :         538 :         double diff = x - lower_bound;
     408                 :         538 :         compressed_load_factor = x + (((1.0 / R) - 1.0) * (diff * diff)) / (DUAL_DIVISOR * W);
     409                 :             :     }
     410                 :             : 
     411                 :             :     // 4. EMA Smoothing on the tactile multiplier (100ms time constant)
     412                 :       12349 :     double alpha_tactile = ctx.dt / (TACTILE_EMA_TAU + ctx.dt);
     413                 :       12349 :     m_smoothed_tactile_mult += alpha_tactile * (compressed_load_factor - m_smoothed_tactile_mult);
     414                 :             : 
     415                 :             :     // 5. Apply to context with user caps
     416                 :       12349 :     double texture_safe_max = (std::min)(USER_CAP_MAX, (double)m_texture_load_cap);
     417                 :       12349 :     ctx.texture_load_factor = (std::min)(texture_safe_max, m_smoothed_tactile_mult);
     418                 :             : 
     419                 :       12349 :     double brake_safe_max = (std::min)(USER_CAP_MAX, (double)m_brake_load_cap);
     420                 :       12349 :     ctx.brake_load_factor = (std::min)(brake_safe_max, m_smoothed_tactile_mult);
     421                 :             :     
     422                 :             :     // Hardware Scaling Safeties
     423                 :       12349 :     double wheelbase_max_safe = (double)m_wheelbase_max_nm;
     424         [ -  + ]:       12349 :     if (wheelbase_max_safe < 1.0) wheelbase_max_safe = 1.0;
     425                 :             : 
     426                 :             :     // Speed Gate - v0.7.2 Smoothstep S-curve
     427                 :       24698 :     ctx.speed_gate = smoothstep(
     428                 :       12349 :         (double)m_speed_gate_lower, 
     429         [ +  - ]:       12349 :         (double)m_speed_gate_upper, 
     430                 :             :         ctx.car_speed
     431                 :             :     );
     432                 :             : 
     433                 :             :     // --- 5. EFFECT CALCULATIONS ---
     434                 :             : 
     435                 :             :     // A. Understeer (Base Torque + Grip Loss)
     436                 :             : 
     437                 :             :     // Grip Estimation (v0.4.5 FIX)
     438                 :       12349 :     GripResult front_grip_res = calculate_grip(fl, fr, ctx.avg_load, m_warned_grip, 
     439                 :       12349 :                                                 m_prev_slip_angle[0], m_prev_slip_angle[1],
     440         [ +  - ]:       12349 :                                                 ctx.car_speed, ctx.dt, data->mVehicleName, data, true /* is_front */);
     441                 :       12349 :     ctx.avg_grip = front_grip_res.value;
     442                 :       12349 :     m_grip_diag.front_original = front_grip_res.original;
     443                 :       12349 :     m_grip_diag.front_approximated = front_grip_res.approximated;
     444                 :       12349 :     m_grip_diag.front_slip_angle = front_grip_res.slip_angle;
     445         [ +  + ]:       12349 :     if (front_grip_res.approximated) ctx.frame_warn_grip = true;
     446                 :             : 
     447                 :             :     // 2. Signal Conditioning (Smoothing, Notch Filters)
     448         [ +  - ]:       12349 :     double game_force_proc = apply_signal_conditioning(raw_torque_input, data, ctx);
     449                 :             : 
     450                 :             :     // Base Steering Force (Issue #178)
     451                 :       12349 :     double base_input = game_force_proc;
     452                 :             :     
     453                 :             :     // Apply Grip Modulation
     454                 :       12349 :     double grip_loss = (1.0 - ctx.avg_grip) * m_understeer_effect;
     455                 :       12349 :     ctx.grip_factor = (std::max)(0.0, 1.0 - grip_loss);
     456                 :             : 
     457                 :             :     // v0.7.63: Passthrough Logic for Direct Torque (TIC mode)
     458         [ +  + ]:       12349 :     double grip_factor_applied = m_torque_passthrough ? 1.0 : ctx.grip_factor;
     459                 :             : 
     460                 :             :     // v0.7.46: Dynamic Weight logic
     461         [ +  + ]:       12349 :     if (m_auto_load_normalization_enabled) {
     462         [ +  - ]:         218 :         update_static_load_reference(ctx.avg_load, ctx.car_speed, ctx.dt);
     463                 :             :     }
     464                 :       12349 :     double dynamic_weight_factor = 1.0;
     465                 :             : 
     466                 :             :     // Only apply if enabled AND we have real load data (no warnings)
     467   [ +  +  +  + ]:       12349 :     if (m_dynamic_weight_gain > 0.0 && !ctx.frame_warn_load) {
     468                 :         133 :         double load_ratio = ctx.avg_load / m_static_front_load;
     469                 :             :         // Blend: 1.0 + (Ratio - 1.0) * Gain
     470                 :         133 :         dynamic_weight_factor = 1.0 + (load_ratio - 1.0) * (double)m_dynamic_weight_gain;
     471                 :         133 :         dynamic_weight_factor = std::clamp(dynamic_weight_factor, DYNAMIC_WEIGHT_MIN, DYNAMIC_WEIGHT_MAX);
     472                 :             :     }
     473                 :             : 
     474                 :             :     // Apply Smoothing to Dynamic Weight (v0.7.47)
     475                 :       12349 :     double dw_alpha = ctx.dt / ((double)m_dynamic_weight_smoothing + ctx.dt + EPSILON_DIV);
     476                 :       12349 :     dw_alpha = (std::max)(0.0, (std::min)(1.0, dw_alpha));
     477                 :       12349 :     m_dynamic_weight_smoothed += dw_alpha * (dynamic_weight_factor - m_dynamic_weight_smoothed);
     478                 :       12349 :     dynamic_weight_factor = m_dynamic_weight_smoothed;
     479                 :             : 
     480                 :             :     // v0.7.63: Final factor application
     481         [ +  + ]:       12349 :     double dw_factor_applied = m_torque_passthrough ? 1.0 : dynamic_weight_factor;
     482                 :             :     
     483         [ +  + ]:       12349 :     double gain_to_apply = (m_torque_source == 1) ? (double)m_ingame_ffb_gain : (double)m_steering_shaft_gain;
     484                 :       12349 :     double output_force = (base_input * gain_to_apply) * dw_factor_applied * grip_factor_applied;
     485                 :       12349 :     output_force *= ctx.speed_gate;
     486                 :             :     
     487                 :             :     // B. SoP Lateral (Oversteer)
     488         [ +  - ]:       12349 :     calculate_sop_lateral(data, ctx);
     489                 :             :     
     490                 :             :     // C. Gyro Damping
     491                 :       12349 :     calculate_gyro_damping(data, ctx);
     492                 :             :     
     493                 :             :     // D. Effects
     494                 :       12349 :     calculate_abs_pulse(data, ctx);
     495         [ +  - ]:       12349 :     calculate_lockup_vibration(data, ctx);
     496         [ +  - ]:       12349 :     calculate_wheel_spin(data, ctx);
     497                 :       12349 :     calculate_slide_texture(data, ctx);
     498                 :       12349 :     calculate_road_texture(data, ctx);
     499                 :       12349 :     calculate_suspension_bottoming(data, ctx);
     500         [ +  - ]:       12349 :     calculate_soft_lock(data, ctx);
     501                 :             : 
     502                 :             :     // v0.7.78 FIX: Support stationary/garage soft lock (Issue #184)
     503                 :             :     // If not allowed (e.g. in garage or AI driving), mute all forces EXCEPT Soft Lock.
     504         [ +  + ]:       12349 :     if (!allowed) {
     505                 :           8 :         output_force = 0.0;
     506                 :           8 :         ctx.sop_base_force = 0.0;
     507                 :           8 :         ctx.rear_torque = 0.0;
     508                 :           8 :         ctx.yaw_force = 0.0;
     509                 :           8 :         ctx.gyro_force = 0.0;
     510                 :           8 :         ctx.scrub_drag_force = 0.0;
     511                 :           8 :         ctx.road_noise = 0.0;
     512                 :           8 :         ctx.slide_noise = 0.0;
     513                 :           8 :         ctx.spin_rumble = 0.0;
     514                 :           8 :         ctx.bottoming_crunch = 0.0;
     515                 :           8 :         ctx.abs_pulse_force = 0.0;
     516                 :           8 :         ctx.lockup_rumble = 0.0;
     517                 :             :         // NOTE: ctx.soft_lock_force is PRESERVED.
     518                 :             : 
     519                 :             :         // Also zero out base_input for snapshot clarity
     520                 :           8 :         base_input = 0.0;
     521                 :             :     }
     522                 :             :     
     523                 :             :     // --- 6. SUMMATION (Issue #152 & #153 Split Scaling) ---
     524                 :             :     // Split into Structural (Dynamic Normalization) and Texture (Absolute Nm) groups
     525                 :             :     // v0.7.77 FIX: Soft Lock moved to Texture group to maintain absolute Nm scaling (Issue #181)
     526                 :       12349 :     double structural_sum = output_force + ctx.sop_base_force + ctx.rear_torque + ctx.yaw_force + ctx.gyro_force +
     527                 :       12349 :                             ctx.scrub_drag_force;
     528                 :             : 
     529                 :             :     // Apply Torque Drop (from Spin/Traction Loss) only to structural physics
     530                 :       12349 :     structural_sum *= ctx.gain_reduction_factor;
     531                 :             :     
     532                 :             :     // Apply Dynamic Normalization to structural forces
     533                 :       12349 :     double norm_structural = structural_sum * m_smoothed_structural_mult;
     534                 :             : 
     535                 :             :     // Tactile Textures are calculated in absolute Nm
     536                 :             :     // v0.7.110: Apply m_tactile_gain to textures, but NOT to Soft Lock (Issue #206)
     537                 :       12349 :     double tactile_sum_nm = ctx.road_noise + ctx.slide_noise + ctx.spin_rumble + ctx.bottoming_crunch + ctx.abs_pulse_force + ctx.lockup_rumble;
     538                 :       12349 :     double final_texture_nm = (tactile_sum_nm * (double)m_tactile_gain) + ctx.soft_lock_force;
     539                 :             : 
     540                 :             :     // --- 7. OUTPUT SCALING (Physical Target Model) ---
     541                 :             :     // Map structural to the target rim torque, then divide by wheelbase max to get DirectInput %
     542                 :       12349 :     double di_structural = norm_structural * ((double)m_target_rim_nm / wheelbase_max_safe);
     543                 :             : 
     544                 :             :     // Map absolute texture Nm directly to the wheelbase max
     545                 :       12349 :     double di_texture = final_texture_nm / wheelbase_max_safe;
     546                 :             : 
     547                 :       12349 :     double norm_force = (di_structural + di_texture) * m_gain;
     548                 :             : 
     549                 :             :     // Min Force
     550                 :             :     // v0.7.85 FIX: Bypass min_force if NOT allowed (e.g. in garage) unless soft lock is significant.
     551                 :             :     // This prevents the "grinding" feel from tiny residuals when FFB should be muted.
     552                 :       12349 :     bool significant_soft_lock = std::abs(ctx.soft_lock_force) > SOFT_LOCK_MUTE_THRESHOLD_NM; // > 0.1 Nm
     553   [ +  +  +  + ]:       12349 :     if (allowed || significant_soft_lock) {
     554   [ +  +  +  +  :       12347 :         if (std::abs(norm_force) > FFB_EPSILON && std::abs(norm_force) < m_min_force) {
                   +  + ]
     555         [ +  + ]:         161 :             double sign = (norm_force > 0.0) ? 1.0 : -1.0;
     556                 :         161 :             norm_force = sign * m_min_force;
     557                 :             :         }
     558                 :             :     }
     559                 :             : 
     560         [ +  + ]:       12349 :     if (m_invert_force) {
     561                 :        1065 :         norm_force *= -1.0;
     562                 :             :     }
     563                 :             : 
     564                 :             :     // --- 8. STATE UPDATES (POST-CALC) ---
     565                 :             :     // CRITICAL: These updates must run UNCONDITIONALLY every frame to prevent
     566                 :             :     // stale state issues when effects are toggled on/off.
     567         [ +  + ]:       61745 :     for (int i = 0; i < 4; i++) {
     568                 :       49396 :         m_prev_vert_deflection[i] = data->mWheel[i].mVerticalTireDeflection;
     569                 :       49396 :         m_prev_rotation[i] = data->mWheel[i].mRotation;
     570                 :       49396 :         m_prev_brake_pressure[i] = data->mWheel[i].mBrakePressure;
     571                 :             :     }
     572                 :       12349 :     m_prev_susp_force[0] = fl.mSuspForce;
     573                 :       12349 :     m_prev_susp_force[1] = fr.mSuspForce;
     574                 :             :     
     575                 :             :     // v0.6.36 FIX: Move m_prev_vert_accel to unconditional section
     576                 :             :     // Previously only updated inside calculate_road_texture when enabled.
     577                 :             :     // Now always updated to prevent stale data if other effects use it.
     578                 :       12349 :     m_prev_vert_accel = data->mLocalAccel.y;
     579                 :             : 
     580                 :             :     // --- 9. SNAPSHOT ---
     581                 :             :     // This block captures the current state of the FFB Engine (inputs, outputs, intermediate calculations)
     582                 :             :     // into a thread-safe buffer. These snapshots are retrieved by the GUI layer (or other consumers)
     583                 :             :     // to visualize real-time telemetry graphs, FFB clipping, and effect contributions.
     584                 :             :     // It uses a mutex to protect the shared circular buffer.
     585                 :             :     {
     586         [ +  - ]:       12349 :         std::lock_guard<std::mutex> lock(m_debug_mutex);
     587         [ +  + ]:       12349 :         if (m_debug_buffer.size() < DEBUG_BUFFER_CAP) {
     588                 :             :             FFBSnapshot snap;
     589                 :        5234 :             snap.total_output = (float)norm_force;
     590                 :        5234 :             snap.base_force = (float)base_input;
     591                 :        5234 :             snap.sop_force = (float)ctx.sop_unboosted_force; // Use unboosted for snapshot
     592                 :        5234 :             snap.understeer_drop = (float)((base_input * m_steering_shaft_gain) * (1.0 - grip_factor_applied));
     593                 :        5234 :             snap.oversteer_boost = (float)(ctx.sop_base_force - ctx.sop_unboosted_force); // Exact boost amount
     594                 :             : 
     595                 :        5234 :             snap.ffb_rear_torque = (float)ctx.rear_torque;
     596                 :        5234 :             snap.ffb_scrub_drag = (float)ctx.scrub_drag_force;
     597                 :        5234 :             snap.ffb_yaw_kick = (float)ctx.yaw_force;
     598                 :        5234 :             snap.ffb_gyro_damping = (float)ctx.gyro_force;
     599                 :        5234 :             snap.texture_road = (float)ctx.road_noise;
     600                 :        5234 :             snap.texture_slide = (float)ctx.slide_noise;
     601                 :        5234 :             snap.texture_lockup = (float)ctx.lockup_rumble;
     602                 :        5234 :             snap.texture_spin = (float)ctx.spin_rumble;
     603                 :        5234 :             snap.texture_bottoming = (float)ctx.bottoming_crunch;
     604                 :        5234 :             snap.ffb_abs_pulse = (float)ctx.abs_pulse_force; 
     605                 :        5234 :             snap.ffb_soft_lock = (float)ctx.soft_lock_force;
     606                 :        5234 :             snap.session_peak_torque = (float)m_session_peak_torque;
     607         [ +  + ]:        5234 :             snap.clipping = (std::abs(norm_force) > (double)CLIPPING_THRESHOLD) ? 1.0f : 0.0f;
     608                 :             : 
     609                 :             :             // Physics
     610                 :        5234 :             snap.calc_front_load = (float)ctx.avg_load;
     611                 :        5234 :             snap.calc_rear_load = (float)ctx.avg_rear_load;
     612                 :        5234 :             snap.calc_rear_lat_force = (float)ctx.calc_rear_lat_force;
     613                 :        5234 :             snap.calc_front_grip = (float)ctx.avg_grip;
     614                 :        5234 :             snap.calc_rear_grip = (float)ctx.avg_rear_grip;
     615                 :        5234 :             snap.calc_front_slip_angle_smoothed = (float)m_grip_diag.front_slip_angle;
     616                 :        5234 :             snap.calc_rear_slip_angle_smoothed = (float)m_grip_diag.rear_slip_angle;
     617                 :             : 
     618         [ +  - ]:        5234 :             snap.raw_front_slip_angle = (float)calculate_raw_slip_angle_pair(fl, fr);
     619         [ +  - ]:        5234 :             snap.raw_rear_slip_angle = (float)calculate_raw_slip_angle_pair(data->mWheel[2], data->mWheel[3]);
     620                 :             : 
     621                 :             :             // Telemetry
     622                 :        5234 :             snap.steer_force = (float)raw_torque;
     623                 :        5234 :             snap.raw_shaft_torque = (float)data->mSteeringShaftTorque;
     624                 :        5234 :             snap.raw_gen_torque = (float)genFFBTorque;
     625                 :        5234 :             snap.raw_input_steering = (float)data->mUnfilteredSteering;
     626                 :        5234 :             snap.raw_front_tire_load = (float)raw_load;
     627                 :        5234 :             snap.raw_front_grip_fract = (float)raw_grip;
     628                 :        5234 :             snap.raw_rear_grip = (float)((data->mWheel[2].mGripFract + data->mWheel[3].mGripFract) / DUAL_DIVISOR);
     629                 :        5234 :             snap.raw_front_susp_force = (float)((fl.mSuspForce + fr.mSuspForce) / DUAL_DIVISOR);
     630                 :        5234 :             snap.raw_front_ride_height = (float)((std::min)(fl.mRideHeight, fr.mRideHeight));
     631                 :        5234 :             snap.raw_rear_lat_force = (float)((data->mWheel[2].mLateralForce + data->mWheel[3].mLateralForce) / DUAL_DIVISOR);
     632                 :        5234 :             snap.raw_car_speed = (float)ctx.car_speed_long;
     633                 :        5234 :             snap.raw_input_throttle = (float)data->mUnfilteredThrottle;
     634                 :        5234 :             snap.raw_input_brake = (float)data->mUnfilteredBrake;
     635                 :        5234 :             snap.accel_x = (float)data->mLocalAccel.x;
     636                 :        5234 :             snap.raw_front_lat_patch_vel = (float)((std::abs(fl.mLateralPatchVel) + std::abs(fr.mLateralPatchVel)) / DUAL_DIVISOR);
     637                 :        5234 :             snap.raw_front_deflection = (float)((fl.mVerticalTireDeflection + fr.mVerticalTireDeflection) / DUAL_DIVISOR);
     638                 :        5234 :             snap.raw_front_long_patch_vel = (float)((fl.mLongitudinalPatchVel + fr.mLongitudinalPatchVel) / DUAL_DIVISOR);
     639                 :        5234 :             snap.raw_rear_lat_patch_vel = (float)((std::abs(data->mWheel[2].mLateralPatchVel) + std::abs(data->mWheel[3].mLateralPatchVel)) / DUAL_DIVISOR);
     640                 :        5234 :             snap.raw_rear_long_patch_vel = (float)((data->mWheel[2].mLongitudinalPatchVel + data->mWheel[3].mLongitudinalPatchVel) / DUAL_DIVISOR);
     641                 :             : 
     642                 :        5234 :             snap.warn_load = ctx.frame_warn_load;
     643   [ +  +  +  + ]:        5234 :             snap.warn_grip = ctx.frame_warn_grip || ctx.frame_warn_rear_grip;
     644                 :        5234 :             snap.warn_dt = ctx.frame_warn_dt;
     645                 :        5234 :             snap.debug_freq = (float)m_debug_freq;
     646                 :        5234 :             snap.tire_radius = (float)fl.mStaticUndeflectedRadius / 100.0f;
     647                 :        5234 :             snap.slope_current = (float)m_slope_current; // v0.7.1: Slope detection diagnostic
     648                 :             : 
     649                 :        5234 :             snap.ffb_rate = (float)m_ffb_rate;
     650                 :        5234 :             snap.telemetry_rate = (float)m_telemetry_rate;
     651                 :        5234 :             snap.hw_rate = (float)m_hw_rate;
     652                 :        5234 :             snap.torque_rate = (float)m_torque_rate;
     653                 :        5234 :             snap.gen_torque_rate = (float)m_gen_torque_rate;
     654                 :             : 
     655         [ +  - ]:        5234 :             m_debug_buffer.push_back(snap);
     656                 :             :         }
     657                 :       12349 :     }
     658                 :             :     
     659                 :             :     // Telemetry Logging (v0.7.x)
     660   [ +  -  +  + ]:       12349 :     if (AsyncLogger::Get().IsLogging()) {
     661                 :             :         LogFrame frame;
     662                 :         276 :         frame.timestamp = data->mElapsedTime;
     663                 :         276 :         frame.delta_time = data->mDeltaTime;
     664                 :             :         
     665                 :             :         // Inputs
     666                 :         276 :         frame.steering = (float)data->mUnfilteredSteering;
     667                 :         276 :         frame.throttle = (float)data->mUnfilteredThrottle;
     668                 :         276 :         frame.brake = (float)data->mUnfilteredBrake;
     669                 :             :         
     670                 :             :         // Vehicle state
     671                 :         276 :         frame.speed = (float)ctx.car_speed;
     672                 :         276 :         frame.lat_accel = (float)data->mLocalAccel.x;
     673                 :         276 :         frame.long_accel = (float)data->mLocalAccel.z;
     674                 :         276 :         frame.yaw_rate = (float)data->mLocalRot.y;
     675                 :             :         
     676                 :             :         // Front Axle raw
     677                 :         276 :         frame.slip_angle_fl = (float)fl.mLateralPatchVel / (float)(std::max)(1.0, ctx.car_speed);
     678                 :         276 :         frame.slip_angle_fr = (float)fr.mLateralPatchVel / (float)(std::max)(1.0, ctx.car_speed);
     679         [ +  - ]:         276 :         frame.slip_ratio_fl = (float)calculate_wheel_slip_ratio(fl);
     680         [ +  - ]:         276 :         frame.slip_ratio_fr = (float)calculate_wheel_slip_ratio(fr);
     681                 :         276 :         frame.grip_fl = (float)fl.mGripFract;
     682                 :         276 :         frame.grip_fr = (float)fr.mGripFract;
     683                 :         276 :         frame.load_fl = (float)fl.mTireLoad;
     684                 :         276 :         frame.load_fr = (float)fr.mTireLoad;
     685                 :             :         
     686                 :             :         // Calculated values
     687                 :         276 :         frame.calc_slip_angle_front = (float)m_grip_diag.front_slip_angle;
     688                 :         276 :         frame.calc_grip_front = (float)ctx.avg_grip;
     689                 :             :         
     690                 :             :         // Slope detection
     691                 :         276 :         frame.dG_dt = (float)m_slope_dG_dt;
     692                 :         276 :         frame.dAlpha_dt = (float)m_slope_dAlpha_dt;
     693                 :         276 :         frame.slope_current = (float)m_slope_current;
     694                 :         276 :         frame.slope_raw_unclamped = (float)m_debug_slope_raw;
     695                 :         276 :         frame.slope_numerator = (float)m_debug_slope_num;
     696                 :         276 :         frame.slope_denominator = (float)m_debug_slope_den;
     697                 :         276 :         frame.hold_timer = (float)m_slope_hold_timer;
     698                 :         276 :         frame.input_slip_smoothed = (float)m_slope_slip_smoothed;
     699                 :         276 :         frame.slope_smoothed = (float)m_slope_smoothed_output;
     700                 :             :         
     701                 :             :         // Use extracted confidence calculation
     702         [ +  - ]:         276 :         frame.confidence = (float)calculate_slope_confidence(m_slope_dAlpha_dt);
     703                 :             :         
     704                 :             :         // Surface types (Accuracy Tools)
     705                 :         276 :         frame.surface_type_fl = (float)fl.mSurfaceType;
     706                 :         276 :         frame.surface_type_fr = (float)fr.mSurfaceType;
     707                 :             : 
     708                 :             :         // Advanced Slope Detection (v0.7.40)
     709                 :         276 :         frame.slope_torque = (float)m_slope_torque_current;
     710                 :         276 :         frame.slew_limited_g = (float)m_debug_lat_g_slew;
     711                 :             : 
     712                 :             :         // Rear axle
     713                 :         276 :         frame.calc_grip_rear = (float)ctx.avg_rear_grip;
     714                 :         276 :         frame.grip_delta = (float)(ctx.avg_grip - ctx.avg_rear_grip);
     715                 :             :         
     716                 :             :         // FFB output
     717                 :         276 :         frame.ffb_total = (float)norm_force;
     718                 :         276 :         frame.ffb_shaft_torque = (float)data->mSteeringShaftTorque;
     719                 :         276 :         frame.ffb_gen_torque = (float)genFFBTorque;
     720                 :         276 :         frame.ffb_grip_factor = (float)ctx.grip_factor;
     721                 :         276 :         frame.ffb_sop = (float)ctx.sop_base_force;
     722                 :         276 :         frame.speed_gate = (float)ctx.speed_gate;
     723                 :         276 :         frame.load_peak_ref = (float)m_auto_peak_load;
     724                 :         276 :         frame.clipping = (std::abs(norm_force) > CLIPPING_THRESHOLD);
     725                 :         276 :         frame.ffb_base = (float)base_input; // Plan included ffb_base
     726                 :             :         
     727   [ +  -  +  - ]:         276 :         AsyncLogger::Get().Log(frame);
     728                 :             :     }
     729                 :             :     
     730                 :       12349 :     return (std::max)(-1.0, (std::min)(1.0, norm_force));
     731                 :       12350 : }
     732                 :             : 
     733                 :             : // Helper: Calculate Seat-of-the-Pants (SoP) Lateral & Oversteer Boost
     734                 :       12350 : void FFBEngine::calculate_sop_lateral(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     735                 :             :     // 1. Raw Lateral G (Chassis-relative X)
     736                 :             :     // Clamp to 5G to prevent numeric instability in crashes
     737                 :       12350 :     double raw_g = (std::max)(-G_LIMIT_5G * GRAVITY_MS2, (std::min)(G_LIMIT_5G * GRAVITY_MS2, data->mLocalAccel.x));
     738                 :       12350 :     double lat_g = (raw_g / GRAVITY_MS2);
     739                 :             :     
     740                 :             :     // Smoothing: Map 0.0-1.0 slider to 0.1-0.0001s tau
     741                 :       12350 :     double smoothness = 1.0 - (double)m_sop_smoothing_factor;
     742                 :       12350 :     smoothness = (std::max)(0.0, (std::min)(SMOOTHNESS_LIMIT_0999, smoothness));
     743                 :       12350 :     double tau = smoothness * SOP_SMOOTHING_MAX_TAU;
     744                 :       12350 :     double alpha = ctx.dt / (tau + ctx.dt);
     745                 :       12350 :     alpha = (std::max)(MIN_LFM_ALPHA, (std::min)(1.0, alpha));
     746                 :       12350 :     m_sop_lat_g_smoothed += alpha * (lat_g - m_sop_lat_g_smoothed);
     747                 :             :     
     748                 :             :     // Base SoP Force
     749                 :       12350 :     double sop_base = m_sop_lat_g_smoothed * m_sop_effect * (double)m_sop_scale;
     750                 :       12350 :     ctx.sop_unboosted_force = sop_base; // Store for snapshot
     751                 :             :     
     752                 :             :     // 2. Oversteer Boost (Grip Differential)
     753                 :             :     // Calculate Rear Grip
     754                 :       12350 :     GripResult rear_grip_res = calculate_grip(data->mWheel[2], data->mWheel[3], ctx.avg_load, m_warned_rear_grip,
     755                 :       12350 :                                                 m_prev_slip_angle[2], m_prev_slip_angle[3],
     756         [ +  - ]:       12350 :                                                 ctx.car_speed, ctx.dt, data->mVehicleName, data, false /* is_front */);
     757                 :       12350 :     ctx.avg_rear_grip = rear_grip_res.value;
     758                 :       12350 :     m_grip_diag.rear_original = rear_grip_res.original;
     759                 :       12350 :     m_grip_diag.rear_approximated = rear_grip_res.approximated;
     760                 :       12350 :     m_grip_diag.rear_slip_angle = rear_grip_res.slip_angle;
     761         [ +  + ]:       12350 :     if (rear_grip_res.approximated) ctx.frame_warn_rear_grip = true;
     762                 :             :     
     763         [ +  + ]:       12350 :     if (!m_slope_detection_enabled) {
     764                 :       10309 :         double grip_delta = ctx.avg_grip - ctx.avg_rear_grip;
     765         [ +  + ]:       10309 :         if (grip_delta > 0.0) {
     766                 :         940 :             sop_base *= (1.0 + (grip_delta * m_oversteer_boost * OVERSTEER_BOOST_MULT));
     767                 :             :         }
     768                 :             :     }
     769                 :       12350 :     ctx.sop_base_force = sop_base;
     770                 :             :     
     771                 :             :     // 3. Rear Aligning Torque (v0.4.9)
     772                 :             :     // Calculate load for rear wheels (for tire stiffness scaling)
     773         [ +  - ]:       12350 :     double calc_load_rl = approximate_rear_load(data->mWheel[2]);
     774         [ +  - ]:       12350 :     double calc_load_rr = approximate_rear_load(data->mWheel[3]);
     775                 :       12350 :     ctx.avg_rear_load = (calc_load_rl + calc_load_rr) / DUAL_DIVISOR;
     776                 :             :     
     777                 :             :     // Rear lateral force estimation: F = Alpha * k * TireLoad
     778                 :       12350 :     double rear_slip_angle = m_grip_diag.rear_slip_angle;
     779                 :       12350 :     ctx.calc_rear_lat_force = rear_slip_angle * ctx.avg_rear_load * REAR_TIRE_STIFFNESS_COEFFICIENT;
     780                 :       12350 :     ctx.calc_rear_lat_force = (std::max)(-MAX_REAR_LATERAL_FORCE, (std::min)(MAX_REAR_LATERAL_FORCE, ctx.calc_rear_lat_force));
     781                 :             :     
     782                 :             :     // Torque = Force * Aligning_Lever
     783                 :             :     // Note negative sign: Oversteer (Rear Slide) pushes wheel TOWARDS slip direction
     784                 :       12350 :     ctx.rear_torque = -ctx.calc_rear_lat_force * REAR_ALIGN_TORQUE_COEFFICIENT * m_rear_align_effect;
     785                 :             :     
     786                 :             :     // 4. Yaw Kick (Inertial Oversteer)
     787                 :       12350 :     double raw_yaw_accel = data->mLocalRotAccel.y;
     788                 :             :     // v0.4.16: Reject yaw at low speeds and below threshold
     789   [ +  +  +  +  :       12350 :     if (ctx.car_speed < MIN_YAW_KICK_SPEED_MS || std::abs(raw_yaw_accel) < (double)m_yaw_kick_threshold) {
                   +  + ]
     790                 :        1022 :         raw_yaw_accel = 0.0;
     791                 :             :     }
     792                 :             :     
     793                 :             :     // Alpha Smoothing (v0.4.16)
     794                 :       12350 :     double tau_yaw = (double)m_yaw_accel_smoothing;
     795         [ +  + ]:       12350 :     if (tau_yaw < MIN_TAU_S) tau_yaw = MIN_TAU_S;
     796                 :       12350 :     double alpha_yaw = ctx.dt / (tau_yaw + ctx.dt);
     797                 :       12350 :     m_yaw_accel_smoothed += alpha_yaw * (raw_yaw_accel - m_yaw_accel_smoothed);
     798                 :             :     
     799                 :       12350 :     ctx.yaw_force = -1.0 * m_yaw_accel_smoothed * m_sop_yaw_gain * (double)BASE_NM_YAW_KICK;
     800                 :             :     
     801                 :             :     // Apply speed gate to all lateral effects
     802                 :       12350 :     ctx.sop_base_force *= ctx.speed_gate;
     803                 :       12350 :     ctx.rear_torque *= ctx.speed_gate;
     804                 :       12350 :     ctx.yaw_force *= ctx.speed_gate;
     805                 :       12350 : }
     806                 :             : 
     807                 :             : // Helper: Calculate Gyroscopic Damping (v0.4.17)
     808                 :       12355 : void FFBEngine::calculate_gyro_damping(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     809                 :             :     // 1. Calculate Steering Velocity (rad/s)
     810                 :       12355 :     float range = data->mPhysicalSteeringWheelRange;
     811         [ +  + ]:       12355 :     if (range <= 0.0f) range = (float)DEFAULT_STEERING_RANGE_RAD;
     812                 :       12355 :     double steer_angle = data->mUnfilteredSteering * (range / DUAL_DIVISOR);
     813                 :       12355 :     double steer_vel = (steer_angle - m_prev_steering_angle) / ctx.dt;
     814                 :       12355 :     m_prev_steering_angle = steer_angle;
     815                 :             :     
     816                 :             :     // 2. Alpha Smoothing
     817                 :       12355 :     double tau_gyro = (double)m_gyro_smoothing;
     818         [ +  + ]:       12355 :     if (tau_gyro < MIN_TAU_S) tau_gyro = MIN_TAU_S;
     819                 :       12355 :     double alpha_gyro = ctx.dt / (tau_gyro + ctx.dt);
     820                 :       12355 :     m_steering_velocity_smoothed += alpha_gyro * (steer_vel - m_steering_velocity_smoothed);
     821                 :             :     
     822                 :             :     // 3. Force = -Vel * Gain * Speed_Scaling
     823                 :             :     // Speed scaling: Gyro effect increases with wheel RPM (car speed)
     824                 :       12355 :     ctx.gyro_force = -1.0 * m_steering_velocity_smoothed * m_gyro_gain * (ctx.car_speed / GYRO_SPEED_SCALE);
     825                 :       12355 : }
     826                 :             : 
     827                 :             : // Helper: Calculate ABS Pulse (v0.7.53)
     828                 :       13357 : void FFBEngine::calculate_abs_pulse(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     829         [ +  + ]:       13357 :     if (!m_abs_pulse_enabled) return;
     830                 :             :     
     831                 :        1074 :     bool abs_active = false;
     832         [ +  + ]:        1322 :     for (int i = 0; i < 4; i++) {
     833                 :             :         // Detection: Sudden pressure oscillation + high brake pedal
     834                 :        1260 :         double pressure_delta = (data->mWheel[i].mBrakePressure - m_prev_brake_pressure[i]) / ctx.dt;
     835   [ +  +  +  +  :        1260 :         if (data->mUnfilteredBrake > ABS_PEDAL_THRESHOLD && std::abs(pressure_delta) > ABS_PRESSURE_RATE_THRESHOLD) {
                   +  + ]
     836                 :        1012 :             abs_active = true;
     837                 :        1012 :             break;
     838                 :             :         }
     839                 :             :     }
     840                 :             :     
     841         [ +  + ]:        1074 :     if (abs_active) {
     842                 :             :         // Generate sine pulse
     843                 :        1012 :         m_abs_phase += (double)m_abs_freq_hz * ctx.dt * TWO_PI;
     844                 :        1012 :         m_abs_phase = std::fmod(m_abs_phase, TWO_PI);
     845                 :        1012 :         ctx.abs_pulse_force = (double)(std::sin(m_abs_phase) * m_abs_gain * ABS_PULSE_MAGNITUDE_SCALER * ctx.speed_gate);
     846                 :             :     }
     847                 :             : }
     848                 :             : 
     849                 :             : // Helper: Calculate Lockup Vibration (v0.4.36 - REWRITTEN as dedicated method)
     850                 :       12349 : void FFBEngine::calculate_lockup_vibration(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     851         [ +  + ]:       12349 :     if (!m_lockup_enabled) return;
     852                 :             :     
     853                 :        1597 :     double worst_severity = 0.0;
     854                 :        1597 :     double chosen_freq_multiplier = 1.0;
     855                 :        1597 :     double chosen_pressure_factor = 0.0;
     856                 :             :     
     857                 :             :     // Calculate reference slip for front wheels (v0.4.38)
     858         [ +  - ]:        1597 :     double slip_fl = calculate_wheel_slip_ratio(data->mWheel[0]);
     859         [ +  - ]:        1597 :     double slip_fr = calculate_wheel_slip_ratio(data->mWheel[1]);
     860                 :        1597 :     double worst_front = (std::min)(slip_fl, slip_fr);
     861                 :             : 
     862         [ +  + ]:        7985 :     for (int i = 0; i < 4; i++) {
     863                 :        6388 :         const auto& w = data->mWheel[i];
     864         [ +  - ]:        6388 :         double slip = calculate_wheel_slip_ratio(w);
     865                 :        6388 :         double slip_abs = std::abs(slip);
     866                 :             : 
     867                 :             :         // 1. Predictive Lockup (v0.4.38)
     868                 :             :         // Detects rapidly decelerating wheels BEFORE they reach full lock
     869                 :        6388 :         double wheel_accel = (w.mRotation - m_prev_rotation[i]) / ctx.dt;
     870                 :        6388 :         double radius = (double)w.mStaticUndeflectedRadius / UNIT_CM_TO_M;
     871         [ +  + ]:        6388 :         if (radius < RADIUS_FALLBACK_MIN_M) radius = RADIUS_FALLBACK_DEFAULT_M;
     872                 :        6388 :         double car_dec_ang = -std::abs(data->mLocalAccel.z / radius);
     873                 :             : 
     874                 :             :         // Signal Quality Check (Reject surface bumps)
     875                 :        6388 :         double susp_vel = std::abs(w.mVerticalTireDeflection - m_prev_vert_deflection[i]) / ctx.dt;
     876                 :        6388 :         bool is_bumpy = (susp_vel > (double)m_lockup_bump_reject);
     877                 :             : 
     878                 :             :         // Pre-conditions
     879                 :        6388 :         bool brake_active = (data->mUnfilteredBrake > PREDICTION_BRAKE_THRESHOLD);
     880                 :        6388 :         bool is_grounded = (w.mSuspForce > PREDICTION_LOAD_THRESHOLD);
     881                 :             : 
     882                 :        6388 :         double start_threshold = (double)m_lockup_start_pct / PERCENT_TO_DECIMAL;
     883                 :        6388 :         double full_threshold = (double)m_lockup_full_pct / PERCENT_TO_DECIMAL;
     884                 :        6388 :         double trigger_threshold = full_threshold;
     885                 :             : 
     886   [ +  +  +  +  :        6388 :         if (brake_active && is_grounded && !is_bumpy) {
                   +  + ]
     887                 :             :             // Predictive Trigger: Wheel decelerating significantly faster than chassis
     888                 :         255 :             double sensitivity_threshold = -1.0 * (double)m_lockup_prediction_sens;
     889   [ +  +  +  - ]:         255 :             if (wheel_accel < car_dec_ang * LOCKUP_ACCEL_MARGIN && wheel_accel < sensitivity_threshold) {
     890                 :           2 :                 trigger_threshold = start_threshold; // Ease into effect earlier
     891                 :             :             }
     892                 :             :         }
     893                 :             : 
     894                 :             :         // 2. Intensity Calculation
     895         [ +  + ]:        6388 :         if (slip_abs > trigger_threshold) {
     896                 :         283 :             double window = full_threshold - start_threshold;
     897         [ -  + ]:         283 :             if (window < MIN_SLIP_WINDOW) window = MIN_SLIP_WINDOW;
     898                 :             : 
     899                 :         283 :             double normalized = (slip_abs - start_threshold) / window;
     900                 :         283 :             double severity = (std::min)(1.0, (std::max)(0.0, normalized));
     901                 :             :             
     902                 :             :             // Apply gamma for curve control
     903                 :         283 :             severity = std::pow(severity, (double)m_lockup_gamma);
     904                 :             :             
     905                 :             :             // Frequency calculation
     906                 :         283 :             double freq_mult = 1.0;
     907         [ +  + ]:         283 :             if (i >= 2) {
     908                 :             :                 // v0.4.38: Rear wheels use a different frequency to distinguish front/rear lockup
     909         [ +  + ]:          34 :                 if (slip < (worst_front - AXLE_DIFF_HYSTERESIS)) {
     910                 :           2 :                     freq_mult = LOCKUP_FREQ_MULTIPLIER_REAR;
     911                 :             :                 }
     912                 :             :             }
     913                 :             : 
     914                 :             :             // Pressure weighting (v0.4.38)
     915                 :         283 :             double pressure_factor = w.mBrakePressure;
     916   [ +  +  +  + ]:         283 :             if (pressure_factor < LOW_PRESSURE_LOCKUP_THRESHOLD && slip_abs > LOW_PRESSURE_LOCKUP_FIX) pressure_factor = LOW_PRESSURE_LOCKUP_FIX; // Catch low-pressure lockups
     917                 :             : 
     918         [ +  + ]:         283 :             if (severity > worst_severity) {
     919                 :         134 :                 worst_severity = severity;
     920                 :         134 :                 chosen_freq_multiplier = freq_mult;
     921                 :         134 :                 chosen_pressure_factor = pressure_factor;
     922                 :             :             }
     923                 :             :         }
     924                 :             :     }
     925                 :             : 
     926                 :             :     // 3. Vibration Synthesis
     927         [ +  + ]:        1597 :     if (worst_severity > 0.0) {
     928                 :         134 :         double base_freq = LOCKUP_BASE_FREQ + (ctx.car_speed * LOCKUP_FREQ_SPEED_MULT);
     929                 :         134 :         double final_freq = base_freq * chosen_freq_multiplier * (double)m_lockup_freq_scale;
     930                 :             :         
     931                 :         134 :         m_lockup_phase += final_freq * ctx.dt * TWO_PI;
     932                 :         134 :         m_lockup_phase = std::fmod(m_lockup_phase, TWO_PI);
     933                 :             :         
     934                 :         134 :         double amp = worst_severity * chosen_pressure_factor * m_lockup_gain * (double)BASE_NM_LOCKUP_VIBRATION * ctx.brake_load_factor;
     935                 :             :         
     936                 :             :         // v0.4.38: Boost rear lockup volume
     937         [ +  + ]:         134 :         if (chosen_freq_multiplier < 1.0) amp *= (double)m_lockup_rear_boost;
     938                 :             : 
     939                 :         134 :         ctx.lockup_rumble = std::sin(m_lockup_phase) * amp * ctx.speed_gate;
     940                 :             :     }
     941                 :             : }
     942                 :             : 
     943                 :             : // Helper: Calculate Wheel Spin Vibration (v0.6.36)
     944                 :       12351 : void FFBEngine::calculate_wheel_spin(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     945   [ +  +  +  + ]:       12351 :     if (m_spin_enabled && data->mUnfilteredThrottle > SPIN_THROTTLE_THRESHOLD) {
     946         [ +  - ]:          26 :         double slip_rl = calculate_wheel_slip_ratio(data->mWheel[2]);
     947         [ +  - ]:          26 :         double slip_rr = calculate_wheel_slip_ratio(data->mWheel[3]);
     948                 :          26 :         double max_slip = (std::max)(slip_rl, slip_rr);
     949                 :             :         
     950         [ +  - ]:          26 :         if (max_slip > SPIN_SLIP_THRESHOLD) {
     951                 :          26 :             double severity = (max_slip - SPIN_SLIP_THRESHOLD) / SPIN_SEVERITY_RANGE;
     952                 :          26 :             severity = (std::min)(1.0, severity);
     953                 :             :             
     954                 :             :             // Attenuate primary torque when spinning (Torque Drop)
     955                 :             :             // v0.6.43: Blunted effect (0.6 multiplier) to prevent complete loss of feel
     956                 :          26 :             ctx.gain_reduction_factor = (1.0 - (severity * m_spin_gain * SPIN_TORQUE_DROP_FACTOR));
     957                 :             :             
     958                 :             :             // Generate vibration based on spin velocity (RPM delta)
     959                 :          26 :             double slip_speed_ms = ctx.car_speed * max_slip;
     960                 :          26 :             double freq = (SPIN_BASE_FREQ + (slip_speed_ms * SPIN_FREQ_SLIP_MULT)) * (double)m_spin_freq_scale;
     961         [ +  + ]:          26 :             if (freq > SPIN_MAX_FREQ) freq = SPIN_MAX_FREQ; // Human sensory limit for gross vibration
     962                 :             :             
     963                 :          26 :             m_spin_phase += freq * ctx.dt * TWO_PI;
     964                 :          26 :             m_spin_phase = std::fmod(m_spin_phase, TWO_PI);
     965                 :             :             
     966                 :          26 :             double amp = severity * m_spin_gain * (double)BASE_NM_SPIN_VIBRATION;
     967                 :          26 :             ctx.spin_rumble = std::sin(m_spin_phase) * amp;
     968                 :             :         }
     969                 :             :     }
     970                 :       12351 : }
     971                 :             : 
     972                 :             : // Helper: Calculate Slide Texture (Friction Vibration)
     973                 :       12352 : void FFBEngine::calculate_slide_texture(const TelemInfoV01* data, FFBCalculationContext& ctx) {
     974         [ +  + ]:       12352 :     if (!m_slide_texture_enabled) return;
     975                 :             :     
     976                 :             :     // Use average lateral patch velocity of front wheels
     977                 :        1157 :     double lat_vel_fl = std::abs(data->mWheel[0].mLateralPatchVel);
     978                 :        1157 :     double lat_vel_fr = std::abs(data->mWheel[1].mLateralPatchVel);
     979                 :        1157 :     double front_slip_avg = (lat_vel_fl + lat_vel_fr) / DUAL_DIVISOR;
     980                 :             : 
     981                 :             :     // Use average lateral patch velocity of rear wheels
     982                 :        1157 :     double lat_vel_rl = std::abs(data->mWheel[2].mLateralPatchVel);
     983                 :        1157 :     double lat_vel_rr = std::abs(data->mWheel[3].mLateralPatchVel);
     984                 :        1157 :     double rear_slip_avg = (lat_vel_rl + lat_vel_rr) / DUAL_DIVISOR;
     985                 :             : 
     986                 :             :     // Use the max slide velocity between axles
     987                 :        1157 :     double effective_slip_vel = (std::max)(front_slip_avg, rear_slip_avg);
     988                 :             :     
     989         [ +  + ]:        1157 :     if (effective_slip_vel > SLIDE_VEL_THRESHOLD) {
     990                 :             :         // High-frequency sawtooth noise for localized friction feel
     991                 :        1143 :         double base_freq = SLIDE_BASE_FREQ + (effective_slip_vel * SLIDE_FREQ_VEL_MULT);
     992                 :        1143 :         double freq = base_freq * (double)m_slide_freq_scale;
     993                 :             :         
     994         [ +  + ]:        1143 :         if (freq > SLIDE_MAX_FREQ) freq = SLIDE_MAX_FREQ; // Hard clamp for hardware safety
     995                 :             :         
     996                 :        1143 :         m_slide_phase += freq * ctx.dt * TWO_PI;
     997                 :        1143 :         m_slide_phase = std::fmod(m_slide_phase, TWO_PI);
     998                 :             :         
     999                 :             :         // Sawtooth generator (0 to 1 range across TWO_PI) -> (-1 to 1)
    1000                 :        1143 :         double sawtooth = (m_slide_phase / TWO_PI) * SAWTOOTH_SCALE - SAWTOOTH_OFFSET;
    1001                 :             :         
    1002                 :             :         // Intensity scaling (Grip based)
    1003                 :        1143 :         double grip_scale = (std::max)(0.0, 1.0 - ctx.avg_grip);
    1004                 :             :         
    1005                 :        1143 :         ctx.slide_noise = sawtooth * m_slide_texture_gain * (double)BASE_NM_SLIDE_TEXTURE * ctx.texture_load_factor * grip_scale;
    1006                 :             :     }
    1007                 :             : }
    1008                 :             : 
    1009                 :             : // Helper: Calculate Road Texture & Scrub Drag
    1010                 :       12351 : void FFBEngine::calculate_road_texture(const TelemInfoV01* data, FFBCalculationContext& ctx) {
    1011                 :             :     // 1. Scrub Drag (Longitudinal resistive force from lateral sliding)
    1012         [ +  + ]:       12351 :     if (m_scrub_drag_gain > 0.0) {
    1013                 :        1085 :         double avg_lat_vel = (data->mWheel[0].mLateralPatchVel + data->mWheel[1].mLateralPatchVel) / DUAL_DIVISOR;
    1014                 :        1085 :         double abs_lat_vel = std::abs(avg_lat_vel);
    1015                 :             :         
    1016         [ +  - ]:        1085 :         if (abs_lat_vel > SCRUB_VEL_THRESHOLD) {
    1017                 :        1085 :             double fade = (std::min)(1.0, abs_lat_vel / SCRUB_FADE_RANGE); // Fade in over 0.5m/s
    1018         [ +  + ]:        1085 :             double drag_dir = (avg_lat_vel > 0.0) ? -1.0 : 1.0;
    1019                 :        1085 :             ctx.scrub_drag_force = drag_dir * m_scrub_drag_gain * (double)BASE_NM_SCRUB_DRAG * fade;
    1020                 :             :         }
    1021                 :             :     }
    1022                 :             : 
    1023         [ +  + ]:       12351 :     if (!m_road_texture_enabled) return;
    1024                 :             :     
    1025                 :             :     // 2. Road Texture (Delta Deflection Method)
    1026                 :             :     // Measures the rate of change in tire vertical compression
    1027                 :        1562 :     double delta_l = data->mWheel[0].mVerticalTireDeflection - m_prev_vert_deflection[0];
    1028                 :        1562 :     double delta_r = data->mWheel[1].mVerticalTireDeflection - m_prev_vert_deflection[1];
    1029                 :             :     
    1030                 :             :     // Outlier rejection (crashes/jumps)
    1031                 :        1562 :     delta_l = (std::max)(-DEFLECTION_DELTA_LIMIT, (std::min)(DEFLECTION_DELTA_LIMIT, delta_l));
    1032                 :        1562 :     delta_r = (std::max)(-DEFLECTION_DELTA_LIMIT, (std::min)(DEFLECTION_DELTA_LIMIT, delta_r));
    1033                 :             :     
    1034                 :        1562 :     double road_noise_val = 0.0;
    1035                 :             :     
    1036                 :             :     // FALLBACK (v0.6.36): If mVerticalTireDeflection is missing (Encrypted DLC),
    1037                 :             :     // use Chassis Vertical Acceleration delta as a secondary source.
    1038   [ +  +  -  + ]:        1562 :     bool deflection_active = (std::abs(delta_l) > DEFLECTION_ACTIVE_THRESHOLD || std::abs(delta_r) > DEFLECTION_ACTIVE_THRESHOLD);
    1039                 :             :     
    1040   [ +  +  +  + ]:        1562 :     if (deflection_active || ctx.car_speed < ROAD_TEXTURE_SPEED_THRESHOLD) {
    1041                 :        1030 :         road_noise_val = (delta_l + delta_r) * DEFLECTION_NM_SCALE; // Scale to NM
    1042                 :             :     } else {
    1043                 :             :         // Fallback to vertical acceleration rate-of-change (jerk-like scaling)
    1044                 :         532 :         double vert_accel = data->mLocalAccel.y;
    1045                 :         532 :         double delta_accel = vert_accel - m_prev_vert_accel;
    1046                 :         532 :         road_noise_val = delta_accel * ACCEL_ROAD_TEXTURE_SCALE * DEFLECTION_NM_SCALE; // Blend into similar range
    1047                 :             :     }
    1048                 :             :     
    1049                 :        1562 :     ctx.road_noise = road_noise_val * m_road_texture_gain * ctx.texture_load_factor;
    1050                 :        1562 :     ctx.road_noise *= ctx.speed_gate;
    1051                 :             : }
    1052                 :             : 
    1053                 :          54 : void FFBEngine::ResetNormalization() {
    1054         [ +  - ]:          54 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
    1055                 :             : 
    1056                 :             :     // 1. Structural Normalization Reset (Stage 1)
    1057                 :             :     // If disabled, we return to the user's manual target.
    1058                 :             :     // If enabled, we reset to the target to restart the learning process.
    1059                 :          54 :     m_session_peak_torque = (std::max)(1.0, (double)m_target_rim_nm);
    1060                 :          54 :     m_smoothed_structural_mult = 1.0 / (m_session_peak_torque + EPSILON_DIV);
    1061                 :          54 :     m_rolling_average_torque = m_session_peak_torque;
    1062                 :             : 
    1063                 :             :     // 2. Tactile Normalization Reset (Stage 3)
    1064                 :             :     // Always return to the class-default seed load.
    1065         [ +  - ]:          54 :     ParsedVehicleClass vclass = ParseVehicleClass(m_current_class_name.c_str(), m_vehicle_name);
    1066         [ +  - ]:          54 :     m_auto_peak_load = GetDefaultLoadForClass(vclass);
    1067                 :             : 
    1068                 :             :     // Reset static load reference
    1069                 :          54 :     m_static_front_load = m_auto_peak_load * 0.5;
    1070                 :          54 :     m_static_load_latched = false;
    1071                 :             : 
    1072                 :             :     // If we have a saved static load, restore it (v0.7.70 logic)
    1073                 :          54 :     double saved_load = 0.0;
    1074   [ +  -  +  -  :         108 :     if (Config::GetSavedStaticLoad(m_vehicle_name, saved_load)) {
                   -  + ]
    1075                 :           0 :         m_static_front_load = saved_load;
    1076                 :           0 :         m_static_load_latched = true;
    1077                 :             :     }
    1078                 :             : 
    1079                 :          54 :     m_smoothed_tactile_mult = 1.0;
    1080                 :             : 
    1081   [ +  -  +  - ]:          54 :     std::cout << "[FFB] Normalization state reset. Structural Peak: " << m_session_peak_torque
    1082   [ +  -  +  -  :          54 :               << " Nm | Load Peak: " << m_auto_peak_load << " N" << std::endl;
             +  -  +  - ]
    1083                 :          54 : }
    1084                 :             : 
    1085                 :             : // Helper: Calculate Suspension Bottoming (v0.6.22)
    1086                 :             : // NOTE: calculate_soft_lock has been moved to SteeringUtils.cpp.
    1087                 :       12355 : void FFBEngine::calculate_suspension_bottoming(const TelemInfoV01* data, FFBCalculationContext& ctx) {
    1088         [ +  + ]:       12355 :     if (!m_bottoming_enabled) return;
    1089                 :        1351 :     bool triggered = false;
    1090                 :        1351 :     double intensity = 0.0;
    1091                 :             :     
    1092                 :             :     // Method 0: Direct Ride Height Monitoring
    1093         [ +  + ]:        1351 :     if (m_bottoming_method == 0) {
    1094                 :        1342 :         double min_rh = (std::min)(data->mWheel[0].mRideHeight, data->mWheel[1].mRideHeight);
    1095   [ +  +  +  - ]:        1342 :         if (min_rh < BOTTOMING_RH_THRESHOLD_M && min_rh > -1.0) { // < 2mm
    1096                 :        1341 :             triggered = true;
    1097                 :        1341 :             intensity = (BOTTOMING_RH_THRESHOLD_M - min_rh) / BOTTOMING_RH_THRESHOLD_M; // Map 2mm->0mm to 0.0->1.0
    1098                 :             :         }
    1099                 :             :     } else {
    1100                 :             :         // Method 1: Suspension Force Impulse (Rate of Change)
    1101                 :           9 :         double dForceL = (data->mWheel[0].mSuspForce - m_prev_susp_force[0]) / ctx.dt;
    1102                 :           9 :         double dForceR = (data->mWheel[1].mSuspForce - m_prev_susp_force[1]) / ctx.dt;
    1103                 :           9 :         double max_dForce = (std::max)(dForceL, dForceR);
    1104                 :             :         
    1105         [ +  + ]:           9 :         if (max_dForce > BOTTOMING_IMPULSE_THRESHOLD_N_S) { // 100kN/s impulse
    1106                 :           5 :             triggered = true;
    1107                 :           5 :             intensity = (max_dForce - BOTTOMING_IMPULSE_THRESHOLD_N_S) / BOTTOMING_IMPULSE_RANGE_N_S;
    1108                 :             :         }
    1109                 :             :     }
    1110                 :             : 
    1111                 :             :     // Safety Trigger: Raw Load Peak (Catches Method 0/1 failures)
    1112         [ +  + ]:        1351 :     if (!triggered) {
    1113                 :           5 :         double max_load = (std::max)(data->mWheel[0].mTireLoad, data->mWheel[1].mTireLoad);
    1114                 :           5 :         double bottoming_threshold = m_static_front_load * BOTTOMING_LOAD_MULT;
    1115         [ +  + ]:           5 :         if (max_load > bottoming_threshold) {
    1116                 :           2 :             triggered = true;
    1117                 :           2 :             double excess = max_load - bottoming_threshold;
    1118                 :           2 :             intensity = std::sqrt(excess) * BOTTOMING_INTENSITY_SCALE; // Non-linear response
    1119                 :             :         }
    1120                 :             :     }
    1121                 :             : 
    1122         [ +  + ]:        1351 :     if (triggered) {
    1123                 :             :         // Generate high-intensity low-frequency "thump"
    1124                 :        1348 :         double bump_magnitude = intensity * m_bottoming_gain * (double)BASE_NM_BOTTOMING;
    1125                 :        1348 :         double freq = BOTTOMING_FREQ_HZ;
    1126                 :             :         
    1127                 :        1348 :         m_bottoming_phase += freq * ctx.dt * TWO_PI;
    1128                 :        1348 :         m_bottoming_phase = std::fmod(m_bottoming_phase, TWO_PI);
    1129                 :             :         
    1130                 :        1348 :         ctx.bottoming_crunch = std::sin(m_bottoming_phase) * bump_magnitude * ctx.speed_gate;
    1131                 :             :     }
    1132                 :             : }
        

Generated by: LCOV version 2.0-1