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