Skip to content

Lap detection & sessions

GT7 doesn't send "lap completed" messages — the datalogger has to cut clean laps out of a 60 Hz packet stream that also includes menus, replays, pauses, and restarts. This page explains exactly how.

What gets sampled

For every packet, a sample is recorded onto the in-progress lap only if all of these hold:

  • the car is on track (flag bit 0) and the game is not paused (bit 1);
  • the race isn't already finished — after the checkered flag GT7 reports current_lap = total_laps + 1 for the cool-down lap, which is skipped.

Lap 0 — GT7's number for the time-trial out-lap — is buffered but can never commit as a normal lap. It has to be buffered, because a single-lap replay streams entirely as lap 0 (see replay salvage below); but the only way a lap-0 buffer becomes a saved lap is that salvage path, where GT7's own reported time must vouch for it — a real out-lap has no such time, so out-laps still record nothing. The lap-0 buffer is capped at 15 minutes: some menu-adjacent states also report lap 0 while nominally on track, and no plausible GT7 lap approaches 15 minutes, so the cap sheds accumulated noise without ever touching a salvageable lap.

Packets with the LOADING flag set are never sampled, but they are not inert: a LOADING transition with a full lap in the buffer is one of the replay-salvage triggers — a replay's ending streams LOADING while the menu builds, and that is the moment the buffered lap is either saved or given up.

Time and distance are synthesized from the console's own packet counter, not from wall clocks or a fixed tick assumption:

  • each sample covers Δpacket_id frames (clamped to 1–60; a pid reset, non-monotonic value, or a gap over 1 s falls back to a single frame);
  • t += Δframes × 1/60 seconds, dist += speed_mps × Δframes × 1/60 meters.

Dropped datagrams therefore widen the time/distance steps instead of silently compressing the axes. The pid tracker also advances on paused, off-track and loading packets, so resuming sees a ~1-frame gap — inactive frames add no lap time or distance. Frames lost in transit are counted and reported as frames_dropped in /api/status and Settings › Health.

Input metrics (full-throttle %, braking %, coasting %, tire spin, TCS/ASM activity) are time-weighted using the t deltas, so a sample recorded after a gap counts for the whole gap it covers.

When a lap completes

A lap-counter change is only accepted as a real completed lap when all four conditions hold:

  1. the previous lap number was > 0 (a real lap, not the out-lap);
  2. the counter moved exactly +1 (monotonic step);
  3. the game reported a positive last_lap_time_ms;
  4. the lap collected at least 600 samples (~10 s of received packets at 60 Hz).

Condition 4 is the phantom-lap guard: in menus and replays GT7's lap counter flickers through stale values and re-reports an old last_lap_time. Requiring 10 seconds of actual on-track samples filters all of that out.

The lap time itself is taken verbatim from GT7's last_lap_time_ms — it is never computed from tick counts, so it matches the in-game timing exactly. (The one exception is the manual Log lap now action, which saves a partial lap and derives its time from the sample clock.)

Why state is committed before the database write

Packets keep arriving at 60 Hz while a lap is being written to SQLite. All processor state — lap counter, sample buffer, fuel and engine aggregates — is committed before the asynchronous save starts; otherwise the lap boundary would re-trigger on the next packet and duplicate the lap. There's a regression test for exactly this.

Replay salvage

GT7 streams a replay exactly like driving — there is no replay flag anywhere in the packet — so a multi-lap race replay records its laps through the normal path above. The failure case was the single-lap replay (the time-trial leaderboard's watch replay): it ends at the finish line, so the +1 counter step that commits a lap is never observed. The fully-driven lap sat in the buffer, the stream cut to LOADING, the buffer was discarded, and the now-empty session was deleted — the replay left no trace at all. (#26 is what made this matter: the lap most worth co-selecting in a cross-session comparison is the leader's.)

Now, when the stream breaks off with a lap in the buffer — a menu/LOADING transition, a lap-counter reset, or a car change — the buffer is saved as a real lap only when GT7's own reported lap time vouches for it:

Check Value Why
Minimum ticks the same 600-sample phantom guard as the counter path menus flicker stale lap counters at stream boundaries too
Time agreement GT7's reported lap time (last/best, read from the final packets) within max(500 ms, 0.5 %) of the integrated duration the only evidence the buffer covers exactly one lap — an aborted half-lap has no matching GT7 time and is still discarded, as is a race replay's final lap when the replay cuts before the flag

The buffer can also hold more than the lap: replays open with pre-roll — footage from before the line — and a stream may run a beat past the crossing before cutting to LOADING. Raw, either would fail the time check or anchor the distance axis in the wrong place. With packet C, GT7's own live lap clock resolves both: the clock re-anchors at every line crossing, so a backward jump of more than 250 ms splits the buffer into segments — pre-roll, the flying lap, a post-line stub — and within each, a frozen head (the clock parked at 0 until the line) or frozen tail (the stream holding its final frame) is walked off. (250 ms because ordinary jitter and packet loss move the clock by a frame or two, while a genuine re-anchor jumps by whole seconds — the two are nowhere near each other.) Each segment is tried against the reported times, latest first, so the lap that just ended wins even when a looping replay holds more than one match. The salvaged lap's distance axis therefore starts at the start line, which is what lets it align with driven laps in a comparison.

A salvaged lap keeps GT7's reported time — the same verbatim rule as a normal lap — flows through the same span guard and save path as any other, and carries a salvaged flag on the completed-lap event, the stored row, and every lap summary, so a time on the Bests board can always be traced to its provenance. Salvage runs before the zero-lap session deletion, so the replay's session persists — and a salvaged lap ends its session unless driving resumes in the same car on the next lap, which is a pit lane taking the car over the line (#136). Otherwise the stream it came from broke off, so whatever streams next (another replay, your own driving in the same car) opens a fresh session. Without that split, a lap-0 replay would share a session with the driving after it — the counter never comes down from above zero, so no lap-counter reset would ever separate them — and excluding the replay from bests would take your own laps with it. A buffer that looked like a lap but failed the time check logs its tick count and the candidate times, so a near-miss can be diagnosed from the log instead of vanishing silently.

A stop is held until driving resumes

A lap salvaged when driving stops — a LOADING gap, or inactive packets that move the lap counter or the car id — is not emitted there and then. It is held, built from a copy of the buffer, and the buffer stays as it was until driving resumes and says what the stop was (#153):

Driving resumes… It was What happens
in the same car on the same lap (lap 1 or later) a stop mid-lap: a pit box before the timing line, as at Montreal the salvage is dropped and the lap goes on in its buffer, to complete with GT7's own time and every sample from before the stop
in the same car on the next lap the pit lane crossed the line the salvage is emitted and the session continues (#136)
anything else a replay ended, a menu, another car the salvage is emitted into the old session, which ends

Before this, the stop itself was the lap. In the pit box the counter goes inactive, the in-lap (line to box) is lap-sized, and its integrated duration can land within the salvage window of a time GT7 still reports — the previous lap's or the session best. At Montreal that made one 12-lap race with two stops into three sessions, each split leaving a phantom lap carrying an earlier lap's time: the car rejoined on the lap it stopped on, and the continuation rule only continues on the next.

A salvage held through 90 s of console frames (SALVAGE_WAIT_FRAMES, counted by packet id) is emitted anyway: no pit stop is that long, and a replay's lap should not wait on the menus for ever. So is one across a packet-counter reset (the console restarted its stream), and the service flushes a held salvage when it shuts down. Lap 0 is never a stop mid-lap: a time-trial replay looping on lap 0 would otherwise run two passes into one buffer. A lap that goes on after a stop keeps its samples from before it, but the integrated clock does not count the stop (the sampler is off while the car is not on track), so the lap's GT7 time exceeds its sample clock by the stop's length — which is what the stint trend reads a pit stop from.

One residual accepted risk: aborting a lap when its elapsed time sits within half a second of a time GT7 last reported — the previous lap or the session best, each opening its own window — can salvage the abandoned lap with that stale time. The windows are tight, the row is flagged salvaged (so it is recognizable and deletable), and the alternative — demanding a counter step — is exactly what replays never provide.

Note what salvage deliberately does not fix: telemetry cannot tell a salvaged replay of someone else's lap from your own driving — that is what the Sessions view's Exclude from bests is for. GT7_SIM_SCENARIO=leader_replay stages exactly this stream (pre-roll, one flying lap as lap 0 with a running packet-C clock, then LOADING) for testing the path without a console.

What's stored per lap

  • The full 60 Hz sample series (~28 channels — see Derived channels), JSON-encoded.
  • Per-lap aggregates: fuel used, full-throttle / full-brake / coasting / tire-spin percentages, max speed, min body height, TCS/ASM usage, engine health, time of day.
  • Detected chassis events, computed once at save time.
  • Gearing metadata (ratios, GT7's calculated top speed, redline) captured from the boundary packet.

Session lifecycle

A session groups consecutive laps that belong together. A new session starts when:

  • the car changes (car_id differs from the session's car), or
  • the lap counter resets — current_lap drops below where it was (race restart, return to menu and back out), or
  • a lap was salvaged — the stream it came from broke off, so whatever streams next is a new stint — unless driving resumes in the same car on the next lap (a pit lane over the line) or, for a salvage held through a stop, on the same lap (a pit box before the line, see above).

Car changes and counter resets are confirmed by on-track, unpaused packets with a nonnegative lap number. Inactive packets retain the last driving lap and its buffer: a temporary 6 → 0 → 6 during a pit interruption stays in one session, while a finished race's 12 → -1 → 1 through a menu starts a new session when driving resumes. Inactive packets alone never open an empty session. New sessions log the boundary reason, car, lap transition and packet ID.

Two completion paths still accept inactive telemetry: the existing time-validated replay salvage, and an unpaused final-lap +1 step from the same car. The latter records the race result even if the finish packet has already cleared the on-track flag; saving the final lap still requires a positive reported time and enough buffered samples. Retaining a buffer does not restore samples missing during an interruption; the normal partial-lap checks still apply.

When a new session starts, the previous one is closed first:

  • a session that ended with zero laps is deleted — menu visits and quick restarts don't pile up as empty rows. Replay salvage runs first, so a replay session whose only lap ended exactly at the line is saved, not swept away;
  • a session with laps triggers the session_summary webhook notification (car, track, laps, best lap, fuel used).

On the first completed lap of a session, the lap's geometry is compared against the saved track signatures and the session is tagged automatically if it matches — see Track identification.

Best-lap tracking

The service tracks the session best and, separately, the best before the just-completed lap (prev_best_ms). The live "Δ best" readout compares against prev_best_ms, so when you set a new personal best you see the improvement (e.g. −0.312) instead of a useless +0.000.

The personal_best webhook fires only when an existing session best is actually beaten — never on the first lap of a session.

Partial-lap guard. A lap the logger only saw part of — a pit out-lap, or capture starting mid-lap — passes every structural check above (GT7 reports a time for it, and it lasts more than 10 s) while covering less of the track, with a distance axis anchored wherever recording began rather than at the line. Its reported time is shorter than a real lap's, so left alone it wins the session, becomes the live-delta reference, and turns every position-based comparison built on it into garbage.

_apply_span_guard judges each lap by its distance span against recent laps, so it needs no knowledge of the track's true length. Two properties matter more than the numbers:

  • Every lap of the session is re-judged when a new one arrives. The yardstick moves as laps accumulate, so a verdict fixed at lap time goes stale — a lap that looked full next to one short lap is partial once three laps agree, and vice versa. Stored rows are rewritten in both directions (mark_session_laps_partial).
  • Dropping a lap promotes the fastest remaining full lap, rather than blanking the best until the next one arrives.

Exclusions and manual rulings sit on top (#74). A user can exclude a lap the guard accepts (off-track, contact, a dirty lap) or count one it rejected. The ruling lives in its own column, best_override, beside the guard's verdict (full_lap, stored in the counts_for_best column it has always used); counts_for_best on the ORM is coalesce(best_override, full_lap), computed in SQL, so every query and aggregate that filtered on it honours rulings without knowing they exist. mark_session_laps_partial writes full_lap alone and so can never overwrite a ruling, and clearing one hands the lap back to whatever the guard said last. For the session being driven the processor keeps the rulings too (set_best_override): its session best, excluded_lap_numbers and session_best_before_ms are computed from the guard's partial set adjusted by the rulings, and a lap number re-driven after a rewind drops the ruling made on the lap it replaced. Track identification still prefers full_lap — an excluded off-track lap covered the route.

Race starts. Lap 1 is excluded with best_override=false and exclude_reason="race-start" when a sampled packet reports total_laps > 0, race_position >= 1 and total_positions >= 2. This applies to standing and rolling starts, including salvaged lap 1 recordings. Race evidence is kept for the current lap and cleared with its buffer; a boundary packet with missing metadata does not erase it. Lap number alone, low speed and being the first recorded lap are insufficient. Qualifying and unknown contexts retain the normal checks.

CompletedLap carries full_lap, best_override and exclude_reason separately; its counts_for_best property resolves them the same way as the database. A full race opening lap can therefore remain useful for track identification while staying out of bests, delta references, coaching comparisons and consistency. The existing consistency calculation is unchanged. Manual rulings can include the lap again; clearing the ruling restores the geometry verdict.

The startup recheck also excludes stored lap 1s belonging to sessions with a positive race distance and a valid recorded finish in a field of at least two. Only rows without an existing override are changed. Geometry uses the existing lap_start_check marker; the SQL exclusion update has its own race_start_check marker. An installation that completed the geometry scan runs only the SQL update. Each marker is saved after its operation succeeds, so a failure retries only the unfinished work. The race-start marker also prevents reapplying the update after a user clears an exclusion.

Calibrated against 850 recorded laps of real driving:

Value Why
Yardstick median span of the last 5 laps 12 of those laps ran longer than their session median (one by 44 %, an off-track excursion), and against a max-based yardstick one such lap makes every normal lap after it look partial
Full-lap ratio 97 % 98 % of laps sit within 0.5 % of their session median; no legitimate lap fell below 94.7 %, while real partials measured 40-95 %
Ratio before 3 laps 93 % with two laps "this one is 6 % short" and "that one ran 6 % wide" are the same picture, so only a flagrant shortfall counts until a third lap settles it

span_confirmed — several laps agreeing on the distance — is exported for consumers that compare laps by position: the Race Engineer's coaching callouts stay silent until it is true (see Race Engineer callouts).

Where a lap begins

Grid starts. Every lap but one begins at the start/finish line by construction: its first sample is the packet in which GT7 stepped the lap counter as the car crossed it. The exception is lap 1 of a race — the counter steps when the race starts, wherever the grid is. On real recordings that was 30–480 m from the line (120 m at Red Bull Ring, 125 m at Spa, 68 m to the side at Daytona's road course, whose grid sits on a parallel stretch). Timed grid-to-line, such a lap is a few seconds off a real lap in either direction, and at under 3 % of the lap the span guard can't see it: session 224's lap 1 was its "fastest" lap.

So when a lap completes, its first sample is compared with the packet that completed it — the first one past the line, and so the line itself to within the distance covered since the packet before. The comparison is made in the direction the lap arrived at the line (over its last 10 m):

Limit Why
Along the track both boundary gaps + 3 m two laps that both started at the line agree to within those gaps; over 996 consecutive real laps they agreed to 0.8 m beyond them
Across the track 25 m laps cross the line up to 6 m apart sideways, so this only catches a start off the circuit altogether

A lap outside either limit is partial whatever its span, and its span stays out of the yardstick. A lap with no positions to judge by (older recordings, a car that never moved) gets the benefit of the doubt. Salvaged laps are exempt — their stream broke off, so nothing marks the line.

Laps recorded before the check are judged once, in the background, the first time the app starts with it: each lap against the next lap of its session, the last lap against another lap whose predecessor is stored too (a recording that began mid-session starts its first lap wherever the car was, so a lap number alone proves nothing). Only the span guard's column is written, so a lap you ruled on keeps your ruling. On the author's history it marked 33 laps: 30 lap 1s and three laps from sessions where GT7 moved the car kilometres at the line.

The clock anchor. The line is crossed somewhere inside the gap between the previous lap's last sample and this lap's first — one frame normally, more when packets were dropped. A lap's first sample therefore sits half that gap in, on both t and dist, so the two axes describe the same instant and dropped frames at the line can't offset one lap's clock against another's. Laps recorded before this started their clock at 0 but their distance a whole gap in (up to ~0.07 s of clock error at 290 km/h on real laps); they are recognisable — t starts at exactly 0, dist doesn't — and converted when read, so stored recordings are never rewritten. Laps cut out of a longer buffer by salvage start both axes at 0 and are left as they are.

What "invalid lap" means here

There is no track-limits detection (GT7 doesn't expose it). Laps are excluded only by the structural rules above: lap-0 out-laps (buffered, but only ever saved through replay salvage), laps under 600 ticks, partial laps, laps that began away from the line, post-finish laps, and paused/off-track ticks. The Analysis view additionally hides laps that ended up with no samples.