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 + 1for 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_idframes (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/60seconds,dist += speed_mps × Δframes × 1/60meters.
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:
- the previous lap number was
> 0(a real lap, not the out-lap); - the counter moved exactly +1 (monotonic step);
- the game reported a positive
last_lap_time_ms; - 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_iddiffers from the session's car), or - the lap counter resets —
current_lapdrops 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_summarywebhook 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.