Branch data Line data Source code
1 : : #include "GuiLayer.h"
2 : : #include "Version.h"
3 : : #include "Config.h"
4 : : #include "Tooltips.h"
5 : : #include "DirectInputFFB.h"
6 : : #include "GameConnector.h"
7 : : #include "GuiWidgets.h"
8 : : #include "AsyncLogger.h"
9 : : #include <iostream>
10 : : #include <vector>
11 : : #include <cmath>
12 : : #include <algorithm>
13 : : #include <cstring>
14 : : #include <mutex>
15 : : #include <chrono>
16 : : #include <ctime>
17 : :
18 : : #ifdef ENABLE_IMGUI
19 : : #include "imgui.h"
20 : :
21 : 310 : static void DisplayRate(const char* label, double rate, double target) {
22 [ + - ]: 310 : ImGui::Text("%s", label);
23 : :
24 : : // Status colors for performance metrics
25 : : static const ImVec4 COLOR_RED(1.0F, 0.4F, 0.4F, 1.0F);
26 : : static const ImVec4 COLOR_GREEN(0.4F, 1.0F, 0.4F, 1.0F);
27 : : static const ImVec4 COLOR_YELLOW(1.0F, 1.0F, 0.4F, 1.0F);
28 : :
29 : 310 : ImVec4 color = COLOR_RED;
30 [ + + ]: 310 : if (rate >= target * 0.95) {
31 : 7 : color = COLOR_GREEN;
32 [ + + ]: 303 : } else if (rate >= target * 0.75) {
33 : 7 : color = COLOR_YELLOW;
34 : : }
35 : :
36 [ + - ]: 310 : ImGui::TextColored(color, "%.1f Hz", rate);
37 : 310 : }
38 : :
39 : :
40 : : // External linkage to FFB loop status
41 : : extern std::atomic<bool> g_running;
42 : : extern std::recursive_mutex g_engine_mutex;
43 : :
44 : : float GuiLayer::m_latest_steering_range = 0.0f;
45 : : float GuiLayer::m_latest_steering_angle = 0.0f;
46 : :
47 : : static const float CONFIG_PANEL_WIDTH = 500.0f;
48 : : static const int LATENCY_WARNING_THRESHOLD_MS = 15;
49 : :
50 : : // Professional "Flat Dark" Theme
51 : 1 : void GuiLayer::SetupGUIStyle() {
52 [ + - ]: 1 : ImGuiStyle& style = ImGui::GetStyle();
53 : :
54 : 1 : style.WindowRounding = 5.0f;
55 : 1 : style.FrameRounding = 4.0f;
56 : 1 : style.GrabRounding = 4.0f;
57 : 1 : style.FramePadding = ImVec2(8, 4);
58 : 1 : style.ItemSpacing = ImVec2(8, 6);
59 : :
60 : 1 : ImVec4* colors = style.Colors;
61 : :
62 : 1 : colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f);
63 : 1 : colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.15f, 0.15f, 1.00f);
64 : 1 : colors[ImGuiCol_PopupBg] = ImVec4(0.15f, 0.15f, 0.15f, 0.98f);
65 : :
66 : 1 : colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.20f, 0.00f);
67 : 1 : colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.25f, 0.25f, 0.50f);
68 : 1 : colors[ImGuiCol_HeaderActive] = ImVec4(0.30f, 0.30f, 0.30f, 0.50f);
69 : :
70 : 1 : colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
71 : 1 : colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
72 : 1 : colors[ImGuiCol_FrameBgActive] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f);
73 : :
74 : 1 : ImVec4 accent = ImVec4(0.00f, 0.60f, 0.85f, 1.00f);
75 : 1 : colors[ImGuiCol_SliderGrab] = accent;
76 : 1 : colors[ImGuiCol_SliderGrabActive] = ImVec4(0.00f, 0.70f, 0.95f, 1.00f);
77 : 1 : colors[ImGuiCol_Button] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
78 : 1 : colors[ImGuiCol_ButtonHovered] = accent;
79 : 1 : colors[ImGuiCol_ButtonActive] = ImVec4(0.00f, 0.50f, 0.75f, 1.00f);
80 : 1 : colors[ImGuiCol_CheckMark] = accent;
81 : :
82 : 1 : colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
83 : 1 : colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
84 : 1 : }
85 : :
86 : :
87 : : static constexpr std::chrono::seconds CONNECT_ATTEMPT_INTERVAL(2);
88 : :
89 : 377 : void GuiLayer::DrawTuningWindow(FFBEngine& engine) {
90 [ + - ]: 377 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
91 : :
92 [ + - ]: 377 : ImGuiViewport* viewport = ImGui::GetMainViewport();
93 [ + + ]: 377 : float current_width = Config::show_graphs ? CONFIG_PANEL_WIDTH : viewport->Size.x;
94 : :
95 [ + - ]: 377 : ImGui::SetNextWindowPos(viewport->Pos);
96 [ + - ]: 377 : ImGui::SetNextWindowSize(ImVec2(current_width, viewport->Size.y));
97 : :
98 : 377 : ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
99 [ + - ]: 377 : ImGui::Begin("MainUI", nullptr, flags);
100 : :
101 [ + - ]: 377 : ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.00f, 0.60f, 0.85f, 1.00f));
102 [ + - ]: 377 : ImGui::Text("lmuFFB");
103 [ + - ]: 377 : ImGui::PopStyleColor();
104 [ + - ]: 377 : ImGui::SameLine();
105 [ + - ]: 377 : ImGui::TextDisabled("v%s", LMUFFB_VERSION);
106 [ + - ]: 377 : ImGui::Separator();
107 : :
108 [ + + + - ]: 377 : static std::chrono::steady_clock::time_point last_check_time = std::chrono::steady_clock::now();
109 : :
110 [ + - + - : 377 : if (!GameConnector::Get().IsConnected()) {
+ + ]
111 [ + - ]: 208 : ImGui::TextColored(ImVec4(1, 1, 0, 1), "Connecting to LMU...");
112 [ + - + - : 208 : if (std::chrono::steady_clock::now() - last_check_time > CONNECT_ATTEMPT_INTERVAL) {
- + ]
113 : 0 : last_check_time = std::chrono::steady_clock::now();
114 [ # # # # ]: 0 : GameConnector::Get().TryConnect();
115 : : }
116 : : } else {
117 [ + - ]: 169 : ImGui::TextColored(ImVec4(0, 1, 0, 1), "Connected to LMU");
118 [ + - ]: 169 : ImGui::SameLine();
119 [ + - ]: 169 : ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "| FFB: %.0fHz | Tel: %.0fHz", engine.m_ffb_rate, engine.m_telemetry_rate);
120 : : }
121 : :
122 [ + + + - ]: 377 : static std::vector<DeviceInfo> devices;
123 : : static int selected_device_idx = -1;
124 : :
125 [ + + ]: 377 : if (devices.empty()) {
126 [ + - + - ]: 1 : devices = DirectInputFFB::Get().EnumerateDevices();
127 [ + - + - : 1 : if (selected_device_idx == -1 && !Config::m_last_device_guid.empty()) {
+ - ]
128 [ + - ]: 1 : GUID target = DirectInputFFB::StringToGuid(Config::m_last_device_guid);
129 [ + + ]: 3 : for (int i = 0; i < (int)devices.size(); i++) {
130 [ - + ]: 2 : if (memcmp(&devices[i].guid, &target, sizeof(GUID)) == 0) {
131 : 0 : selected_device_idx = i;
132 [ # # # # ]: 0 : DirectInputFFB::Get().SelectDevice(devices[i].guid);
133 : 0 : break;
134 : : }
135 : : }
136 : : }
137 : : }
138 : :
139 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
140 [ - + + - : 377 : if (ImGui::BeginCombo("FFB Device", selected_device_idx >= 0 ? devices[selected_device_idx].name.c_str() : "Select Device...")) {
- + ]
141 [ # # ]: 0 : for (int i = 0; i < (int)devices.size(); i++) {
142 : 0 : bool is_selected = (selected_device_idx == i);
143 [ # # ]: 0 : ImGui::PushID(i);
144 [ # # # # ]: 0 : if (ImGui::Selectable(devices[i].name.c_str(), is_selected)) {
145 : 0 : selected_device_idx = i;
146 [ # # # # ]: 0 : DirectInputFFB::Get().SelectDevice(devices[i].guid);
147 [ # # ]: 0 : Config::m_last_device_guid = DirectInputFFB::GuidToString(devices[i].guid);
148 [ # # # # ]: 0 : Config::Save(engine);
149 : : }
150 [ # # # # ]: 0 : if (is_selected) ImGui::SetItemDefaultFocus();
151 [ # # ]: 0 : ImGui::PopID();
152 : : }
153 [ # # ]: 0 : ImGui::EndCombo();
154 : : }
155 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_SELECT);
+ - ]
156 : :
157 [ + - ]: 377 : ImGui::SameLine();
158 [ + - - + ]: 377 : if (ImGui::Button("Rescan")) {
159 [ # # # # ]: 0 : devices = DirectInputFFB::Get().EnumerateDevices();
160 : 0 : selected_device_idx = -1;
161 : : }
162 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_RESCAN);
+ - ]
163 [ + - ]: 377 : ImGui::SameLine();
164 [ + - - + ]: 377 : if (ImGui::Button("Unbind")) {
165 [ # # # # ]: 0 : DirectInputFFB::Get().ReleaseDevice();
166 : 0 : selected_device_idx = -1;
167 : : }
168 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_UNBIND);
- - ]
169 : :
170 [ + - - + ]: 377 : if (DirectInputFFB::Get().IsActive()) {
171 [ # # # # ]: 0 : if (DirectInputFFB::Get().IsExclusive()) {
172 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Mode: EXCLUSIVE (Game FFB Blocked)");
173 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_EXCLUSIVE);
# # ]
174 : : } else {
175 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.4f, 1.0f), "Mode: SHARED (Potential Conflict)");
176 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_SHARED);
# # ]
177 : : }
178 : : } else {
179 [ + - ]: 377 : ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "No device selected.");
180 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::NO_DEVICE);
+ - ]
181 : : }
182 : :
183 [ + - - + ]: 377 : if (ImGui::Checkbox("Always on Top", &Config::m_always_on_top)) {
184 [ # # ]: 0 : SetWindowAlwaysOnTopPlatform(Config::m_always_on_top);
185 [ # # # # ]: 0 : Config::Save(engine);
186 : : }
187 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::ALWAYS_ON_TOP);
+ - ]
188 [ + - ]: 377 : ImGui::SameLine();
189 : :
190 : 377 : bool toggled = Config::show_graphs;
191 [ + - - + ]: 377 : if (ImGui::Checkbox("Graphs", &toggled)) {
192 [ # # ]: 0 : SaveCurrentWindowGeometryPlatform(Config::show_graphs);
193 : 0 : Config::show_graphs = toggled;
194 [ # # ]: 0 : int target_w = Config::show_graphs ? Config::win_w_large : Config::win_w_small;
195 [ # # ]: 0 : int target_h = Config::show_graphs ? Config::win_h_large : Config::win_h_small;
196 [ # # ]: 0 : ResizeWindowPlatform(Config::win_pos_x, Config::win_pos_y, target_w, target_h);
197 [ # # # # ]: 0 : Config::Save(engine);
198 : : }
199 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::SHOW_GRAPHS);
- - ]
200 : :
201 [ + - ]: 377 : ImGui::Separator();
202 [ + - ]: 377 : bool is_logging = AsyncLogger::Get().IsLogging();
203 [ + + ]: 377 : if (is_logging) {
204 [ + - - + ]: 3 : if (ImGui::Button("STOP LOG", ImVec2(80, 0))) {
205 [ # # ]: 0 : AsyncLogger::Get().Stop();
206 : : }
207 [ + - - + : 3 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_STOP);
- - ]
208 [ + - ]: 3 : ImGui::SameLine();
209 [ + - ]: 3 : float time = (float)ImGui::GetTime();
210 : 3 : bool blink = (fmod(time, 1.0f) < 0.5f);
211 [ + + + - ]: 3 : ImGui::TextColored(blink ? ImVec4(1,0,0,1) : ImVec4(0.6f,0,0,1), "REC");
212 [ + - - + : 3 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_REC);
- - ]
213 : :
214 [ + - ]: 3 : ImGui::SameLine();
215 [ + - ]: 3 : size_t bytes = AsyncLogger::Get().GetFileSizeBytes();
216 [ + - ]: 3 : if (bytes < 1024ULL * 1024ULL)
217 [ + - + - ]: 3 : ImGui::Text("%zu f (%.0f KB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / 1024.0f);
218 : : else
219 [ # # # # ]: 0 : ImGui::Text("%zu f (%.1f MB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / (1024.0f * 1024.0f));
220 : :
221 [ + - ]: 3 : ImGui::SameLine();
222 [ + - - + ]: 3 : if (ImGui::Button("MARKER")) {
223 [ # # ]: 0 : AsyncLogger::Get().SetMarker();
224 : : }
225 [ + - - + : 3 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_MARKER);
- - ]
226 : : } else {
227 [ + - - + ]: 374 : if (ImGui::Button("START LOGGING", ImVec2(120, 0))) {
228 : 0 : SessionInfo info;
229 [ # # ]: 0 : info.app_version = LMUFFB_VERSION;
230 [ # # # # ]: 0 : if (engine.m_vehicle_name[0] != '\0') info.vehicle_name = engine.m_vehicle_name;
231 [ # # ]: 0 : else info.vehicle_name = "UnknownCar";
232 : :
233 [ # # # # ]: 0 : if (engine.m_track_name[0] != '\0') info.track_name = engine.m_track_name;
234 [ # # ]: 0 : else info.track_name = "UnknownTrack";
235 : :
236 [ # # ]: 0 : info.driver_name = "Auto";
237 : :
238 : 0 : info.gain = engine.m_gain;
239 : 0 : info.understeer_effect = engine.m_understeer_effect;
240 : 0 : info.sop_effect = engine.m_sop_effect;
241 : 0 : info.slope_enabled = engine.m_slope_detection_enabled;
242 : 0 : info.slope_sensitivity = engine.m_slope_sensitivity;
243 : 0 : info.slope_threshold = (float)engine.m_slope_min_threshold;
244 : 0 : info.slope_alpha_threshold = engine.m_slope_alpha_threshold;
245 : 0 : info.slope_decay_rate = engine.m_slope_decay_rate;
246 : 0 : info.torque_passthrough = engine.m_torque_passthrough;
247 : :
248 [ # # # # ]: 0 : AsyncLogger::Get().Start(info, Config::m_log_path);
249 : 0 : }
250 [ + - + + : 374 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_START);
+ - ]
251 [ + - ]: 374 : ImGui::SameLine();
252 [ + - ]: 374 : ImGui::TextDisabled("(Diagnostics)");
253 : : }
254 : :
255 : :
256 [ + - ]: 377 : ImGui::Separator();
257 : :
258 : : static int selected_preset = 0;
259 : :
260 : 3363 : auto FormatDecoupled = [&](float val, float base_nm) {
261 : 3363 : float estimated_nm = val * base_nm;
262 : : static char buf[64];
263 : 3363 : snprintf(buf, 64, "%.1f%%%% (~%.1f Nm)", val * 100.0f, estimated_nm);
264 : 3363 : return (const char*)buf;
265 : : };
266 : :
267 : 2262 : auto FormatPct = [&](float val) {
268 : : static char buf[32];
269 : 2262 : snprintf(buf, 32, "%.1f%%%%", val * 100.0f);
270 : 2262 : return (const char*)buf;
271 : : };
272 : :
273 : 18664 : auto FloatSetting = [&](const char* label, float* v, float min, float max, const char* fmt = "%.2f", const char* tooltip = nullptr, std::function<void()> decorator = nullptr) {
274 [ + - + - ]: 18664 : GuiWidgets::Result res = GuiWidgets::Float(label, v, min, max, fmt, tooltip, decorator);
275 [ - + ]: 18664 : if (res.deactivated) {
276 [ # # # # ]: 0 : Config::Save(engine);
277 : : }
278 : 18664 : };
279 : :
280 : 4147 : auto BoolSetting = [&](const char* label, bool* v, const char* tooltip = nullptr) {
281 [ + - ]: 4147 : GuiWidgets::Result res = GuiWidgets::Checkbox(label, v, tooltip);
282 [ - + ]: 4147 : if (res.deactivated) {
283 [ # # # # ]: 0 : Config::Save(engine);
284 : : }
285 : 4147 : };
286 : :
287 : 754 : auto IntSetting = [&](const char* label, int* v, const char* const items[], int items_count, const char* tooltip = nullptr) {
288 [ + - ]: 754 : GuiWidgets::Result res = GuiWidgets::Combo(label, v, items, items_count, tooltip);
289 [ - + ]: 754 : if (res.changed) {
290 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
291 : : // v is already updated by ImGui, but we lock to ensure visibility and consistency
292 [ # # # # ]: 0 : Config::Save(engine);
293 : 0 : }
294 : 754 : };
295 : :
296 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Presets and Configuration", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
297 [ - + - - ]: 377 : if (Config::presets.empty()) Config::LoadPresets();
298 : :
299 : : static bool first_run = true;
300 [ + + + - : 377 : if (first_run && !Config::presets.empty()) {
+ + ]
301 [ + - ]: 1 : for (int i = 0; i < (int)Config::presets.size(); i++) {
302 [ + - ]: 1 : if (Config::presets[i].name == Config::m_last_preset_name) {
303 : 1 : selected_preset = i;
304 : 1 : break;
305 : : }
306 : : }
307 : 1 : first_run = false;
308 : : }
309 : :
310 [ + + + - ]: 377 : static std::string preview_buf;
311 : 377 : const char* preview_value = "Custom";
312 [ + - + - : 377 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
+ - ]
313 [ + - ]: 377 : preview_buf = Config::presets[selected_preset].name;
314 [ + - + + ]: 377 : if (Config::IsEngineDirtyRelativeToPreset(selected_preset, engine)) {
315 [ + - ]: 370 : preview_buf += "*";
316 : : }
317 : 377 : preview_value = preview_buf.c_str();
318 : : }
319 : :
320 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f);
321 [ + - - + ]: 377 : if (ImGui::BeginCombo("Load Preset", preview_value)) {
322 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
323 : 0 : bool is_selected = (selected_preset == i);
324 [ # # ]: 0 : ImGui::PushID(i);
325 [ # # # # ]: 0 : if (ImGui::Selectable(Config::presets[i].name.c_str(), is_selected)) {
326 : 0 : selected_preset = i;
327 [ # # ]: 0 : Config::ApplyPreset(i, engine);
328 : : }
329 [ # # # # ]: 0 : if (is_selected) ImGui::SetItemDefaultFocus();
330 [ # # ]: 0 : ImGui::PopID();
331 : : }
332 [ # # ]: 0 : ImGui::EndCombo();
333 : : }
334 : :
335 : : static char new_preset_name[64] = "";
336 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
337 [ + - ]: 377 : ImGui::InputText("##NewPresetName", new_preset_name, 64);
338 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_NAME);
+ - ]
339 [ + - ]: 377 : ImGui::SameLine();
340 [ + - - + ]: 377 : if (ImGui::Button("Save New")) {
341 [ # # ]: 0 : if (strlen(new_preset_name) > 0) {
342 [ # # # # ]: 0 : Config::AddUserPreset(std::string(new_preset_name), engine);
343 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
344 [ # # # # ]: 0 : if (Config::presets[i].name == std::string(new_preset_name)) {
345 : 0 : selected_preset = i;
346 : 0 : break;
347 : : }
348 : : }
349 : 0 : new_preset_name[0] = '\0';
350 : : }
351 : : }
352 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_NEW);
- - ]
353 : :
354 [ + - - + ]: 377 : if (ImGui::Button("Save Current Config")) {
355 [ # # # # : 0 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin) {
# # # # ]
356 [ # # ]: 0 : Config::AddUserPreset(Config::presets[selected_preset].name, engine);
357 : : } else {
358 [ # # # # ]: 0 : Config::Save(engine);
359 : : }
360 : : }
361 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_CURRENT);
+ - ]
362 [ + - ]: 377 : ImGui::SameLine();
363 [ + - - + ]: 377 : if (ImGui::Button("Reset Defaults")) {
364 [ # # ]: 0 : Config::ApplyPreset(0, engine);
365 : 0 : selected_preset = 0;
366 : : }
367 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_RESET);
- - ]
368 [ + - ]: 377 : ImGui::SameLine();
369 [ + - - + ]: 377 : if (ImGui::Button("Duplicate")) {
370 [ # # ]: 0 : if (selected_preset >= 0) {
371 [ # # ]: 0 : Config::DuplicatePreset(selected_preset, engine);
372 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
373 [ # # ]: 0 : if (Config::presets[i].name == Config::m_last_preset_name) {
374 : 0 : selected_preset = i;
375 : 0 : break;
376 : : }
377 : : }
378 : : }
379 : : }
380 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DUPLICATE);
+ - ]
381 [ + - ]: 377 : ImGui::SameLine();
382 [ + - + - : 377 : bool can_delete = (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin);
+ - ]
383 [ - + - - ]: 377 : if (!can_delete) ImGui::BeginDisabled();
384 [ + - - + ]: 377 : if (ImGui::Button("Delete")) {
385 [ # # ]: 0 : Config::DeletePreset(selected_preset, engine);
386 : 0 : selected_preset = 0;
387 [ # # ]: 0 : Config::ApplyPreset(0, engine);
388 : : }
389 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DELETE);
- - ]
390 [ - + - - ]: 377 : if (!can_delete) ImGui::EndDisabled();
391 : :
392 [ + - ]: 377 : ImGui::Separator();
393 [ + - - + ]: 377 : if (ImGui::Button("Import Preset...")) {
394 : 0 : std::string path;
395 [ # # # # ]: 0 : if (OpenPresetFileDialogPlatform(path)) {
396 [ # # # # ]: 0 : if (Config::ImportPreset(path, engine)) {
397 : 0 : selected_preset = (int)Config::presets.size() - 1;
398 : : }
399 : : }
400 : 0 : }
401 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_IMPORT);
+ - ]
402 [ + - ]: 377 : ImGui::SameLine();
403 [ + - - + ]: 377 : if (ImGui::Button("Export Selected...")) {
404 [ # # # # : 0 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
# # ]
405 : 0 : std::string path;
406 [ # # ]: 0 : std::string defaultName = Config::presets[selected_preset].name + ".ini";
407 [ # # # # ]: 0 : if (SavePresetFileDialogPlatform(path, defaultName)) {
408 [ # # ]: 0 : Config::ExportPreset(selected_preset, path);
409 : : }
410 : 0 : }
411 : : }
412 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_EXPORT);
- - ]
413 : :
414 [ + - ]: 377 : ImGui::TreePop();
415 : : }
416 : :
417 [ + - ]: 377 : ImGui::Spacing();
418 : :
419 [ + - ]: 377 : ImGui::Columns(2, "SettingsGrid", false);
420 [ + - + - ]: 377 : ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.45f);
421 : :
422 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("General FFB", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
423 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
424 : :
425 [ + - ]: 377 : ImGui::TextDisabled("Steering: %.1f° (%.0f)", m_latest_steering_angle, m_latest_steering_range);
426 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
427 : :
428 [ + - ]: 377 : BoolSetting("Steerlock from REST API", &engine.m_rest_api_enabled, Tooltips::REST_API_ENABLE);
429 : :
430 [ + - ]: 377 : ImGui::Spacing();
431 : 377 : bool use_in_game_ffb = (engine.m_torque_source == 1);
432 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Use In-Game FFB (400Hz Native)", &use_in_game_ffb, Tooltips::USE_INGAME_FFB).changed) {
433 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
434 [ # # ]: 0 : engine.m_torque_source = use_in_game_ffb ? 1 : 0;
435 [ # # # # ]: 0 : Config::Save(engine);
436 : 0 : }
437 : :
438 [ + - ]: 377 : BoolSetting("Invert FFB Signal", &engine.m_invert_force, Tooltips::INVERT_FFB);
439 : :
440 : 377 : bool prev_structural = engine.m_dynamic_normalization_enabled;
441 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Enable Dynamic Normalization (Session Peak)", &engine.m_dynamic_normalization_enabled, Tooltips::DYNAMIC_NORMALIZATION_ENABLE).changed) {
442 [ # # # # ]: 0 : if (prev_structural && !engine.m_dynamic_normalization_enabled) {
443 [ # # ]: 0 : engine.ResetNormalization();
444 : : }
445 [ # # # # ]: 0 : Config::Save(engine);
446 : : }
447 [ + - ]: 377 : FloatSetting("Master Gain", &engine.m_gain, 0.0f, 2.0f, FormatPct(engine.m_gain), Tooltips::MASTER_GAIN);
448 [ + - ]: 377 : FloatSetting("Wheelbase Max Torque", &engine.m_wheelbase_max_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::WHEELBASE_MAX_TORQUE);
449 [ + - ]: 377 : FloatSetting("Target Rim Torque", &engine.m_target_rim_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::TARGET_RIM_TORQUE);
450 [ + - ]: 377 : FloatSetting("Min Force", &engine.m_min_force, 0.0f, 0.20f, "%.3f", Tooltips::MIN_FORCE);
451 : :
452 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Soft Lock", ImGuiTreeNodeFlags_DefaultOpen)) {
453 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
454 [ + - ]: 377 : BoolSetting("Enable Soft Lock", &engine.m_soft_lock_enabled, Tooltips::SOFT_LOCK_ENABLE);
455 [ + - ]: 377 : if (engine.m_soft_lock_enabled) {
456 [ + - ]: 377 : FloatSetting(" Stiffness", &engine.m_soft_lock_stiffness, 0.0f, 100.0f, "%.1f", Tooltips::SOFT_LOCK_STIFFNESS);
457 [ + - ]: 377 : FloatSetting(" Damping", &engine.m_soft_lock_damping, 0.0f, 5.0f, "%.2f", Tooltips::SOFT_LOCK_DAMPING);
458 : : }
459 [ + - ]: 377 : ImGui::TreePop();
460 [ + - ]: 377 : ImGui::Separator();
461 : : }
462 : :
463 [ + - ]: 377 : ImGui::TreePop();
464 : : } else {
465 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
466 : : }
467 : :
468 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Front Axle (Understeer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
469 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
470 : :
471 [ + + ]: 377 : if (engine.m_torque_source == 1) {
472 [ + - ]: 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);
473 : : } else {
474 [ + - ]: 211 : FloatSetting("Steering Shaft Gain", &engine.m_steering_shaft_gain, 0.0f, 2.0f, FormatPct(engine.m_steering_shaft_gain), Tooltips::STEERING_SHAFT_GAIN);
475 : : }
476 : :
477 [ + - ]: 377 : FloatSetting("Steering Shaft Smoothing", &engine.m_steering_shaft_smoothing, 0.000f, 0.100f, "%.3f s",
478 : : Tooltips::STEERING_SHAFT_SMOOTHING,
479 : 377 : [&]() {
480 : 377 : int ms = (int)std::lround(engine.m_steering_shaft_smoothing * 1000.0f);
481 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
482 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
483 : 377 : });
484 : :
485 [ + - ]: 377 : FloatSetting("Understeer Effect", &engine.m_understeer_effect, 0.0f, 2.0f, FormatPct(engine.m_understeer_effect),
486 : : Tooltips::UNDERSTEER_EFFECT);
487 : :
488 [ + - ]: 377 : FloatSetting("Dynamic Weight", &engine.m_dynamic_weight_gain, 0.0f, 2.0f, FormatPct(engine.m_dynamic_weight_gain),
489 : : Tooltips::DYNAMIC_WEIGHT);
490 : :
491 [ + - ]: 377 : FloatSetting(" Weight Smoothing", &engine.m_dynamic_weight_smoothing, 0.000f, 0.500f, "%.3f s",
492 : : Tooltips::WEIGHT_SMOOTHING);
493 : :
494 : 377 : const char* torque_sources[] = { "Shaft Torque (100Hz Legacy)", "In-Game FFB (400Hz LMU 1.2+)" };
495 [ + - ]: 377 : IntSetting("Torque Source", &engine.m_torque_source, torque_sources, sizeof(torque_sources)/sizeof(torque_sources[0]),
496 : : Tooltips::TORQUE_SOURCE);
497 : :
498 [ + - ]: 377 : BoolSetting("Pure Passthrough", &engine.m_torque_passthrough, Tooltips::PURE_PASSTHROUGH);
499 : :
500 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Signal Filtering", ImGuiTreeNodeFlags_DefaultOpen)) {
501 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
502 : :
503 [ + - ]: 377 : BoolSetting(" Flatspot Suppression", &engine.m_flatspot_suppression, Tooltips::FLATSPOT_SUPPRESSION);
504 [ + + ]: 377 : if (engine.m_flatspot_suppression) {
505 [ + - ]: 364 : FloatSetting(" Filter Width (Q)", &engine.m_notch_q, 0.5f, 10.0f, "Q: %.2f", Tooltips::NOTCH_Q);
506 [ + - ]: 364 : FloatSetting(" Suppression Strength", &engine.m_flatspot_strength, 0.0f, 1.0f, "%.2f", Tooltips::SUPPRESSION_STRENGTH);
507 [ + - ]: 364 : ImGui::Text(" Est. / Theory Freq");
508 [ + - ]: 364 : ImGui::NextColumn();
509 [ + - ]: 364 : ImGui::TextDisabled("%.1f Hz / %.1f Hz", engine.m_debug_freq, engine.m_theoretical_freq);
510 [ + - ]: 364 : ImGui::NextColumn();
511 : : }
512 : :
513 [ + - ]: 377 : BoolSetting(" Static Noise Filter", &engine.m_static_notch_enabled, Tooltips::STATIC_NOISE_FILTER);
514 [ + + ]: 377 : if (engine.m_static_notch_enabled) {
515 [ + - ]: 363 : FloatSetting(" Target Frequency", &engine.m_static_notch_freq, 10.0f, 100.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_FREQ);
516 [ + - ]: 363 : FloatSetting(" Filter Width", &engine.m_static_notch_width, 0.1f, 10.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_WIDTH);
517 : : }
518 : :
519 [ + - ]: 377 : ImGui::TreePop();
520 : : } else {
521 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
522 : : }
523 : :
524 [ + - ]: 377 : ImGui::TreePop();
525 : : } else {
526 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
527 : : }
528 : :
529 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Rear Axle (Oversteer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
530 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
531 : :
532 [ + - ]: 377 : FloatSetting("Lateral G Boost (Slide)", &engine.m_oversteer_boost, 0.0f, 4.0f, FormatPct(engine.m_oversteer_boost),
533 : : Tooltips::OVERSTEER_BOOST);
534 [ + - ]: 377 : FloatSetting("Lateral G", &engine.m_sop_effect, 0.0f, 2.0f, FormatDecoupled(engine.m_sop_effect, FFBEngine::BASE_NM_SOP_LATERAL), Tooltips::LATERAL_G);
535 [ + - ]: 377 : 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),
536 : : Tooltips::REAR_ALIGN_TORQUE);
537 [ + - ]: 377 : FloatSetting("Yaw Kick", &engine.m_sop_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_sop_yaw_gain, FFBEngine::BASE_NM_YAW_KICK),
538 : : Tooltips::YAW_KICK);
539 [ + - ]: 377 : FloatSetting(" Activation Threshold", &engine.m_yaw_kick_threshold, 0.0f, 10.0f, "%.2f rad/s²", Tooltips::YAW_KICK_THRESHOLD);
540 : :
541 [ + - ]: 377 : FloatSetting(" Kick Response", &engine.m_yaw_accel_smoothing, 0.000f, 0.050f, "%.3f s",
542 : : Tooltips::YAW_KICK_RESPONSE,
543 : 377 : [&]() {
544 : 377 : int ms = (int)std::lround(engine.m_yaw_accel_smoothing * 1000.0f);
545 [ + - ]: 377 : ImVec4 color = (ms <= 15) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
546 [ + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms", ms);
547 : 377 : });
548 : :
549 [ + - ]: 377 : FloatSetting("Gyro Damping", &engine.m_gyro_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_gyro_gain, FFBEngine::BASE_NM_GYRO_DAMPING), Tooltips::GYRO_DAMPING);
550 : :
551 [ + - ]: 377 : FloatSetting(" Gyro Smooth", &engine.m_gyro_smoothing, 0.000f, 0.050f, "%.3f s",
552 : : Tooltips::GYRO_SMOOTH,
553 : 377 : [&]() {
554 : 377 : int ms = (int)std::lround(engine.m_gyro_smoothing * 1000.0f);
555 [ + - ]: 377 : ImVec4 color = (ms <= 20) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
556 [ + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms", ms);
557 : 377 : });
558 : :
559 [ + - ]: 377 : ImGui::TextColored(ImVec4(0.0f, 0.6f, 0.85f, 1.0f), "Advanced SoP");
560 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
561 : :
562 [ + - ]: 377 : FloatSetting("SoP Smoothing", &engine.m_sop_smoothing_factor, 0.0f, 1.0f, "%.2f",
563 : : Tooltips::SOP_SMOOTHING,
564 : 377 : [&]() {
565 : 377 : int ms = (int)std::lround((1.0f - engine.m_sop_smoothing_factor) * 100.0f);
566 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
567 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
568 : 377 : });
569 : :
570 [ + - ]: 377 : FloatSetting("Grip Smoothing", &engine.m_grip_smoothing_steady, 0.000f, 0.100f, "%.3f s",
571 : : Tooltips::GRIP_SMOOTHING);
572 : :
573 [ + - ]: 377 : FloatSetting(" SoP Scale", &engine.m_sop_scale, 0.0f, 20.0f, "%.2f", Tooltips::SOP_SCALE);
574 : :
575 [ + - ]: 377 : ImGui::TreePop();
576 : : } else {
577 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
578 : : }
579 : :
580 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Grip & Slip Angle Estimation", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
581 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
582 : :
583 [ + - ]: 377 : FloatSetting("Slip Angle Smoothing", &engine.m_slip_angle_smoothing, 0.000f, 0.100f, "%.3f s",
584 : : Tooltips::SLIP_ANGLE_SMOOTHING,
585 : 377 : [&]() {
586 : 377 : int ms = (int)std::lround(engine.m_slip_angle_smoothing * 1000.0f);
587 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
588 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
589 : 377 : });
590 : :
591 [ + - ]: 377 : FloatSetting("Chassis Inertia (Load)", &engine.m_chassis_inertia_smoothing, 0.000f, 0.100f, "%.3f s",
592 : : Tooltips::CHASSIS_INERTIA,
593 : 377 : [&]() {
594 : 377 : int ms = (int)std::lround(engine.m_chassis_inertia_smoothing * 1000.0f);
595 [ + - ]: 377 : ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), "Simulation: %d ms", ms);
596 : 377 : });
597 : :
598 [ + - ]: 377 : FloatSetting("Optimal Slip Angle", &engine.m_optimal_slip_angle, 0.05f, 0.20f, "%.2f rad",
599 : : Tooltips::OPTIMAL_SLIP_ANGLE);
600 [ + - ]: 377 : FloatSetting("Optimal Slip Ratio", &engine.m_optimal_slip_ratio, 0.05f, 0.20f, "%.2f",
601 : : Tooltips::OPTIMAL_SLIP_RATIO);
602 : :
603 [ + - ]: 377 : ImGui::Separator();
604 [ + - ]: 377 : ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Slope Detection (Experimental)");
605 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
606 : :
607 : 377 : bool prev_slope_enabled = engine.m_slope_detection_enabled;
608 [ + - ]: 377 : GuiWidgets::Result slope_res = GuiWidgets::Checkbox("Enable Slope Detection", &engine.m_slope_detection_enabled,
609 : : Tooltips::SLOPE_DETECTION_ENABLE);
610 : :
611 [ - + ]: 377 : if (slope_res.changed) {
612 [ # # # # ]: 0 : if (!prev_slope_enabled && engine.m_slope_detection_enabled) {
613 : 0 : engine.m_slope_buffer_count = 0;
614 : 0 : engine.m_slope_buffer_index = 0;
615 : 0 : engine.m_slope_smoothed_output = 1.0;
616 : : }
617 : : }
618 [ - + ]: 377 : if (slope_res.deactivated) {
619 [ # # # # ]: 0 : Config::Save(engine);
620 : : }
621 : :
622 [ + + + + ]: 377 : if (engine.m_slope_detection_enabled && engine.m_oversteer_boost > 0.01f) {
623 [ + - ]: 362 : ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
624 : : "Note: Lateral G Boost (Slide) is auto-disabled when Slope Detection is ON.");
625 [ + - + - ]: 362 : ImGui::NextColumn(); ImGui::NextColumn();
626 : : }
627 : :
628 [ + + ]: 377 : if (engine.m_slope_detection_enabled) {
629 : 363 : int window = engine.m_slope_sg_window;
630 [ + - - + ]: 363 : if (ImGui::SliderInt(" Filter Window", &window, 5, 41)) {
631 [ # # ]: 0 : if (window % 2 == 0) window++;
632 : 0 : engine.m_slope_sg_window = window;
633 : : }
634 [ + - - + ]: 363 : if (ImGui::IsItemHovered()) {
635 [ # # ]: 0 : ImGui::SetTooltip("%s", Tooltips::SLOPE_FILTER_WINDOW);
636 : : }
637 [ + - - + : 363 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
- - - - ]
638 : :
639 [ + - ]: 363 : ImGui::SameLine();
640 : 363 : float latency_ms = (static_cast<float>(engine.m_slope_sg_window) / 2.0f) * 2.5f;
641 [ + - ]: 363 : ImVec4 color = (latency_ms < 25.0f) ? ImVec4(0,1,0,1) : ImVec4(1,0.5f,0,1);
642 [ + - ]: 363 : ImGui::TextColored(color, "~%.0f ms latency", latency_ms);
643 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
644 : :
645 [ + - ]: 363 : FloatSetting(" Sensitivity", &engine.m_slope_sensitivity, 0.1f, 5.0f, "%.1fx",
646 : : Tooltips::SLOPE_SENSITIVITY);
647 : :
648 [ + - - + ]: 363 : if (ImGui::TreeNode("Advanced Slope Settings")) {
649 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
650 [ # # ]: 0 : FloatSetting(" Slope Threshold", &engine.m_slope_min_threshold, -1.0f, 0.0f, "%.2f", Tooltips::SLOPE_THRESHOLD);
651 [ # # ]: 0 : FloatSetting(" Output Smoothing", &engine.m_slope_smoothing_tau, 0.005f, 0.100f, "%.3f s", Tooltips::SLOPE_OUTPUT_SMOOTHING);
652 : :
653 [ # # ]: 0 : ImGui::Separator();
654 [ # # ]: 0 : ImGui::Text("Stability Fixes (v0.7.3)");
655 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
656 [ # # ]: 0 : FloatSetting(" Alpha Threshold", &engine.m_slope_alpha_threshold, 0.001f, 0.100f, "%.3f", Tooltips::SLOPE_ALPHA_THRESHOLD);
657 [ # # ]: 0 : FloatSetting(" Decay Rate", &engine.m_slope_decay_rate, 0.5f, 20.0f, "%.1f", Tooltips::SLOPE_DECAY_RATE);
658 [ # # ]: 0 : BoolSetting(" Confidence Gate", &engine.m_slope_confidence_enabled, Tooltips::SLOPE_CONFIDENCE_GATE);
659 : :
660 [ # # ]: 0 : ImGui::TreePop();
661 : : } else {
662 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
663 : : }
664 : :
665 : 363 : ImGui::Text(" Live Slope: %.3f | Grip: %.0f%%",
666 : : engine.m_slope_current,
667 [ + - ]: 363 : engine.m_slope_smoothed_output * 100.0f);
668 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
669 : : }
670 : :
671 [ + - ]: 377 : ImGui::TreePop();
672 : : } else {
673 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
674 : : }
675 : :
676 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Braking & Lockup", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
677 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
678 : :
679 [ + - ]: 377 : BoolSetting("Lockup Vibration", &engine.m_lockup_enabled, Tooltips::LOCKUP_VIBRATION);
680 [ + + ]: 377 : if (engine.m_lockup_enabled) {
681 [ + - ]: 373 : FloatSetting(" Lockup Strength", &engine.m_lockup_gain, 0.0f, 3.0f, FormatDecoupled(engine.m_lockup_gain, FFBEngine::BASE_NM_LOCKUP_VIBRATION), Tooltips::LOCKUP_STRENGTH);
682 [ + - ]: 373 : FloatSetting(" Brake Load Cap", &engine.m_brake_load_cap, 1.0f, 10.0f, "%.2fx", Tooltips::BRAKE_LOAD_CAP);
683 [ + - ]: 373 : FloatSetting(" Vibration Pitch", &engine.m_lockup_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::VIBRATION_PITCH);
684 : :
685 [ + - ]: 373 : ImGui::Separator();
686 [ + - ]: 373 : ImGui::Text("Response Curve");
687 [ + - + - ]: 373 : ImGui::NextColumn(); ImGui::NextColumn();
688 : :
689 [ + - ]: 373 : FloatSetting(" Gamma", &engine.m_lockup_gamma, 0.1f, 3.0f, "%.1f", Tooltips::LOCKUP_GAMMA);
690 [ + - ]: 373 : FloatSetting(" Start Slip %", &engine.m_lockup_start_pct, 1.0f, 10.0f, "%.1f%%", Tooltips::LOCKUP_START_PCT);
691 [ + - ]: 373 : FloatSetting(" Full Slip %", &engine.m_lockup_full_pct, 5.0f, 25.0f, "%.1f%%", Tooltips::LOCKUP_FULL_PCT);
692 : :
693 [ + - ]: 373 : ImGui::Separator();
694 [ + - ]: 373 : ImGui::Text("Prediction (Advanced)");
695 [ + - + - ]: 373 : ImGui::NextColumn(); ImGui::NextColumn();
696 : :
697 [ + - ]: 373 : FloatSetting(" Sensitivity", &engine.m_lockup_prediction_sens, 10.0f, 100.0f, "%.0f", Tooltips::LOCKUP_PREDICTION_SENS);
698 [ + - ]: 373 : FloatSetting(" Bump Rejection", &engine.m_lockup_bump_reject, 0.1f, 5.0f, "%.1f m/s", Tooltips::LOCKUP_BUMP_REJECT);
699 [ + - ]: 373 : FloatSetting(" Rear Boost", &engine.m_lockup_rear_boost, 1.0f, 10.0f, "%.2fx", Tooltips::LOCKUP_REAR_BOOST);
700 : : }
701 : :
702 [ + - ]: 377 : ImGui::Separator();
703 [ + - ]: 377 : ImGui::Text("ABS & Hardware");
704 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
705 : :
706 [ + - ]: 377 : BoolSetting("ABS Pulse", &engine.m_abs_pulse_enabled, Tooltips::ABS_PULSE);
707 [ + + ]: 377 : if (engine.m_abs_pulse_enabled) {
708 [ + - ]: 360 : FloatSetting(" Pulse Gain", &engine.m_abs_gain, 0.0f, 10.0f, "%.2f", Tooltips::ABS_PULSE_GAIN);
709 [ + - ]: 360 : FloatSetting(" Pulse Frequency", &engine.m_abs_freq_hz, 10.0f, 50.0f, "%.1f Hz", Tooltips::ABS_PULSE_FREQ);
710 : : }
711 : :
712 [ + - ]: 377 : ImGui::TreePop();
713 : : } else {
714 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
715 : : }
716 : :
717 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Vibration Effects", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
718 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
719 : :
720 : 377 : bool prev_vibration_norm = engine.m_auto_load_normalization_enabled;
721 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Enable Dynamic Load Normalization", &engine.m_auto_load_normalization_enabled, Tooltips::DYNAMIC_LOAD_NORMALIZATION_ENABLE).changed) {
722 [ # # # # ]: 0 : if (prev_vibration_norm && !engine.m_auto_load_normalization_enabled) {
723 [ # # ]: 0 : engine.ResetNormalization();
724 : : }
725 [ # # # # ]: 0 : Config::Save(engine);
726 : : }
727 : :
728 [ + - ]: 377 : FloatSetting("Texture Load Cap", &engine.m_texture_load_cap, 1.0f, 3.0f, "%.2fx", Tooltips::TEXTURE_LOAD_CAP);
729 [ + - ]: 377 : FloatSetting("Vibration Strength", &engine.m_vibration_gain, 0.0f, 2.0f, FormatPct(engine.m_vibration_gain), Tooltips::VIBRATION_GAIN);
730 : :
731 [ + - ]: 377 : BoolSetting("Slide Rumble", &engine.m_slide_texture_enabled, Tooltips::SLIDE_RUMBLE);
732 [ + + ]: 377 : if (engine.m_slide_texture_enabled) {
733 [ + - ]: 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);
734 [ + - ]: 359 : FloatSetting(" Slide Pitch", &engine.m_slide_freq_scale, 0.5f, 5.0f, "%.2fx", Tooltips::SLIDE_PITCH);
735 : : }
736 : :
737 [ + - ]: 377 : BoolSetting("Road Details", &engine.m_road_texture_enabled, Tooltips::ROAD_DETAILS);
738 [ + + ]: 377 : if (engine.m_road_texture_enabled) {
739 [ + - ]: 373 : 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);
740 : : }
741 : :
742 [ + - ]: 377 : BoolSetting("Spin Vibration", &engine.m_spin_enabled, Tooltips::SPIN_VIBRATION);
743 [ + + ]: 377 : if (engine.m_spin_enabled) {
744 [ + - ]: 373 : FloatSetting(" Spin Strength", &engine.m_spin_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_spin_gain, FFBEngine::BASE_NM_SPIN_VIBRATION), Tooltips::SPIN_STRENGTH);
745 [ + - ]: 373 : FloatSetting(" Spin Pitch", &engine.m_spin_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::SPIN_PITCH);
746 : : }
747 : :
748 [ + - ]: 377 : 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);
749 : :
750 : 377 : const char* bottoming_modes[] = { "Method A: Scraping", "Method B: Susp. Spike" };
751 [ + - ]: 377 : IntSetting("Bottoming Logic", &engine.m_bottoming_method, bottoming_modes, sizeof(bottoming_modes)/sizeof(bottoming_modes[0]), Tooltips::BOTTOMING_LOGIC);
752 : :
753 [ + - ]: 377 : ImGui::TreePop();
754 : : } else {
755 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
756 : : }
757 : :
758 [ + - - + ]: 377 : if (ImGui::CollapsingHeader("Advanced Settings")) {
759 [ # # ]: 0 : ImGui::Indent();
760 : :
761 [ # # # # ]: 0 : if (ImGui::TreeNode("Stationary Vibration Gate")) {
762 : 0 : float lower_kmh = engine.m_speed_gate_lower * 3.6f;
763 [ # # # # ]: 0 : if (ImGui::SliderFloat("Mute Below", &lower_kmh, 0.0f, 20.0f, "%.1f km/h")) {
764 : 0 : engine.m_speed_gate_lower = lower_kmh / 3.6f;
765 [ # # ]: 0 : if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
766 : 0 : engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
767 : : }
768 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MUTE_BELOW);
# # ]
769 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
770 : :
771 : 0 : float upper_kmh = engine.m_speed_gate_upper * 3.6f;
772 [ # # # # ]: 0 : if (ImGui::SliderFloat("Full Above", &upper_kmh, 1.0f, 50.0f, "%.1f km/h")) {
773 : 0 : engine.m_speed_gate_upper = upper_kmh / 3.6f;
774 [ # # ]: 0 : if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
775 : 0 : engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
776 : : }
777 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::FULL_ABOVE);
# # ]
778 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
779 : :
780 [ # # ]: 0 : ImGui::TreePop();
781 : : }
782 : :
783 [ # # # # ]: 0 : if (ImGui::TreeNode("Telemetry Logger")) {
784 [ # # # # ]: 0 : if (ImGui::Checkbox("Auto-Start on Session", &Config::m_auto_start_logging)) {
785 [ # # # # ]: 0 : Config::Save(engine);
786 : : }
787 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::AUTO_START_LOGGING);
# # ]
788 : :
789 : : char log_path_buf[256];
790 : : #ifdef _WIN32
791 : : strncpy_s(log_path_buf, sizeof(log_path_buf), Config::m_log_path.c_str(), _TRUNCATE);
792 : : #else
793 : 0 : strncpy(log_path_buf, Config::m_log_path.c_str(), 255);
794 : : #endif
795 : 0 : log_path_buf[255] = '\0';
796 [ # # # # ]: 0 : if (ImGui::InputText("Log Path", log_path_buf, 255)) {
797 [ # # ]: 0 : Config::m_log_path = log_path_buf;
798 : : }
799 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_PATH);
# # ]
800 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
801 : :
802 [ # # # # ]: 0 : if (AsyncLogger::Get().IsLogging()) {
803 [ # # # # : 0 : ImGui::BulletText("Filename: %s", AsyncLogger::Get().GetFilename().c_str());
# # ]
804 : : }
805 : :
806 [ # # ]: 0 : ImGui::TreePop();
807 : : }
808 [ # # ]: 0 : ImGui::Unindent();
809 : : }
810 : :
811 [ + - ]: 377 : ImGui::Columns(1);
812 [ + - ]: 377 : ImGui::End();
813 : 377 : }
814 : :
815 : : const float PLOT_HISTORY_SEC = 10.0f;
816 : : const int PHYSICS_RATE_HZ = 400;
817 : : const int PLOT_BUFFER_SIZE = (int)(PLOT_HISTORY_SEC * PHYSICS_RATE_HZ);
818 : :
819 : : struct RollingBuffer {
820 : : std::vector<float> data;
821 : : int offset = 0;
822 : :
823 : 46 : RollingBuffer() {
824 [ + - ]: 46 : data.resize(PLOT_BUFFER_SIZE, 0.0f);
825 : 46 : }
826 : :
827 : 0 : void Add(float val) {
828 : 0 : data[offset] = val;
829 : 0 : offset = (offset + 1) % (int)data.size();
830 : 0 : }
831 : :
832 : 992 : float GetCurrent() const {
833 [ - + ]: 992 : if (data.empty()) return 0.0f;
834 : 992 : size_t idx = (offset - 1 + (int)data.size()) % (int)data.size();
835 : 992 : return data[idx];
836 : : }
837 : :
838 : 992 : float GetMin() const {
839 [ - + ]: 992 : if (data.empty()) return 0.0f;
840 [ + - ]: 992 : return *std::min_element(data.begin(), data.end());
841 : : }
842 : :
843 : 992 : float GetMax() const {
844 [ - + ]: 992 : if (data.empty()) return 0.0f;
845 [ + - ]: 992 : return *std::max_element(data.begin(), data.end());
846 : : }
847 : : };
848 : :
849 : 992 : inline void PlotWithStats(const char* label, const RollingBuffer& buffer,
850 : : float scale_min, float scale_max,
851 : : const ImVec2& size = ImVec2(0, 40),
852 : : const char* tooltip = nullptr) {
853 [ + - ]: 992 : ImGui::Text("%s", label);
854 : : char hidden_label[256];
855 : 992 : snprintf(hidden_label, sizeof(hidden_label), "##%s", label);
856 [ + - ]: 992 : ImGui::PlotLines(hidden_label, buffer.data.data(), (int)buffer.data.size(),
857 : 992 : buffer.offset, NULL, scale_min, scale_max, size);
858 [ - + - - : 992 : if (tooltip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
- - - + -
- ]
859 : :
860 : 992 : float current = buffer.GetCurrent();
861 [ + - ]: 992 : float min_val = buffer.GetMin();
862 [ + - ]: 992 : float max_val = buffer.GetMax();
863 : : char stats_overlay[128];
864 : 992 : snprintf(stats_overlay, sizeof(stats_overlay), "Cur:%.4f Min:%.3f Max:%.3f", current, min_val, max_val);
865 : :
866 [ + - ]: 992 : ImVec2 p_min = ImGui::GetItemRectMin();
867 [ + - ]: 992 : ImVec2 p_max = ImGui::GetItemRectMax();
868 : 992 : float plot_width = p_max.x - p_min.x;
869 : 992 : p_min.x += 2; p_min.y += 2;
870 : :
871 [ + - ]: 992 : ImDrawList* draw_list = ImGui::GetWindowDrawList();
872 [ + - ]: 992 : ImFont* font = ImGui::GetFont();
873 [ + - ]: 992 : float font_size = ImGui::GetFontSize();
874 [ + - ]: 992 : ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
875 : :
876 [ - + ]: 992 : if (text_size.x > plot_width - 4) {
877 : 0 : snprintf(stats_overlay, sizeof(stats_overlay), "%.4f [%.3f, %.3f]", current, min_val, max_val);
878 [ # # ]: 0 : text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
879 [ # # ]: 0 : if (text_size.x > plot_width - 4) {
880 : 0 : snprintf(stats_overlay, sizeof(stats_overlay), "Val: %.4f", current);
881 [ # # ]: 0 : text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
882 : : }
883 : : }
884 [ + - ]: 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));
885 [ + - ]: 992 : draw_list->AddText(font, font_size, p_min, IM_COL32(255, 255, 255, 255), stats_overlay);
886 : 992 : }
887 : :
888 : : // Global Buffers
889 : : 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;
890 : : 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;
891 : : 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;
892 : :
893 : : static bool g_warn_dt = false;
894 : :
895 : 0 : void GuiLayer::UpdateTelemetry(FFBEngine& engine) {
896 [ # # ]: 0 : auto snapshots = engine.GetDebugBatch();
897 [ # # ]: 0 : for (const auto& snap : snapshots) {
898 : 0 : m_latest_steering_range = snap.steering_range_deg;
899 : 0 : m_latest_steering_angle = snap.steering_angle_deg;
900 : :
901 : 0 : plot_total.Add(snap.total_output);
902 : 0 : plot_base.Add(snap.base_force);
903 : 0 : plot_sop.Add(snap.sop_force);
904 : 0 : plot_yaw_kick.Add(snap.ffb_yaw_kick);
905 : 0 : plot_rear_torque.Add(snap.ffb_rear_torque);
906 : 0 : plot_gyro_damping.Add(snap.ffb_gyro_damping);
907 : 0 : plot_scrub_drag.Add(snap.ffb_scrub_drag);
908 : 0 : plot_soft_lock.Add(snap.ffb_soft_lock);
909 : 0 : plot_oversteer.Add(snap.oversteer_boost);
910 : 0 : plot_understeer.Add(snap.understeer_drop);
911 : 0 : plot_clipping.Add(snap.clipping);
912 : 0 : plot_road.Add(snap.texture_road);
913 : 0 : plot_slide.Add(snap.texture_slide);
914 : 0 : plot_lockup.Add(snap.texture_lockup);
915 : 0 : plot_spin.Add(snap.texture_spin);
916 : 0 : plot_bottoming.Add(snap.texture_bottoming);
917 : 0 : plot_calc_front_load.Add(snap.calc_front_load);
918 : 0 : plot_calc_rear_load.Add(snap.calc_rear_load);
919 : 0 : plot_calc_front_grip.Add(snap.calc_front_grip);
920 : 0 : plot_calc_rear_grip.Add(snap.calc_rear_grip);
921 : 0 : plot_calc_slip_ratio.Add(snap.calc_front_slip_ratio);
922 : 0 : plot_calc_slip_angle_smoothed.Add(snap.calc_front_slip_angle_smoothed);
923 : 0 : plot_calc_rear_slip_angle_smoothed.Add(snap.calc_rear_slip_angle_smoothed);
924 : 0 : plot_calc_rear_lat_force.Add(snap.calc_rear_lat_force);
925 : 0 : plot_slope_current.Add(snap.slope_current);
926 : 0 : plot_raw_steer.Add(snap.steer_force);
927 : 0 : plot_raw_shaft_torque.Add(snap.raw_shaft_torque);
928 : 0 : plot_raw_gen_torque.Add(snap.raw_gen_torque);
929 : 0 : plot_raw_input_steering.Add(snap.raw_input_steering);
930 : 0 : plot_raw_throttle.Add(snap.raw_input_throttle);
931 : 0 : plot_raw_brake.Add(snap.raw_input_brake);
932 : 0 : plot_input_accel.Add(snap.accel_x);
933 : 0 : plot_raw_car_speed.Add(snap.raw_car_speed);
934 : 0 : plot_raw_load.Add(snap.raw_front_tire_load);
935 : 0 : plot_raw_grip.Add(snap.raw_front_grip_fract);
936 : 0 : plot_raw_rear_grip.Add(snap.raw_rear_grip);
937 : 0 : plot_raw_front_slip_ratio.Add(snap.raw_front_slip_ratio);
938 : 0 : plot_raw_susp_force.Add(snap.raw_front_susp_force);
939 : 0 : plot_raw_ride_height.Add(snap.raw_front_ride_height);
940 : 0 : plot_raw_front_lat_patch_vel.Add(snap.raw_front_lat_patch_vel);
941 : 0 : plot_raw_front_long_patch_vel.Add(snap.raw_front_long_patch_vel);
942 : 0 : plot_raw_rear_lat_patch_vel.Add(snap.raw_rear_lat_patch_vel);
943 : 0 : plot_raw_rear_long_patch_vel.Add(snap.raw_rear_long_patch_vel);
944 : 0 : plot_raw_slip_angle.Add(snap.raw_front_slip_angle);
945 : 0 : plot_raw_rear_slip_angle.Add(snap.raw_rear_slip_angle);
946 : 0 : plot_raw_front_deflection.Add(snap.raw_front_deflection);
947 : 0 : g_warn_dt = snap.warn_dt;
948 : : }
949 : 0 : }
950 : :
951 : 63 : void GuiLayer::DrawDebugWindow(FFBEngine& engine) {
952 [ + + ]: 63 : if (!Config::show_graphs) return;
953 : :
954 : 62 : ImGuiViewport* viewport = ImGui::GetMainViewport();
955 [ + - ]: 62 : ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + CONFIG_PANEL_WIDTH, viewport->Pos.y));
956 [ + - ]: 62 : ImGui::SetNextWindowSize(ImVec2(viewport->Size.x - CONFIG_PANEL_WIDTH, viewport->Size.y));
957 : :
958 : 62 : ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
959 : 62 : ImGui::Begin("FFB Analysis", nullptr, flags);
960 : :
961 : : // System Health Diagnostics (Moved from Tuning window - Issue #149)
962 [ + - ]: 62 : if (ImGui::CollapsingHeader("System Health (Hz)", ImGuiTreeNodeFlags_DefaultOpen)) {
963 : 62 : ImGui::Columns(5, "RateCols", false);
964 : 62 : DisplayRate("FFB Loop", engine.m_ffb_rate, 400.0);
965 : 62 : ImGui::NextColumn();
966 : 62 : DisplayRate("Telemetry", engine.m_telemetry_rate, 400.0);
967 : 62 : ImGui::NextColumn();
968 : 62 : DisplayRate("Hardware", engine.m_hw_rate, 400.0);
969 : 62 : ImGui::NextColumn();
970 : 62 : DisplayRate("S.Torque", engine.m_torque_rate, 400.0);
971 : 62 : ImGui::NextColumn();
972 : 62 : DisplayRate("G.Torque", engine.m_gen_torque_rate, 400.0);
973 : 62 : ImGui::Columns(1);
974 [ - + - - : 62 : if ((engine.m_telemetry_rate < 380.0 || engine.m_torque_rate < 380.0) && engine.m_telemetry_rate > 1.0 && GameConnector::Get().IsConnected()) {
+ + + - +
+ ]
975 [ + - ]: 6 : ImGui::TextColored(ImVec4(1, 1, 0, 1), "Warning: Low telemetry/torque rate. Check game FFB settings.");
976 : : }
977 : 62 : ImGui::Separator();
978 : : }
979 : :
980 : :
981 [ - + ]: 62 : if (g_warn_dt) {
982 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
983 : 0 : ImGui::Text("TELEMETRY WARNINGS: - Invalid DeltaTime");
984 : 0 : ImGui::PopStyleColor();
985 : 0 : ImGui::Separator();
986 : : }
987 : :
988 [ + - ]: 62 : if (ImGui::CollapsingHeader("A. FFB Components (Output)", ImGuiTreeNodeFlags_DefaultOpen)) {
989 [ + - ]: 62 : PlotWithStats("Total Output", plot_total, -1.0f, 1.0f, ImVec2(0, 60));
990 : 62 : ImGui::Separator();
991 : 62 : ImGui::Columns(3, "FFBMain", false);
992 [ + - ]: 62 : ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), "[Main Forces]");
993 [ + - ]: 62 : PlotWithStats("Base Torque (Nm)", plot_base, -30.0f, 30.0f);
994 [ + - ]: 62 : PlotWithStats("SoP (Chassis G)", plot_sop, -20.0f, 20.0f);
995 [ + - ]: 62 : PlotWithStats("Yaw Kick", plot_yaw_kick, -20.0f, 20.0f);
996 [ + - ]: 62 : PlotWithStats("Rear Align", plot_rear_torque, -20.0f, 20.0f);
997 [ + - ]: 62 : PlotWithStats("Gyro Damping", plot_gyro_damping, -20.0f, 20.0f);
998 [ + - ]: 62 : PlotWithStats("Scrub Drag", plot_scrub_drag, -20.0f, 20.0f);
999 [ + - ]: 62 : PlotWithStats("Soft Lock", plot_soft_lock, -50.0f, 50.0f);
1000 : 62 : ImGui::NextColumn();
1001 [ + - ]: 62 : ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.7f, 1.0f), "[Modifiers]");
1002 [ + - ]: 62 : PlotWithStats("Lateral G Boost", plot_oversteer, -20.0f, 20.0f);
1003 [ + - ]: 62 : PlotWithStats("Understeer Cut", plot_understeer, -20.0f, 20.0f);
1004 [ + - ]: 62 : PlotWithStats("Clipping", plot_clipping, 0.0f, 1.1f);
1005 : 62 : ImGui::NextColumn();
1006 [ + - ]: 62 : ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "[Textures]");
1007 [ + - ]: 62 : PlotWithStats("Road Texture", plot_road, -10.0f, 10.0f);
1008 [ + - ]: 62 : PlotWithStats("Slide Texture", plot_slide, -10.0f, 10.0f);
1009 [ + - ]: 62 : PlotWithStats("Lockup Vib", plot_lockup, -10.0f, 10.0f);
1010 [ + - ]: 62 : PlotWithStats("Spin Vib", plot_spin, -10.0f, 10.0f);
1011 [ + - ]: 62 : PlotWithStats("Bottoming", plot_bottoming, -10.0f, 10.0f);
1012 : 62 : ImGui::Columns(1);
1013 : : }
1014 : :
1015 [ - + ]: 62 : if (ImGui::CollapsingHeader("B. Internal Physics (Brain)", ImGuiTreeNodeFlags_None)) {
1016 [ # # ]: 0 : ImGui::Columns(3, "PhysCols", false);
1017 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Loads]");
1018 [ # # ]: 0 : ImGui::Text("Front: %.0f N | Rear: %.0f N", plot_calc_front_load.GetCurrent(), plot_calc_rear_load.GetCurrent());
1019 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 1.0f, 1.0f));
1020 [ # # ]: 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));
1021 [ # # ]: 0 : ImGui::PopStyleColor();
1022 [ # # ]: 0 : ImVec2 pos_load = ImGui::GetItemRectMin();
1023 [ # # ]: 0 : ImGui::SetCursorScreenPos(pos_load);
1024 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0,0,0,0));
1025 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 1.0f, 1.0f));
1026 [ # # ]: 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));
1027 [ # # ]: 0 : ImGui::PopStyleColor(2);
1028 [ # # ]: 0 : ImGui::NextColumn();
1029 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Grip/Slip]");
1030 [ # # ]: 0 : PlotWithStats("Calc Front Grip", plot_calc_front_grip, 0.0f, 1.2f);
1031 [ # # ]: 0 : PlotWithStats("Calc Rear Grip", plot_calc_rear_grip, 0.0f, 1.2f);
1032 [ # # ]: 0 : PlotWithStats("Front Slip Ratio", plot_calc_slip_ratio, -1.0f, 1.0f);
1033 [ # # ]: 0 : PlotWithStats("Front Slip Angle", plot_calc_slip_angle_smoothed, 0.0f, 1.0f);
1034 [ # # ]: 0 : PlotWithStats("Rear Slip Angle", plot_calc_rear_slip_angle_smoothed, 0.0f, 1.0f);
1035 [ # # # # ]: 0 : if (engine.m_slope_detection_enabled) PlotWithStats("Slope", plot_slope_current, -5.0f, 5.0f);
1036 [ # # ]: 0 : ImGui::NextColumn();
1037 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Forces]");
1038 [ # # ]: 0 : PlotWithStats("Calc Rear Lat Force", plot_calc_rear_lat_force, -5000.0f, 5000.0f);
1039 [ # # ]: 0 : ImGui::Columns(1);
1040 : : }
1041 : :
1042 [ - + ]: 62 : if (ImGui::CollapsingHeader("C. Raw Game Telemetry (Input)", ImGuiTreeNodeFlags_None)) {
1043 [ # # ]: 0 : ImGui::Columns(4, "TelCols", false);
1044 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Driver Input]");
1045 [ # # ]: 0 : PlotWithStats("Selected Torque", plot_raw_steer, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SELECTED_TORQUE);
1046 [ # # ]: 0 : PlotWithStats("Shaft Torque (100Hz)", plot_raw_shaft_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SHAFT_TORQUE);
1047 [ # # ]: 0 : PlotWithStats("In-Game FFB (400Hz)", plot_raw_gen_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_INGAME_FFB);
1048 [ # # ]: 0 : PlotWithStats("Steering Input", plot_raw_input_steering, -1.0f, 1.0f);
1049 [ # # ]: 0 : ImGui::Text("Combined Input");
1050 [ # # ]: 0 : ImVec2 pos = ImGui::GetCursorScreenPos();
1051 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
1052 [ # # ]: 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));
1053 [ # # ]: 0 : ImGui::PopStyleColor();
1054 [ # # ]: 0 : ImGui::SetCursorScreenPos(pos);
1055 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
1056 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
1057 [ # # ]: 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));
1058 [ # # ]: 0 : ImGui::PopStyleColor(2);
1059 [ # # ]: 0 : ImGui::NextColumn();
1060 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Vehicle State]");
1061 [ # # ]: 0 : PlotWithStats("Lat Accel", plot_input_accel, -20.0f, 20.0f);
1062 [ # # ]: 0 : PlotWithStats("Speed (m/s)", plot_raw_car_speed, 0.0f, 100.0f);
1063 [ # # ]: 0 : ImGui::NextColumn();
1064 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Raw Tire Data]");
1065 [ # # ]: 0 : PlotWithStats("Raw Front Load", plot_raw_load, 0.0f, 10000.0f);
1066 [ # # ]: 0 : PlotWithStats("Raw Front Grip", plot_raw_grip, 0.0f, 1.2f);
1067 [ # # ]: 0 : PlotWithStats("Raw Rear Grip", plot_raw_rear_grip, 0.0f, 1.2f);
1068 [ # # ]: 0 : ImGui::NextColumn();
1069 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Patch Velocities]");
1070 [ # # ]: 0 : PlotWithStats("F-Lat PatchVel", plot_raw_front_lat_patch_vel, 0.0f, 20.0f);
1071 [ # # ]: 0 : PlotWithStats("R-Lat PatchVel", plot_raw_rear_lat_patch_vel, 0.0f, 20.0f);
1072 [ # # ]: 0 : PlotWithStats("F-Long PatchVel", plot_raw_front_long_patch_vel, -20.0f, 20.0f);
1073 [ # # ]: 0 : PlotWithStats("R-Long PatchVel", plot_raw_rear_long_patch_vel, -20.0f, 20.0f);
1074 [ # # ]: 0 : ImGui::Columns(1);
1075 : : }
1076 : :
1077 : 62 : ImGui::End();
1078 : : }
1079 : : #endif
|