Skip to content

Avoid FPS meter overlay redraw stutter#2578

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
rfoust:codex/fix-fps-meter-stutter
May 12, 2026
Merged

Avoid FPS meter overlay redraw stutter#2578
ten9876 merged 1 commit into
aethersdr:mainfrom
rfoust:codex/fix-fps-meter-stutter

Conversation

@rfoust

@rfoust rfoust commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Move the PAN/WF FPS meter readouts from the static spectrum overlay texture into lightweight child QLabel overlays.
  • Keep the existing FPS meter placement and styling behavior, including hiding when the panadapter/waterfall areas are too small.
  • Reposition the labels when the widget resizes or the spectrum/waterfall split changes.

Bug Fixed

When View -> FPS Meters is enabled, the displayed PAN/WF FPS values update about once per second. Before this change, each value update called markOverlayDirty(), which forced the static overlay QImage to be repainted and re-uploaded to the GPU texture. That extra once-per-second overlay repaint/upload caused a small visible pause in the waterfall/spectrum display exactly when the FPS text changed.

Fix

The FPS values now update QLabel children directly instead of invalidating the static overlay. Toggling the meters still dirties the overlay once so the old painted fallback path is cleared, but normal once-per-second FPS updates only change the label text and geometry. This avoids the full static overlay repaint/upload during live panadapter and waterfall rendering.

Verification

  • git diff --check
  • cmake --build build-cli --target AetherSDR

Note: I did not run the app from Codex. The user verified the FPS-meter pause appears fixed locally.

Copilot AI review requested due to automatic review settings May 11, 2026 08:53
@rfoust rfoust requested review from jensenpat and ten9876 as code owners May 11, 2026 08:53

Copilot AI 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.

Pull request overview

This PR refactors the PAN/WF FPS meter rendering in SpectrumWidget to avoid stuttering caused by once-per-second static overlay texture repaints/uploads when FPS text updates.

Changes:

  • Replaces FPS meter drawing on the static overlay with two lightweight child QLabel overlays updated once per second.
  • Adds label creation, text updates, and geometry positioning hooks for resize and split-ratio/divider changes.
  • Keeps a fallback QPainter path for FPS meters only when labels are not available.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/gui/SpectrumWidget.h Adds private helpers and member pointers for FPS meter QLabel overlays.
src/gui/SpectrumWidget.cpp Creates/updates/positions FPS meter labels and stops FPS updates from invalidating the static overlay each second.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +694 to +697
label->move(x, y);
label->show();
label->raise();
};
Comment on lines +652 to +663
const int chromeH = FREQ_SCALE_H + DIVIDER_H;
const int contentH = height() - chromeH;
if (contentH <= 0) {
hideMeters();
return;
}

const int specH = static_cast<int>(contentH * m_spectrumFrac);
const int wfY = specH + DIVIDER_H + FREQ_SCALE_H;
const QRect specRect(0, 0, width(), specH);
const QRect wfRect(0, wfY, width(), height() - wfY);

@rfoust rfoust force-pushed the codex/fix-fps-meter-stutter branch from 8294bf2 to a125d15 Compare May 11, 2026 10:53
@rfoust

rfoust commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the actionable z-order concern from review:

  • Removed the repeated label->raise() from FPS meter repositioning so the labels no longer jump above other child overlays during updates.
  • Stacked the FPS labels under m_overlayMenu when they are created.
  • Re-raise m_overlayMenu->raiseAll() after FPS label positioning so the menu and floating panels remain above the FPS labels.

For the geometry-helper suggestion: I left the rect calculation local for this PR to keep the change narrowly scoped to the FPS-meter stutter fix. I agree a shared helper could reduce drift risk, but that would touch broader layout/rendering structure and seems better handled separately if maintainers want it.

@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.

Thanks @rfoust — the analysis is convincing and the fix is well-scoped. Moving FPS meter readouts off the static overlay texture is exactly the right call here: those values were the only once-per-second invalidator of an otherwise stable QImage that's uploaded to a GPU texture, so the upload pause maps cleanly to the symptom.

A couple of notes on the code itself and the Copilot comments:

Copilot z-order concern (false positive) — Copilot flagged repeated label->raise() calls, but the diff doesn't actually call raise() on the FPS labels. stackUnder(m_overlayMenu) is set once at creation, and positionFpsMeterLabels() only re-raises m_overlayMenu via raiseAll() — which is the same pattern used at SpectrumWidget.cpp:468, :486, and :4432 to keep the overlay menu above VFO widgets. Show/move on a previously-shown sibling doesn't reorder Qt's stack, so the labels stay under the overlay/TNF popup and don't need any additional management.

Copilot geometry-duplication concern (valid but pre-existing) — The chromeH / contentH / specH / wfY pattern is already duplicated in ~10 spots across this file (e.g. lines 1900, 1961, 2430, 2922, 3060, 3145, 4285). Adding one more occurrence isn't great, but extracting a shared helper is a broader refactor that's out of scope for a bug fix. Worth keeping in mind for a follow-up cleanup, not blocking here.

Minor observation: createFpsMeterLabels() runs in the constructor after m_overlayMenu is created at line 283, so the stackUnder(m_overlayMenu) guarded by if (m_overlayMenu) is always taken — that's fine and defensive, just noting it works as intended.

RAII/lifetime, AppSettings, null guards (m_panFpsMeterLabel && m_wfFpsMeterLabel, m_overlayMenu) all look correct. The painted fallback in drawFpsMeters() early-returns when the labels exist, so there's no double-rendering. Toggling visibility correctly dirties the overlay once to clear the old painted path.

LGTM as a focused fix. Thanks for the clear before/after description in the PR body.

@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.

Claude here, reviewing on Jeremy's behalf.

Verdict

Clean fix for a real per-second stutter. Built locally, no warnings introduced. Merge after Jeremy eyeballs the placement.

The bug, plainly

The FPS meters timer fires every second to update m_panadapterFps / m_waterfallFps. Pre-PR, that update called markOverlayDirty(), which forces a full QImage overlay repaint AND a GPU texture re-upload. So every second, the rendering pipeline took a small synchronous hit for what amounts to a four-character text change. Operators with the meters enabled saw a visible pause exactly when the digit changed.

Why the fix works

Promoting the two FPS readouts from painter-text-in-overlay-QImage to QLabel children of SpectrumWidget:

  • setText() on a QLabel only invalidates the label's tiny rectangle (~80×20 px), not the whole spectrum overlay
  • No GPU texture re-upload (the QLabel renders into its own widget backing store, composited by Qt's normal child-widget paint path)
  • The per-second cost drops to "two QLabel repaints" instead of "full spectrum overlay repaint + texture upload"

Verified

  • WA_TransparentForMouseEvents set on both labels — spectrum click/drag/scroll/divider-drag still work through the label area
  • stackUnder(m_overlayMenu) with m_overlayMenu->raiseAll() at the end of positionFpsMeterLabels() — the overlay menu drawer still covers the meters when open. Good UX preservation
  • positionFpsMeterLabels() called from all the right places: resizeEvent, setSpectrumFrac, mouseMoveEvent (during divider drag), and on every updateFpsMeterLabels(). Labels track the split correctly
  • Hide-on-too-small logic at lines area.width() < 56 || area.height() < 18 mirrors the painter version's behavior
  • Band plan inset preservedpanBottomInset = m_bandPlanFontSize + 12 keeps the PAN meter above the band plan text, same as before
  • applyFpsMeterVisibility still calls markOverlayDirty() at the end — needed to erase the legacy painter text once on toggle. Good defensive code
  • Threading: updateFpsMeterValues() runs on the GUI thread (driven by m_fpsMeterTimer which is a QTimer parented to this), so the label updates are GUI-thread safe by construction
  • CI all green: build, analyze (cpp), check-paths, CodeQL, check-windows (skipped — header-only changes shouldn't affect Windows-specific paths)
  • Built locally clean on Linux, only pre-existing unrelated warning

One observation worth noting

drawFpsMeters() now has an early-return when both labels exist (line 3393), so the legacy painter path is effectively dead code under normal operation. Two reasonable options for the future:

  • (a) Keep as-is — useful as a defensive fallback if label construction ever fails (e.g., OOM in the ctor). Tiny code size cost.
  • (b) Delete drawFpsMeters() entirely in a follow-up — labels are always constructed in the ctor, so the function is unreachable in practice.

I'd lean (a) for this PR (don't expand scope) and leave the cleanup to a future small PR if it ever feels worth doing. Right now the early-return makes the dead path harmless.

Minor styling

The CSS in createFpsMeterLabels():

background: rgba(15, 15, 26, 210);
border: 1px solid rgba(255, 255, 255, 170);
color: #9ceeff;
font-size: 9pt;
font-weight: bold;
padding: 3px 6px;

Matches the visual idiom of the existing painter version. Worth a visual side-by-side comparison just to confirm the label-rendered version doesn't look noticeably different — Qt's font hinting in widget mode can differ subtly from QPainter::drawText, especially at small sizes and across HiDPI scales. Not a blocker; flag for the maintainer's eyes during testing.

Recommendation

Merge after Jeremy enables View → FPS Meters on a live panadapter and confirms:

  1. The per-second pause is gone
  2. The labels render at the expected position (bottom-right of spectrum and waterfall, above the band plan in spectrum)
  3. The labels disappear correctly when the panel is shrunk below the size thresholds
  4. The overlay menu still covers them when opened

Thanks @rfoust — moving from painter-into-shared-overlay to per-widget child labels is the right Qt idiom for this kind of "small, frequent update with bounded geometry" UI element. The positioning helper preserves every special-case of the original placement.

73, Jeremy KK7GWY & Claude (AI dev partner)

@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.

Approved — clean fix for the per-second overlay re-upload stutter.

@ten9876 ten9876 merged commit b8c156b into aethersdr:main May 12, 2026
5 checks passed
aethersdr-agent Bot added a commit that referenced this pull request May 13, 2026
The painter-based FPS readout was superseded by QLabel children in #2578
and immediately gated with an early-return when both labels exist. Since
createFpsMeterLabels() runs unconditionally in the SpectrumWidget
constructor, the painter path was unreachable.

Drops the function body, declaration, and both call sites in
renderGpuFrame() and paintEvent(). FPS accumulators, timer, and label
plumbing stay — they drive the live readouts.
aethersdr-agent Bot added a commit that referenced this pull request May 14, 2026
The painter-based FPS readout was superseded by QLabel children in #2578
and immediately gated with an early-return when both labels exist. Since
createFpsMeterLabels() runs unconditionally in the SpectrumWidget
constructor, the painter path was unreachable.

Drops the function body, declaration, and both call sites in
renderGpuFrame() and paintEvent(). FPS accumulators, timer, and label
plumbing stay — they drive the live readouts.

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
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.

3 participants