feat: add Gantry race control overlay - #654
Draft
23Kev wants to merge 15 commits into
Draft
Conversation
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
15 tasks
Ports the Gantry feature from feat/gantry (merge-base ~105 commits behind) onto current main. Replanted rather than rebased: 27 files carry over unchanged, the registration edits were re-done by hand against main's current structure, and four contested integrations were ported by intent. Deliberate deviations from the original branch: - Gantry window: ported the window itself, dropped the bundled hasInteractiveWidgets mouse-event rework. No widget config defines `interactive`, so it was dead code, and it conflicts with main's rewritten click-through/shrink-wrap handling. - App.tsx: the Gantry route now sits inside ErrorBoundary (main added it after the branch diverged) so a crash no longer white-screens the window with no recovery. - padCarNum: fixed an off-by-one. /^0+/ on an all-zero number like "00" also consumed the significant digit and over-counted the padding; leading zeros are now derived by length difference. - Settings nav: main moved widget entries into menuItems.ts, so the Gantry entry goes there rather than into SettingsMenu.tsx. - Dropped GANTRY_PERF_INVESTIGATION.md (scratch notes, superseded by main's perf harness). - Replaced stale react-hooks/exhaustive-deps suppressions with the real dependencies; main migrated to @eslint-react and the old rule name no longer resolves. The missing deps were all stable Zustand actions. The native irsdk_node.cc change routes ReplaySearchSessionTime and ReplaySetPlayPosition through the int broadcast overload; the float overload scales by 65536 and corrupted the seek target. This needs a native rebuild to take effect. Known issues carried over and NOT fixed here — see the review notes: standings freeze after first paint, lap graph only receives the sliced driver list, no session-lifecycle reset on either new store, sync fs I/O in incidentStorage, and a false-positive incident burst when replay is paused. These are follow-up commits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
calculateSpeed returned 0 for three different "cannot compute" cases: a non-advancing session clock, an unchanged position, and backwards movement. Callers could not tell those apart from a genuinely stationary car, so each one fed 0 km/h into the rolling average and tripped the sustained-slow crash detector. Two observed consequences: - Pausing or scrubbing a replay freezes SessionTime, so every on-track car reported 0 km/h and was crashed within ~0.4s. This fired during the exact workflow the feature exists for. - Remote cars' CarIdxLapDistPct arrives over the network slower than we poll, so a moving car produces ticks with an unchanged position. A car trickling toward pit entry accumulated enough of these to be reported as crashed several seconds before PitEntry was recognised. calculateSpeed now returns null for all three cases. A null sample is skipped rather than buffered, so the car keeps its last known speed, and the two speed-based detectors (sustained-slow, sudden-stop) sit out that tick. slowFrameCount is held rather than reset on a null tick: resetting would let a genuinely stopped car escape detection whenever its position failed to refresh. Thresholds, PitEntry, off-track and slowdown-flag detection are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eport Two false-classification bugs in the crash detectors, both found from live race logs. Crashes off the racing surface were never reported as crashes. Both detectors gated on `surface === OnTrack`, but a car in a gravel trap or against a barrier reports OffTrack — so the most common crash there is only ever produced an OffTrack incident. A driver who speared off into the barrier at the Nordschleife was logged as "Off-track for 3 frames" and nothing more. Both detectors now accept OnTrack or OffTrack. Pit surfaces stay excluded: a stationary car in its pit stall is not an incident. The race-start grid teleport was reported as a crash for every car. On a practice/qualifying -> race changeover iRacing lifts each car off the track at whatever speed it was doing and sets it down stationary on the grid, which leaves racing speeds in the sudden-stop buffer and then reads as stopped: Crash car=22 sudden-stop "Speed dropped from 226.2 to 0.2 km/h" Crash car=6 sudden-stop "Speed dropped from 209.9 to 0.1 km/h" Sustained-slow already gated on sessionState === Racing; sudden-stop never did. Gridding happens in GetInCar/Warmup/ParadeLaps, so the same gate discards the sequence. Stale buffer entries age out within suddenStopFrames ticks, long before the green flag. Also adds the positive sudden-stop test the suite never had — without it, gating the detector could have disabled it outright with nothing failing — plus coverage for a car coming to rest in gravel and for a car sitting stationary in its pit stall. updateSession no longer logs on every session-YAML republish (~1/sec); it logs only when the driver roster actually changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two drivers who collided were each logged only as "Off-track for 3 frames", with nothing tying them together or marking the incident as serious. iRacing's telemetry carries no collision flag, so contact has to be inferred. When a car's off-track debounce trips, the detector now looks for another car that had an incident-worthy moment within 3 seconds and 30 metres of the same track position. If it finds one, the incident is emitted as a Crash instead of an OffTrack and the evidence names the other driver. Incident stays single-car, so storage and the UI are unchanged — the incident is upgraded in place rather than gaining a new type. The window is deliberately loose. In the logged collision the two cars' incidents were 2 seconds apart: one spun immediately while the other ran on and only left the road later. Same-tick matching would have missed it entirely. The trade-off is that two cars independently running wide at the same corner can be paired, which is why the evidence reads "likely contact" rather than asserting it. Anomalies are also recorded when either crash detector fires, so a car that is hit and stops still pairs with a partner that leaves the road afterwards. Only the most recent moment per car is kept, so the map is bounded by field size; it is cleared with carStates on a session change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The standings tab took one snapshot when it loaded and never updated again, so positions, gaps and pit status were frozen. Each view now reads standings itself, so only that view redraws instead of the whole window. The lap graph was only tracking the few drivers near you, not the whole field. Neither of the new stores was ever cleared, so incidents and lap history carried over between sessions. Both now reset when the sim disconnects, and the incident list is capped. Also widened the debug frame history from 10 frames to 60 — half a second was only capturing the aftermath of a crash, not the impact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Incident storage used synchronous file reads and writes, which stall the main process and every renderer with it. Worst of all, saving one incident re-read and re-parsed the whole session file first, so the cost grew with every incident logged. Incidents are now kept in memory and written asynchronously, with writes debounced by 250ms so a burst becomes one write. Pending writes are flushed on quit so nothing is lost. Also fixes a race where two incidents arriving at once could overwrite each other, and updates two call sites for the now-async API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A car that stopped after its qualifying laps was logged as a crash. The check for "is the session running" was SessionState.Racing, which is also true during qualifying and practice, so any car parking up looked like an incident. The detector now reads the session type from the session data and only reports a stopped car during a race. Sudden stops are still detected in every session, so a real impact in qualifying is still caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two cars running wide at the same corner were being reported as contact, because the check was just "another car had an incident nearby at about the same time". At tracks where cars regularly cut a corner, that happens constantly. Contact now also requires that at least one of the two cars lost a meaningful amount of speed. A car that runs wide at 113 km/h and carries straight on at the same pace has missed the apex, not been hit. Each car tracks a recent peak speed that decays over about two seconds, so "lost speed" means slower than it was a moment ago rather than slower than its best all lap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gap and Int were empty in the standings. Those values are only worked out when the settings say they're enabled, and the Gantry was passing nothing. The lap graph drew every driver in the same colour with no legend, so the lines were impossible to tell apart. It now colours up to 8 drivers (the player plus the leaders) and draws the rest as faint grey context lines, because colouring 40 drivers is unreadable no matter what palette you use. A legend lists the coloured drivers and says how many others are on the chart. Clicking a driver highlights their line. Colours follow the driver, not their position, so a car keeps its colour as positions change through the race. Axis labels were 8px and nearly invisible; they're now readable, and the Y axis says what it's actually measuring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lap graph mixed practice data into the race. Lap gaps are stored against a lap number, so practice lap 5 and race lap 5 landed in the same slot. Nothing ever cleared the store between sessions. It now listens to the session lifecycle events the main process already publishes, and clears on a session change or a disconnect. Each car's last seen lap number is cleared at the same time. Without that the new session's early laps look like going backwards, and no gaps get recorded until cars pass their old lap count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the same shape as fuel projection. Incidents now go over a named channel, so only the Gantry window receives them instead of every overlay on every screen. Detection logic is unchanged - wrapped, not rewritten, with its tests untouched. Also picked up: - The six race control IPC handlers now validate renderer input. Three of them pass values to the sim. - Detection and publishing show up separately in the performance log. - Detection keeps running when the Gantry window is closed, so incidents are still recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an Auto/km/h/mph picker like the other overlays have. Values are still saved in km/h, so incident detection does not change. Only the settings inputs convert. Rebuilds the settings screen to use the same tabs and rows as every other overlay, and gives each setting a description. Auto cannot read iRacing's unit setting in the settings window, so it shows km/h there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every tab, filter chip, dropdown, replay button and legend entry now explains what it does on hover and on keyboard focus. The window is full of overflow-hidden panels, so the tooltip renders into document.body through a portal and is placed from the trigger's rect. Otherwise it gets clipped. Also adds a header row to the standings so the columns can be explained once, instead of on every driver row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main added src/frontend/utils/units.ts as the single home for speed conversion, so the Gantry copy has to go. Deletes gantryUnits.ts and the two hooks that wrapped it. Nothing used them. Renames the setting to speedUnit with 'mph' | 'km/h' | 'auto' values, matching Battle, so it passes straight into resolveSpeedUnit with no mapping in between. Auto now falls back to mph when it cannot read iRacing's setting. That is what the shared util does. It used to fall back to km/h. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
|
Hey @23Kev here's a prompt to migrate the remaining telemetry to channels: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds the Gantry overlay - a race control view for spectating and replay review. It shows a full standings list, a live incident feed, and a lap-by-lap gap graph, and it is built for a spare screen rather than for use while driving.
Incidents are detected in the main process from telemetry: off-tracks, pit entries, sustained slow running, sudden stops, black flags, and car-to-car contact. Clicking an incident jumps the replay to it. Clicking a driver in the standings points the camera at that car.
Still draft. Two architecture items are outstanding and will be added to this PR:
How it is built
Incident detection follows the processor and channel pattern rather than its own wiring:
IncidentProcessorimplementsTelemetryProcessor<Incident[]>. It is deterministic and does no I/O.IncidentRuntimeowns the channel publishing, persistence, and performance metrics.raceControl.incidentschannel, so only the Gantry window receives them rather than every overlay on every screen.One deliberate difference from
FuelProjectionRuntime: the incident processor is created eagerly and is not gated on subscriber count. Gating it would stop detection and disk logging whenever the Gantry window was closed.Incident storage is async and debounced, with a synchronous flush on quit. The stored list is capped and old sessions are pruned on a retention setting.
The six race control IPC handlers validate renderer input before acting; three of them pass values through to the sim.
Testing notes
The detection logic was built and corrected against real race logs across many sessions - AI races, spectated races, and practice - and several false-positive classes were fixed that way. The two most recent commits (the lap gap reset and the processor conversion) are covered by unit tests but have not yet had a session in the sim.
One unrelated test fails locally on Windows:
tools/telemetry-replay/validator.spec.tshitsENOTEMPTYcleaning up its own temp directory. It fails the same way on a cleanmainand this branch does not touchtools/.Screenshots
Before
N/A - new widget.
After
Type of Change
Checklist
npm testnpm run lintand fixed any issues🤖 Generated with Claude Code