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 : : }
|