Skip to content

Fix ±1 px jitter on VFO/TNF/filter edges during tuning (#1272, #1369)#1703

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/1272-tuning-jitter
Apr 20, 2026
Merged

Fix ±1 px jitter on VFO/TNF/filter edges during tuning (#1272, #1369)#1703
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/1272-tuning-jitter

Conversation

@M7HNF-Ian

Copy link
Copy Markdown
Contributor

Problem

Vertical lines — VFO marker, TNF notch edges, filter passband edges — jitter by ±1 pixel during continuous frequency tuning (#1272) and the cursor frequency readout never exactly matches the VFO flag value (#1369).

Root cause: mhzToX() used integer truncation (static_cast<int>(px)) instead of rounding. For a fixed frequency, the raw pixel value (f − startMhz) / bw × width is a real number. During tuning or pan-follow animation, startMhz changes continuously, which shifts the fractional part of that real number. Truncation maps the same frequency to different pixel columns on successive frames — producing 1–2 px back-and-forth jitter on every vertical element drawn with mhzToX.

Fix

mhzToX()std::round — a frequency is now always assigned to its nearest pixel column. This makes the mapping deterministic across frames and eliminates the jitter.

This single-line change fixes all affected elements simultaneously: VFO center lines, TNF marker edges, filter passband edges, RIT/XIT lines, band-plan markers, spot markers, grid lines — anything that calls mhzToX.

Secondary changes

  • Cursor frequency snap (both render paths): when the cursor is within 8 px of a slice marker, the readout snaps to the exact VFO frequency rather than showing a pixel-grid approximation. The GPU path (renderGpuFrame) already had this; the software path (paintEvent) was missing it (Cursor Calibration and Default Frequency Increment #1369).

  • setSliceOverlayFreq correctness: added missing markOverlayDirty() so the overlay is redrawn when this function is called, and added an early-out for unchanged values.

Verification

  • Tune with scroll wheel or encoder at 100 Hz step with a narrow zoom (e.g. 200 kHz pan): VFO line, TNF edges, and filter edges no longer flicker.
  • Hover cursor near a VFO flag: readout snaps to exact flag frequency in both GPU and software render modes.

Closes #1272
Closes #1369

🤖 Generated with Claude Code

…r#1272, aethersdr#1369)

The core cause was integer truncation in mhzToX(): for a fixed frequency,
the pixel position (f - startMhz) / bw * width() lands at fractional
values that round differently depending on the current pan centre.  During
continuous tuning or pan-follow animation this caused the same frequency to
map to different pixel columns on successive frames, producing 1-2 px
back-and-forth jitter on every vertical line (VFO marker, TNF notch edges,
filter passband edges, grid lines).

Fix: use std::round instead of implicit truncation in mhzToX().  This makes
the mapping deterministic — a frequency is always assigned to its nearest
pixel — eliminating the jitter.

Secondary fixes:
- Cursor frequency label: snap to exact VFO frequency when the cursor is
  within 8 px of a slice marker so the readout shows the flag value rather
  than a pixel-grid approximation.  Applied to both the GPU (renderGpuFrame)
  and software (paintEvent) render paths; previously only the GPU path had
  this.  (aethersdr#1369)
- setSliceOverlayFreq(): add missing markOverlayDirty() call so callers
  get a repaint, and add an early-out for unchanged values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@M7HNF-Ian M7HNF-Ian requested a review from ten9876 as a code owner April 19, 2026 18:24

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — PR #1703

Nice, clean fix. The root cause analysis is spot-on: static_cast<int> truncates toward zero, so the same frequency drifts between pixel columns as startMhz shifts during tuning. std::round is the right call here.

Changes look good

mhzToXstd::round — Correct fix. The clamp stays in place to guard against extreme values, and rounding happens outside the clamp, which is the right order (round(clamp(...))) since we want to round the already-bounded value. This fixes the jitter globally for every caller (VFO, TNF, filter edges, spots, grid lines, etc.) with a single-line change.

setSliceOverlayFreq — missing markOverlayDirty() — Good catch. Without this, callers that update the frequency through this path wouldn't trigger a repaint, so the overlay could show stale positions. The early-out on == is a reasonable optimisation given that frequency values come from the radio protocol at fixed precision.

Cursor frequency snap (both render paths) — The 8 px snap radius is sensible at typical zoom levels. The logic is duplicated between renderGpuFrame and paintEvent, which is consistent with how this codebase handles dual render paths.

Minor observations (non-blocking)

  1. Floating-point == in the early-out: so.freqMhz == freqMhz works here because both values originate from the same radio protocol parsing (no arithmetic drift), but a brief comment noting that would help future readers understand why exact comparison is safe.

  2. Snap finds first match, not nearest: The cursor snap loop breaks on the first slice overlay within 8 px. If two slices are very close together (within 16 px of each other), the snap target depends on m_sliceOverlays ordering rather than proximity. This is a minor edge case — at that zoom level both flags would be nearly on top of each other anyway — but if you wanted to be precise, you could track the minimum distance. Not worth blocking on.

  3. Overlap with PR #1699: This PR supersedes #1699 (which addresses only #1369 with the same cursor-snap approach). You may want to close #1699 to avoid merge conflicts.

Verdict

Scoped correctly to the stated problem, only touches SpectrumWidget.cpp, no convention issues, no resource leaks or null risks. LGTM — thanks for the well-documented fix, @Chaosuk97!

@ten9876 ten9876 merged commit 8c33310 into aethersdr:main Apr 20, 2026
5 checks passed

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks Ian! Clean fix — rounding vs truncation is exactly right. Merged.

ten9876 pushed a commit that referenced this pull request Apr 21, 2026
TNF UX rework contributed by Robbie Foust (rfoust). Applied from PR #1547
with the two non-TNF scopes stripped out and two Copilot review items
addressed:

- Stripped: overlay texture format change (RGBA8 → BGRA8 with fallback,
  QImage Format_RGBA8888_Premultiplied → Format_ARGB32_Premultiplied).
  Unrelated to TNF, affects every overlay rendering, no stated reason —
  revisit separately if a driver-specific need surfaces.
- Kept: cursor-tracking refactor (`updateTrackedCursorState`).  Intertwined
  with TNF hover detection (`m_hoveredTnfId`), extracting it would have
  required rewriting the hover logic.
- Fixed: added `#include <algorithm>` to TnfModel.cpp for the newly-used
  `std::max` / `std::clamp` — relying on transitive includes is fragile.
- Fixed: TNF context-menu header `QWidgetAction` is now
  `WA_TransparentForMouseEvents` + `setEnabled(false)` so clicking the
  freq/width info block no longer closes the menu.

TNF changes preserved intact:
- Markers stop at the bottom of the spectrum pane (no waterfall extend).
- Hatched depth visualisation (12/8/5 px spacing for depth 1/2/3).
- Floating `RF Tracking Notch` popup on hover showing freq + width.
- Width/depth submenu checkmarks show current value.
- Bidirectional TNF drag: horizontal tunes, vertical resizes via
  2^(-dy/48) octaves, clamped 10-12000 Hz.
- Preferred-TNF hit-testing for overlapping notches.
- Tune guides suppressed while a TNF is hovered or dragged.
- TnfModel: optimistic local updates in setters, width parser tolerates
  both Hz ("width=100") and fractional-MHz forms for firmware variance.

Rebased onto current main (#1703 cursor-snap and #1701 filter-passband
changes merged cleanly into rfoust's cursor-tracking refactor).

Unaddressed follow-ups (intentional deferrals):
- #1547 removes `QToolTip::hideText()` when the cursor leaves a spot
  label; stale tooltip could linger. Needs a small re-add.
- `updateTrackedCursorState` is only called for spectrum-area hover; the
  waterfall path still leaves `m_cursorPos` / `m_hoveredTnfId` stale in
  the software paintEvent render path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dk3ww

dk3ww commented Apr 21, 2026

Copy link
Copy Markdown

The problem hasn't been solved.
I tried checking it in CW (400 Hz) mode. The jitter is still the same.

@M7HNF-Ian M7HNF-Ian deleted the fix/1272-tuning-jitter branch June 7, 2026 14:58
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.

Cursor Calibration and Default Frequency Increment low jitter during frequency tuning

3 participants