This document outlines the technical requirements for processing noisy longitudinal forces (
Noise often originates from jittery timing rather than the physics engine itself.
- Verify Frequency: Ensure your loop is polling the Shared Memory at exactly 333Hz (~3.003ms). — Current:
Thread.Sleep(0)spin-loop withtimeBeginPeriod(1). PacketId dedup prevents double-reads. ~333Hz achieved via spin-wait. - Check Packet Continuity: Monitor the
packetIdin the shared memory. If IDs are skipped, your app is lagging; if IDs repeat, you are polling too fast. — Current:_lastPhysicsPacketIdcheck inSharedMemoryReader.TryReadPhysics()line 99. - High-Precision Sleep: Avoid
Thread.Sleep(1). On Windows, useMultimedia Timer(winmm.dll) or aSpinWaitloop with aStopwatchto ensure microsecond accuracy. — Current: UsestimeBeginPeriod(1)+Thread.Sleep(0). Falls back toSleep(1)after 3 idle spins.
Apply these filters in order before sending the final value to the motor.
Purpose: Removes high-frequency "fuzz" and electrical hum.
- Implement an Exponential Moving Average:
output = (alpha * raw) + (1.0 - alpha) * lastOutput. — Current: Channel-level EMA inFfbChannelMixer(alpha=0.05, line 82-87). Output EMA inFfbPipeline(alpha=0.08, line 92). - ISSUE FOUND: Channel-level EMA alpha is too aggressive (0.05). This means 95% of the previous value is retained per tick, creating significant lag. The checklist recommends alpha=0.15 as a starting point. Fix: Tune
ChannelSmoothfrom0.95f→0.85f(i.e., alpha from 0.05 → 0.15) inFfbChannelMixer.cs. — Fixed: Per-channel alphas implemented (Mz: 0.20, Fx: 0.08, Fy: 0.12). - ISSUE FOUND: No per-channel alpha control. Fx needs ~3x more smoothing than Mz. Fix: Add separate smoothing constants per channel type:
Mz: alpha = 0.20 (responsive, steering detail matters)Fx: alpha = 0.08 (heavy smoothing, longitudinal is noisy)Fy: alpha = 0.12 (moderate smoothing)
Purpose: Prevents "mechanical clacking" by capping how fast the motor can change direction.
- Calculate Delta:
delta = currentFilteredValue - lastFilteredValue. — Current:FfbPipeline.cslines 87-89. - Clamp Delta: If
abs(delta) > maxSlew, force delta tomaxSlew * sign(delta). — Current:MaxSlewRate = 0.02f, clamped at line 88. - ISSUE FOUND: Slew rate is applied AFTER the output EMA, creating double-smoothing. The pipeline does: raw → channel EMA → slew clamp → output EMA. The slew clamp at 0.02/tick and the output EMA at alpha=0.08 are redundant and compound the lag. Fix: Remove the final output EMA (
_outputSmooth) and rely on the slew limiter alone for output smoothing, OR increase MaxSlewRate to 0.05 and remove the output EMA. — Fixed: Output EMA removed, slew-only smoothing. - ISSUE FOUND: MaxSlewRate = 0.02 is too tight. At 333Hz, a signal needs 50 ticks (150ms) to go from 0→1.0. This causes noticeable lag in quick direction changes. Fix: Increased to
0.35fper tick (covers 0→1.0 in ~6 ticks = 18ms). The 1kHz interpolation thread handles smooth transitions, so slew rate only needs to filter noise spikes.
Purpose: Stops the wheel from vibrating/humming when the car is stationary or driving straight.
- Implement Deadzone: If
abs(Fx) < 0.005, return0. — Current:NoiseFloor = 0.008finFfbPipeline.csline 84. - Apply Power Curve: Use
output = sign(Fx) * pow(abs(Fx), 1.1)to soften the center response without losing total force. — Implemented asCenterKneePowerparameter (currently 1.0 = disabled). Can enable by setting to 1.1.
- ISSUE FOUND: No interpolation (upsampling). The wheel polls at ~1000Hz but physics runs at 333Hz. Currently,
SendConstantForce()is called only when a new physics packet arrives. Between physics frames, the device holds the last value, creating "stair-step" force transitions. Fix: Implement a high-frequency output thread (1kHz) that linearly interpolates between the current and target force values:// In TelemetryLoop or FfbDeviceManager: // Store _targetForce and _currentForce // In 1ms timer: _currentForce = Lerp(_currentForce, _targetForce, 0.33) // Send _currentForce to device every 1ms - Soft Clipping: If the
$F_x$ signal exceeds 1.0 (100% force), use a soft-clipping function (liketanh) instead of a hard limit to avoid a "flat" feel at high loads. — Current:MathF.Tanh(absNorm * CompressionPower)inFfbPipeline.csline 49, plusFfbOutputClipper.Process()with sqrt-based soft clip.
- ISSUE FOUND: Slip-Ratio filtering is not adaptive. The checklist recommends increasing filter strength when
slipRatiois high (ABS/TC active). Currently,FfbSlipEnhancerADDS force during slip but doesn't increase smoothing. Fix: InFfbChannelMixer.Mix(), detect high-slip conditions and dynamically reduce the EMA alpha (increase smoothing) for Fx channels:// Pseudocode: float maxFrontSlip = Math.Max(Math.Abs(raw.SlipRatio[0]), Math.Abs(raw.SlipRatio[1])); float slipAlpha = maxFrontSlip > SlipThreshold ? 0.04f : 0.08f; // heavier smoothing during slip _smFxFront = _smFxFront * (1f - slipAlpha) + fxFront * slipAlpha;
- Gear Shift Smoothing: Implement a brief (20ms) "Slew override" during gear changes to prevent the harsh jolt from the sudden change in longitudinal acceleration. — Not currently implemented. Fix: Detect gear change from
raw.Geardelta, temporarily override MaxSlewRate to a very low value (0.01) for ~6-7 ticks, then restore.
- Live Telemetry Plotter: Create a simple graph in your app showing Raw
$F_x$ (Red) vs. Filtered$F_x$ (Green). — Current:MainWindow.UpdatePlot()using ScottPlot, called fromOnUiUpdateat 30Hz. - ISSUE FOUND: No latency measurement. The checklist recommends measuring time from Shared Memory read to wheel command. Fix: Add
Stopwatchtiming inTelemetryLoop.Loop()around the read→process→send cycle and log/display the round-trip latency. Target: < 5ms.
| # | Fix | File | Change |
|---|---|---|---|
| 1a | Tune channel EMA alpha | FfbChannelMixer.cs |
Change ChannelSmooth = 0.95f → 0.85f, or add per-channel alphas |
| 1b | Remove redundant output EMA | FfbPipeline.cs |
Remove _outputSmooth EMA (lines 92-94), use slew-limited value directly |
| 1c | Increase MaxSlewRate | FfbPipeline.cs |
Changed MaxSlewRate = 0.05f → 0.35f |
| 1d | Add per-channel smoothing | FfbChannelMixer.cs |
Separate _smFxFront alpha (0.08) from _smMzFront alpha (0.20) |
| # | Fix | File | Change |
|---|---|---|---|
| 2a | Add output interpolation thread | FfbDeviceManager.cs + TelemetryLoop.cs |
1kHz Lerp thread between _currentForce and _targetForce |
| 2b | Adaptive slip-based Fx smoothing | FfbChannelMixer.cs |
Dynamically reduce Fx alpha when front slip ratio > threshold |
| 2c | Gear shift smoothing | FfbPipeline.cs |
Detect gear delta, temporarily reduce MaxSlewRate |
| # | Fix | File | Change |
|---|---|---|---|
| 3a | Center power curve | FfbPipeline.cs |
Add pow(abs(output), 1.1) stage or default LUT to progressive(1.1) |
| 3b | Latency measurement | TelemetryLoop.cs |
Add Stopwatch around read→process→send, display in UI |
SharedMemory → MapRawData → FfbPipeline.Process():
1. ChannelMixer.Mix() — Spike clamp → Normalize → per-channel EMA (Mz: 0.20, Fx: 0.08*, Fy: 0.12)
*Fx drops to 0.04 when front slip ratio > 0.05 (adaptive)
2. MasterGain * AutoGain / ForceScale
3. Tanh compression
4. LUT curve
5. SlipEnhancer.Apply() — adds slip-based force
6. FfbDamping.Apply()
7. FfbDynamicEffects.Apply()
8. OutputClipper.Process() * OutputGain
9. Sign correction
10. Center power curve — pow(abs, CenterKneePower=1.0) [disabled]
11. Noise floor cut — below 0.001 → 0
12. Slew rate limiter — max 0.35/tick (0.01 during gear shifts for 7 ticks)
13. Speed gate — < 2 km/h → 0, < 10 km/h → fade
→ FfbDeviceManager.SendConstantForce() → sets _targetForce
→ 1kHz Interpolation Thread → timestamp-based sliding lerp → SendConstantForceDirect()
Developer Tip:
The per-channel alpha split creates a phase offset between Mz and Fx:
- Mz (alpha=0.20): time constant ≈ 5 ticks ≈ 15ms
- Fx (alpha=0.08): time constant ≈ 12.5 ticks ≈ 37ms
- Phase delta: ~22ms
When this matters: In heavy trail-braking scenarios, the longitudinal weight transfer (
Tuning guidance:
| Symptom | Adjustment |
|---|---|
| Feels disconnected during trail braking | Increase FxAlpha from 0.08 → 0.12 |
| Fx still too noisy at 0.12 | Reduce MzAlpha from 0.20 → 0.15 (bring them closer) |
| Want zero phase offset | Set all alphas equal (0.15) — trades noise for coherence |
| Need precision AND silence | Add a "Lead Compensation" delay buffer on Mz to match Fx lag |
Current values (0.08 / 0.20) are the recommended starting point. Tune only after driving.
| Feature | Importance | Status | Detail |
|---|---|---|---|
| High-Precision Timing | 10/10 | ✅ Implemented | Spin-wait + timeBeginPeriod(1) |
| Per-Channel Noise Filtering | 9/10 | ✅ Implemented | Mz: 0.20, Fx: 0.08, Fy: 0.12 |
| Mechanical Protection (Slew) | 8/10 | ✅ Implemented | 0.35/tick + gear shift override at 0.01 |
| DD Motor Smoothness (Interpolation) | 10/10 | ✅ Implemented | 1kHz timestamp-based sliding lerp |
| Adaptive Slip Filtering | 9/10 | ✅ Implemented | Fx alpha → 0.04 when slip > 0.05 |
| Center Power Curve | 7/10 | ✅ Implemented | pow(abs, 1.0) — disabled; center suppression handles soft-center |
| Center Suppression | 9/10 | ✅ Updated | t^1.5 curve (was cubic t³), 8° base zone (was 10°), 1.5× max speed scale (was 2×) |
| Noise Floor | 8/10 | ✅ Updated | 0.001 (was 0.008) — lets suppressed forces through |
| Gear Shift Smoothing | 6/10 | ✅ Implemented | 7-tick slew override at 0.01 |
| Latency Monitor | 5/10 | ✅ Implemented | Per-frame + 60-frame rolling average |
| Output EMA Removal | 8/10 | ✅ Implemented | Eliminated double-smoothing lag source |
| Phase Shift Compensation | 7/10 | ⏳ Pending test | Needs driving validation — see tuning table above |