Skip to content

Fix sim-log channel scaling, 1-byte channels, and graph panel UI - #20

Merged
Friss merged 4 commits into
mainfrom
fix/sim-log-scaling-and-graph-ui
Jul 27, 2026
Merged

Fix sim-log channel scaling, 1-byte channels, and graph panel UI#20
Friss merged 4 commits into
mainfrom
fix/sim-log-scaling-and-graph-ui

Conversation

@Friss

@Friss Friss commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Fixes #18, plus three graph-panel issues found while testing it.

Sample scaling applied shift before mul

(raw / scale * 10^-dec + shift) * mul     ← was
 raw / scale * 10^-dec * mul + shift      ← now

shift is an offset already expressed in engineering units, so it belongs after the multiplier. The wrong order offsets a channel by shift * (mul - 1) — zero whenever mul == 1, which is the case for every .ld file in this repo (VIR_LAP: 199 channels, all mul=1; both S1 examples likewise). That's why it went unnoticed.

Sim exporters use mul = 2 with a non-zero shift to pack a signed range into int16. Throttle Pos (shift=50, mul=2, scale=1, dec_places=3) stores 0–100 % as raw −25000…25000, which the old order reported as 50–150 % — exactly the reporter's screenshot. Ground Speed (shift=641, mul=2) carried a constant +641 km/h.

Verified against the reporter's log:

Channel Before After
Ground Speed 641 … 942 km/h 0 … 301 km/h
Throttle / Brake Pos 50 … 150 % 0 … 100 %
Grip Fract (×4) 50 … 150 % 0 … 100 %
Brake Bias Rear 97.2 % 47.2 %
Eng Water Temp 637 … 679 °C 52 … 94 °C
Tyre Temp FL Centre 605 … 728 °C 19.5 … 143 °C
Battery Charge Level 72 … 150 % 21.9 … 100 %

Wheel rotation speed cross-checks it independently: 301.28 km/h ÷ 248.5 rad/s gives a 0.337 m rolling radius, right for the car.

The reporter also found that setting manual min/max to 0/100 looked "even weirder" — that was this same bug, not the scaling editor. The data genuinely spanned 50–150, so a 0–100 axis pushed the trace off the top.

docs/ld-file-format.md specified the wrong formula too; corrected with a note on why mul == 1 hides it.

Gear and Marker failed to decode

from_codes had no case for a 1-byte sample width, so (0x03, 1) fell through to Unknown, bytes_per_sample() returned None, and the channels refused to load.

For integer channels dtype_code is the width in bytes. Confirmed empirically rather than guessed: walking the channel list sorted by data_ptr and dividing each gap by n_data gives exactly 1.000 for Gear and Marker, 2.000 for every dt=2 channel. Gear then decodes to 0–7 — neutral plus seven forward gears, mean 3.19, distributed the way real driving looks. Decoded signed, since reverse logs as -1.

Graph tiles didn't reflow when adding a graph

ensure_tile_heights computed available_height / tile_count but handed it to Vec::resize, which only fills newly appended slots. Existing tiles kept their old heights, so 3 → 4 tiles in a 600 px panel gave 200+200+200+150 = 750 px and the stack overflowed into the scroll area instead of shrinking.

Redistribution now happens inside ensure_tile_heights when the count changes, scaling rather than resetting so proportions you dragged survive: 300/150/150 plus a fourth becomes 240/120/120/120 — still 2:1:1, still exactly 600 px. Removal scales back up symmetrically. A resize drag is zero-sum, so the reflow is a no-op when the count is unchanged and can't fight an in-progress drag.

This also removes three ad-hoc tile_heights.clear() calls that were papering over it. They only covered the drag-drop path (and discarded proportions while doing so); add_channel, the channel picker, and move_channel_to_new_graph never reset at all, which is why the bug looked inconsistent.

Manage Channels rows were inert

egui's dnd_drag_source layers a bare Sense::drag() over the contents it just added (ui.rs:2674-2681). Registered last, it sits on top of the row button and swallows the press. The code checked the inner button's clicked(), which never fired — so pending_selection never got set, the Remove button stayed permanently disabled, and the context menu (attached to the same dead response) couldn't open either. There was no way to remove a channel at all.

Replaced with a clickable_drag_source helper sensing click_and_drag on the row itself — the pattern channel_browser.rs already uses. Drag-to-reorder is unaffected. Added a per-row remove button so removal doesn't depend on discovering right-click or select-then-Remove.

Consistent scrollbar gutter

The main graph stack now reserves a permanent gutter (ScrollStyle::solid + AlwaysVisible), so plot width no longer shifts as tiles are added or removed. egui's default scroll style floats over content, so always-visible alone wouldn't have been enough. Pairs well with the existing ScrollSource::SCROLL_BAR on that view, where the wheel is reserved for plot zoom and the bar is the only way to scroll.

Dialog lists keep the default floating bars — permanent gutters there would just eat width.

GPS Satellites and three others never decoded — including in our own fixture

Auditing every (dtype_a, dtype_code) pair across all six .ld files on hand turned up a third integer family, 0x06, missing from the type table. Four channels per file fell through to Unknown and silently failed to load — including in VIR_LAP.ld, the checked-in fixture:

Channel as int32 as float32
GPS Satellites 6 … 10 8.4e-45
CP Lotus ESP System State 1 (constant) 1.4e-45
Lap GPS Closest Beacon 25900 … 26100 3.6e-41
Lap Beacon Ticks 248M … 374M 5.1e-30

Unambiguous — as float32 they're all denormal garbage. Data-block spacing confirms the 4-byte width. The vir_lap_parser_snapshot test counts metadata entries, so it was blind to this; there's now an integration test that actually reads the samples and asserts the satellite count is a plausible fix.

Tile reflow overflowed when a tile sat near the floor

Found by strengthening the reflow test. Scaling proportionally and then clamping to the floor overflows the panel, because clamping a tile back up adds height the others never gave away: [440, 80, 80] plus a fourth tile came to 632 px in a 600 px panel. Realistic — it happens on a single add whenever one tile has been dragged small.

fit_tile_heights now pins tiles that would land under the floor and rescales the rest into the height that frees up, so the stack fits exactly whenever the floor fits at all.

Testing

cargo test --workspace: 145 passed, 0 failed. Clippy clean.

The vir_lap_parser_snapshot test passes unchanged, confirming no regression on existing files. Also ran the GUI against the reporter's log to check each fix interactively.

Notes

  • GPS is untouched and still reads ~0°, −135°. That's the sim's exporter writing a synthetic origin; relative geometry is correct (~1.1 km × 0.65 km bounding box matches Interlagos), so the track map works. mul = 1 there.
  • Beacon still reads −32767…17377. dtype_a=0x00 with shift=0, mul=1, so neither fix touches it. Possibly correct-as-logged, possibly a third issue.
  • Panel/window resize still doesn't reflow tiles — same class of staleness as the add/remove bug, but left alone deliberately since some prefer fixed heights when scrolling a tall stack. Two-line change if wanted.
  • No .ld fixture added; the reporter's file is 5.4 MB. Worth considering, as nothing in the repo exercises mul != 1 or 1-byte channels.
  • apply_scaling now scales in place rather than collecting into a second Vec — bit-exact, and saves a 1.5 MB transient allocation per channel read. The bigger cost in that path is the per-sample match in read_raw_samples (~166 µs vs ~30 µs for scaling on a 190k channel), left alone as pre-existing and out of scope.

https://claude.ai/code/session_01PHdY3DhzFpD3QtSJUN1PQv

Friss added 2 commits July 26, 2026 22:10
Two parsing bugs that only surface on sim-exported ADL logs, both
reported in #18.

Scaling applied `shift` before `mul`:

    (raw / scale * 10^-dec + shift) * mul

`shift` is an offset already in engineering units, so it belongs after
the multiplier. The wrong order offsets a channel by shift * (mul - 1),
which is zero whenever mul == 1 — the case in every .ld file in the
repo, hence the blind spot. Sim exporters use mul = 2 with a non-zero
shift to pack a signed range into int16, so Throttle Pos (shift=50,
mul=2, dec_places=3) read 50-150 % instead of 0-100 %, and Ground Speed
(shift=641, mul=2) carried a constant +641 km/h.

Verified against the reporter's log: ground speed 0-301 km/h, water temp
52-94 C, tyre temps 19.6-219.6 C, brake bias 47.2 %, and wheel rotation
speed that now agrees with ground speed for a 0.337 m rolling radius.

Separately, `from_codes` had no case for a 1-byte sample width, so Gear
and Marker fell through to Unknown and failed to load entirely. For
integer channels `dtype_code` is the width in bytes — confirmed by
walking the channel list by data_ptr, where the gap divided by n_data is
exactly 1.000 for those two channels and 2.000 for every dt=2 channel.
Added DataType::Int8, decoded signed since reverse gear logs as -1.

Fixes #18

Claude-Session: https://claude.ai/code/session_01PHdY3DhzFpD3QtSJUN1PQv
Three unrelated graph-panel issues found while testing #18.

Tiles did not reflow when a graph was added. `ensure_tile_heights`
computed available_height / tile_count but passed it to Vec::resize,
which only fills newly appended slots — the existing tiles kept their old
heights, so the stack overflowed into the scroll area instead of
shrinking to make room. Redistribution now happens inside
ensure_tile_heights whenever the count changes, scaling rather than
resetting so proportions set by dragging survive. That also replaces
three ad-hoc tile_heights.clear() calls which only papered over the
add-by-drag path and discarded those proportions; the paths that went
through add_channel, the channel picker or move_channel_to_new_graph
never reset at all.

Manage Channels rows could not be clicked. egui's dnd_drag_source layers
a bare Sense::drag() over the contents it just added, and because that
interaction registers last it sat on top of the row button and swallowed
the press. Selection never happened, so the Remove button stayed disabled
— and the context menu, attached to the same dead response, could not
open either, leaving no way to remove a channel. Replaced with a
clickable_drag_source helper that senses click_and_drag on the row
itself, matching the pattern channel_browser.rs already uses. Added a
per-row remove button so removal does not depend on discovering
right-click or select-then-Remove.

The main graph scroll area now reserves a permanent gutter
(ScrollStyle::solid plus AlwaysVisible) so plot width no longer shifts
as tiles are added or removed. egui's default scroll style floats over
the content, so visibility alone would not have been enough.

Claude-Session: https://claude.ai/code/session_01PHdY3DhzFpD3QtSJUN1PQv

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73c1a0bbb6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/i3rs-app/src/panels/graph.rs Outdated
Friss added 2 commits July 26, 2026 22:19
Follow-up from reviewing the previous two commits.

Auditing every (dtype_a, dtype_code) pair across all six .ld files on hand
turned up a third integer family, 0x06, missing from the data-type table.
Four channels per file fell through to Unknown and silently failed to
load — including in VIR_LAP.ld, the checked-in fixture: CP Lotus ESP
System State, Lap Beacon Ticks, Lap GPS Closest Beacon and GPS
Satellites. Decoded as int32 they read 1, ~248-374M ticks, beacon indices
~26000, and 6-10 satellites; as float32 they are all denormal garbage, so
the family is unambiguous. Data-block spacing confirms the 4-byte width.
The snapshot test counts metadata entries, so it was blind to this.

apply_scaling now scales in place instead of collecting into a second
Vec — a channel can hold ~190k samples, so that was a 1.5 MB transient
allocation per read for nothing. Bit-exact: same operations, same order.

fit_tile_heights replaces the scale-then-clamp in ensure_tile_heights.
Clamping after scaling overflows the panel whenever a tile already sits
near the floor, because clamping it back up adds height the others never
gave away: [440, 80, 80] plus a fourth tile came to 632 px in a 600 px
panel. Now tiles that would land under the floor are pinned there and the
rest rescaled into the height that frees up, which fits exactly whenever
the floor fits at all.

Also from review: read_i8 helper so the Int8 arm matches its five
siblings; the row remove button uses the existing RichText("X").small()
idiom rather than a new glyph; clickable_drag_source documented as a fork
of egui's dnd_drag_source with a pointer for re-diffing on upgrade, and
its redundant drag_started payload set dropped to match upstream; dropped
the render-site floor clamps that duplicated the invariant
ensure_tile_heights already guarantees; consolidated the data-type and
reflow assertion tables.

Claude-Session: https://claude.ai/code/session_01PHdY3DhzFpD3QtSJUN1PQv
The graph panel edits left a block that rustfmt reflows, failing the
format check. The data-type coverage test asserted the fixture still
contained undecodable channels, which stopped being true once the 0x06
integer family was mapped — invert it to assert full coverage instead.

Claude-Session: https://claude.ai/code/session_01FV6mm15SerkbAhgKByu2e9
@Friss
Friss merged commit 7ffa94d into main Jul 27, 2026
7 checks passed
@Friss
Friss deleted the fix/sim-log-scaling-and-graph-ui branch July 27, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Certain Graph Values Start and End at Weird Points

1 participant