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 [ + + ]: 7676 : while (g_running) {
94 [ + - ]: 7667 : loopMonitor.RecordEvent();
95 [ + - + - ]: 7667 : next_tick += target_period;
96 : :
97 : : // --- 1. Physics Phase Accumulator (5/2 Ratio) ---
98 : 7667 : phase_accumulator += 2; // M = 2
99 : 7667 : bool run_physics = false;
100 : :
101 [ + + ]: 7667 : if (phase_accumulator >= 5) { // L = 5
102 : 3066 : phase_accumulator -= 5;
103 : 3066 : run_physics = true;
104 : : }
105 : :
106 [ + + ]: 7667 : if (run_physics) {
107 [ + - ]: 3066 : physicsMonitor.RecordEvent();
108 : 3066 : bool should_output = false;
109 : 3066 : double force_physics = 0.0;
110 : :
111 : 3066 : bool in_realtime_phys = false;
112 [ + - + - : 3066 : if (g_ffb_active && GameConnector::Get().IsConnected()) {
+ - + - +
- ]
113 [ + - + - ]: 3066 : GameConnector::Get().CopyTelemetry(g_localData);
114 [ + - ]: 3066 : g_engine.m_metadata.UpdateMetadata(g_localData); // Update names/classes immediately
115 : :
116 [ + - ]: 3066 : in_realtime_phys = GameConnector::Get().IsInRealtime();
117 [ + - ]: 3066 : long current_session = GameConnector::Get().GetSessionType();
118 : :
119 [ + - + - ]: 3066 : 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 [ + - ]: 3066 : bool is_driving = GameConnector::Get().IsPlayerActivelyDriving();
127 : :
128 [ - + - - ]: 3066 : bool should_start_log = (is_driving && !was_driving);
129 [ + - - + ]: 3066 : bool should_stop_log = (!is_driving && was_driving);
130 [ - + - - : 3066 : bool session_changed = (is_driving && was_driving && last_session != -1 && current_session != last_session);
- - - - ]
131 : :
132 [ - + ]: 3066 : 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 [ + - + - : 3066 : bool manual_start_requested = Config::m_auto_start_logging && !AsyncLogger::Get().IsLogging() && is_driving;
+ - - + ]
139 : :
140 [ + - - + ]: 3066 : 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.general = g_engine.m_general;
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 : AsyncLogger::Get().Start(info, Config::m_log_path);
174 : 0 : }
175 : : }
176 [ - + ]: 3066 : } else if (should_stop_log) {
177 [ # # # # ]: 0 : Logger::Get().LogFile("[Game] User exited driving session.");
178 [ # # # # ]: 0 : if (AsyncLogger::Get().IsLogging()) {
179 [ # # ]: 0 : AsyncLogger::Get().Stop();
180 : : }
181 : : }
182 : 3066 : was_driving = is_driving;
183 : 3066 : last_session = current_session;
184 : :
185 [ + - - + ]: 3066 : if (!is_stale && g_localData.telemetry.playerHasVehicle) {
186 : 0 : uint8_t idx = g_localData.telemetry.playerVehicleIdx;
187 : :
188 : : // --- LOST FRAME DETECTION (Issue #303) ---
189 : : static double last_telem_et = -1.0;
190 [ # # # # : 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)) {
# # ]
191 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
192 [ # # ]: 0 : g_engine.m_safety.TriggerSafetyWindow("Lost Frames");
193 : 0 : }
194 : 0 : last_telem_et = g_localData.telemetry.telemInfo[idx].mElapsedTime;
195 : :
196 [ # # ]: 0 : if (idx < 104) {
197 : 0 : auto& scoring = g_localData.scoring.vehScoringInfo[idx];
198 : 0 : TelemInfoV01* pPlayerTelemetry = &g_localData.telemetry.telemInfo[idx];
199 : :
200 : : // Track telemetry update rate
201 [ # # ]: 0 : if (pPlayerTelemetry->mElapsedTime != lastET) {
202 [ # # ]: 0 : telemMonitor.RecordEvent();
203 : 0 : lastET = pPlayerTelemetry->mElapsedTime;
204 : : }
205 : :
206 : : // Track torque update rates
207 [ # # ]: 0 : if (pPlayerTelemetry->mSteeringShaftTorque != lastTorque) {
208 [ # # ]: 0 : torqueMonitor.RecordEvent();
209 : 0 : lastTorque = pPlayerTelemetry->mSteeringShaftTorque;
210 : : }
211 [ # # ]: 0 : if (g_localData.generic.FFBTorque != lastGenTorque) {
212 [ # # ]: 0 : genTorqueMonitor.RecordEvent();
213 : 0 : lastGenTorque = g_localData.generic.FFBTorque;
214 : : }
215 : :
216 : : // Extended monitoring (Issue #133)
217 [ # # ]: 0 : mAccX.Update(pPlayerTelemetry->mLocalAccel.x);
218 [ # # ]: 0 : mAccY.Update(pPlayerTelemetry->mLocalAccel.y);
219 [ # # ]: 0 : mAccZ.Update(pPlayerTelemetry->mLocalAccel.z);
220 [ # # ]: 0 : mVelX.Update(pPlayerTelemetry->mLocalVel.x);
221 [ # # ]: 0 : mVelY.Update(pPlayerTelemetry->mLocalVel.y);
222 [ # # ]: 0 : mVelZ.Update(pPlayerTelemetry->mLocalVel.z);
223 [ # # ]: 0 : mRotX.Update(pPlayerTelemetry->mLocalRot.x);
224 [ # # ]: 0 : mRotY.Update(pPlayerTelemetry->mLocalRot.y);
225 [ # # ]: 0 : mRotZ.Update(pPlayerTelemetry->mLocalRot.z);
226 [ # # ]: 0 : mRotAccX.Update(pPlayerTelemetry->mLocalRotAccel.x);
227 [ # # ]: 0 : mRotAccY.Update(pPlayerTelemetry->mLocalRotAccel.y);
228 [ # # ]: 0 : mRotAccZ.Update(pPlayerTelemetry->mLocalRotAccel.z);
229 [ # # ]: 0 : mUnfSteer.Update(pPlayerTelemetry->mUnfilteredSteering);
230 [ # # ]: 0 : mFilSteer.Update(pPlayerTelemetry->mFilteredSteering);
231 [ # # ]: 0 : mRPM.Update(pPlayerTelemetry->mEngineRPM);
232 [ # # ]: 0 : mLoadFL.Update(pPlayerTelemetry->mWheel[0].mTireLoad);
233 [ # # ]: 0 : mLoadFR.Update(pPlayerTelemetry->mWheel[1].mTireLoad);
234 [ # # ]: 0 : mLoadRL.Update(pPlayerTelemetry->mWheel[2].mTireLoad);
235 [ # # ]: 0 : mLoadRR.Update(pPlayerTelemetry->mWheel[3].mTireLoad);
236 [ # # ]: 0 : mLatFL.Update(pPlayerTelemetry->mWheel[0].mLateralForce);
237 [ # # ]: 0 : mLatFR.Update(pPlayerTelemetry->mWheel[1].mLateralForce);
238 [ # # ]: 0 : mLatRL.Update(pPlayerTelemetry->mWheel[2].mLateralForce);
239 [ # # ]: 0 : mLatRR.Update(pPlayerTelemetry->mWheel[3].mLateralForce);
240 [ # # ]: 0 : mPosX.Update(pPlayerTelemetry->mPos.x);
241 [ # # ]: 0 : mPosY.Update(pPlayerTelemetry->mPos.y);
242 [ # # ]: 0 : mPosZ.Update(pPlayerTelemetry->mPos.z);
243 [ # # ]: 0 : mDtMon.Update(pPlayerTelemetry->mDeltaTime);
244 : :
245 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
246 [ # # # # : 0 : bool full_allowed = g_engine.m_safety.IsFFBAllowed(scoring, g_localData.scoring.scoringInfo.mGamePhase) && is_driving;
# # ]
247 : :
248 [ # # ]: 0 : force_physics = g_engine.calculate_force(pPlayerTelemetry, scoring.mVehicleClass, scoring.mVehicleName, g_localData.generic.FFBTorque, full_allowed, 0.0025, scoring.mControl);
249 : :
250 : : // v0.7.153: Explicitly target zero force only when player is not in control (Issue #281).
251 : : // This allows Soft Lock to remain active in the garage and during pause (ControlMode::PLAYER),
252 : : // while ensuring that AI takeover or other non-player states slew to zero.
253 [ # # ]: 0 : if (scoring.mControl != static_cast<signed char>(ControlMode::PLAYER)) force_physics = 0.0;
254 : :
255 : : // Safety Layer (v0.7.49): Slew Rate Limiting (400Hz)
256 : : // Applied before up-sampling to prevent reconstruction artifacts on spikes.
257 [ # # # # ]: 0 : bool restricted = !full_allowed || (scoring.mFinishStatus != 0);
258 [ # # ]: 0 : force_physics = g_engine.m_safety.ApplySafetySlew(force_physics, 0.0025, restricted);
259 : :
260 : 0 : should_output = true;
261 : 0 : }
262 : : }
263 : : }
264 : :
265 [ + - ]: 3066 : if (!should_output) {
266 [ + - ]: 3066 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
267 [ + - ]: 3066 : force_physics = g_engine.m_safety.ApplySafetySlew(0.0, 0.0025, true);
268 : 3066 : }
269 : 3066 : current_physics_force = force_physics;
270 : :
271 : : // Warning for low sample rate (Issue #133)
272 [ + + + - ]: 3066 : static auto lastWarningTime = TimeUtils::GetTime();
273 : 3066 : HealthStatus health;
274 : : {
275 [ + - ]: 3066 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
276 [ - + ]: 3066 : double t_rate = (g_engine.m_torque_source == 1) ? genTorqueMonitor.GetRate() : torqueMonitor.GetRate();
277 : 3066 : health = HealthMonitor::Check(loopMonitor.GetRate(), telemMonitor.GetRate(), t_rate, g_engine.m_torque_source, physicsMonitor.GetRate(),
278 [ + - + - : 3066 : GameConnector::Get().IsConnected(), GameConnector::Get().IsSessionActive(), GameConnector::Get().GetSessionType(), GameConnector::Get().IsInRealtime(), GameConnector::Get().GetPlayerControl());
+ - + - +
- + - ]
279 : 3066 : }
280 : :
281 [ - + - - ]: 3066 : if (in_realtime_phys && !health.is_healthy) {
282 : 0 : auto now = TimeUtils::GetTime();
283 [ # # # # : 0 : if (std::chrono::duration_cast<std::chrono::seconds>(now - lastWarningTime).count() >= 60) {
# # ]
284 [ # # ]: 0 : std::string reason = "";
285 [ # # # # : 0 : if (health.loop_low) reason += "Loop=" + std::to_string((int)health.loop_rate) + "Hz ";
# # # # ]
286 [ # # # # : 0 : if (health.physics_low) reason += "Physics=" + std::to_string((int)health.physics_rate) + "Hz ";
# # # # ]
287 [ # # # # : 0 : if (health.telem_low) reason += "Telemetry=" + std::to_string((int)health.telem_rate) + "Hz ";
# # # # ]
288 [ # # # # : 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) ";
# # # # #
# # # ]
289 : :
290 [ # # # # ]: 0 : Logger::Get().LogFile("Low Sample Rate detected: %s", reason.c_str());
291 : 0 : lastWarningTime = now;
292 : 0 : }
293 : : }
294 : : }
295 : :
296 : : // --- 2. 1000Hz Output Phase ---
297 : :
298 : : // Pass physics output through Polyphase Resampler
299 [ + - ]: 7667 : double force = resampler.Process(current_physics_force, run_physics);
300 : :
301 : : // Push rates to engine for GUI/Snapshot (1000Hz)
302 : : {
303 [ + - ]: 7667 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
304 : 7667 : g_engine.m_ffb_rate = loopMonitor.GetRate();
305 : 7667 : g_engine.m_physics_rate = physicsMonitor.GetRate();
306 : 7667 : g_engine.m_telemetry_rate = telemMonitor.GetRate();
307 : 7667 : g_engine.m_hw_rate = hwMonitor.GetRate();
308 : 7667 : g_engine.m_torque_rate = torqueMonitor.GetRate();
309 : 7667 : g_engine.m_gen_torque_rate = genTorqueMonitor.GetRate();
310 : 7667 : }
311 : :
312 [ + - + - : 7667 : if (DirectInputFFB::Get().UpdateForce(force)) {
- + ]
313 [ # # ]: 0 : hwMonitor.RecordEvent();
314 : : }
315 : :
316 : : // Precise Timing: Sleep until next tick
317 : : #ifdef LMUFFB_UNIT_TEST
318 [ + + ]: 7667 : if (g_use_mock_time) {
319 : : // In unit test mode with mock time, we don't sleep.
320 : : // We expect the test to advance g_mock_time.
321 : : } else {
322 [ + - ]: 1151 : std::this_thread::sleep_until(next_tick);
323 : : }
324 : : #else
325 : : std::this_thread::sleep_until(next_tick);
326 : : #endif
327 : : }
328 : :
329 [ + - + - ]: 9 : Logger::Get().LogFile("[FFB] Loop Stopped.");
330 : 9 : }
331 : :
332 : : #ifndef _WIN32
333 : 2 : void handle_sigterm(int sig) {
334 : 2 : g_running = false;
335 : 2 : }
336 : : #endif
337 : :
338 : : #ifdef LMUFFB_UNIT_TEST
339 : 4 : int lmuffb_app_main(int argc, char* argv[]) noexcept {
340 : : #else
341 : : int main(int argc, char* argv[]) noexcept {
342 : : #endif
343 : : try {
344 : : #ifdef _WIN32
345 : : timeBeginPeriod(1);
346 : : #else
347 : 4 : signal(SIGTERM, handle_sigterm);
348 : 4 : signal(SIGINT, handle_sigterm);
349 : : #endif
350 : :
351 : 4 : bool headless = false;
352 [ + + ]: 8 : for (int i = 1; i < argc; ++i) {
353 [ + - + - : 8 : if (std::string(argv[i]) == "--headless") headless = true;
+ + ]
354 : : }
355 : :
356 : : // Initialize persistent debug logging for crash analysis
357 : : // First init in current directory or logs folder to catch startup
358 [ + - + - : 16 : Logger::Get().Init("lmuffb_debug.log", "logs");
+ - + - ]
359 [ + - + - ]: 4 : Logger::Get().Log("Starting lmuFFB (C++ Port)...");
360 [ + - + - ]: 4 : Logger::Get().LogFile("Application Started. Version: %s", LMUFFB_VERSION);
361 [ + + + - : 4 : if (headless) Logger::Get().LogFile("Mode: HEADLESS");
+ - ]
362 [ + - + - ]: 1 : else Logger::Get().LogFile("Mode: GUI");
363 : :
364 [ + - ]: 4 : Preset::ApplyDefaultsToEngine(g_engine);
365 [ + - + - ]: 4 : Config::Load(g_engine);
366 : :
367 : : // Re-initialize logger with user-configured path if it changed
368 [ + - + - : 4 : if (!Config::m_log_path.empty() && Config::m_log_path != "logs") {
+ - + - ]
369 [ + - + - : 8 : Logger::Get().Init("lmuffb_debug.log", Config::m_log_path);
+ - ]
370 [ + - + - ]: 4 : Logger::Get().LogFile("Logger re-initialized with user path: %s", Config::m_log_path.c_str());
371 : : }
372 : :
373 [ + + ]: 4 : if (!headless) {
374 [ + - - + ]: 1 : if (!GuiLayer::Init()) {
375 [ # # # # ]: 0 : Logger::Get().Log("Failed to initialize GUI.");
376 [ # # # # ]: 0 : Logger::Get().Log("Failed to initialize GUI.");
377 : : }
378 [ + - + - : 1 : DirectInputFFB::Get().Initialize(reinterpret_cast<HWND>(GuiLayer::GetWindowHandle()));
+ - ]
379 : : } else {
380 [ + - + - ]: 3 : Logger::Get().Log("Running in HEADLESS mode.");
381 [ + - + - ]: 3 : Logger::Get().Log("Running in HEADLESS mode.");
382 [ + - + - ]: 3 : DirectInputFFB::Get().Initialize(NULL);
383 : : }
384 : :
385 [ + - + - : 4 : if (GameConnector::Get().CheckLegacyConflict()) {
- + ]
386 [ # # # # ]: 0 : Logger::Get().LogFile("[Info] Legacy rF2 plugin detected (not a problem for LMU 1.2+)");
387 : : }
388 : :
389 [ + - + - : 4 : if (!GameConnector::Get().TryConnect()) {
- + ]
390 [ # # # # ]: 0 : Logger::Get().LogFile("Game not running or Shared Memory not ready. Waiting...");
391 : : }
392 : :
393 [ + - ]: 4 : std::thread ffb_thread(FFBThread);
394 [ + - + - ]: 4 : Logger::Get().LogFile("[GUI] Main Loop Started.");
395 : :
396 [ + + ]: 30 : while (g_running) {
397 [ + - ]: 26 : GuiLayer::Render(g_engine);
398 : :
399 : : // Process background save requests from the FFB thread (v0.7.70)
400 [ + + ]: 26 : if (Config::m_needs_save.exchange(false)) {
401 [ + - + - ]: 2 : Config::Save(g_engine);
402 : : }
403 : :
404 : : // Maintain a consistent 60Hz message loop even when backgrounded
405 : : // to ensure DirectInput performance and reliability.
406 [ + - ]: 26 : std::this_thread::sleep_for(std::chrono::milliseconds(16));
407 : : }
408 : :
409 [ + - + - ]: 4 : Config::Save(g_engine);
410 [ + + ]: 4 : if (!headless) {
411 [ + - + - ]: 1 : Logger::Get().LogFile("Shutting down GUI...");
412 [ + - ]: 1 : GuiLayer::Shutdown(g_engine);
413 : : }
414 [ + - ]: 4 : if (ffb_thread.joinable()) {
415 [ + - + - ]: 4 : Logger::Get().LogFile("Stopping FFB Thread...");
416 : 4 : g_running = false; // Ensure loop breaks
417 [ + - ]: 4 : ffb_thread.join();
418 [ + - + - ]: 4 : Logger::Get().LogFile("FFB Thread Stopped.");
419 : : }
420 [ + - + - ]: 4 : DirectInputFFB::Get().Shutdown();
421 [ + - + - ]: 4 : Logger::Get().Log("Main Loop Ended. Clean Exit.");
422 : :
423 : 4 : return 0;
424 [ - - ]: 4 : } catch (const std::exception& e) {
425 : 0 : Logger::Get().LogFile("Fatal exception: %s", e.what());
426 : 0 : return 1;
427 : 0 : } catch (...) {
428 : 0 : Logger::Get().LogFile("Fatal unknown exception.");
429 : 0 : return 1;
430 : 0 : }
431 : : }
|