Add clock-aligned waterfall time markers - #5538
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The unresolved font-dependent atlas-cache issue and rendering breadth require human review before approval.
Pull request overview
Adds optional clock-aligned UTC markers to the waterfall with persistent per-panadapter intervals, shared rendering, and theme integration.
Changes:
- Adds marker geometry, timestamp tracking, scrolling/history integration, and menu selection.
- Persists marker intervals in Display settings.
- Adds focused geometry/settings tests and theme-token updates.
File summaries
| File | Summary |
|---|---|
tests/waterfall_time_markers_test.cpp |
Tests marker geometry and edge cases. |
tests/waterfall_time_marker_settings_test.cpp |
Tests persistence, validation, and slot isolation. |
tests/tests.cmake |
Registers the focused tests. |
src/gui/WaterfallTimeMarkers.h |
Defines marker intervals and geometry. |
src/gui/SpectrumWidget.h |
Declares marker state and rendering APIs. |
src/gui/SpectrumWidget.cpp |
Integrates timestamps, menus, history, and rendering. |
src/gui/SpectrumWidget_TimeMarkers.cpp |
Implements atlas, software, and GPU rendering. Moderate: the atlas cache omits the widget font; also requires direct Qt includes. |
src/gui/DisplaySettings.h |
Persists per-slot marker settings. |
src/core/ThemeSeedGenerated.cpp |
Adds fallback marker theme values. |
resources/themes/default-light.json |
Adds light-theme marker colors. |
resources/themes/default-dark.json |
Adds dark-theme marker colors. |
docs/theming/canonical-tokens.md |
Documents marker theme tokens. |
docs/architecture/aetherd-touchpoints.md |
Updates touchpoint metadata. |
CMakeLists.txt |
Registers the marker implementation source. |
Review details
Suppressed comments (1)
src/gui/SpectrumWidget_TimeMarkers.cpp:60
- The atlas cache key omits the widget font even though
labelFontis rebuilt fromfont()on every call. If the application/widget font or style changes while the labels, pixel size, and DPR stay the same, this early return keeps the old glyphs andm_wfTimeMarkerLabelHeight, so marker labels can be clipped or misaligned; include the relevant font in the cache key or invalidate the atlas on font changes.
if (!m_wfTimeMarkerAtlasDirty && labels == m_wfTimeMarkerLabels
&& m_wfTimeMarkerAtlas.size() == pixels
&& m_wfTimeMarkerAtlas.devicePixelRatio() == dpr) {
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
1. Issue fit
Yes. Issue #5537 lists eight behaviours and the diff covers all eight: the submenu's nine intervals map exactly onto kWaterfallMarkerIntervals with the labels the issue names; default Off with per-pan-slot persistence; thin line plus left-edge UTC stamp; clock-boundary alignment with the boundary time as the label (boundaryMs = (t / intervalMs) * intervalMs, not arrival time); rows followed through live scroll, pause, history scrub and resize; two theme tokens; both GPU and software render paths; and the preference stored in the nested Display document rather than a flat key.
I also checked the three "review checkpoints" the triage comment on #5537 asked for, and all three hold:
- Ring re-laid from origin 0 on rebuild —
rebuildWaterfallViewportForFrameresetsm_wfVisibleTimeRowsalongsidem_wfWriteRow = 0(SpectrumWidget.cpp:6129), andpaintWaterfallRowsFromHistoryre-lays it with the samewaterfallVisibleRowForAge(writeRowOrigin, age, height)mapping it uses for pixels, so markers cannot shear away from their signal rows. - History scrub refills from
m_wfHistoryTimestamps— done atSpectrumWidget.cpp:5988-5991usingm_wfHistoryOffsetRows + age, matching therowIndexlookup on the line above it. - GPU markers get their own quad —
drawWaterfallTimeMarkersGpudraws from a dedicated dynamic VBO and does not touchm_overlayStatic/m_overlayStaticDirty, so smooth scroll does not force a full-overlay repaint plus upload.
2. Scope
| File(s) | Change | Claimed? | Verdict |
|---|---|---|---|
src/gui/WaterfallTimeMarkers.h |
New header-only interval table + pure geometry function | Yes | In scope |
src/gui/SpectrumWidget_TimeMarkers.cpp |
New TU: setter, marker query, text atlas, CPU + GPU draw | Yes | In scope |
src/gui/SpectrumWidget.{h,cpp} |
Row-timestamp ring, submenu, both draw hooks, save/restore, automation snapshot | Yes | In scope |
CMakeLists.txt |
Registers the new TU in GUI_SOURCES |
Yes | In scope (verified it is actually referenced) |
src/gui/DisplaySettings.h |
Per-pan-slot accessor pair | Yes | In scope; mirrors panMenuExpanded exactly |
resources/themes/*.json, src/core/ThemeSeedGenerated.cpp, docs/theming/canonical-tokens.md |
Two new tokens + doc | Yes | In scope; seed insertion is correctly alphabetical and Static checks passed |
docs/architecture/aetherd-touchpoints.md |
ThemeManager.h includer count 145 → 146 |
Not in the title, but mechanically forced | In scope — the new TU includes core/ThemeManager.h, so the manifest count is exactly right |
tests/tests.cmake, two new tests |
Registration + coverage | Yes | In scope; settings test correctly joins AETHER_SETTINGS_CONSUMERS |
Everything in the diff is explained by #5537. No unrelated files, no formatting churn, no deleted guards — I read the - lines and the only removals are the context lines the hunks re-emit. CHANGELOG.md correctly untouched.
New public surface, flagged for a maintainer ruling rather than waved through: a Q_PROPERTY(int waterfallTimeMarkerSeconds) on SpectrumWidget; two automation-snapshot keys (waterfallTimeMarkerSeconds, waterfallTimeMarkers); a waterfallTimeMarkers key inside the Display settings document; two theme tokens; and the waterfallTimeMarkersMenu / waterfallTimeMarkers<N> object names. All of it is what the feature needs, and #5537 is labelled maintainer-review with "Final visual/UX direction is subject to maintainer review", so this is a decision to make, not a violation.
Preference check (§6): this adds a new overlay defaulting to Off; it does not restyle, reorder or re-default any existing UI. Not smuggled preference.
Socket tests: neither new test opens, binds, listens or connects — waterfall_time_markers_test links Qt6::Core only and drives the pure geometry function; waterfall_time_marker_settings_test links aethercore + Qt6::Core and touches only the settings store. No fake radio/amp/tuner peer added, and none removed. Nothing to record beyond that.
3. Blockers
None. I tried hard to find one — see §5 for the specific attacks and why each failed.
4. Nits (all non-blocking)
- Test settings isolation hand-rolls the sandbox.
tests/waterfall_time_marker_settings_test.cpp:10setsAETHER_SETTINGS_DIRdirectly instead of usingTestSettingsProfile.h, which the CMake contract names as the isolation mechanism. Inline comment has the detail; it is genuinely lower-risk than it sounds and there is precedent, so a nit. - Two assertions in that test are tautological (lines 31-34) — the getter validates independently of the setter, so they pass even if the setter wrote garbage. Inline comment.
m_wfVisibleTimeRowsis the only restored vector without a post-restore size re-init (SpectrumWidget.cpp:6821) — currently unreachable, but the parity gap is worth a comment for the next editor. Inline.- The atlas-caching comment slightly overstates the code (
SpectrumWidget_TimeMarkers.cpp:85) — the label dedup depends onmarker.y, so row motion can invalidate the atlas. Rare at ≥15 s intervals. Inline. QAccessibleValueChangeEventon theSpectrumWidget(SpectrumWidget_TimeMarkers.cpp:22) is an odd fit — the widget is not a value-type accessible, and the checkable menu actions already expose the selection. Not wrong, just probably not doing anything for a screen reader.- The 30-minute and 1-hour entries will rarely place a visible line. Already raised in the #5537 triage:
kWaterfallHistoryMscaps retention and the visible ring is onlyheightrows, so at typical rates a screenful is well under a minute and an hour boundary lands inside it only occasionally. This matches WSJT-X and is harmless — flagging only because it is a maintainer call on menu space, not a defect. - Neither new test is on the frozen per-PR ctest gate.
ci.yml's-Rsteps cover the four DV tests,cross_needle_meter_test,mac_nr_filter_testandasr_gpu_probe_test. The five green checks here compiled the new code but did not execute either new test; they run infull-suite.ymlat merge. Correct per canon — noting it so "CI is green" is not read as "the new tests passed in CI".
5. What I tried to break (and failed)
Everything below is reasoned from reading the code in /tmp/aetherclaude/pr-5538. I have no build and no way to drive the GUI, so nothing here was reproduced at runtime — where I say a path is safe, I mean I traced every writer, not that I watched it run.
- Out-of-bounds read on the new ring.
appendVisibleRowreadsm_wfVisibleTimeRows[m_wfWriteRow]before the pre-existingm_wfWriteRow = (m_wfWriteRow - 1 + h) % hnormalizes it — and that modulo is itself the tell that the old code did not assumem_wfWriteRow < h. I traced every reallocation ofm_waterfall:resetCurrentWaterfallRowsForSizeends inclearCurrentWaterfallRows()which setsm_wfWriteRow = 0(:6556); the resize commit sets it to 0 (:11808);rebuildWaterfallViewportForFramesets it to 0 (:6129); andrestoreCurrentWaterfallStreamStatebails to the reset branch onstateHasWrongWaterfallbefore ever assigningrestored.wfWriteRow. The invariant holds at entry, so the unnormalized index is in range. This was my best blocker candidate and it does not survive. - Stale timestamp latch.
m_wfIncomingTimestampMsis a latch, so I checked all threeappendVisibleRowcall sites — the Flex native-tile loop (:8751), the fallback path (:12350) and the Kiwi path (:12407). Each is immediately preceded byappendHistoryRow, and the latch assignment is the first statement inappendHistoryRow, ahead of the hidden-source early returns. No path reachesappendVisibleRowwith a stale stamp. - Markers invisible when the time-scale strip is hidden. The width is
std::min(wfContentRect.width(), waterfallTimeScaleRect(wfRect).left() - wfRect.left()), which goes negative if that rect is ever null.waterfallTimeScaleRectis unconditional and anchored towfRect.right(), soleft() - wfRect.left()is alwayswfRect.width() - stripWidth. No negative-width clip. - GPU SRB layout incompatibility with the borrowed
m_ovPipeline. The new SRB has one binding;m_ovSrb, which the pipeline was created against, also has exactly one (sampledTexture(1, FragmentStage, …)) — layout-compatible, sosetShaderResourcesis legal. - The #3294 premultiply-squared class of bug. The atlas is
Format_RGBA8888_Premultipliedandm_ovPipeline's blend issrcColor = One/dstColor = OneMinusSrcAlpha— the correct pairing for a premultiplied source. Had it beenSrcAlpha, the translucent label backing would have been crushed exactly as the passband fill was. - Scroll-offset sign, i.e. markers sliding against their rows during animation.
m_waterfallScrollDistanceRows - waterfallScrollProgressRows()is byte-identical to the pre-existingsampleOffsetRowsatSpectrumWidget.cpp:15699, which is whatdrawWaterfall'ssourceStartuses. Same convention, not a re-derivation. - Duplicate/wrong labels from the
m_wfTimeMarkerLabels.indexOf(...)lookup. For a monotonic run, two visible markers cannot share a boundary bucket (the younger row's predecessor would already be inside it, so it would not register a crossing). The one non-monotonic case — an NTP step back that re-crosses the same boundary — yields two markers with the same correct label, which is whatindexOfrenders. - Degenerate geometry. The line quad's zero-height source rect, an empty
labelslist, and zero-width clips: theclipped.isEmpty()early return precedes every division,atlasHeightis ≥ 2 even with no labels, andlabelsis non-empty whenevermarkersis (the first marker always clears the dedup threshold). - Token hex byte order.
#dc0f0f1ais 9 chars, so Qt reads#AARRGGBB— alpha 220 over#0f0f1a.colorToTokenStringemitsHexArgbfor alpha < 255, and#5a0f0f1aalready ships indefault-dark.jsonwith the same RGB. The value follows the house convention rather than inventing one. - Whether the new tests would pass against a broken implementation.
waterfall_time_markers_testis real: I hand-evaluated the wrapped-ring case (head 3, size 4, offset 0.25, height 40) and the assertedyvalues 7.5 and 27.5 and the count of 2 all follow from the guards. Deleting thetimestampMs <= previousMscheck breaks "a batched tile must not repeat a timestamp"; deleting the bucket comparison breaks "before boundary"; returning arrival time instead of the boundary breaks the per-interval delayed-row case. It does not assert the implementation back to itself. - Could not check: any runtime/visual behaviour, the actual GPU draw on a real RHI backend, and whether the 1 px line reads well against a live waterfall. The PR body's macOS ARM64 automation-bridge verification is not something I can reproduce headless — a maintainer eyeballing the overlay in both render paths (and at DPR 2) would settle the visual half.
6. Recommendation
Needs maintainer decision — on the feature itself, not on the code. #5537 carries maintainer-review and explicitly reserves visual/UX direction, and this PR adds new public surface (a Q_PROPERTY, two automation keys, a settings key, two theme tokens). On engineering grounds I would merge it: it sits on structures that already existed rather than adding a retention layer, the geometry is a pure function with a genuine test behind it, both render paths are covered, the per-slot scope correctly avoids the global propagation the triage warned about, and it survived every attack in §5. The two open calls are whether the 30-minute and 1-hour entries earn their menu space, and a visual check of the overlay in both paths. Nits are all cosmetic or test-hygiene and none need to block.
Nice work — this is unusually careful about the ring/scroll invariants, which is where this feature had the most room to go quietly wrong.
🤖 aethersdr-agent · cost: $14.2168 · model: claude-opus-5
Summary
Closes #5537.
Right-click the panadapter or waterfall and choose Waterfall Time Markers to display thin horizontal lines with small UTC timestamps at the left edge. Intervals are Off, 15/30 seconds, 1/5/10/15/30 minutes, and 1 hour. Off remains the default, and the selection persists independently per panadapter slot in the existing Display document.
Markers align to clock boundaries (for example, HH:mm:00 for one-minute markers) and follow their captured signal rows through live scrolling, paused history, and resize. Changing the waterfall rate affects future row cadence; existing history retains its captured row spacing. GPU and software rendering share timestamp geometry and a cached label atlas. Dedicated theme tokens provide subdued pale gray-blue lines/text with a dark label backing.
This is a client display feature with no radio protocol, TX, dependency, or unrelated behavior changes. Final visual/UX acceptance is requested from the maintainer. The minimum interval is intentionally 15 seconds to limit clutter.
Constitution principle honored
Test plan
waterfall_time_markers_test,waterfall_time_marker_settings_test,waterfall_history_buffer_test, andspectrum_preview_logic_test.New CTests are socket-free. Bridge proof is a separate manual Demo session. No live-radio validation is claimed; reopening the earlier testing profile briefly auto-connected to a saved FLEX, so that instance was closed without transmit and the final handoff used the clean Demo profile.
Checklist
Prepared with Codex assistance. Meter smoothing and GHSA references are not applicable to this display-only feature.