LCOV - code coverage report
Current view: top level - core - main.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 52.2 % 249 130
Test Date: 2026-03-18 19:01:10 Functions: 75.0 % 4 3
Branches: 31.3 % 566 177

             Branch data     Line data    Source code
       1                 :             : #ifdef _WIN32
       2                 :             : #include <windows.h>
       3                 :             : #else
       4                 :             : #include <signal.h>
       5                 :             : #endif
       6                 :             : #include <iostream>
       7                 :             : #include <cmath>
       8                 :             : #include <algorithm>
       9                 :             : #include <thread>
      10                 :             : #include <chrono>
      11                 :             : 
      12                 :             : #include "FFBEngine.h"
      13                 :             : #include "GuiLayer.h"
      14                 :             : #include "Config.h"
      15                 :             : #include "DirectInputFFB.h"
      16                 :             : #include "GameConnector.h"
      17                 :             : #include "Version.h"
      18                 :             : #include "Logger.h"    // Added Logger
      19                 :             : #include "RateMonitor.h"
      20                 :             : #include "HealthMonitor.h"
      21                 :             : #include "UpSampler.h"
      22                 :             : #include "TimeUtils.h"
      23                 :             : #include <optional>
      24                 :             : #include <atomic>
      25                 :             : #include <mutex>
      26                 :             : 
      27                 :             : // Constants
      28                 :             : 
      29                 :             : // Threading Globals
      30                 :             : #ifndef LMUFFB_UNIT_TEST
      31                 :             : std::atomic<bool> g_running(true);
      32                 :             : std::atomic<bool> g_ffb_active(true);
      33                 :             : 
      34                 :             : SharedMemoryObjectOut g_localData; // Local copy of shared memory
      35                 :             : 
      36                 :             : FFBEngine g_engine;
      37                 :             : std::recursive_mutex g_engine_mutex; // Protects settings access if GUI changes them
      38                 :             : #else
      39                 :             : extern std::atomic<bool> g_running;
      40                 :             : extern std::atomic<bool> g_ffb_active;
      41                 :             : extern SharedMemoryObjectOut g_localData;
      42                 :             : extern FFBEngine g_engine;
      43                 :             : extern std::recursive_mutex g_engine_mutex;
      44                 :             : extern std::chrono::steady_clock::time_point g_mock_time;
      45                 :             : extern bool g_use_mock_time;
      46                 :             : #endif
      47                 :             : 
      48                 :             : // --- FFB Loop (High Priority 1000Hz) ---
      49                 :           9 : void FFBThread() {
      50   [ +  -  +  - ]:           9 :     Logger::Get().LogFile("[FFB] Loop Started.");
      51         [ +  - ]:           9 :     RateMonitor loopMonitor;
      52         [ +  - ]:           9 :     RateMonitor telemMonitor;
      53         [ +  - ]:           9 :     RateMonitor hwMonitor;
      54         [ +  - ]:           9 :     RateMonitor torqueMonitor;
      55         [ +  - ]:           9 :     RateMonitor genTorqueMonitor;
      56         [ +  - ]:           9 :     RateMonitor physicsMonitor; // New v0.7.117 (Issue #217)
      57                 :             : 
      58         [ +  - ]:           9 :     PolyphaseResampler resampler;
      59                 :           9 :     int phase_accumulator = 0;
      60                 :           9 :     double current_physics_force = 0.0;
      61                 :             : 
      62                 :           9 :     double lastET = -1.0;
      63                 :           9 :     double lastTorque = -9999.0;
      64                 :           9 :     float lastGenTorque = -9999.0f;
      65                 :             : 
      66                 :             :     // Extended monitors for Issue #133
      67                 :             :     struct ChannelMonitor {
      68                 :             :         RateMonitor monitor;
      69                 :             :         double lastValue = -1e18;
      70                 :           0 :         void Update(double newValue) {
      71         [ #  # ]:           0 :             if (newValue != lastValue) {
      72                 :           0 :                 monitor.RecordEvent();
      73                 :           0 :                 lastValue = newValue;
      74                 :             :             }
      75                 :           0 :         }
      76                 :             :     };
      77                 :             : 
      78   [ +  -  +  -  :           9 :     ChannelMonitor mAccX, mAccY, mAccZ;
                   +  - ]
      79   [ +  -  +  -  :           9 :     ChannelMonitor mVelX, mVelY, mVelZ;
                   +  - ]
      80   [ +  -  +  -  :           9 :     ChannelMonitor mRotX, mRotY, mRotZ;
                   +  - ]
      81   [ +  -  +  -  :           9 :     ChannelMonitor mRotAccX, mRotAccY, mRotAccZ;
                   +  - ]
      82   [ +  -  +  - ]:           9 :     ChannelMonitor mUnfSteer, mFilSteer;
      83         [ +  - ]:           9 :     ChannelMonitor mRPM;
      84   [ +  -  +  -  :           9 :     ChannelMonitor mLoadFL, mLoadFR, mLoadRL, mLoadRR;
             +  -  +  - ]
      85   [ +  -  +  -  :           9 :     ChannelMonitor mLatFL, mLatFR, mLatRL, mLatRR;
             +  -  +  - ]
      86   [ +  -  +  -  :           9 :     ChannelMonitor mPosX, mPosY, mPosZ;
                   +  - ]
      87         [ +  - ]:           9 :     ChannelMonitor mDtMon;
      88                 :             : 
      89                 :             :     // Precise Timing: Target 1000Hz (1000 microseconds)
      90                 :           9 :     const std::chrono::microseconds target_period(1000);
      91                 :           9 :     auto next_tick = TimeUtils::GetTime();
      92                 :             : 
      93         [ +  + ]:        7918 :     while (g_running) {
      94         [ +  - ]:        7909 :         loopMonitor.RecordEvent();
      95   [ +  -  +  - ]:        7909 :         next_tick += target_period;
      96                 :             : 
      97                 :             :         // --- 1. Physics Phase Accumulator (5/2 Ratio) ---
      98                 :        7909 :         phase_accumulator += 2; // M = 2
      99                 :        7909 :         bool run_physics = false;
     100                 :             : 
     101         [ +  + ]:        7909 :         if (phase_accumulator >= 5) { // L = 5
     102                 :        3163 :             phase_accumulator -= 5;
     103                 :        3163 :             run_physics = true;
     104                 :             :         }
     105                 :             : 
     106         [ +  + ]:        7909 :         if (run_physics) {
     107         [ +  - ]:        3163 :             physicsMonitor.RecordEvent();
     108                 :        3163 :             bool should_output = false;
     109                 :        3163 :             double force_physics = 0.0;
     110                 :             : 
     111                 :        3163 :             bool in_realtime_phys = false;
     112   [ +  -  +  -  :        3163 :             if (g_ffb_active && GameConnector::Get().IsConnected()) {
          +  -  +  -  +  
                      - ]
     113   [ +  -  +  - ]:        3163 :                 GameConnector::Get().CopyTelemetry(g_localData);
     114         [ +  - ]:        3163 :                 g_engine.m_metadata.UpdateMetadata(g_localData); // Update names/classes immediately
     115                 :             : 
     116         [ +  - ]:        3163 :                 in_realtime_phys = GameConnector::Get().IsInRealtime();
     117         [ +  - ]:        3163 :                 long current_session = GameConnector::Get().GetSessionType();
     118                 :             : 
     119   [ +  -  +  - ]:        3163 :                 bool is_stale = GameConnector::Get().IsStale(100);
     120                 :             : 
     121                 :             :                 static bool was_driving = false;
     122                 :             :                 static long last_session = -1;
     123                 :             : 
     124                 :             :                 // is_driving uses IsPlayerActivelyDriving() which correctly gates on
     125                 :             :                 // inRealtime AND playerControl==0 AND gamePhase!=9 (paused).
     126         [ +  - ]:        3163 :                 bool is_driving = GameConnector::Get().IsPlayerActivelyDriving();
     127                 :             : 
     128   [ -  +  -  - ]:        3163 :                 bool should_start_log = (is_driving && !was_driving);
     129   [ +  -  -  + ]:        3163 :                 bool should_stop_log  = (!is_driving && was_driving);
     130   [ -  +  -  -  :        3163 :                 bool session_changed  = (is_driving && was_driving && last_session != -1 && current_session != last_session);
             -  -  -  - ]
     131                 :             : 
     132         [ -  + ]:        3163 :                 if (session_changed) {
     133   [ #  #  #  # ]:           0 :                     Logger::Get().LogFile("[Game] Session Type Changed (%ld -> %ld). Restarting log.", last_session, current_session);
     134         [ #  # ]:           0 :                     AsyncLogger::Get().Stop();
     135                 :           0 :                     should_start_log = true;
     136                 :             :                 }
     137                 :             : 
     138   [ +  -  +  -  :        3163 :                 bool manual_start_requested = Config::m_auto_start_logging && !AsyncLogger::Get().IsLogging() && is_driving;
             +  -  -  + ]
     139                 :             : 
     140   [ +  -  -  + ]:        3163 :                 if (should_start_log || manual_start_requested) {
     141   [ #  #  #  #  :           0 :                     if (should_start_log) Logger::Get().LogFile("[Game] User entered driving session (Control: Player).");
                   #  # ]
     142   [ #  #  #  # ]:           0 :                     else Logger::Get().LogFile("[Game] Logging manually enabled while driving.");
     143                 :             : 
     144   [ #  #  #  #  :           0 :                     if (Config::m_auto_start_logging && !AsyncLogger::Get().IsLogging()) {
             #  #  #  # ]
     145                 :           0 :                         uint8_t idx = g_localData.telemetry.playerVehicleIdx;
     146         [ #  # ]:           0 :                         if (idx < 104) {
     147                 :           0 :                             auto& scoring = g_localData.scoring.vehScoringInfo[idx];
     148                 :           0 :                             const char* tName = g_localData.scoring.scoringInfo.mTrackName;
     149                 :             : 
     150                 :           0 :                             SessionInfo info;
     151         [ #  # ]:           0 :                             info.app_version = LMUFFB_VERSION;
     152         [ #  # ]:           0 :                             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     153         [ #  # ]:           0 :                             info.vehicle_name = scoring.mVehicleName;
     154   [ #  #  #  #  :           0 :                             info.vehicle_class = VehicleClassToString(ParseVehicleClass(scoring.mVehicleClass, scoring.mVehicleName));
                   #  # ]
     155   [ #  #  #  # ]:           0 :                             info.vehicle_brand = ParseVehicleBrand(scoring.mVehicleClass, scoring.mVehicleName);
     156         [ #  # ]:           0 :                             info.track_name = tName;
     157         [ #  # ]:           0 :                             info.driver_name = "Auto";
     158                 :           0 :                             info.gain = g_engine.m_gain;
     159                 :           0 :                             info.understeer_effect = g_engine.m_understeer_effect;
     160                 :           0 :                             info.sop_effect = g_engine.m_sop_effect;
     161                 :           0 :                             info.lat_load_effect = g_engine.m_lat_load_effect;
     162                 :           0 :                             info.long_load_effect = g_engine.m_long_load_effect;
     163                 :           0 :                             info.sop_scale = g_engine.m_sop_scale;
     164                 :           0 :                             info.sop_smoothing = g_engine.m_sop_smoothing_factor;
     165                 :           0 :                             info.optimal_slip_angle = g_engine.m_optimal_slip_angle;
     166                 :           0 :                             info.optimal_slip_ratio = g_engine.m_optimal_slip_ratio;
     167                 :           0 :                             info.slope_enabled = g_engine.m_slope_detection_enabled;
     168                 :           0 :                             info.slope_sensitivity = g_engine.m_slope_sensitivity;
     169                 :           0 :                             info.slope_threshold = (float)g_engine.m_slope_min_threshold;
     170                 :           0 :                             info.slope_alpha_threshold = g_engine.m_slope_alpha_threshold;
     171                 :           0 :                             info.slope_decay_rate = g_engine.m_slope_decay_rate;
     172                 :           0 :                             info.torque_passthrough = g_engine.m_torque_passthrough;
     173                 :           0 :                             info.dynamic_normalization = g_engine.m_dynamic_normalization_enabled;
     174                 :           0 :                             info.auto_load_normalization = g_engine.m_auto_load_normalization_enabled;
     175   [ #  #  #  # ]:           0 :                             AsyncLogger::Get().Start(info, Config::m_log_path);
     176                 :           0 :                         }
     177                 :             :                     }
     178         [ -  + ]:        3163 :                 } else if (should_stop_log) {
     179   [ #  #  #  # ]:           0 :                     Logger::Get().LogFile("[Game] User exited driving session.");
     180   [ #  #  #  # ]:           0 :                     if (AsyncLogger::Get().IsLogging()) {
     181         [ #  # ]:           0 :                         AsyncLogger::Get().Stop();
     182                 :             :                     }
     183                 :             :                 }
     184                 :        3163 :                 was_driving = is_driving;
     185                 :        3163 :                 last_session = current_session;
     186                 :             : 
     187   [ +  -  -  + ]:        3163 :                 if (!is_stale && g_localData.telemetry.playerHasVehicle) {
     188                 :           0 :                     uint8_t idx = g_localData.telemetry.playerVehicleIdx;
     189                 :             : 
     190                 :             :                     // --- LOST FRAME DETECTION (Issue #303) ---
     191                 :             :                     static double last_telem_et = -1.0;
     192   [ #  #  #  #  :           0 :                     if (g_engine.m_safety.m_stutter_safety_enabled && last_telem_et > 0.0 && (g_localData.telemetry.telemInfo[idx].mElapsedTime - last_telem_et) > (g_localData.telemetry.telemInfo[idx].mDeltaTime * g_engine.m_safety.m_stutter_threshold)) {
                   #  # ]
     193         [ #  # ]:           0 :                         std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     194         [ #  # ]:           0 :                         g_engine.m_safety.TriggerSafetyWindow("Lost Frames");
     195                 :           0 :                     }
     196                 :           0 :                     last_telem_et = g_localData.telemetry.telemInfo[idx].mElapsedTime;
     197                 :             : 
     198         [ #  # ]:           0 :                     if (idx < 104) {
     199                 :           0 :                         auto& scoring = g_localData.scoring.vehScoringInfo[idx];
     200                 :           0 :                         TelemInfoV01* pPlayerTelemetry = &g_localData.telemetry.telemInfo[idx];
     201                 :             : 
     202                 :             :                         // Track telemetry update rate
     203         [ #  # ]:           0 :                         if (pPlayerTelemetry->mElapsedTime != lastET) {
     204         [ #  # ]:           0 :                             telemMonitor.RecordEvent();
     205                 :           0 :                             lastET = pPlayerTelemetry->mElapsedTime;
     206                 :             :                         }
     207                 :             : 
     208                 :             :                         // Track torque update rates
     209         [ #  # ]:           0 :                         if (pPlayerTelemetry->mSteeringShaftTorque != lastTorque) {
     210         [ #  # ]:           0 :                             torqueMonitor.RecordEvent();
     211                 :           0 :                             lastTorque = pPlayerTelemetry->mSteeringShaftTorque;
     212                 :             :                         }
     213         [ #  # ]:           0 :                         if (g_localData.generic.FFBTorque != lastGenTorque) {
     214         [ #  # ]:           0 :                             genTorqueMonitor.RecordEvent();
     215                 :           0 :                             lastGenTorque = g_localData.generic.FFBTorque;
     216                 :             :                         }
     217                 :             : 
     218                 :             :                         // Extended monitoring (Issue #133)
     219         [ #  # ]:           0 :                         mAccX.Update(pPlayerTelemetry->mLocalAccel.x);
     220         [ #  # ]:           0 :                         mAccY.Update(pPlayerTelemetry->mLocalAccel.y);
     221         [ #  # ]:           0 :                         mAccZ.Update(pPlayerTelemetry->mLocalAccel.z);
     222         [ #  # ]:           0 :                         mVelX.Update(pPlayerTelemetry->mLocalVel.x);
     223         [ #  # ]:           0 :                         mVelY.Update(pPlayerTelemetry->mLocalVel.y);
     224         [ #  # ]:           0 :                         mVelZ.Update(pPlayerTelemetry->mLocalVel.z);
     225         [ #  # ]:           0 :                         mRotX.Update(pPlayerTelemetry->mLocalRot.x);
     226         [ #  # ]:           0 :                         mRotY.Update(pPlayerTelemetry->mLocalRot.y);
     227         [ #  # ]:           0 :                         mRotZ.Update(pPlayerTelemetry->mLocalRot.z);
     228         [ #  # ]:           0 :                         mRotAccX.Update(pPlayerTelemetry->mLocalRotAccel.x);
     229         [ #  # ]:           0 :                         mRotAccY.Update(pPlayerTelemetry->mLocalRotAccel.y);
     230         [ #  # ]:           0 :                         mRotAccZ.Update(pPlayerTelemetry->mLocalRotAccel.z);
     231         [ #  # ]:           0 :                         mUnfSteer.Update(pPlayerTelemetry->mUnfilteredSteering);
     232         [ #  # ]:           0 :                         mFilSteer.Update(pPlayerTelemetry->mFilteredSteering);
     233         [ #  # ]:           0 :                         mRPM.Update(pPlayerTelemetry->mEngineRPM);
     234         [ #  # ]:           0 :                         mLoadFL.Update(pPlayerTelemetry->mWheel[0].mTireLoad);
     235         [ #  # ]:           0 :                         mLoadFR.Update(pPlayerTelemetry->mWheel[1].mTireLoad);
     236         [ #  # ]:           0 :                         mLoadRL.Update(pPlayerTelemetry->mWheel[2].mTireLoad);
     237         [ #  # ]:           0 :                         mLoadRR.Update(pPlayerTelemetry->mWheel[3].mTireLoad);
     238         [ #  # ]:           0 :                         mLatFL.Update(pPlayerTelemetry->mWheel[0].mLateralForce);
     239         [ #  # ]:           0 :                         mLatFR.Update(pPlayerTelemetry->mWheel[1].mLateralForce);
     240         [ #  # ]:           0 :                         mLatRL.Update(pPlayerTelemetry->mWheel[2].mLateralForce);
     241         [ #  # ]:           0 :                         mLatRR.Update(pPlayerTelemetry->mWheel[3].mLateralForce);
     242         [ #  # ]:           0 :                         mPosX.Update(pPlayerTelemetry->mPos.x);
     243         [ #  # ]:           0 :                         mPosY.Update(pPlayerTelemetry->mPos.y);
     244         [ #  # ]:           0 :                         mPosZ.Update(pPlayerTelemetry->mPos.z);
     245         [ #  # ]:           0 :                         mDtMon.Update(pPlayerTelemetry->mDeltaTime);
     246                 :             : 
     247         [ #  # ]:           0 :                         std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     248   [ #  #  #  #  :           0 :                         bool full_allowed = g_engine.m_safety.IsFFBAllowed(scoring, g_localData.scoring.scoringInfo.mGamePhase) && is_driving;
                   #  # ]
     249                 :             : 
     250         [ #  # ]:           0 :                         force_physics = g_engine.calculate_force(pPlayerTelemetry, scoring.mVehicleClass, scoring.mVehicleName, g_localData.generic.FFBTorque, full_allowed, 0.0025, scoring.mControl);
     251                 :             : 
     252                 :             :                         // v0.7.153: Explicitly target zero force only when player is not in control (Issue #281).
     253                 :             :                         // This allows Soft Lock to remain active in the garage and during pause (ControlMode::PLAYER),
     254                 :             :                         // while ensuring that AI takeover or other non-player states slew to zero.
     255         [ #  # ]:           0 :                         if (scoring.mControl != static_cast<signed char>(ControlMode::PLAYER)) force_physics = 0.0;
     256                 :             : 
     257                 :             :                         // Safety Layer (v0.7.49): Slew Rate Limiting (400Hz)
     258                 :             :                         // Applied before up-sampling to prevent reconstruction artifacts on spikes.
     259   [ #  #  #  # ]:           0 :                         bool restricted = !full_allowed || (scoring.mFinishStatus != 0);
     260         [ #  # ]:           0 :                         force_physics = g_engine.m_safety.ApplySafetySlew(force_physics, 0.0025, restricted);
     261                 :             : 
     262                 :           0 :                         should_output = true;
     263                 :           0 :                     }
     264                 :             :                 }
     265                 :             :             }
     266                 :             :             
     267         [ +  - ]:        3163 :             if (!should_output) {
     268         [ +  - ]:        3163 :                 std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     269         [ +  - ]:        3163 :                 force_physics = g_engine.m_safety.ApplySafetySlew(0.0, 0.0025, true);
     270                 :        3163 :             }
     271                 :        3163 :             current_physics_force = force_physics;
     272                 :             : 
     273                 :             :             // Warning for low sample rate (Issue #133)
     274   [ +  +  +  - ]:        3163 :             static auto lastWarningTime = TimeUtils::GetTime();
     275                 :        3163 :             HealthStatus health;
     276                 :             :             {
     277         [ +  - ]:        3163 :                 std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     278         [ -  + ]:        3163 :                 double t_rate = (g_engine.m_torque_source == 1) ? genTorqueMonitor.GetRate() : torqueMonitor.GetRate();
     279                 :        3163 :                 health = HealthMonitor::Check(loopMonitor.GetRate(), telemMonitor.GetRate(), t_rate, g_engine.m_torque_source, physicsMonitor.GetRate(),
     280   [ +  -  +  -  :        3163 :                                               GameConnector::Get().IsConnected(), GameConnector::Get().IsSessionActive(), GameConnector::Get().GetSessionType(), GameConnector::Get().IsInRealtime(), GameConnector::Get().GetPlayerControl());
          +  -  +  -  +  
                -  +  - ]
     281                 :        3163 :             }
     282                 :             : 
     283   [ -  +  -  - ]:        3163 :             if (in_realtime_phys && !health.is_healthy) {
     284                 :           0 :                  auto now = TimeUtils::GetTime();
     285   [ #  #  #  #  :           0 :                  if (std::chrono::duration_cast<std::chrono::seconds>(now - lastWarningTime).count() >= 60) {
                   #  # ]
     286         [ #  # ]:           0 :                      std::string reason = "";
     287   [ #  #  #  #  :           0 :                      if (health.loop_low) reason += "Loop=" + std::to_string((int)health.loop_rate) + "Hz ";
             #  #  #  # ]
     288   [ #  #  #  #  :           0 :                      if (health.physics_low) reason += "Physics=" + std::to_string((int)health.physics_rate) + "Hz ";
             #  #  #  # ]
     289   [ #  #  #  #  :           0 :                      if (health.telem_low) reason += "Telemetry=" + std::to_string((int)health.telem_rate) + "Hz ";
             #  #  #  # ]
     290   [ #  #  #  #  :           0 :                      if (health.torque_low) reason += "Torque=" + std::to_string((int)health.torque_rate) + "Hz (Target " + std::to_string((int)health.expected_torque_rate) + "Hz) ";
          #  #  #  #  #  
                #  #  # ]
     291                 :             : 
     292   [ #  #  #  # ]:           0 :                      Logger::Get().LogFile("Low Sample Rate detected: %s", reason.c_str());
     293                 :           0 :                      lastWarningTime = now;
     294                 :           0 :                  }
     295                 :             :             }
     296                 :             :         }
     297                 :             : 
     298                 :             :         // --- 2. 1000Hz Output Phase ---
     299                 :             : 
     300                 :             :         // Pass physics output through Polyphase Resampler
     301         [ +  - ]:        7909 :         double force = resampler.Process(current_physics_force, run_physics);
     302                 :             : 
     303                 :             :         // Push rates to engine for GUI/Snapshot (1000Hz)
     304                 :             :         {
     305         [ +  - ]:        7909 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     306                 :        7909 :             g_engine.m_ffb_rate = loopMonitor.GetRate();
     307                 :        7909 :             g_engine.m_physics_rate = physicsMonitor.GetRate();
     308                 :        7909 :             g_engine.m_telemetry_rate = telemMonitor.GetRate();
     309                 :        7909 :             g_engine.m_hw_rate = hwMonitor.GetRate();
     310                 :        7909 :             g_engine.m_torque_rate = torqueMonitor.GetRate();
     311                 :        7909 :             g_engine.m_gen_torque_rate = genTorqueMonitor.GetRate();
     312                 :        7909 :         }
     313                 :             : 
     314   [ +  -  +  -  :        7909 :         if (DirectInputFFB::Get().UpdateForce(force)) {
                   -  + ]
     315         [ #  # ]:           0 :             hwMonitor.RecordEvent();
     316                 :             :         }
     317                 :             : 
     318                 :             :         // Precise Timing: Sleep until next tick
     319                 :             : #ifdef LMUFFB_UNIT_TEST
     320         [ +  + ]:        7909 :         if (g_use_mock_time) {
     321                 :             :             // In unit test mode with mock time, we don't sleep.
     322                 :             :             // We expect the test to advance g_mock_time.
     323                 :             :         } else {
     324         [ +  - ]:        1151 :             std::this_thread::sleep_until(next_tick);
     325                 :             :         }
     326                 :             : #else
     327                 :             :         std::this_thread::sleep_until(next_tick);
     328                 :             : #endif
     329                 :             :     }
     330                 :             : 
     331   [ +  -  +  - ]:           9 :     Logger::Get().LogFile("[FFB] Loop Stopped.");
     332                 :           9 : }
     333                 :             : 
     334                 :             : #ifndef _WIN32
     335                 :           2 : void handle_sigterm(int sig) {
     336                 :           2 :     g_running = false;
     337                 :           2 : }
     338                 :             : #endif
     339                 :             : 
     340                 :             : #ifdef LMUFFB_UNIT_TEST
     341                 :           4 : int lmuffb_app_main(int argc, char* argv[]) noexcept {
     342                 :             : #else
     343                 :             : int main(int argc, char* argv[]) noexcept {
     344                 :             : #endif
     345                 :             :     try {
     346                 :             : #ifdef _WIN32
     347                 :             :         timeBeginPeriod(1);
     348                 :             : #else
     349                 :           4 :         signal(SIGTERM, handle_sigterm);
     350                 :           4 :         signal(SIGINT, handle_sigterm);
     351                 :             : #endif
     352                 :             : 
     353                 :           4 :     bool headless = false;
     354         [ +  + ]:           8 :     for (int i = 1; i < argc; ++i) {
     355   [ +  -  +  -  :           8 :         if (std::string(argv[i]) == "--headless") headless = true;
                   +  + ]
     356                 :             :     }
     357                 :             : 
     358                 :             :     // Initialize persistent debug logging for crash analysis
     359                 :             :     // First init in current directory or logs folder to catch startup
     360   [ +  -  +  -  :          16 :     Logger::Get().Init("lmuffb_debug.log", "logs");
             +  -  +  - ]
     361   [ +  -  +  - ]:           4 :     Logger::Get().Log("Starting lmuFFB (C++ Port)...");
     362   [ +  -  +  - ]:           4 :     Logger::Get().LogFile("Application Started. Version: %s", LMUFFB_VERSION);
     363   [ +  +  +  -  :           4 :     if (headless) Logger::Get().LogFile("Mode: HEADLESS");
                   +  - ]
     364   [ +  -  +  - ]:           1 :     else Logger::Get().LogFile("Mode: GUI");
     365                 :             : 
     366         [ +  - ]:           4 :     Preset::ApplyDefaultsToEngine(g_engine);
     367   [ +  -  +  - ]:           4 :     Config::Load(g_engine);
     368                 :             : 
     369                 :             :     // Re-initialize logger with user-configured path if it changed
     370   [ +  -  +  -  :           4 :     if (!Config::m_log_path.empty() && Config::m_log_path != "logs") {
             +  -  +  - ]
     371   [ +  -  +  -  :           8 :         Logger::Get().Init("lmuffb_debug.log", Config::m_log_path);
                   +  - ]
     372   [ +  -  +  - ]:           4 :         Logger::Get().LogFile("Logger re-initialized with user path: %s", Config::m_log_path.c_str());
     373                 :             :     }
     374                 :             : 
     375         [ +  + ]:           4 :     if (!headless) {
     376   [ +  -  -  + ]:           1 :         if (!GuiLayer::Init()) {
     377   [ #  #  #  # ]:           0 :             Logger::Get().Log("Failed to initialize GUI.");
     378   [ #  #  #  # ]:           0 :             Logger::Get().Log("Failed to initialize GUI.");
     379                 :             :         }
     380   [ +  -  +  -  :           1 :         DirectInputFFB::Get().Initialize(reinterpret_cast<HWND>(GuiLayer::GetWindowHandle()));
                   +  - ]
     381                 :             :     } else {
     382   [ +  -  +  - ]:           3 :         Logger::Get().Log("Running in HEADLESS mode.");
     383   [ +  -  +  - ]:           3 :         Logger::Get().Log("Running in HEADLESS mode.");
     384   [ +  -  +  - ]:           3 :         DirectInputFFB::Get().Initialize(NULL);
     385                 :             :     }
     386                 :             : 
     387   [ +  -  +  -  :           4 :     if (GameConnector::Get().CheckLegacyConflict()) {
                   -  + ]
     388   [ #  #  #  # ]:           0 :         Logger::Get().LogFile("[Info] Legacy rF2 plugin detected (not a problem for LMU 1.2+)");
     389                 :             :     }
     390                 :             : 
     391   [ +  -  +  -  :           4 :     if (!GameConnector::Get().TryConnect()) {
                   -  + ]
     392   [ #  #  #  # ]:           0 :         Logger::Get().LogFile("Game not running or Shared Memory not ready. Waiting...");
     393                 :             :     }
     394                 :             : 
     395         [ +  - ]:           4 :     std::thread ffb_thread(FFBThread);
     396   [ +  -  +  - ]:           4 :     Logger::Get().LogFile("[GUI] Main Loop Started.");
     397                 :             : 
     398         [ +  + ]:          30 :     while (g_running) {
     399         [ +  - ]:          26 :         GuiLayer::Render(g_engine);
     400                 :             : 
     401                 :             :         // Process background save requests from the FFB thread (v0.7.70)
     402         [ +  + ]:          26 :         if (Config::m_needs_save.exchange(false)) {
     403   [ +  -  +  - ]:           2 :             Config::Save(g_engine);
     404                 :             :         }
     405                 :             : 
     406                 :             :         // Maintain a consistent 60Hz message loop even when backgrounded
     407                 :             :         // to ensure DirectInput performance and reliability.
     408         [ +  - ]:          26 :         std::this_thread::sleep_for(std::chrono::milliseconds(16));
     409                 :             :     }
     410                 :             :     
     411   [ +  -  +  - ]:           4 :     Config::Save(g_engine);
     412         [ +  + ]:           4 :     if (!headless) {
     413   [ +  -  +  - ]:           1 :         Logger::Get().LogFile("Shutting down GUI...");
     414         [ +  - ]:           1 :         GuiLayer::Shutdown(g_engine);
     415                 :             :     }
     416         [ +  - ]:           4 :     if (ffb_thread.joinable()) {
     417   [ +  -  +  - ]:           4 :         Logger::Get().LogFile("Stopping FFB Thread...");
     418                 :           4 :         g_running = false; // Ensure loop breaks
     419         [ +  - ]:           4 :         ffb_thread.join();
     420   [ +  -  +  - ]:           4 :         Logger::Get().LogFile("FFB Thread Stopped.");
     421                 :             :     }
     422   [ +  -  +  - ]:           4 :     DirectInputFFB::Get().Shutdown();
     423   [ +  -  +  - ]:           4 :     Logger::Get().Log("Main Loop Ended. Clean Exit.");
     424                 :             :     
     425                 :           4 :     return 0;
     426         [ -  - ]:           4 :     } catch (const std::exception& e) {
     427                 :           0 :         Logger::Get().LogFile("Fatal exception: %s", e.what());
     428                 :           0 :         return 1;
     429                 :           0 :     } catch (...) {
     430                 :           0 :         Logger::Get().LogFile("Fatal unknown exception.");
     431                 :           0 :         return 1;
     432                 :           0 :     }
     433                 :             : }
        

Generated by: LCOV version 2.0-1