LCOV - code coverage report
Current view: top level - gui - GuiLayer_Common.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 58.2 % 859 500
Test Date: 2026-03-18 19:01:10 Functions: 90.0 % 30 27
Branches: 31.0 % 1830 567

             Branch data     Line data    Source code
       1                 :             : #include "GuiLayer.h"
       2                 :             : #include "Version.h"
       3                 :             : #include "Config.h"
       4                 :             : #include "Tooltips.h"
       5                 :             : #include "StringUtils.h" // Added StringUtils.h
       6                 :             : #include "DirectInputFFB.h"
       7                 :             : #include "GameConnector.h"
       8                 :             : #include "GuiWidgets.h"
       9                 :             : #include "AsyncLogger.h"
      10                 :             : #include "VehicleUtils.h"
      11                 :             : #include "HealthMonitor.h"
      12                 :             : #include <iostream>
      13                 :             : #include <vector>
      14                 :             : #include <cmath>
      15                 :             : #include <algorithm>
      16                 :             : #include <mutex>
      17                 :             : #include <chrono>
      18                 :             : #include <ctime>
      19                 :             : #include <filesystem>
      20                 :             : 
      21                 :             : #ifdef ENABLE_IMGUI
      22                 :             : #include "imgui.h"
      23                 :             : 
      24                 :             : #ifdef _WIN32
      25                 :             : #define WIN32_LEAN_AND_MEAN
      26                 :             : #include <windows.h>
      27                 :             : #endif
      28                 :             : 
      29                 :         372 : static void DisplayRate(const char* label, double rate, double target) {
      30         [ +  - ]:         372 :     ImGui::Text("%s", label);
      31                 :             :     
      32                 :             :     // Status colors for performance metrics
      33                 :             :     static const ImVec4 COLOR_RED(1.0F, 0.4F, 0.4F, 1.0F);
      34                 :             :     static const ImVec4 COLOR_GREEN(0.4F, 1.0F, 0.4F, 1.0F);
      35                 :             :     static const ImVec4 COLOR_YELLOW(1.0F, 1.0F, 0.4F, 1.0F);
      36                 :             : 
      37                 :         372 :     ImVec4 color = COLOR_RED;
      38         [ +  + ]:         372 :     if (rate >= target * 0.95) {
      39                 :           6 :         color = COLOR_GREEN;
      40         [ -  + ]:         366 :     } else if (rate >= target * 0.75) {
      41                 :           0 :         color = COLOR_YELLOW;
      42                 :             :     }
      43                 :             :     
      44         [ +  - ]:         372 :     ImGui::TextColored(color, "%.1f Hz", rate);
      45                 :         372 : }
      46                 :             : 
      47                 :             : 
      48                 :             : // External linkage to FFB loop status
      49                 :             : extern std::atomic<bool> g_running;
      50                 :             : extern std::recursive_mutex g_engine_mutex;
      51                 :             : 
      52                 :             : float GuiLayer::m_latest_steering_range = 0.0f;
      53                 :             : float GuiLayer::m_latest_steering_angle = 0.0f;
      54                 :             : 
      55                 :             : static const float CONFIG_PANEL_WIDTH = 500.0f;
      56                 :             : static const int LATENCY_WARNING_THRESHOLD_MS = 15;
      57                 :             : 
      58                 :             : // Professional "Flat Dark" Theme
      59                 :           1 : void GuiLayer::SetupGUIStyle() {
      60         [ +  - ]:           1 :     ImGuiStyle& style = ImGui::GetStyle();
      61                 :             : 
      62                 :           1 :     style.WindowRounding = 5.0f;
      63                 :           1 :     style.ChildRounding = 5.0f;
      64                 :           1 :     style.PopupRounding = 5.0f;
      65                 :           1 :     style.FrameRounding = 4.0f;
      66                 :           1 :     style.GrabRounding = 4.0f;
      67                 :           1 :     style.WindowPadding = ImVec2(10, 10);
      68                 :           1 :     style.FramePadding = ImVec2(8, 4);
      69                 :           1 :     style.ItemSpacing = ImVec2(8, 6);
      70                 :             : 
      71                 :           1 :     ImVec4* colors = style.Colors;
      72                 :             : 
      73                 :           1 :     colors[ImGuiCol_WindowBg]       = ImVec4(0.12f, 0.12f, 0.12f, 1.00f);
      74                 :           1 :     colors[ImGuiCol_ChildBg]        = ImVec4(0.15f, 0.15f, 0.15f, 1.00f);
      75                 :           1 :     colors[ImGuiCol_PopupBg]        = ImVec4(0.15f, 0.15f, 0.15f, 0.98f);
      76                 :             : 
      77                 :           1 :     colors[ImGuiCol_Header]         = ImVec4(0.20f, 0.20f, 0.20f, 0.00f);
      78                 :           1 :     colors[ImGuiCol_HeaderHovered]  = ImVec4(0.25f, 0.25f, 0.25f, 0.50f);
      79                 :           1 :     colors[ImGuiCol_HeaderActive]   = ImVec4(0.30f, 0.30f, 0.30f, 0.50f);
      80                 :             : 
      81                 :           1 :     colors[ImGuiCol_FrameBg]        = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
      82                 :           1 :     colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
      83                 :           1 :     colors[ImGuiCol_FrameBgActive]  = ImVec4(0.30f, 0.30f, 0.30f, 1.00f);
      84                 :             : 
      85                 :           1 :     ImVec4 accent = ImVec4(0.00f, 0.60f, 0.85f, 1.00f);
      86                 :           1 :     colors[ImGuiCol_SliderGrab]     = accent;
      87                 :           1 :     colors[ImGuiCol_SliderGrabActive] = ImVec4(0.00f, 0.70f, 0.95f, 1.00f);
      88                 :           1 :     colors[ImGuiCol_Button]         = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
      89                 :           1 :     colors[ImGuiCol_ButtonHovered]  = accent;
      90                 :           1 :     colors[ImGuiCol_ButtonActive]   = ImVec4(0.00f, 0.50f, 0.75f, 1.00f);
      91                 :           1 :     colors[ImGuiCol_CheckMark]      = accent;
      92                 :             : 
      93                 :           1 :     colors[ImGuiCol_Text]           = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
      94                 :           1 :     colors[ImGuiCol_TextDisabled]   = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
      95                 :           1 :     colors[ImGuiCol_MenuBarBg]      = ImVec4(0.12f, 0.12f, 0.12f, 1.00f);
      96                 :           1 : }
      97                 :             : 
      98                 :           1 : void GuiLayer::DrawMenuBar(FFBEngine& engine) {
      99         [ +  - ]:           1 :     if (ImGui::BeginMainMenuBar()) {
     100         [ -  + ]:           1 :         if (ImGui::BeginMenu("Logs")) {
     101         [ #  # ]:           0 :             if (ImGui::MenuItem("Analyze last log")) {
     102                 :             :                 namespace fs = std::filesystem;
     103                 :           0 :                 fs::path latest_path;
     104                 :           0 :                 fs::file_time_type latest_time;
     105                 :           0 :                 bool found = false;
     106                 :             : 
     107   [ #  #  #  #  :           0 :                 fs::path search_path = Config::m_log_path.empty() ? "." : Config::m_log_path;
          #  #  #  #  #  
                #  #  # ]
     108                 :             : 
     109                 :             :                 try {
     110   [ #  #  #  # ]:           0 :                     if (fs::exists(search_path)) {
     111   [ #  #  #  #  :           0 :                         for (const auto& entry : fs::directory_iterator(search_path)) {
                   #  # ]
     112   [ #  #  #  # ]:           0 :                             if (entry.is_regular_file()) {
     113   [ #  #  #  # ]:           0 :                                 std::string filename = entry.path().filename().string();
     114                 :           0 :                                 bool looks_like_log = filename.find("lmuffb_log_") == 0;
     115   [ #  #  #  #  :           0 :                                 bool has_ext = (filename.length() >= 4 && (filename.substr(filename.length() - 4) == ".bin" || filename.substr(filename.length() - 4) == ".csv"));
          #  #  #  #  #  
          #  #  #  #  #  
          #  #  #  #  #  
                #  #  # ]
     116   [ #  #  #  # ]:           0 :                                 if (looks_like_log && has_ext) {
     117         [ #  # ]:           0 :                                     auto ftime = fs::last_write_time(entry);
     118   [ #  #  #  #  :           0 :                                     if (!found || ftime > latest_time) {
             #  #  #  # ]
     119                 :           0 :                                         latest_time = ftime;
     120         [ #  # ]:           0 :                                         latest_path = entry.path();
     121                 :           0 :                                         found = true;
     122                 :             :                                     }
     123                 :             :                                 }
     124                 :           0 :                             }
     125                 :           0 :                         }
     126                 :             :                     }
     127         [ -  - ]:           0 :                 } catch (const std::exception& e) {
     128   [ -  -  -  -  :           0 :                     std::cerr << "Log analysis error: " << e.what() << "\n";
                   -  - ]
     129                 :           0 :                 } catch (...) {
     130         [ -  - ]:           0 :                     std::cerr << "Log analysis unknown error\n";
     131         [ -  - ]:           0 :                 }
     132                 :             : 
     133         [ #  # ]:           0 :                 if (found) {
     134         [ #  # ]:           0 :                     std::string log_file = latest_path.string();
     135                 :             :                     
     136                 :             :                     // Get executable directory to find tools/ relative to the binary
     137         [ #  # ]:           0 :                     fs::path exe_dir = fs::current_path();
     138                 :             : #ifdef _WIN32
     139                 :             :                     char buffer[MAX_PATH];
     140                 :             :                     if (GetModuleFileNameA(NULL, buffer, MAX_PATH)) {
     141                 :             :                         exe_dir = fs::path(buffer).parent_path();
     142                 :             :                     }
     143                 :             : #endif
     144                 :             :                     
     145                 :             :                     // Robust PYTHONPATH lookup
     146   [ #  #  #  #  :           0 :                     std::string python_path = (exe_dir / "tools").string();
                   #  # ]
     147   [ #  #  #  #  :           0 :                     if (!fs::exists(exe_dir / "tools/lmuffb_log_analyzer")) {
             #  #  #  # ]
     148                 :             :                         // Dev environment fallbacks from CWD
     149   [ #  #  #  #  :           0 :                         if (fs::exists("tools/lmuffb_log_analyzer")) python_path = "tools";
             #  #  #  # ]
     150   [ #  #  #  #  :           0 :                         else if (fs::exists("../tools/lmuffb_log_analyzer")) python_path = "../tools";
             #  #  #  # ]
     151   [ #  #  #  #  :           0 :                         else if (fs::exists("../../tools/lmuffb_log_analyzer")) python_path = "../../tools";
             #  #  #  # ]
     152                 :             :                     }
     153                 :             : 
     154   [ #  #  #  #  :           0 :                     std::string cmd = "start cmd /c \"set PYTHONPATH=" + python_path + " && python -m lmuffb_log_analyzer.cli analyze-full \"" + log_file + "\" & pause\"";
             #  #  #  # ]
     155         [ #  # ]:           0 :                     system(cmd.c_str());
     156                 :           0 :                 }
     157                 :           0 :             }
     158                 :           0 :             ImGui::EndMenu();
     159                 :             :         }
     160                 :           1 :         ImGui::EndMainMenuBar();
     161                 :             :     }
     162                 :           1 : }
     163                 :             : 
     164                 :             : 
     165                 :             : static constexpr std::chrono::seconds CONNECT_ATTEMPT_INTERVAL(2);
     166                 :             : 
     167                 :         378 : void GuiLayer::DrawTuningWindow(FFBEngine& engine) {
     168         [ +  - ]:         378 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     169                 :             : 
     170         [ +  - ]:         378 :     ImGuiViewport* viewport = ImGui::GetMainViewport();
     171         [ +  + ]:         378 :     float current_width = Config::show_graphs ? CONFIG_PANEL_WIDTH : viewport->WorkSize.x;
     172                 :             : 
     173         [ +  - ]:         378 :     ImGui::SetNextWindowPos(viewport->WorkPos);
     174         [ +  - ]:         378 :     ImGui::SetNextWindowSize(ImVec2(current_width, viewport->WorkSize.y));
     175                 :             : 
     176                 :         378 :     ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
     177         [ +  - ]:         378 :     ImGui::Begin("MainUI", nullptr, flags);
     178                 :             : 
     179   [ +  +  +  - ]:         378 :     static std::chrono::steady_clock::time_point last_check_time = std::chrono::steady_clock::now();
     180                 :             : 
     181   [ +  -  +  -  :         378 :     if (!GameConnector::Get().IsConnected()) {
                   +  + ]
     182         [ +  - ]:         209 :       ImGui::TextColored(ImVec4(1, 1, 0, 1), "Connecting to LMU...");
     183   [ +  -  +  -  :         209 :       if (std::chrono::steady_clock::now() - last_check_time > CONNECT_ATTEMPT_INTERVAL) {
                   -  + ]
     184                 :           0 :         last_check_time = std::chrono::steady_clock::now();
     185   [ #  #  #  # ]:           0 :         GameConnector::Get().TryConnect();
     186                 :             :       }
     187                 :             :     } else {
     188         [ +  - ]:         169 :       ImGui::TextColored(ImVec4(0, 1, 0, 1), "Connected to LMU");
     189                 :             :     }
     190                 :             : 
     191   [ +  +  +  - ]:         378 :     static std::vector<DeviceInfo> devices;
     192                 :             :     static int selected_device_idx = -1;
     193                 :             : 
     194         [ +  + ]:         378 :     if (devices.empty()) {
     195   [ +  -  +  - ]:           1 :         devices = DirectInputFFB::Get().EnumerateDevices();
     196   [ +  -  +  -  :           1 :         if (selected_device_idx == -1 && !Config::m_last_device_guid.empty()) {
                   +  - ]
     197         [ +  - ]:           1 :             GUID target = DirectInputFFB::StringToGuid(Config::m_last_device_guid);
     198         [ +  - ]:           1 :             for (int i = 0; i < (int)devices.size(); i++) {
     199         [ +  - ]:           1 :                 if (memcmp(&devices[i].guid, &target, sizeof(GUID)) == 0) {
     200                 :           1 :                     selected_device_idx = i;
     201   [ +  -  +  - ]:           1 :                     DirectInputFFB::Get().SelectDevice(devices[i].guid);
     202                 :           1 :                     break;
     203                 :             :                 }
     204                 :             :             }
     205                 :             :         }
     206                 :             :     }
     207                 :             : 
     208   [ +  -  +  - ]:         378 :     ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
     209   [ +  -  +  -  :         378 :     if (ImGui::BeginCombo("FFB Device", selected_device_idx >= 0 ? devices[selected_device_idx].name.c_str() : "Select Device...")) {
                   -  + ]
     210         [ #  # ]:           0 :         for (int i = 0; i < (int)devices.size(); i++) {
     211                 :           0 :             bool is_selected = (selected_device_idx == i);
     212         [ #  # ]:           0 :             ImGui::PushID(i);
     213   [ #  #  #  # ]:           0 :             if (ImGui::Selectable(devices[i].name.c_str(), is_selected)) {
     214                 :           0 :                 selected_device_idx = i;
     215   [ #  #  #  # ]:           0 :                 DirectInputFFB::Get().SelectDevice(devices[i].guid);
     216         [ #  # ]:           0 :                 Config::m_last_device_guid = DirectInputFFB::GuidToString(devices[i].guid);
     217   [ #  #  #  # ]:           0 :                 Config::Save(engine);
     218                 :             :             }
     219   [ #  #  #  # ]:           0 :             if (is_selected) ImGui::SetItemDefaultFocus();
     220         [ #  # ]:           0 :             ImGui::PopID();
     221                 :             :         }
     222         [ #  # ]:           0 :         ImGui::EndCombo();
     223                 :             :     }
     224   [ +  -  +  +  :         378 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_SELECT);
                   +  - ]
     225                 :             : 
     226         [ +  - ]:         378 :     ImGui::SameLine();
     227   [ +  -  -  + ]:         378 :     if (ImGui::Button("Rescan")) {
     228   [ #  #  #  # ]:           0 :         devices = DirectInputFFB::Get().EnumerateDevices();
     229                 :           0 :         selected_device_idx = -1;
     230                 :             :     }
     231   [ +  -  +  +  :         378 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_RESCAN);
                   +  - ]
     232         [ +  - ]:         378 :     ImGui::SameLine();
     233   [ +  -  -  + ]:         378 :     if (ImGui::Button("Unbind")) {
     234   [ #  #  #  # ]:           0 :         DirectInputFFB::Get().ReleaseDevice();
     235                 :           0 :         selected_device_idx = -1;
     236                 :             :     }
     237   [ +  -  -  +  :         378 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_UNBIND);
                   -  - ]
     238                 :             : 
     239   [ +  -  +  - ]:         378 :     if (DirectInputFFB::Get().IsActive()) {
     240   [ +  -  +  - ]:         378 :         if (DirectInputFFB::Get().IsExclusive()) {
     241         [ +  - ]:         378 :             ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Mode: EXCLUSIVE (Game FFB Blocked)");
     242   [ +  -  +  +  :         378 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_EXCLUSIVE);
                   +  - ]
     243                 :             :         } else {
     244         [ #  # ]:           0 :             ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.4f, 1.0f), "Mode: SHARED (Potential Conflict)");
     245   [ #  #  #  #  :           0 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_SHARED);
                   #  # ]
     246                 :             :         }
     247                 :             :     } else {
     248         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "No device selected.");
     249   [ #  #  #  #  :           0 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::NO_DEVICE);
                   #  # ]
     250                 :             :     }
     251                 :             : 
     252   [ +  -  -  + ]:         378 :     if (ImGui::Checkbox("Always on Top", &Config::m_always_on_top)) {
     253         [ #  # ]:           0 :         SetWindowAlwaysOnTopPlatform(Config::m_always_on_top);
     254   [ #  #  #  # ]:           0 :         Config::Save(engine);
     255                 :             :     }
     256   [ +  -  +  +  :         378 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::ALWAYS_ON_TOP);
                   +  - ]
     257         [ +  - ]:         378 :     ImGui::SameLine();
     258                 :             : 
     259                 :         378 :     bool toggled = Config::show_graphs;
     260   [ +  -  -  + ]:         378 :     if (ImGui::Checkbox("Graphs", &toggled)) {
     261         [ #  # ]:           0 :         SaveCurrentWindowGeometryPlatform(Config::show_graphs);
     262                 :           0 :         Config::show_graphs = toggled;
     263         [ #  # ]:           0 :         int target_w = Config::show_graphs ? Config::win_w_large : Config::win_w_small;
     264         [ #  # ]:           0 :         int target_h = Config::show_graphs ? Config::win_h_large : Config::win_h_small;
     265         [ #  # ]:           0 :         ResizeWindowPlatform(Config::win_pos_x, Config::win_pos_y, target_w, target_h);
     266   [ #  #  #  # ]:           0 :         Config::Save(engine);
     267                 :             :     }
     268   [ +  -  -  +  :         378 :     if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::SHOW_GRAPHS);
                   -  - ]
     269                 :             : 
     270         [ +  - ]:         378 :     ImGui::Separator();
     271                 :         378 :     bool is_logging = Config::m_auto_start_logging;
     272         [ +  - ]:         378 :     if (is_logging) {
     273   [ +  -  -  + ]:         378 :          if (ImGui::Button("STOP LOG", ImVec2(80, 0))) {
     274                 :           0 :              Config::m_auto_start_logging = false;
     275         [ #  # ]:           0 :              AsyncLogger::Get().Stop();
     276   [ #  #  #  # ]:           0 :              Config::Save(engine);
     277                 :             :          }
     278   [ +  -  -  +  :         378 :          if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_STOP);
                   -  - ]
     279         [ +  - ]:         378 :          ImGui::SameLine();
     280   [ +  -  +  + ]:         378 :          if (AsyncLogger::Get().IsLogging()) {
     281         [ +  - ]:           3 :              float time = (float)ImGui::GetTime();
     282                 :           3 :              bool blink = (fmod(time, 1.0f) < 0.5f);
     283   [ +  +  +  - ]:           3 :              ImGui::TextColored(blink ? ImVec4(1, 0, 0, 1) : ImVec4(0.6f, 0, 0, 1), "REC");
     284   [ +  -  -  +  :           3 :              if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_REC);
                   -  - ]
     285                 :             : 
     286         [ +  - ]:           3 :              ImGui::SameLine();
     287         [ +  - ]:           3 :              size_t bytes = AsyncLogger::Get().GetFileSizeBytes();
     288         [ +  - ]:           3 :              if (bytes < 1024ULL * 1024ULL)
     289   [ +  -  +  - ]:           3 :                  ImGui::Text("%zu f (%.0f KB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / 1024.0f);
     290                 :             :              else
     291   [ #  #  #  # ]:           0 :                  ImGui::Text("%zu f (%.1f MB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / (1024.0f * 1024.0f));
     292                 :             : 
     293         [ +  - ]:           3 :              ImGui::SameLine();
     294   [ +  -  -  + ]:           3 :              if (ImGui::Button("MARKER")) {
     295         [ #  # ]:           0 :                  AsyncLogger::Get().SetMarker();
     296                 :             :              }
     297   [ +  -  -  +  :           3 :              if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_MARKER);
                   -  - ]
     298                 :             :          } else {
     299         [ +  - ]:         375 :              ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "ARMED");
     300   [ +  -  +  +  :         375 :              if (ImGui::IsItemHovered()) ImGui::SetTooltip("Waiting for driving to start...");
                   +  - ]
     301                 :             :          }
     302                 :             :     } else {
     303   [ #  #  #  # ]:           0 :          if (ImGui::Button("START LOGGING", ImVec2(120, 0))) {
     304                 :           0 :              Config::m_auto_start_logging = true;
     305   [ #  #  #  # ]:           0 :              Config::Save(engine);
     306                 :             :          }
     307   [ #  #  #  #  :           0 :          if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_START);
                   #  # ]
     308         [ #  # ]:           0 :          ImGui::SameLine();
     309         [ #  # ]:           0 :          ImGui::TextDisabled("(Diagnostics)");
     310                 :             :     }
     311                 :             : 
     312                 :             : 
     313         [ +  - ]:         378 :     ImGui::Separator();
     314                 :             : 
     315                 :             :     static int selected_preset = 0;
     316                 :             : 
     317                 :        4505 :     auto FormatDecoupled = [&](float val, float base_nm) {
     318                 :        4505 :         float estimated_nm = val * base_nm;
     319                 :             :         static char buf[64];
     320                 :        4505 :         StringUtils::SafeFormat(buf, sizeof(buf), "%.1f%%%% (~%.1f Nm)", val * 100.0f, estimated_nm);
     321                 :        4505 :         return (const char*)buf;
     322                 :             :     };
     323                 :             : 
     324                 :        3402 :     auto FormatPct = [&](float val) {
     325                 :             :         static char buf[32];
     326                 :        3402 :         StringUtils::SafeFormat(buf, sizeof(buf), "%.1f%%%%", val * 100.0f);
     327                 :        3402 :         return (const char*)buf;
     328                 :             :     };
     329                 :             : 
     330                 :       25887 :     auto FloatSetting = [&](const char* label, float* v, float min, float max, const char* fmt = "%.2f", const char* tooltip = nullptr, std::function<void()> decorator = nullptr) {
     331   [ +  -  +  - ]:       25887 :         GuiWidgets::Result res = GuiWidgets::Float(label, v, min, max, fmt, tooltip, decorator);
     332         [ -  + ]:       25887 :         if (res.deactivated) {
     333   [ #  #  #  # ]:           0 :             Config::Save(engine);
     334                 :             :         }
     335                 :       25887 :     };
     336                 :             : 
     337                 :        4536 :     auto BoolSetting = [&](const char* label, bool* v, const char* tooltip = nullptr) {
     338         [ +  - ]:        4536 :         GuiWidgets::Result res = GuiWidgets::Checkbox(label, v, tooltip);
     339         [ -  + ]:        4536 :         if (res.deactivated) {
     340   [ #  #  #  # ]:           0 :             Config::Save(engine);
     341                 :             :         }
     342                 :        4536 :     };
     343                 :             : 
     344                 :         756 :     auto IntSetting = [&](const char* label, int* v, const char* const items[], int items_count, const char* tooltip = nullptr) {
     345         [ +  - ]:         756 :         GuiWidgets::Result res = GuiWidgets::Combo(label, v, items, items_count, tooltip);
     346         [ -  + ]:         756 :         if (res.changed) {
     347         [ #  # ]:           0 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     348                 :             :             // v is already updated by ImGui, but we lock to ensure visibility and consistency
     349   [ #  #  #  # ]:           0 :             Config::Save(engine);
     350                 :           0 :         }
     351                 :         756 :     };
     352                 :             : 
     353   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Presets and Configuration", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     354   [ -  +  -  - ]:         378 :         if (Config::presets.empty()) Config::LoadPresets();
     355                 :             : 
     356                 :             :         static bool first_run = true;
     357   [ +  +  +  -  :         378 :         if (first_run && !Config::presets.empty()) {
                   +  + ]
     358         [ +  - ]:           1 :             for (int i = 0; i < (int)Config::presets.size(); i++) {
     359         [ +  - ]:           1 :                 if (Config::presets[i].name == Config::m_last_preset_name) {
     360                 :           1 :                     selected_preset = i;
     361                 :           1 :                     break;
     362                 :             :                 }
     363                 :             :             }
     364                 :           1 :             first_run = false;
     365                 :             :         }
     366                 :             : 
     367   [ +  +  +  - ]:         378 :         static std::string preview_buf;
     368                 :         378 :         const char* preview_value = "Custom";
     369   [ +  -  +  -  :         378 :         if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
                   +  - ]
     370         [ +  - ]:         378 :             preview_buf = Config::presets[selected_preset].name;
     371   [ +  -  +  + ]:         378 :             if (Config::IsEngineDirtyRelativeToPreset(selected_preset, engine)) {
     372         [ +  - ]:         370 :                 preview_buf += "*";
     373                 :             :             }
     374                 :         378 :             preview_value = preview_buf.c_str();
     375                 :             :         }
     376                 :             : 
     377   [ +  -  +  - ]:         378 :         ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f);
     378   [ +  -  -  + ]:         378 :         if (ImGui::BeginCombo("Load Preset", preview_value)) {
     379         [ #  # ]:           0 :             for (int i = 0; i < (int)Config::presets.size(); i++) {
     380                 :           0 :                 bool is_selected = (selected_preset == i);
     381         [ #  # ]:           0 :                 ImGui::PushID(i);
     382   [ #  #  #  # ]:           0 :                 if (ImGui::Selectable(Config::presets[i].name.c_str(), is_selected)) {
     383                 :           0 :                     selected_preset = i;
     384         [ #  # ]:           0 :                     Config::ApplyPreset(i, engine);
     385                 :             :                 }
     386   [ #  #  #  # ]:           0 :                 if (is_selected) ImGui::SetItemDefaultFocus();
     387         [ #  # ]:           0 :                 ImGui::PopID();
     388                 :             :             }
     389         [ #  # ]:           0 :             ImGui::EndCombo();
     390                 :             :         }
     391                 :             : 
     392                 :             :         static char new_preset_name[64] = "";
     393   [ +  -  +  - ]:         378 :         ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
     394         [ +  - ]:         378 :         ImGui::InputTextWithHint("##NewPresetName", "Enter Name...", new_preset_name, 64);
     395   [ +  -  +  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_NAME);
                   +  - ]
     396         [ +  - ]:         378 :         ImGui::SameLine();
     397                 :         378 :         bool name_empty = (strlen(new_preset_name) == 0);
     398   [ +  -  +  - ]:         378 :         if (name_empty) ImGui::BeginDisabled();
     399   [ +  -  -  + ]:         378 :         if (ImGui::Button("Save New")) {
     400   [ #  #  #  # ]:           0 :             Config::AddUserPreset(std::string(new_preset_name), engine);
     401         [ #  # ]:           0 :             for (int i = 0; i < (int)Config::presets.size(); i++) {
     402   [ #  #  #  # ]:           0 :                 if (Config::presets[i].name == std::string(new_preset_name)) {
     403                 :           0 :                     selected_preset = i;
     404                 :           0 :                     break;
     405                 :             :                 }
     406                 :             :             }
     407                 :           0 :             new_preset_name[0] = '\0';
     408                 :             :         }
     409   [ +  -  +  - ]:         378 :         if (name_empty) ImGui::EndDisabled();
     410   [ +  -  -  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_NEW);
                   -  - ]
     411                 :             : 
     412   [ +  -  -  + ]:         378 :         if (ImGui::Button("Save Current Config")) {
     413   [ #  #  #  #  :           0 :             if (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin) {
             #  #  #  # ]
     414         [ #  # ]:           0 :                 Config::AddUserPreset(Config::presets[selected_preset].name, engine);
     415                 :             :             } else {
     416   [ #  #  #  # ]:           0 :                 Config::Save(engine);
     417                 :             :             }
     418                 :             :         }
     419   [ +  -  +  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_CURRENT);
                   +  - ]
     420         [ +  - ]:         378 :         ImGui::SameLine();
     421   [ +  -  -  + ]:         378 :         if (ImGui::Button("Reset Defaults")) {
     422         [ #  # ]:           0 :             Config::ApplyPreset(0, engine);
     423                 :           0 :             selected_preset = 0;
     424                 :             :         }
     425   [ +  -  -  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_RESET);
                   -  - ]
     426         [ +  - ]:         378 :         ImGui::SameLine();
     427   [ +  -  -  + ]:         378 :         if (ImGui::Button("Duplicate")) {
     428         [ #  # ]:           0 :             if (selected_preset >= 0) {
     429         [ #  # ]:           0 :                 Config::DuplicatePreset(selected_preset, engine);
     430         [ #  # ]:           0 :                 for (int i = 0; i < (int)Config::presets.size(); i++) {
     431         [ #  # ]:           0 :                     if (Config::presets[i].name == Config::m_last_preset_name) {
     432                 :           0 :                         selected_preset = i;
     433                 :           0 :                         break;
     434                 :             :                     }
     435                 :             :                 }
     436                 :             :             }
     437                 :             :         }
     438   [ +  -  +  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DUPLICATE);
                   +  - ]
     439         [ +  - ]:         378 :         ImGui::SameLine();
     440   [ +  -  +  -  :         378 :         bool can_delete = (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin);
                   +  + ]
     441   [ +  +  +  - ]:         378 :         if (!can_delete) ImGui::BeginDisabled();
     442   [ +  -  -  + ]:         378 :         if (ImGui::Button("Delete")) {
     443         [ #  # ]:           0 :             ImGui::OpenPopup("Confirm Delete?");
     444                 :             :         }
     445   [ +  -  -  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DELETE);
                   -  - ]
     446   [ +  +  +  - ]:         378 :         if (!can_delete) ImGui::EndDisabled();
     447                 :             : 
     448   [ +  -  -  + ]:         378 :         if (ImGui::BeginPopupModal("Confirm Delete?", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
     449         [ #  # ]:           0 :             ImGui::Text("Are you sure you want to delete the preset:\n\"%s\"?", Config::presets[selected_preset].name.c_str());
     450         [ #  # ]:           0 :             ImGui::Separator();
     451                 :             : 
     452         [ #  # ]:           0 :             ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.1f, 0.1f, 1.0f));
     453         [ #  # ]:           0 :             ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
     454         [ #  # ]:           0 :             ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.6f, 0.0f, 0.0f, 1.0f));
     455   [ #  #  #  # ]:           0 :             if (ImGui::Button("Delete", ImVec2(120, 0))) {
     456         [ #  # ]:           0 :                 Config::DeletePreset(selected_preset, engine);
     457                 :           0 :                 selected_preset = 0;
     458         [ #  # ]:           0 :                 Config::ApplyPreset(0, engine);
     459         [ #  # ]:           0 :                 ImGui::CloseCurrentPopup();
     460                 :             :             }
     461         [ #  # ]:           0 :             ImGui::PopStyleColor(3);
     462         [ #  # ]:           0 :             ImGui::SetItemDefaultFocus();
     463         [ #  # ]:           0 :             ImGui::SameLine();
     464   [ #  #  #  # ]:           0 :             if (ImGui::Button("Cancel", ImVec2(120, 0))) {
     465         [ #  # ]:           0 :                 ImGui::CloseCurrentPopup();
     466                 :             :             }
     467         [ #  # ]:           0 :             ImGui::EndPopup();
     468                 :             :         }
     469                 :             : 
     470         [ +  - ]:         378 :         ImGui::Separator();
     471   [ +  -  -  + ]:         378 :         if (ImGui::Button("Import Preset...")) {
     472                 :           0 :             std::string path;
     473   [ #  #  #  # ]:           0 :             if (OpenPresetFileDialogPlatform(path)) {
     474   [ #  #  #  # ]:           0 :                 if (Config::ImportPreset(path, engine)) {
     475                 :           0 :                     selected_preset = (int)Config::presets.size() - 1;
     476                 :             :                 }
     477                 :             :             }
     478                 :           0 :         }
     479   [ +  -  +  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_IMPORT);
                   +  - ]
     480         [ +  - ]:         378 :         ImGui::SameLine();
     481   [ +  -  -  + ]:         378 :         if (ImGui::Button("Export Selected...")) {
     482   [ #  #  #  #  :           0 :             if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
                   #  # ]
     483                 :           0 :                 std::string path;
     484         [ #  # ]:           0 :                 std::string defaultName = Config::presets[selected_preset].name + ".ini";
     485   [ #  #  #  # ]:           0 :                 if (SavePresetFileDialogPlatform(path, defaultName)) {
     486         [ #  # ]:           0 :                     Config::ExportPreset(selected_preset, path);
     487                 :             :                 }
     488                 :           0 :             }
     489                 :             :         }
     490   [ +  -  -  +  :         378 :         if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_EXPORT);
                   -  - ]
     491                 :             : 
     492         [ +  - ]:         378 :         ImGui::TreePop();
     493                 :             :     }
     494                 :             : 
     495         [ +  - ]:         378 :     ImGui::Spacing();
     496                 :             : 
     497         [ +  - ]:         378 :     ImGui::Columns(2, "SettingsGrid", false);
     498   [ +  -  +  - ]:         378 :     ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.45f);
     499                 :             : 
     500   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("General FFB", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     501   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     502                 :             : 
     503         [ +  - ]:         378 :         ImGui::TextDisabled("Steering: %.1f° (%.0f)", m_latest_steering_angle, m_latest_steering_range);
     504   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     505                 :             : 
     506         [ +  - ]:         378 :         BoolSetting("Steerlock from REST API", &engine.m_rest_api_enabled, Tooltips::REST_API_ENABLE);
     507                 :             : 
     508         [ +  - ]:         378 :         ImGui::Spacing();
     509                 :         378 :         bool use_in_game_ffb = (engine.m_torque_source == 1);
     510   [ +  -  -  + ]:         378 :         if (GuiWidgets::Checkbox("Use In-Game FFB (400Hz Native)", &use_in_game_ffb, Tooltips::USE_INGAME_FFB).changed) {
     511         [ #  # ]:           0 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     512         [ #  # ]:           0 :             engine.m_torque_source = use_in_game_ffb ? 1 : 0;
     513   [ #  #  #  # ]:           0 :             Config::Save(engine);
     514                 :           0 :         }
     515                 :             : 
     516         [ +  - ]:         378 :         BoolSetting("Invert FFB Signal", &engine.m_invert_force, Tooltips::INVERT_FFB);
     517                 :             : 
     518                 :         378 :         bool prev_structural = engine.m_dynamic_normalization_enabled;
     519   [ +  -  -  + ]:         378 :         if (GuiWidgets::Checkbox("Enable Dynamic Normalization (Session Peak)", &engine.m_dynamic_normalization_enabled, Tooltips::DYNAMIC_NORMALIZATION_ENABLE).changed) {
     520   [ #  #  #  # ]:           0 :             if (prev_structural && !engine.m_dynamic_normalization_enabled) {
     521         [ #  # ]:           0 :                 engine.ResetNormalization();
     522                 :             :             }
     523   [ #  #  #  # ]:           0 :             Config::Save(engine);
     524                 :             :         }
     525         [ +  - ]:         378 :         FloatSetting("Master Gain", &engine.m_gain, 0.0f, 2.0f, FormatPct(engine.m_gain), Tooltips::MASTER_GAIN);
     526         [ +  - ]:         378 :         FloatSetting("Wheelbase Max Torque", &engine.m_wheelbase_max_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::WHEELBASE_MAX_TORQUE);
     527         [ +  - ]:         378 :         FloatSetting("Target Rim Torque", &engine.m_target_rim_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::TARGET_RIM_TORQUE);
     528         [ +  - ]:         378 :         FloatSetting("Min Force", &engine.m_min_force, 0.0f, 0.20f, "%.3f", Tooltips::MIN_FORCE);
     529                 :             : 
     530   [ +  -  +  - ]:         378 :         if (ImGui::TreeNodeEx("Soft Lock", ImGuiTreeNodeFlags_DefaultOpen)) {
     531   [ +  -  +  - ]:         378 :             ImGui::NextColumn(); ImGui::NextColumn();
     532         [ +  - ]:         378 :             BoolSetting("Enable Soft Lock", &engine.m_soft_lock_enabled, Tooltips::SOFT_LOCK_ENABLE);
     533         [ +  - ]:         378 :             if (engine.m_soft_lock_enabled) {
     534         [ +  - ]:         378 :                 FloatSetting("  Stiffness", &engine.m_soft_lock_stiffness, 0.0f, 100.0f, "%.1f", Tooltips::SOFT_LOCK_STIFFNESS);
     535         [ +  - ]:         378 :                 FloatSetting("  Damping", &engine.m_soft_lock_damping, 0.0f, 5.0f, "%.2f", Tooltips::SOFT_LOCK_DAMPING);
     536                 :             :             }
     537         [ +  - ]:         378 :             ImGui::TreePop();
     538         [ +  - ]:         378 :             ImGui::Separator();
     539                 :             :         }
     540                 :             : 
     541         [ +  - ]:         378 :         ImGui::TreePop();
     542                 :             :     } else {
     543   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     544                 :             :     }
     545                 :             : 
     546   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Front Axle (Understeer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     547   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     548                 :             : 
     549         [ +  + ]:         378 :         if (engine.m_torque_source == 1) {
     550         [ +  - ]:         166 :             FloatSetting("In-Game FFB Gain", &engine.m_ingame_ffb_gain, 0.0f, 2.0f, FormatPct(engine.m_ingame_ffb_gain), Tooltips::INGAME_FFB_GAIN);
     551                 :             :         } else {
     552         [ +  - ]:         212 :             FloatSetting("Steering Shaft Gain", &engine.m_steering_shaft_gain, 0.0f, 2.0f, FormatPct(engine.m_steering_shaft_gain), Tooltips::STEERING_SHAFT_GAIN);
     553                 :             :         }
     554                 :             : 
     555         [ +  - ]:         378 :         FloatSetting("Steering Shaft Smoothing", &engine.m_steering_shaft_smoothing, 0.000f, 0.100f, "%.3f s",
     556                 :             :             Tooltips::STEERING_SHAFT_SMOOTHING,
     557                 :         378 :             [&]() {
     558                 :         378 :                 int ms = (int)std::lround(engine.m_steering_shaft_smoothing * 1000.0f);
     559         [ +  - ]:         378 :                 ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
     560   [ +  -  +  - ]:         378 :                 ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
     561                 :         378 :             });
     562                 :             : 
     563         [ +  - ]:         378 :         FloatSetting("Understeer Effect", &engine.m_understeer_effect, 0.0f, 2.0f, FormatPct(engine.m_understeer_effect),
     564                 :             :             Tooltips::UNDERSTEER_EFFECT);
     565                 :             : 
     566         [ +  - ]:         378 :         FloatSetting("Response Curve (Gamma)", &engine.m_understeer_gamma, 0.1f, 4.0f, "%.1f",
     567                 :             :             Tooltips::UNDERSTEER_GAMMA);
     568                 :             : 
     569                 :         378 :         const char* torque_sources[] = { "Shaft Torque (100Hz Legacy)", "In-Game FFB (400Hz LMU 1.2+)" };
     570         [ +  - ]:         378 :         IntSetting("Torque Source", &engine.m_torque_source, torque_sources, sizeof(torque_sources)/sizeof(torque_sources[0]),
     571                 :             :             Tooltips::TORQUE_SOURCE);
     572                 :             : 
     573         [ +  - ]:         378 :         BoolSetting("Pure Passthrough", &engine.m_torque_passthrough, Tooltips::PURE_PASSTHROUGH);
     574                 :             : 
     575   [ +  -  +  - ]:         378 :         if (ImGui::TreeNodeEx("Signal Filtering", ImGuiTreeNodeFlags_DefaultOpen)) {
     576   [ +  -  +  - ]:         378 :             ImGui::NextColumn(); ImGui::NextColumn();
     577                 :             : 
     578         [ +  - ]:         378 :             BoolSetting("  Flatspot Suppression", &engine.m_flatspot_suppression, Tooltips::FLATSPOT_SUPPRESSION);
     579         [ +  + ]:         378 :             if (engine.m_flatspot_suppression) {
     580         [ +  - ]:         364 :                 FloatSetting("    Filter Width (Q)", &engine.m_notch_q, 0.5f, 10.0f, "Q: %.2f", Tooltips::NOTCH_Q);
     581         [ +  - ]:         364 :                 FloatSetting("    Suppression Strength", &engine.m_flatspot_strength, 0.0f, 1.0f, "%.2f", Tooltips::SUPPRESSION_STRENGTH);
     582         [ +  - ]:         364 :                 ImGui::Text("    Est. / Theory Freq");
     583         [ +  - ]:         364 :                 ImGui::NextColumn();
     584         [ +  - ]:         364 :                 ImGui::TextDisabled("%.1f Hz / %.1f Hz", engine.m_debug_freq, engine.m_theoretical_freq);
     585         [ +  - ]:         364 :                 ImGui::NextColumn();
     586                 :             :             }
     587                 :             : 
     588         [ +  - ]:         378 :             BoolSetting("  Static Noise Filter", &engine.m_static_notch_enabled, Tooltips::STATIC_NOISE_FILTER);
     589         [ +  + ]:         378 :             if (engine.m_static_notch_enabled) {
     590         [ +  - ]:         363 :                 FloatSetting("    Target Frequency", &engine.m_static_notch_freq, 10.0f, 100.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_FREQ);
     591         [ +  - ]:         363 :                 FloatSetting("    Filter Width", &engine.m_static_notch_width, 0.1f, 10.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_WIDTH);
     592                 :             :             }
     593                 :             : 
     594         [ +  - ]:         378 :             ImGui::TreePop();
     595                 :             :         } else {
     596   [ #  #  #  # ]:           0 :             ImGui::NextColumn(); ImGui::NextColumn();
     597                 :             :         }
     598                 :             : 
     599         [ +  - ]:         378 :         ImGui::TreePop();
     600                 :             :     } else {
     601   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     602                 :             :     }
     603                 :             : 
     604   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("FFB Safety Features", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     605   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     606                 :             : 
     607   [ +  -  +  - ]:         756 :         FloatSetting("Safety Duration", &engine.m_safety.m_safety_window_duration, 0.0f, 10.0f, "%.1f s", Tooltips::SAFETY_WINDOW_DURATION, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     608   [ +  -  +  - ]:         756 :         FloatSetting("Gain Reduction", &engine.m_safety.m_safety_gain_reduction, 0.0f, 1.0f, FormatPct(engine.m_safety.m_safety_gain_reduction), Tooltips::SAFETY_GAIN_REDUCTION, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     609   [ +  -  +  - ]:         756 :         FloatSetting("Safety Smoothing", &engine.m_safety.m_safety_smoothing_tau, 0.001f, 1.0f, "%.3f s", Tooltips::SAFETY_SMOOTHING_TAU, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     610   [ +  -  +  - ]:         756 :         FloatSetting("Slew Restriction", &engine.m_safety.m_safety_slew_full_scale_time_s, 0.1f, 5.0f, "%.2f s", Tooltips::SAFETY_SLEW_FULL_SCALE_TIME_S, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     611                 :             : 
     612         [ +  - ]:         378 :         ImGui::Separator();
     613         [ +  - ]:         378 :         ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Stuttering (Lost Frames)");
     614   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     615                 :             : 
     616         [ +  - ]:         378 :         BoolSetting("Safety on Stuttering", &engine.m_safety.m_stutter_safety_enabled, Tooltips::STUTTER_SAFETY_ENABLE);
     617         [ -  + ]:         378 :         if (engine.m_safety.m_stutter_safety_enabled) {
     618   [ #  #  #  # ]:           0 :             FloatSetting("Stutter Threshold", &engine.m_safety.m_stutter_threshold, 1.1f, 5.0f, "%.2fx", Tooltips::STUTTER_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     619                 :             :         }
     620                 :             : 
     621         [ +  - ]:         378 :         ImGui::Separator();
     622         [ +  - ]:         378 :         ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Spike Detection");
     623   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     624                 :             : 
     625   [ +  -  +  - ]:         756 :         FloatSetting("Spike Threshold", &engine.m_safety.m_spike_detection_threshold, 10.0f, 2000.0f, "%.0f u/s", Tooltips::SPIKE_DETECTION_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     626   [ +  -  +  - ]:         756 :         FloatSetting("Immediate Spike", &engine.m_safety.m_immediate_spike_threshold, 100.0f, 5000.0f, "%.0f u/s", Tooltips::IMMEDIATE_SPIKE_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
     627                 :             : 
     628         [ +  - ]:         378 :         ImGui::TreePop();
     629                 :             :     } else {
     630   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     631                 :             :     }
     632                 :             : 
     633   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Load Forces", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     634   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     635                 :             : 
     636         [ +  - ]:         378 :         FloatSetting("Lateral Load", &engine.m_lat_load_effect, 0.0f, 10.0f, FormatDecoupled(engine.m_lat_load_effect, FFBEngine::BASE_NM_SOP_LATERAL), Tooltips::LATERAL_LOAD);
     637                 :             : 
     638                 :         378 :         const char* load_transforms[] = { "Linear (Raw)", "Cubic (Smooth)", "Quadratic (Broad)", "Hermite (Locked Center)" };
     639                 :         378 :         int lat_transform = static_cast<int>(engine.m_lat_load_transform);
     640   [ +  -  -  + ]:         378 :         if (GuiWidgets::Combo("  Lateral Transform", &lat_transform, load_transforms, 4, "Mathematical transformation to soften the lateral load limits and remove 'notchiness'.").changed) {
     641         [ #  # ]:           0 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     642                 :           0 :             engine.m_lat_load_transform = static_cast<LoadTransform>(lat_transform);
     643   [ #  #  #  # ]:           0 :             Config::Save(engine);
     644                 :           0 :         }
     645                 :             : 
     646         [ +  - ]:         378 :         ImGui::Spacing();
     647                 :             : 
     648         [ +  - ]:         378 :         FloatSetting("Longitudinal G-Force", &engine.m_long_load_effect, 0.0f, 10.0f, FormatPct(engine.m_long_load_effect), Tooltips::DYNAMIC_WEIGHT);
     649         [ +  - ]:         378 :         FloatSetting("  G-Force Smoothing", &engine.m_long_load_smoothing, 0.000f, 0.500f, "%.3f s", Tooltips::WEIGHT_SMOOTHING);
     650                 :             : 
     651                 :         378 :         int long_transform = static_cast<int>(engine.m_long_load_transform);
     652   [ +  -  -  + ]:         378 :         if (GuiWidgets::Combo("  G-Force Transform", &long_transform, load_transforms, 4, "Mathematical transformation to soften the longitudinal load limits and remove 'notchiness'.").changed) {
     653         [ #  # ]:           0 :             std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
     654                 :           0 :             engine.m_long_load_transform = static_cast<LoadTransform>(long_transform);
     655   [ #  #  #  # ]:           0 :             Config::Save(engine);
     656                 :           0 :         }
     657                 :             : 
     658         [ +  - ]:         378 :         ImGui::TreePop();
     659                 :             :     } else {
     660   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     661                 :             :     }
     662                 :             : 
     663   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Rear Axle (Oversteer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     664   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     665                 :             : 
     666         [ +  - ]:         378 :         FloatSetting("Lateral G Boost (Slide)", &engine.m_oversteer_boost, 0.0f, 4.0f, FormatPct(engine.m_oversteer_boost),
     667                 :             :             Tooltips::OVERSTEER_BOOST);
     668         [ +  - ]:         378 :         FloatSetting("Lateral G", &engine.m_sop_effect, 0.0f, 2.0f, FormatDecoupled(engine.m_sop_effect, FFBEngine::BASE_NM_SOP_LATERAL), Tooltips::LATERAL_G);
     669                 :             : 
     670         [ +  - ]:         378 :         FloatSetting("SoP Self-Aligning Torque", &engine.m_rear_align_effect, 0.0f, 2.0f, FormatDecoupled(engine.m_rear_align_effect, FFBEngine::BASE_NM_REAR_ALIGN),
     671                 :             :             Tooltips::REAR_ALIGN_TORQUE);
     672         [ +  - ]:         378 :         FloatSetting("  Kerb Strike Rejection", &engine.m_kerb_strike_rejection, 0.0f, 1.0f, FormatPct(engine.m_kerb_strike_rejection),
     673                 :             :             Tooltips::KERB_STRIKE_REJECTION);
     674         [ +  - ]:         378 :         FloatSetting("Yaw Kick", &engine.m_sop_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_sop_yaw_gain, FFBEngine::BASE_NM_YAW_KICK),
     675                 :             :             Tooltips::YAW_KICK);
     676         [ +  - ]:         378 :         FloatSetting("  Activation Threshold", &engine.m_yaw_kick_threshold, 0.0f, 10.0f, "%.2f rad/s²", Tooltips::YAW_KICK_THRESHOLD);
     677                 :             : 
     678         [ +  - ]:         378 :         FloatSetting("  Kick Response", &engine.m_yaw_accel_smoothing, 0.000f, 0.050f, "%.3f s",
     679                 :             :             Tooltips::YAW_KICK_RESPONSE,
     680                 :         378 :             [&]() {
     681                 :         378 :                 int ms = (int)std::lround(engine.m_yaw_accel_smoothing * 1000.0f);
     682         [ +  - ]:         378 :                 ImVec4 color = (ms <= 15) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
     683         [ +  - ]:         378 :                 ImGui::TextColored(color, "Latency: %d ms", ms);
     684                 :         378 :             });
     685                 :             : 
     686   [ +  -  +  - ]:         378 :         if (ImGui::TreeNodeEx("Unloaded Yaw Kick (Braking)", ImGuiTreeNodeFlags_DefaultOpen)) {
     687   [ +  -  +  - ]:         378 :             ImGui::NextColumn(); ImGui::NextColumn();
     688         [ +  - ]:         378 :             FloatSetting("  Gain", &engine.m_unloaded_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_unloaded_yaw_gain, FFBEngine::BASE_NM_YAW_KICK), Tooltips::UNLOADED_YAW_GAIN);
     689         [ +  - ]:         378 :             FloatSetting("  Threshold", &engine.m_unloaded_yaw_threshold, 0.0f, 2.0f, "%.2f rad/s²", Tooltips::UNLOADED_YAW_THRESHOLD);
     690         [ +  - ]:         378 :             FloatSetting("  Unload Sens.", &engine.m_unloaded_yaw_sens, 0.1f, 5.0f, "%.1fx", Tooltips::UNLOADED_YAW_SENS);
     691         [ +  - ]:         378 :             FloatSetting("  Gamma", &engine.m_unloaded_yaw_gamma, 0.1f, 2.0f, "%.1f", Tooltips::UNLOADED_YAW_GAMMA);
     692         [ +  - ]:         378 :             FloatSetting("  Punch (Jerk)", &engine.m_unloaded_yaw_punch, 0.0f, 0.2f, "%.2fx", Tooltips::UNLOADED_YAW_PUNCH);
     693         [ +  - ]:         378 :             ImGui::TreePop();
     694                 :             :         } else {
     695   [ #  #  #  # ]:           0 :             ImGui::NextColumn(); ImGui::NextColumn();
     696                 :             :         }
     697                 :             : 
     698   [ +  -  +  - ]:         378 :         if (ImGui::TreeNodeEx("Power Yaw Kick (Acceleration)", ImGuiTreeNodeFlags_DefaultOpen)) {
     699   [ +  -  +  - ]:         378 :             ImGui::NextColumn(); ImGui::NextColumn();
     700         [ +  - ]:         378 :             FloatSetting("  Gain", &engine.m_power_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_power_yaw_gain, FFBEngine::BASE_NM_YAW_KICK), Tooltips::POWER_YAW_GAIN);
     701         [ +  - ]:         378 :             FloatSetting("  Threshold", &engine.m_power_yaw_threshold, 0.0f, 2.0f, "%.2f rad/s²", Tooltips::POWER_YAW_THRESHOLD);
     702         [ +  - ]:         378 :             FloatSetting("  TC Slip Target", &engine.m_power_slip_threshold, 0.01f, 0.5f, FormatPct(engine.m_power_slip_threshold), Tooltips::POWER_SLIP_THRESHOLD);
     703         [ +  - ]:         378 :             FloatSetting("  Gamma", &engine.m_power_yaw_gamma, 0.1f, 2.0f, "%.1f", Tooltips::POWER_YAW_GAMMA);
     704         [ +  - ]:         378 :             FloatSetting("  Punch (Jerk)", &engine.m_power_yaw_punch, 0.0f, 0.2f, "%.2fx", Tooltips::POWER_YAW_PUNCH);
     705         [ +  - ]:         378 :             ImGui::TreePop();
     706                 :             :         } else {
     707   [ #  #  #  # ]:           0 :             ImGui::NextColumn(); ImGui::NextColumn();
     708                 :             :         }
     709                 :             : 
     710         [ +  - ]:         378 :         FloatSetting("Gyro Damping", &engine.m_gyro_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_gyro_gain, FFBEngine::BASE_NM_GYRO_DAMPING), Tooltips::GYRO_DAMPING);
     711                 :             : 
     712         [ +  - ]:         378 :         FloatSetting("  Gyro Smooth", &engine.m_gyro_smoothing, 0.000f, 0.050f, "%.3f s",
     713                 :             :             Tooltips::GYRO_SMOOTH,
     714                 :         378 :             [&]() {
     715                 :         378 :                 int ms = (int)std::lround(engine.m_gyro_smoothing * 1000.0f);
     716         [ +  - ]:         378 :                 ImVec4 color = (ms <= 20) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
     717         [ +  - ]:         378 :                 ImGui::TextColored(color, "Latency: %d ms", ms);
     718                 :         378 :             });
     719                 :             : 
     720         [ +  - ]:         378 :         ImGui::TextColored(ImVec4(0.0f, 0.6f, 0.85f, 1.0f), "Advanced SoP");
     721   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     722                 :             : 
     723         [ +  - ]:         378 :         FloatSetting("SoP Smoothing", &engine.m_sop_smoothing_factor, 0.0f, 1.0f, "%.2f",
     724                 :             :             Tooltips::SOP_SMOOTHING,
     725                 :         378 :             [&]() {
     726                 :         378 :                 int ms = (int)std::lround(engine.m_sop_smoothing_factor * 100.0f);
     727         [ +  - ]:         378 :                 ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
     728   [ +  -  +  - ]:         378 :                 ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
     729                 :         378 :             });
     730                 :             : 
     731         [ +  - ]:         378 :         FloatSetting("Grip Smoothing", &engine.m_grip_smoothing_steady, 0.000f, 0.100f, "%.3f s",
     732                 :             :             Tooltips::GRIP_SMOOTHING);
     733                 :             : 
     734         [ +  - ]:         378 :         FloatSetting("  SoP Scale", &engine.m_sop_scale, 0.0f, 20.0f, "%.2f", Tooltips::SOP_SCALE);
     735                 :             : 
     736         [ +  - ]:         378 :         ImGui::TreePop();
     737                 :             :     } else {
     738   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     739                 :             :     }
     740                 :             : 
     741   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Grip & Slip Angle Estimation", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     742   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     743                 :             : 
     744         [ +  - ]:         378 :         FloatSetting("Slip Angle Smoothing", &engine.m_slip_angle_smoothing, 0.000f, 0.100f, "%.3f s",
     745                 :             :             Tooltips::SLIP_ANGLE_SMOOTHING,
     746                 :         378 :             [&]() {
     747                 :         378 :                 int ms = (int)std::lround(engine.m_slip_angle_smoothing * 1000.0f);
     748         [ +  - ]:         378 :                 ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
     749   [ +  -  +  - ]:         378 :                 ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
     750                 :         378 :             });
     751                 :             : 
     752         [ +  - ]:         378 :         FloatSetting("Chassis Inertia (Load)", &engine.m_chassis_inertia_smoothing, 0.000f, 0.100f, "%.3f s",
     753                 :             :             Tooltips::CHASSIS_INERTIA,
     754                 :         378 :             [&]() {
     755                 :         378 :                 int ms = (int)std::lround(engine.m_chassis_inertia_smoothing * 1000.0f);
     756         [ +  - ]:         378 :                 ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), "Simulation: %d ms", ms);
     757                 :         378 :             });
     758                 :             : 
     759         [ +  - ]:         378 :         FloatSetting("Optimal Slip Angle", &engine.m_optimal_slip_angle, 0.040f, 0.200f, "%.3f rad",
     760                 :             :             Tooltips::OPTIMAL_SLIP_ANGLE);
     761         [ +  - ]:         378 :         FloatSetting("Optimal Slip Ratio", &engine.m_optimal_slip_ratio, 0.04f, 0.20f, "%.3f",
     762                 :             :             Tooltips::OPTIMAL_SLIP_RATIO);
     763                 :             : 
     764         [ +  - ]:         378 :         ImGui::Separator();
     765         [ +  - ]:         378 :         ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Slope Detection (Experimental)");
     766   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     767                 :             : 
     768                 :         378 :         bool prev_slope_enabled = engine.m_slope_detection_enabled;
     769         [ +  - ]:         378 :         GuiWidgets::Result slope_res = GuiWidgets::Checkbox("Enable Slope Detection", &engine.m_slope_detection_enabled,
     770                 :             :             Tooltips::SLOPE_DETECTION_ENABLE);
     771                 :             : 
     772         [ -  + ]:         378 :         if (slope_res.changed) {
     773   [ #  #  #  # ]:           0 :             if (!prev_slope_enabled && engine.m_slope_detection_enabled) {
     774                 :           0 :                 engine.m_slope_buffer_count = 0;
     775                 :           0 :                 engine.m_slope_buffer_index = 0;
     776                 :           0 :                 engine.m_slope_smoothed_output = 1.0;
     777                 :             :             }
     778                 :             :         }
     779         [ -  + ]:         378 :         if (slope_res.deactivated) {
     780   [ #  #  #  # ]:           0 :             Config::Save(engine);
     781                 :             :         }
     782                 :             : 
     783   [ +  +  +  + ]:         378 :         if (engine.m_slope_detection_enabled && engine.m_oversteer_boost > 0.01f) {
     784         [ +  - ]:         362 :             ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
     785                 :             :                 "Note: Lateral G Boost (Slide) is auto-disabled when Slope Detection is ON.");
     786   [ +  -  +  - ]:         362 :             ImGui::NextColumn(); ImGui::NextColumn();
     787                 :             :         }
     788                 :             : 
     789         [ +  + ]:         378 :         if (engine.m_slope_detection_enabled) {
     790                 :         363 :             int window = engine.m_slope_sg_window;
     791   [ +  -  -  + ]:         363 :             if (ImGui::SliderInt("  Filter Window", &window, 5, 41)) {
     792         [ #  # ]:           0 :                 if (window % 2 == 0) window++;
     793                 :           0 :                 engine.m_slope_sg_window = window;
     794                 :             :             }
     795   [ +  -  -  + ]:         363 :             if (ImGui::IsItemHovered()) {
     796         [ #  # ]:           0 :                 ImGui::SetTooltip("%s", Tooltips::SLOPE_FILTER_WINDOW);
     797                 :             :             }
     798   [ +  -  -  +  :         363 :             if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
             -  -  -  - ]
     799                 :             : 
     800         [ +  - ]:         363 :             ImGui::SameLine();
     801                 :         363 :             float latency_ms = (static_cast<float>(engine.m_slope_sg_window) / 2.0f) * 2.5f;
     802         [ +  - ]:         363 :             ImVec4 color = (latency_ms < 25.0f) ? ImVec4(0,1,0,1) : ImVec4(1,0.5f,0,1);
     803         [ +  - ]:         363 :             ImGui::TextColored(color, "~%.0f ms latency", latency_ms);
     804   [ +  -  +  - ]:         363 :             ImGui::NextColumn(); ImGui::NextColumn();
     805                 :             : 
     806         [ +  - ]:         363 :             FloatSetting("  Sensitivity", &engine.m_slope_sensitivity, 0.1f, 5.0f, "%.1fx",
     807                 :             :                 Tooltips::SLOPE_SENSITIVITY);
     808                 :             : 
     809   [ +  -  -  + ]:         363 :             if (ImGui::TreeNode("Advanced Slope Settings")) {
     810   [ #  #  #  # ]:           0 :                 ImGui::NextColumn(); ImGui::NextColumn();
     811         [ #  # ]:           0 :                 FloatSetting("  Slope Threshold", &engine.m_slope_min_threshold, -1.0f, 0.0f, "%.2f", Tooltips::SLOPE_THRESHOLD);
     812         [ #  # ]:           0 :                 FloatSetting("  Output Smoothing", &engine.m_slope_smoothing_tau, 0.005f, 0.100f, "%.3f s", Tooltips::SLOPE_OUTPUT_SMOOTHING);
     813                 :             : 
     814         [ #  # ]:           0 :                 ImGui::Separator();
     815         [ #  # ]:           0 :                 ImGui::Text("Stability Fixes (v0.7.3)");
     816   [ #  #  #  # ]:           0 :                 ImGui::NextColumn(); ImGui::NextColumn();
     817         [ #  # ]:           0 :                 FloatSetting("  Alpha Threshold", &engine.m_slope_alpha_threshold, 0.001f, 0.100f, "%.3f", Tooltips::SLOPE_ALPHA_THRESHOLD);
     818         [ #  # ]:           0 :                 FloatSetting("  Decay Rate", &engine.m_slope_decay_rate, 0.5f, 20.0f, "%.1f", Tooltips::SLOPE_DECAY_RATE);
     819         [ #  # ]:           0 :                 BoolSetting("  Confidence Gate", &engine.m_slope_confidence_enabled, Tooltips::SLOPE_CONFIDENCE_GATE);
     820                 :             : 
     821         [ #  # ]:           0 :                 ImGui::TreePop();
     822                 :             :             } else {
     823   [ +  -  +  - ]:         363 :                 ImGui::NextColumn(); ImGui::NextColumn();
     824                 :             :             }
     825                 :             : 
     826                 :         363 :             ImGui::Text("  Live Slope: %.3f | Grip: %.0f%%",
     827                 :             :                 engine.m_slope_current,
     828         [ +  - ]:         363 :                 engine.m_slope_smoothed_output * 100.0f);
     829   [ +  -  +  - ]:         363 :             ImGui::NextColumn(); ImGui::NextColumn();
     830                 :             :         }
     831                 :             : 
     832         [ +  - ]:         378 :         ImGui::TreePop();
     833                 :             :     } else {
     834   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     835                 :             :     }
     836                 :             : 
     837   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Braking & Lockup", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     838   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     839                 :             : 
     840         [ +  - ]:         378 :         BoolSetting("Lockup Vibration", &engine.m_lockup_enabled, Tooltips::LOCKUP_VIBRATION);
     841         [ +  + ]:         378 :         if (engine.m_lockup_enabled) {
     842         [ +  - ]:         374 :             FloatSetting("  Lockup Strength", &engine.m_lockup_gain, 0.0f, 3.0f, FormatDecoupled(engine.m_lockup_gain, FFBEngine::BASE_NM_LOCKUP_VIBRATION), Tooltips::LOCKUP_STRENGTH);
     843         [ +  - ]:         374 :             FloatSetting("  Brake Load Cap", &engine.m_brake_load_cap, 1.0f, 10.0f, "%.2fx", Tooltips::BRAKE_LOAD_CAP);
     844         [ +  - ]:         374 :             FloatSetting("  Vibration Pitch", &engine.m_lockup_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::VIBRATION_PITCH);
     845                 :             : 
     846         [ +  - ]:         374 :             ImGui::Separator();
     847         [ +  - ]:         374 :             ImGui::Text("Response Curve");
     848   [ +  -  +  - ]:         374 :             ImGui::NextColumn(); ImGui::NextColumn();
     849                 :             : 
     850         [ +  - ]:         374 :             FloatSetting("  Gamma", &engine.m_lockup_gamma, 0.1f, 3.0f, "%.1f", Tooltips::LOCKUP_GAMMA);
     851         [ +  - ]:         374 :             FloatSetting("  Start Slip %", &engine.m_lockup_start_pct, 1.0f, 10.0f, "%.1f%%", Tooltips::LOCKUP_START_PCT);
     852         [ +  - ]:         374 :             FloatSetting("  Full Slip %", &engine.m_lockup_full_pct, 5.0f, 25.0f, "%.1f%%", Tooltips::LOCKUP_FULL_PCT);
     853                 :             : 
     854         [ +  - ]:         374 :             ImGui::Separator();
     855         [ +  - ]:         374 :             ImGui::Text("Prediction (Advanced)");
     856   [ +  -  +  - ]:         374 :             ImGui::NextColumn(); ImGui::NextColumn();
     857                 :             : 
     858         [ +  - ]:         374 :             FloatSetting("  Sensitivity", &engine.m_lockup_prediction_sens, 10.0f, 100.0f, "%.0f", Tooltips::LOCKUP_PREDICTION_SENS);
     859         [ +  - ]:         374 :             FloatSetting("  Bump Rejection", &engine.m_lockup_bump_reject, 0.1f, 5.0f, "%.1f m/s", Tooltips::LOCKUP_BUMP_REJECT);
     860         [ +  - ]:         374 :             FloatSetting("  Rear Boost", &engine.m_lockup_rear_boost, 1.0f, 10.0f, "%.2fx", Tooltips::LOCKUP_REAR_BOOST);
     861                 :             :         }
     862                 :             : 
     863         [ +  - ]:         378 :         ImGui::Separator();
     864         [ +  - ]:         378 :         ImGui::Text("ABS & Hardware");
     865   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     866                 :             : 
     867         [ +  - ]:         378 :         BoolSetting("ABS Pulse", &engine.m_abs_pulse_enabled, Tooltips::ABS_PULSE);
     868         [ +  + ]:         378 :         if (engine.m_abs_pulse_enabled) {
     869         [ +  - ]:         360 :             FloatSetting("  Pulse Gain", &engine.m_abs_gain, 0.0f, 10.0f, "%.2f", Tooltips::ABS_PULSE_GAIN);
     870         [ +  - ]:         360 :             FloatSetting("  Pulse Frequency", &engine.m_abs_freq_hz, 10.0f, 50.0f, "%.1f Hz", Tooltips::ABS_PULSE_FREQ);
     871                 :             :         }
     872                 :             : 
     873         [ +  - ]:         378 :         ImGui::TreePop();
     874                 :             :     } else {
     875   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     876                 :             :     }
     877                 :             : 
     878   [ +  -  +  - ]:         378 :     if (ImGui::TreeNodeEx("Vibration Effects", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
     879   [ +  -  +  - ]:         378 :         ImGui::NextColumn(); ImGui::NextColumn();
     880                 :             : 
     881                 :         378 :         bool prev_vibration_norm = engine.m_auto_load_normalization_enabled;
     882   [ +  -  -  + ]:         378 :         if (GuiWidgets::Checkbox("Enable Dynamic Load Normalization", &engine.m_auto_load_normalization_enabled, Tooltips::DYNAMIC_LOAD_NORMALIZATION_ENABLE).changed) {
     883   [ #  #  #  # ]:           0 :             if (prev_vibration_norm && !engine.m_auto_load_normalization_enabled) {
     884         [ #  # ]:           0 :                 engine.ResetNormalization();
     885                 :             :             }
     886   [ #  #  #  # ]:           0 :             Config::Save(engine);
     887                 :             :         }
     888                 :             : 
     889         [ +  - ]:         378 :         FloatSetting("Texture Load Cap", &engine.m_texture_load_cap, 1.0f, 3.0f, "%.2fx", Tooltips::TEXTURE_LOAD_CAP);
     890         [ +  - ]:         378 :         FloatSetting("Vibration Strength", &engine.m_vibration_gain, 0.0f, 2.0f, FormatPct(engine.m_vibration_gain), Tooltips::VIBRATION_GAIN);
     891                 :             : 
     892         [ +  - ]:         378 :         BoolSetting("Slide Rumble", &engine.m_slide_texture_enabled, Tooltips::SLIDE_RUMBLE);
     893         [ +  + ]:         378 :         if (engine.m_slide_texture_enabled) {
     894         [ +  - ]:         359 :             FloatSetting("  Slide Gain", &engine.m_slide_texture_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_slide_texture_gain, FFBEngine::BASE_NM_SLIDE_TEXTURE), Tooltips::SLIDE_GAIN);
     895         [ +  - ]:         359 :             FloatSetting("  Slide Pitch", &engine.m_slide_freq_scale, 0.5f, 5.0f, "%.2fx", Tooltips::SLIDE_PITCH);
     896                 :             :         }
     897                 :             : 
     898         [ +  - ]:         378 :         BoolSetting("Road Details", &engine.m_road_texture_enabled, Tooltips::ROAD_DETAILS);
     899         [ +  + ]:         378 :         if (engine.m_road_texture_enabled) {
     900         [ +  - ]:         374 :             FloatSetting("  Road Gain", &engine.m_road_texture_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_road_texture_gain, FFBEngine::BASE_NM_ROAD_TEXTURE), Tooltips::ROAD_GAIN);
     901                 :             :         }
     902                 :             : 
     903         [ +  - ]:         378 :         BoolSetting("Spin Vibration", &engine.m_spin_enabled, Tooltips::SPIN_VIBRATION);
     904         [ +  + ]:         378 :         if (engine.m_spin_enabled) {
     905         [ +  - ]:         374 :             FloatSetting("  Spin Strength", &engine.m_spin_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_spin_gain, FFBEngine::BASE_NM_SPIN_VIBRATION), Tooltips::SPIN_STRENGTH);
     906         [ +  - ]:         374 :             FloatSetting("  Spin Pitch", &engine.m_spin_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::SPIN_PITCH);
     907                 :             :         }
     908                 :             : 
     909         [ +  - ]:         378 :         FloatSetting("Scrub Drag", &engine.m_scrub_drag_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_scrub_drag_gain, FFBEngine::BASE_NM_SCRUB_DRAG), Tooltips::SCRUB_DRAG);
     910                 :             : 
     911                 :         378 :         const char* bottoming_modes[] = { "Method A: Scraping", "Method B: Susp. Spike" };
     912         [ +  - ]:         378 :         IntSetting("Bottoming Logic", &engine.m_bottoming_method, bottoming_modes, sizeof(bottoming_modes)/sizeof(bottoming_modes[0]), Tooltips::BOTTOMING_LOGIC);
     913                 :             : 
     914         [ +  - ]:         378 :         ImGui::TreePop();
     915                 :             :     } else {
     916   [ #  #  #  # ]:           0 :         ImGui::NextColumn(); ImGui::NextColumn();
     917                 :             :     }
     918                 :             : 
     919   [ +  -  -  + ]:         378 :     if (ImGui::CollapsingHeader("Advanced Settings")) {
     920         [ #  # ]:           0 :         ImGui::Indent();
     921                 :             : 
     922   [ #  #  #  # ]:           0 :         if (ImGui::TreeNode("Stationary Vibration Gate")) {
     923                 :           0 :             float lower_kmh = engine.m_speed_gate_lower * 3.6f;
     924   [ #  #  #  # ]:           0 :             if (ImGui::SliderFloat("Mute Below", &lower_kmh, 0.0f, 20.0f, "%.1f km/h")) {
     925                 :           0 :                 engine.m_speed_gate_lower = lower_kmh / 3.6f;
     926         [ #  # ]:           0 :                 if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
     927                 :           0 :                     engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
     928                 :             :             }
     929   [ #  #  #  #  :           0 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MUTE_BELOW);
                   #  # ]
     930   [ #  #  #  #  :           0 :             if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
             #  #  #  # ]
     931                 :             : 
     932                 :           0 :             float upper_kmh = engine.m_speed_gate_upper * 3.6f;
     933   [ #  #  #  # ]:           0 :             if (ImGui::SliderFloat("Full Above", &upper_kmh, 1.0f, 50.0f, "%.1f km/h")) {
     934                 :           0 :                 engine.m_speed_gate_upper = upper_kmh / 3.6f;
     935         [ #  # ]:           0 :                 if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
     936                 :           0 :                     engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
     937                 :             :             }
     938   [ #  #  #  #  :           0 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::FULL_ABOVE);
                   #  # ]
     939   [ #  #  #  #  :           0 :             if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
             #  #  #  # ]
     940                 :             : 
     941         [ #  # ]:           0 :             ImGui::TreePop();
     942                 :             :         }
     943                 :             : 
     944   [ #  #  #  # ]:           0 :         if (ImGui::TreeNode("Telemetry Logger")) {
     945   [ #  #  #  # ]:           0 :             if (ImGui::Checkbox("Enable Logging Logic", &Config::m_auto_start_logging)) {
     946   [ #  #  #  # ]:           0 :                 Config::Save(engine);
     947                 :             :             }
     948   [ #  #  #  #  :           0 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::AUTO_START_LOGGING);
                   #  # ]
     949                 :             : 
     950                 :             :             char log_path_buf[256];
     951                 :           0 :             StringUtils::SafeCopy(log_path_buf, sizeof(log_path_buf), Config::m_log_path.c_str());
     952   [ #  #  #  # ]:           0 :             if (ImGui::InputText("Log Path", log_path_buf, 255)) {
     953         [ #  # ]:           0 :                 Config::m_log_path = log_path_buf;
     954                 :             :             }
     955   [ #  #  #  #  :           0 :             if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_PATH);
                   #  # ]
     956   [ #  #  #  #  :           0 :             if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
             #  #  #  # ]
     957                 :             : 
     958   [ #  #  #  # ]:           0 :             if (AsyncLogger::Get().IsLogging()) {
     959   [ #  #  #  #  :           0 :                 ImGui::BulletText("Filename: %s", AsyncLogger::Get().GetFilename().c_str());
                   #  # ]
     960                 :             :             }
     961                 :             : 
     962         [ #  # ]:           0 :             ImGui::TreePop();
     963                 :             :         }
     964         [ #  # ]:           0 :         ImGui::Unindent();
     965                 :             :     }
     966                 :             : 
     967         [ +  - ]:         378 :     ImGui::Columns(1);
     968         [ +  - ]:         378 :     ImGui::End();
     969                 :         378 : }
     970                 :             : 
     971                 :             : const float PLOT_HISTORY_SEC = 10.0f;
     972                 :             : const int PHYSICS_RATE_HZ = 400;
     973                 :             : const int PLOT_BUFFER_SIZE = (int)(PLOT_HISTORY_SEC * PHYSICS_RATE_HZ);
     974                 :             : 
     975                 :             : struct RollingBuffer {
     976                 :             :     std::vector<float> data;
     977                 :             :     int offset = 0;
     978                 :             : 
     979                 :          46 :     RollingBuffer() {
     980         [ +  - ]:          46 :         data.resize(PLOT_BUFFER_SIZE, 0.0f);
     981                 :          46 :     }
     982                 :             : 
     983                 :           0 :     void Add(float val) {
     984                 :           0 :         data[offset] = val;
     985                 :           0 :         offset = (offset + 1) % (int)data.size();
     986                 :           0 :     }
     987                 :             : 
     988                 :         992 :     float GetCurrent() const {
     989         [ -  + ]:         992 :         if (data.empty()) return 0.0f;
     990                 :         992 :         size_t idx = (offset - 1 + (int)data.size()) % (int)data.size();
     991                 :         992 :         return data[idx];
     992                 :             :     }
     993                 :             : 
     994                 :         992 :     float GetMin() const {
     995         [ -  + ]:         992 :         if (data.empty()) return 0.0f;
     996         [ +  - ]:         992 :         return *std::min_element(data.begin(), data.end());
     997                 :             :     }
     998                 :             : 
     999                 :         992 :     float GetMax() const {
    1000         [ -  + ]:         992 :         if (data.empty()) return 0.0f;
    1001         [ +  - ]:         992 :         return *std::max_element(data.begin(), data.end());
    1002                 :             :     }
    1003                 :             : };
    1004                 :             : 
    1005                 :         992 : inline void PlotWithStats(const char* label, const RollingBuffer& buffer,
    1006                 :             :                           float scale_min, float scale_max,
    1007                 :             :                           const ImVec2& size = ImVec2(0, 40),
    1008                 :             :                           const char* tooltip = nullptr) {
    1009         [ +  - ]:         992 :     ImGui::Text("%s", label);
    1010                 :             :     char hidden_label[256];
    1011                 :         992 :     StringUtils::SafeFormat(hidden_label, sizeof(hidden_label), "##%s", label);
    1012         [ +  - ]:         992 :     ImGui::PlotLines(hidden_label, buffer.data.data(), (int)buffer.data.size(),
    1013                 :         992 :                      buffer.offset, NULL, scale_min, scale_max, size);
    1014   [ -  +  -  -  :         992 :     if (tooltip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
          -  -  -  +  -  
                      - ]
    1015                 :             : 
    1016                 :         992 :     float current = buffer.GetCurrent();
    1017         [ +  - ]:         992 :     float min_val = buffer.GetMin();
    1018         [ +  - ]:         992 :     float max_val = buffer.GetMax();
    1019                 :             :     char stats_overlay[128];
    1020                 :         992 :     StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "Cur:%.4f Min:%.3f Max:%.3f", current, min_val, max_val);
    1021                 :             : 
    1022         [ +  - ]:         992 :     ImVec2 p_min = ImGui::GetItemRectMin();
    1023         [ +  - ]:         992 :     ImVec2 p_max = ImGui::GetItemRectMax();
    1024                 :         992 :     float plot_width = p_max.x - p_min.x;
    1025                 :         992 :     p_min.x += 2; p_min.y += 2;
    1026                 :             : 
    1027         [ +  - ]:         992 :     ImDrawList* draw_list = ImGui::GetWindowDrawList();
    1028         [ +  - ]:         992 :     ImFont* font = ImGui::GetFont();
    1029         [ +  - ]:         992 :     float font_size = ImGui::GetFontSize();
    1030         [ +  - ]:         992 :     ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
    1031                 :             : 
    1032         [ -  + ]:         992 :     if (text_size.x > plot_width - 4) {
    1033                 :           0 :          StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "%.4f [%.3f, %.3f]", current, min_val, max_val);
    1034         [ #  # ]:           0 :          text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
    1035         [ #  # ]:           0 :          if (text_size.x > plot_width - 4) {
    1036                 :           0 :              StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "Val: %.4f", current);
    1037         [ #  # ]:           0 :              text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
    1038                 :             :          }
    1039                 :             :     }
    1040         [ +  - ]:         992 :     draw_list->AddRectFilled(ImVec2(p_min.x - 1, p_min.y), ImVec2(p_min.x + text_size.x + 2, p_min.y + text_size.y), IM_COL32(0, 0, 0, 90));
    1041         [ +  - ]:         992 :     draw_list->AddText(font, font_size, p_min, IM_COL32(255, 255, 255, 255), stats_overlay);
    1042                 :         992 : }
    1043                 :             : 
    1044                 :             : // Global Buffers
    1045                 :             : static RollingBuffer plot_total, plot_base, plot_sop, plot_yaw_kick, plot_rear_torque, plot_gyro_damping, plot_scrub_drag, plot_soft_lock, plot_oversteer, plot_understeer, plot_clipping, plot_road, plot_slide, plot_lockup, plot_spin, plot_bottoming;
    1046                 :             : static RollingBuffer plot_calc_front_load, plot_calc_rear_load, plot_calc_front_grip, plot_calc_rear_grip, plot_calc_slip_ratio, plot_calc_slip_angle_smoothed, plot_calc_rear_slip_angle_smoothed, plot_slope_current, plot_calc_rear_lat_force;
    1047                 :             : static RollingBuffer plot_raw_steer, plot_raw_shaft_torque, plot_raw_gen_torque, plot_raw_input_steering, plot_raw_throttle, plot_raw_brake, plot_input_accel, plot_raw_car_speed, plot_raw_load, plot_raw_grip, plot_raw_rear_grip, plot_raw_front_slip_ratio, plot_raw_susp_force, plot_raw_ride_height, plot_raw_front_lat_patch_vel, plot_raw_front_long_patch_vel, plot_raw_rear_lat_patch_vel, plot_raw_rear_long_patch_vel, plot_raw_slip_angle, plot_raw_rear_slip_angle, plot_raw_front_deflection;
    1048                 :             : 
    1049                 :             : static bool g_warn_dt = false;
    1050                 :             : 
    1051                 :           0 : void GuiLayer::UpdateTelemetry(FFBEngine& engine) {
    1052         [ #  # ]:           0 :     auto snapshots = engine.GetDebugBatch();
    1053         [ #  # ]:           0 :     for (const auto& snap : snapshots) {
    1054                 :           0 :         m_latest_steering_range = snap.steering_range_deg;
    1055                 :           0 :         m_latest_steering_angle = snap.steering_angle_deg;
    1056                 :             : 
    1057                 :           0 :         plot_total.Add(snap.total_output);
    1058                 :           0 :         plot_base.Add(snap.base_force);
    1059                 :           0 :         plot_sop.Add(snap.sop_force);
    1060                 :           0 :         plot_yaw_kick.Add(snap.ffb_yaw_kick);
    1061                 :           0 :         plot_rear_torque.Add(snap.ffb_rear_torque);
    1062                 :           0 :         plot_gyro_damping.Add(snap.ffb_gyro_damping);
    1063                 :           0 :         plot_scrub_drag.Add(snap.ffb_scrub_drag);
    1064                 :           0 :         plot_soft_lock.Add(snap.ffb_soft_lock);
    1065                 :           0 :         plot_oversteer.Add(snap.oversteer_boost);
    1066                 :           0 :         plot_understeer.Add(snap.understeer_drop);
    1067                 :           0 :         plot_clipping.Add(snap.clipping);
    1068                 :           0 :         plot_road.Add(snap.texture_road);
    1069                 :           0 :         plot_slide.Add(snap.texture_slide);
    1070                 :           0 :         plot_lockup.Add(snap.texture_lockup);
    1071                 :           0 :         plot_spin.Add(snap.texture_spin);
    1072                 :           0 :         plot_bottoming.Add(snap.texture_bottoming);
    1073                 :           0 :         plot_calc_front_load.Add(snap.calc_front_load);
    1074                 :           0 :         plot_calc_rear_load.Add(snap.calc_rear_load);
    1075                 :           0 :         plot_calc_front_grip.Add(snap.calc_front_grip);
    1076                 :           0 :         plot_calc_rear_grip.Add(snap.calc_rear_grip);
    1077                 :           0 :         plot_calc_slip_ratio.Add(snap.calc_front_slip_ratio);
    1078                 :           0 :         plot_calc_slip_angle_smoothed.Add(snap.calc_front_slip_angle_smoothed);
    1079                 :           0 :         plot_calc_rear_slip_angle_smoothed.Add(snap.calc_rear_slip_angle_smoothed);
    1080                 :           0 :         plot_calc_rear_lat_force.Add(snap.calc_rear_lat_force);
    1081                 :           0 :         plot_slope_current.Add(snap.slope_current);
    1082                 :           0 :         plot_raw_steer.Add(snap.steer_force);
    1083                 :           0 :         plot_raw_shaft_torque.Add(snap.raw_shaft_torque);
    1084                 :           0 :         plot_raw_gen_torque.Add(snap.raw_gen_torque);
    1085                 :           0 :         plot_raw_input_steering.Add(snap.raw_input_steering);
    1086                 :           0 :         plot_raw_throttle.Add(snap.raw_input_throttle);
    1087                 :           0 :         plot_raw_brake.Add(snap.raw_input_brake);
    1088                 :           0 :         plot_input_accel.Add(snap.accel_x);
    1089                 :           0 :         plot_raw_car_speed.Add(snap.raw_car_speed);
    1090                 :           0 :         plot_raw_load.Add(snap.raw_front_tire_load);
    1091                 :           0 :         plot_raw_grip.Add(snap.raw_front_grip_fract);
    1092                 :           0 :         plot_raw_rear_grip.Add(snap.raw_rear_grip);
    1093                 :           0 :         plot_raw_front_slip_ratio.Add(snap.raw_front_slip_ratio);
    1094                 :           0 :         plot_raw_susp_force.Add(snap.raw_front_susp_force);
    1095                 :           0 :         plot_raw_ride_height.Add(snap.raw_front_ride_height);
    1096                 :           0 :         plot_raw_front_lat_patch_vel.Add(snap.raw_front_lat_patch_vel);
    1097                 :           0 :         plot_raw_front_long_patch_vel.Add(snap.raw_front_long_patch_vel);
    1098                 :           0 :         plot_raw_rear_lat_patch_vel.Add(snap.raw_rear_lat_patch_vel);
    1099                 :           0 :         plot_raw_rear_long_patch_vel.Add(snap.raw_rear_long_patch_vel);
    1100                 :           0 :         plot_raw_slip_angle.Add(snap.raw_front_slip_angle);
    1101                 :           0 :         plot_raw_rear_slip_angle.Add(snap.raw_rear_slip_angle);
    1102                 :           0 :         plot_raw_front_deflection.Add(snap.raw_front_deflection);
    1103                 :           0 :         g_warn_dt = snap.warn_dt;
    1104                 :             :     }
    1105                 :           0 : }
    1106                 :             : 
    1107                 :          63 : void GuiLayer::DrawDebugWindow(FFBEngine& engine) {
    1108         [ +  + ]:          63 :     if (!Config::show_graphs) return;
    1109                 :             : 
    1110                 :          62 :     ImGuiViewport* viewport = ImGui::GetMainViewport();
    1111         [ +  - ]:          62 :     ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + CONFIG_PANEL_WIDTH, viewport->WorkPos.y));
    1112         [ +  - ]:          62 :     ImGui::SetNextWindowSize(ImVec2(viewport->WorkSize.x - CONFIG_PANEL_WIDTH, viewport->WorkSize.y));
    1113                 :             : 
    1114                 :          62 :     ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
    1115                 :          62 :     ImGui::Begin("FFB Analysis", nullptr, flags);
    1116                 :             : 
    1117                 :             :     // System Health Diagnostics (Moved from Tuning window - Issue #149)
    1118         [ +  - ]:          62 :     if (ImGui::CollapsingHeader("System Health", ImGuiTreeNodeFlags_DefaultOpen)) {
    1119                 :         310 :         HealthStatus hs = HealthMonitor::Check(engine.m_ffb_rate, engine.m_telemetry_rate, engine.m_gen_torque_rate, engine.m_torque_source, engine.m_physics_rate,
    1120   [ +  -  +  -  :          62 :                                               GameConnector::Get().IsConnected(), GameConnector::Get().IsSessionActive(), GameConnector::Get().GetSessionType(), GameConnector::Get().IsInRealtime(), GameConnector::Get().GetPlayerControl());
          +  -  +  -  +  
                -  +  - ]
    1121                 :             : 
    1122         [ +  - ]:          62 :         ImGui::Columns(6, "RateCols", false);
    1123         [ +  - ]:          62 :         DisplayRate("USB Loop", engine.m_ffb_rate, 1000.0);
    1124         [ +  - ]:          62 :         ImGui::NextColumn();
    1125         [ +  - ]:          62 :         DisplayRate("Physics", engine.m_physics_rate, 400.0);
    1126         [ +  - ]:          62 :         ImGui::NextColumn();
    1127         [ +  - ]:          62 :         DisplayRate("Telemetry", engine.m_telemetry_rate, 100.0); // Standard LMU is 100Hz
    1128         [ +  - ]:          62 :         ImGui::NextColumn();
    1129         [ +  - ]:          62 :         DisplayRate("Hardware", engine.m_hw_rate, 1000.0);
    1130         [ +  - ]:          62 :         ImGui::NextColumn();
    1131         [ +  - ]:          62 :         DisplayRate("S.Torque", engine.m_torque_rate, 100.0);
    1132         [ +  - ]:          62 :         ImGui::NextColumn();
    1133         [ +  - ]:          62 :         DisplayRate("G.Torque", engine.m_gen_torque_rate, 400.0);
    1134         [ +  - ]:          62 :         ImGui::Columns(1);
    1135                 :             :         
    1136         [ +  - ]:          62 :         ImGui::Separator();
    1137                 :             : 
    1138                 :             :         // Robust State Machine (#269, #274)
    1139         [ +  + ]:          62 :         if (!hs.is_connected) {
    1140         [ +  - ]:          55 :             ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Sim: Disconnected from LMU");
    1141                 :             :         } else {
    1142                 :           7 :             bool active = hs.session_active;
    1143   [ -  +  -  +  :           7 :             ImGui::TextColored(active ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
                   +  - ]
    1144                 :             :                 "Sim: %s", active ? "Track Loaded" : "Main Menu");
    1145                 :             :         }
    1146                 :             : 
    1147   [ +  +  -  + ]:          62 :         if (hs.is_connected && hs.session_active) {
    1148         [ #  # ]:           0 :             ImGui::SameLine();
    1149                 :           0 :             const char* sessionStr = "Unknown";
    1150                 :           0 :             long stype = hs.session_type;
    1151         [ #  # ]:           0 :             if (stype == 0) sessionStr = "Test Day";
    1152   [ #  #  #  # ]:           0 :             else if (stype >= 1 && stype <= 4) sessionStr = "Practice";
    1153   [ #  #  #  # ]:           0 :             else if (stype >= 5 && stype <= 8) sessionStr = "Qualifying";
    1154         [ #  # ]:           0 :             else if (stype == 9) sessionStr = "Warmup";
    1155   [ #  #  #  # ]:           0 :             else if (stype >= 10 && stype <= 13) sessionStr = "Race";
    1156         [ #  # ]:           0 :             ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "| Session: %s", sessionStr);
    1157                 :             : 
    1158                 :           0 :             bool driving = hs.is_realtime;
    1159   [ #  #  #  #  :           0 :             ImGui::TextColored(driving ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(1.0f, 1.0f, 0.4f, 1.0f),
                   #  # ]
    1160                 :             :                 "State: %s", driving ? "Driving" : "In Menu");
    1161                 :             : 
    1162         [ #  # ]:           0 :             ImGui::SameLine();
    1163                 :           0 :             const char* ctrlStr = "Unknown";
    1164                 :           0 :             signed char ctrl = hs.player_control;
    1165   [ #  #  #  #  :           0 :             switch (ctrl) {
                   #  # ]
    1166                 :           0 :                 case -1: ctrlStr = "Nobody"; break;
    1167                 :           0 :                 case 0: ctrlStr = "Player"; break;
    1168                 :           0 :                 case 1: ctrlStr = "AI"; break;
    1169                 :           0 :                 case 2: ctrlStr = "Remote"; break;
    1170                 :           0 :                 case 3: ctrlStr = "Replay"; break;
    1171                 :           0 :                 default: ctrlStr = "Unknown"; break;
    1172                 :             :             }
    1173         [ #  # ]:           0 :             ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "| Control: %s", ctrlStr);
    1174                 :             :         }
    1175                 :             : 
    1176   [ +  +  +  +  :          62 :         if (!hs.is_healthy && engine.m_telemetry_rate > 1.0 && GameConnector::Get().IsConnected()) {
          +  -  +  -  +  
                -  +  + ]
    1177         [ +  - ]:           6 :             ImGui::TextColored(ImVec4(1, 1, 0, 1), "Warning: Sub-optimal sample rates detected. Check game settings.");
    1178                 :             :         }
    1179         [ +  - ]:          62 :         ImGui::Separator();
    1180                 :             :     }
    1181                 :             : 
    1182                 :             : 
    1183         [ -  + ]:          62 :     if (g_warn_dt) {
    1184         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
    1185                 :           0 :         ImGui::Text("TELEMETRY WARNINGS: - Invalid DeltaTime");
    1186                 :           0 :         ImGui::PopStyleColor();
    1187                 :           0 :         ImGui::Separator();
    1188                 :             :     }
    1189                 :             : 
    1190         [ +  - ]:          62 :     if (ImGui::CollapsingHeader("A. FFB Components (Output)", ImGuiTreeNodeFlags_DefaultOpen)) {
    1191         [ +  - ]:          62 :         PlotWithStats("Total Output", plot_total, -1.0f, 1.0f, ImVec2(0, 60));
    1192                 :          62 :         ImGui::Separator();
    1193                 :          62 :         ImGui::Columns(3, "FFBMain", false);
    1194         [ +  - ]:          62 :         ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), "[Main Forces]");
    1195         [ +  - ]:          62 :         PlotWithStats("Base Torque (Nm)", plot_base, -30.0f, 30.0f);
    1196         [ +  - ]:          62 :         PlotWithStats("SoP (Chassis G)", plot_sop, -20.0f, 20.0f);
    1197         [ +  - ]:          62 :         PlotWithStats("Yaw Kick", plot_yaw_kick, -20.0f, 20.0f);
    1198         [ +  - ]:          62 :         PlotWithStats("Rear Align", plot_rear_torque, -20.0f, 20.0f);
    1199         [ +  - ]:          62 :         PlotWithStats("Gyro Damping", plot_gyro_damping, -20.0f, 20.0f);
    1200         [ +  - ]:          62 :         PlotWithStats("Scrub Drag", plot_scrub_drag, -20.0f, 20.0f);
    1201         [ +  - ]:          62 :         PlotWithStats("Soft Lock", plot_soft_lock, -50.0f, 50.0f);
    1202                 :          62 :         ImGui::NextColumn();
    1203         [ +  - ]:          62 :         ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.7f, 1.0f), "[Modifiers]");
    1204         [ +  - ]:          62 :         PlotWithStats("Lateral G Boost", plot_oversteer, -20.0f, 20.0f);
    1205         [ +  - ]:          62 :         PlotWithStats("Understeer Cut", plot_understeer, -20.0f, 20.0f);
    1206         [ +  - ]:          62 :         PlotWithStats("Clipping", plot_clipping, 0.0f, 1.1f);
    1207                 :          62 :         ImGui::NextColumn();
    1208         [ +  - ]:          62 :         ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "[Textures]");
    1209         [ +  - ]:          62 :         PlotWithStats("Road Texture", plot_road, -10.0f, 10.0f);
    1210         [ +  - ]:          62 :         PlotWithStats("Slide Texture", plot_slide, -10.0f, 10.0f);
    1211         [ +  - ]:          62 :         PlotWithStats("Lockup Vib", plot_lockup, -10.0f, 10.0f);
    1212         [ +  - ]:          62 :         PlotWithStats("Spin Vib", plot_spin, -10.0f, 10.0f);
    1213         [ +  - ]:          62 :         PlotWithStats("Bottoming", plot_bottoming, -10.0f, 10.0f);
    1214                 :          62 :         ImGui::Columns(1);
    1215                 :             :     }
    1216                 :             : 
    1217         [ -  + ]:          62 :     if (ImGui::CollapsingHeader("B. Internal Physics (Brain)", ImGuiTreeNodeFlags_None)) {
    1218         [ #  # ]:           0 :         ImGui::Columns(3, "PhysCols", false);
    1219         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Loads]");
    1220         [ #  # ]:           0 :         ImGui::Text("Front: %.0f N | Rear: %.0f N", plot_calc_front_load.GetCurrent(), plot_calc_rear_load.GetCurrent());
    1221         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 1.0f, 1.0f));
    1222         [ #  # ]:           0 :         ImGui::PlotLines("##CLoadF", plot_calc_front_load.data.data(), (int)plot_calc_front_load.data.size(), plot_calc_front_load.offset, NULL, 0.0f, 10000.0f, ImVec2(0, 40));
    1223         [ #  # ]:           0 :         ImGui::PopStyleColor();
    1224         [ #  # ]:           0 :         ImVec2 pos_load = ImGui::GetItemRectMin();
    1225         [ #  # ]:           0 :         ImGui::SetCursorScreenPos(pos_load);
    1226         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0,0,0,0));
    1227         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 1.0f, 1.0f));
    1228         [ #  # ]:           0 :         ImGui::PlotLines("##CLoadR", plot_calc_rear_load.data.data(), (int)plot_calc_rear_load.data.size(), plot_calc_rear_load.offset, NULL, 0.0f, 10000.0f, ImVec2(0, 40));
    1229         [ #  # ]:           0 :         ImGui::PopStyleColor(2);
    1230         [ #  # ]:           0 :         ImGui::NextColumn();
    1231         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Grip/Slip]");
    1232         [ #  # ]:           0 :         PlotWithStats("Calc Front Grip", plot_calc_front_grip, 0.0f, 1.2f);
    1233         [ #  # ]:           0 :         PlotWithStats("Calc Rear Grip", plot_calc_rear_grip, 0.0f, 1.2f);
    1234         [ #  # ]:           0 :         PlotWithStats("Front Slip Ratio", plot_calc_slip_ratio, -1.0f, 1.0f);
    1235         [ #  # ]:           0 :         PlotWithStats("Front Slip Angle", plot_calc_slip_angle_smoothed, 0.0f, 1.0f);
    1236         [ #  # ]:           0 :         PlotWithStats("Rear Slip Angle", plot_calc_rear_slip_angle_smoothed, 0.0f, 1.0f);
    1237   [ #  #  #  # ]:           0 :         if (engine.m_slope_detection_enabled) PlotWithStats("Slope", plot_slope_current, -5.0f, 5.0f);
    1238         [ #  # ]:           0 :         ImGui::NextColumn();
    1239         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Forces]");
    1240         [ #  # ]:           0 :         PlotWithStats("Calc Rear Lat Force", plot_calc_rear_lat_force, -5000.0f, 5000.0f);
    1241         [ #  # ]:           0 :         ImGui::Columns(1);
    1242                 :             :     }
    1243                 :             : 
    1244         [ -  + ]:          62 :     if (ImGui::CollapsingHeader("C. Raw Game Telemetry (Input)", ImGuiTreeNodeFlags_None)) {
    1245         [ #  # ]:           0 :         ImGui::Columns(4, "TelCols", false);
    1246         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Driver Input]");
    1247         [ #  # ]:           0 :         PlotWithStats("Selected Torque", plot_raw_steer, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SELECTED_TORQUE);
    1248         [ #  # ]:           0 :         PlotWithStats("Shaft Torque (100Hz)", plot_raw_shaft_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SHAFT_TORQUE);
    1249         [ #  # ]:           0 :         PlotWithStats("In-Game FFB (400Hz)", plot_raw_gen_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_INGAME_FFB);
    1250         [ #  # ]:           0 :         PlotWithStats("Steering Input", plot_raw_input_steering, -1.0f, 1.0f);
    1251         [ #  # ]:           0 :         ImGui::Text("Combined Input");
    1252         [ #  # ]:           0 :         ImVec2 pos = ImGui::GetCursorScreenPos();
    1253         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
    1254         [ #  # ]:           0 :         ImGui::PlotLines("##BrkComb", plot_raw_brake.data.data(), (int)plot_raw_brake.data.size(), plot_raw_brake.offset, NULL, 0.0f, 1.0f, ImVec2(0, 40));
    1255         [ #  # ]:           0 :         ImGui::PopStyleColor();
    1256         [ #  # ]:           0 :         ImGui::SetCursorScreenPos(pos);
    1257         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
    1258         [ #  # ]:           0 :         ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
    1259         [ #  # ]:           0 :         ImGui::PlotLines("##ThrComb", plot_raw_throttle.data.data(), (int)plot_raw_throttle.data.size(), plot_raw_throttle.offset, NULL, 0.0f, 1.0f, ImVec2(0, 40));
    1260         [ #  # ]:           0 :         ImGui::PopStyleColor(2);
    1261         [ #  # ]:           0 :         ImGui::NextColumn();
    1262         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Vehicle State]");
    1263         [ #  # ]:           0 :         PlotWithStats("Lat Accel", plot_input_accel, -20.0f, 20.0f);
    1264         [ #  # ]:           0 :         PlotWithStats("Speed (m/s)", plot_raw_car_speed, 0.0f, 100.0f);
    1265         [ #  # ]:           0 :         ImGui::NextColumn();
    1266         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Raw Tire Data]");
    1267         [ #  # ]:           0 :         PlotWithStats("Raw Front Load", plot_raw_load, 0.0f, 10000.0f);
    1268         [ #  # ]:           0 :         PlotWithStats("Raw Front Grip", plot_raw_grip, 0.0f, 1.2f);
    1269         [ #  # ]:           0 :         PlotWithStats("Raw Rear Grip", plot_raw_rear_grip, 0.0f, 1.2f);
    1270         [ #  # ]:           0 :         ImGui::NextColumn();
    1271         [ #  # ]:           0 :         ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Patch Velocities]");
    1272         [ #  # ]:           0 :         PlotWithStats("F-Lat PatchVel", plot_raw_front_lat_patch_vel, 0.0f, 20.0f);
    1273         [ #  # ]:           0 :         PlotWithStats("R-Lat PatchVel", plot_raw_rear_lat_patch_vel, 0.0f, 20.0f);
    1274         [ #  # ]:           0 :         PlotWithStats("F-Long PatchVel", plot_raw_front_long_patch_vel, -20.0f, 20.0f);
    1275         [ #  # ]:           0 :         PlotWithStats("R-Long PatchVel", plot_raw_rear_long_patch_vel, -20.0f, 20.0f);
    1276         [ #  # ]:           0 :         ImGui::Columns(1);
    1277                 :             :     }
    1278                 :             : 
    1279                 :          62 :     ImGui::End();
    1280                 :             : }
    1281                 :             : #endif
    1282                 :             : 
    1283                 :             : #ifndef ENABLE_IMGUI
    1284                 :             : void GuiLayer::DrawMenuBar(FFBEngine& engine) {}
    1285                 :             : #endif
        

Generated by: LCOV version 2.0-1