* Upgrade Windows CI to llvm-mingw 20260908
Bumps the llvm-mingw release used by the Windows CI/CD build, sanitizer,
and PGO jobs from 20251216 to 20260908.
Also fixes two latent bugs surfaced while verifying the upgrade by
cross-compiling locally with the macOS build of llvm-mingw:
- plot_waterfall.cpp used std::atomic without including <atomic>,
relying on it being pulled in transitively. Newer libc++ no longer
does so.
- CMakeLists.txt chose the top-level project() LANGUAGES via if(APPLE)
before any project()/enable_language() call had run, so APPLE
reflected the host platform rather than CMAKE_SYSTEM_NAME from the
cross-compile toolchain file. On a macOS host this silently took the
Apple branch even when cross-compiling for Windows, adding
-Wl,-ld_classic to the link flags -- a flag ld.lld doesn't understand.
Windows CI (Linux-hosted) never hit this, but it broke any Windows
cross-build attempted locally from macOS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Add PR #1489 to changelog.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* CI (Windows): add RADE loss testing (prerequisite for sanitizer coverage)
Ports the RADE loss test infrastructure from the ms-rade-v2 branch
(as of commit f013450b there) so the Windows sanitizer jobs added in
the following commits have a RADE loss test to actually run:
- test/TestFreeDVRadeLoss.ps1: transmits a known corpus through FreeDV,
records the result, plays it back through RX, and compares TX/RX RADE
features via loss.py against a threshold.
- test/RadeVerificationReport.ps1: assembles the RADE integration
verification report from a test run.
- cmake-windows.yml: adds the rade-loss-baseline job (computes the loss
threshold from a software-only baseline on Linux, since rade_tx_wav/
rade_rx_wav are excluded from the Windows build), wires RADE loss
testing and verification-report generation into the `test` job, adds
crash dump collection (WER LocalDumps) for post-mortem diagnosis, and
switches audio-endpoint readiness waiting to Wait-AudioDevices.ps1 to
avoid starting a test before a virtual cable is actually enumerable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Windows CI: add UBSan/ASan sanitizer build+test jobs
llvm-mingw supports UBSan on every architecture it targets and ASan on
x86 only (confirmed against the toolchain's own release assets: only
x86_64/i386 ship a libclang_rt.asan_dynamic-*.dll, no aarch64 variant
exists). Adds build-sanitizer (mirrors build-pgo-inst minus PGO, one
variant per {UBSAN x86_64, UBSAN aarch64, ASAN x86_64}) and
test-sanitizer (mirrors test, pointed at the new artifacts).
No manual DLL copy step needed for ASan: cmake/GetDependencies.cmake.in
already walks freedv.exe's PE import table via objdump and derives the
toolchain's <arch>-w64-mingw32/bin/ directory as a search path when
FREEDV_USING_LLVM_MINGW is set -- the same mechanism that bundles every
other DLL dependency already picks up libclang_rt.asan_dynamic-*.dll
automatically once ENABLE_ASAN is on. This path was already prepared
for in a prior commit (73cd6f49, "Fix cross-compile definitions to
allow asan to be used in the first place") but never wired into CI.
Sanitizer test steps are continue-on-error, matching the existing
SANITIZERS_ENABLED leniency on macOS/Linux (ctest doesn't enforce the
PASS_REGULAR_EXPRESSION under sanitizers there either) -- a sanitizer
build is slower and less reliable for real-time audio, so what matters
here is whether a sanitizer catches a genuine memory-safety/UB bug
(an abort with a diagnostic in the log), not the loss threshold.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Fix Windows sanitizer options: drive-letter colon breaks the parser
Every test-sanitizer run was failing at freedv.exe startup with
"AddressSanitizer: ERROR: expected '=' in ASAN_OPTIONS" (and the UBSan
equivalent), masked as job "success" by continue-on-error -- meaning
zero actual testing happened in the previous push.
ASAN_OPTIONS/UBSAN_OPTIONS are colon-separated key=value pairs. The
absolute path used for suppressions= started with a Windows drive
letter (D:\...), and the sanitizer's own option parser split on that
colon too: "suppressions=D" parsed as one pair, then "\...\foo.txt"
(no '=') aborted the parse before FreeDV ever started.
Fix: copy the suppression files next to freedv.exe (alongside the
existing test script copies) and reference them by bare relative
filename, which contains no colon to collide with.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Suppress known wxWidgets MSW UBSan finding (null pointer in tooltip.cpp)
wxToolInfo's constructor does a member access through a null pointer of
type TTTOOLINFOW in wx's own MSW backend (tooltip.cpp:100), hit
consistently on both new Windows UBSAN test-sanitizer jobs
(windows-2022 and windows-11-arm). Third-party code, not ours -- same
treatment as the existing macOS vptr suppressions for wx's own latent
UB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Suppress libressl UBSan finding; make Windows sanitizer jobs actually fail
test/ubsan_suppressions.txt: add function:crypto/stack/stack.c for
"call to function local_sk_X509_NAME_ENTRY_pop_free through pointer to
incorrect function type" -- a well-known, benign pattern in OpenSSL/
LibreSSL's type-erased sk_TYPE_pop_free callback casting, not a real bug.
cmake-windows.yml: the three test-sanitizer test steps are
continue-on-error (needed so an ordinary loss-threshold miss under
instrumentation overhead doesn't stop the other tests, matching the
SANITIZERS_ENABLED leniency ctest already gives sanitizer builds on
macOS/Linux) -- but continue-on-error suppresses the job's conclusion,
not just the step's outcome, so a genuine sanitizer abort was *also*
being swallowed into job "success" with no fix. Added a "Check for
sanitizer errors" step (no continue-on-error) that scans every test's
captured output for an actual AddressSanitizer/UndefinedBehaviorSanitizer/
LeakSanitizer report and fails the job for real if one is found, while
a plain "Test failed" from a loss-threshold miss (no sanitizer report
in the output) still doesn't. Also wired Tee-Object logging into the
FullDuplex and Reporting test steps (RadeLoss already had it) so all
three are covered by the scan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* CI (Windows): drop RadeVerificationReport, fix missing hamlibserver.py
Remove RadeVerificationReport.ps1 and the workflow steps that generate/
upload the RADE verification report from the `test` job -- out of scope
for this PR (Windows ASan/UBSan CI + the RADE loss test infra it needs).
Separately: PR CI showed both `test` and `test-sanitizer (ASAN, ...)`
failing the RADE Reporting step with "python.exe: can't open file
'...\bin\hamlibserver.py'". TestFreeDVReporting.ps1 unconditionally
launches hamlibserver.py (a mock rigctld) as a subprocess before
starting FreeDV, but neither job's "Copy test scripts to install
folder" step actually copied it there -- only TestFreeDVReporting.ps1
itself and its conf template were copied. Added the missing Copy-Item
to both jobs.
(The consequent "Couldn't connect to Radio with hamlib" fatal error
also triggered an apparent AddressSanitizer heap-use-after-free in
wxMutexInternal::LockTimeout during the ASan job's abnormal shutdown --
very likely collateral damage from the broken test flow rather than a
real bug, but worth re-checking once this fix lands.)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* CI (Windows): actually commit the RadeVerificationReport/hamlib workflow changes
Follow-up to 54aa6988 -- that commit only picked up the file deletion;
the cmake-windows.yml edits (removing the report generation/upload
steps and the RadeVerificationReport.ps1 copy, adding the missing
hamlibserver.py copy to both the test and test-sanitizer jobs) didn't
get staged due to a failed `git add` on the already-removed path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* master still uses RADEV1, not V2.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* TxRxThread: log a processing-time histogram alongside min/max/mean/stdev
min/max/mean/stdev over a run of several thousand frames can't tell a
lone freak outlier apart from a real cluster of slow frames -- a rare
but non-negligible tail gets averaged away by mean/stdev, and only the
single worst frame shows up in max. Several Windows CI failures this
session showed catastrophic RADE loss with a completely unremarkable
existing stats line (low single-digit ms max), so the shape of the
distribution is the piece we've been missing.
Buckets by upper bound in ms (0.5/1/2/5/10/20/50/100/200/500/1000/2000,
plus an overflow bucket for >=2000ms), incremented in the same
startTimer_/endTimer_ pair already used for the existing stats, so no
new measurement points. Verified locally: a healthy run already shows
real spread across multiple buckets (not just noise near one value),
so this should give much better signal on the next Windows failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* TxRxThread: also measure time spent waiting between wake cycles
Every processing-time stat we had only wrapped pipeline_->execute() --
none of it covered the helper->stopRealTimeWork() semaphore wait between
wake cycles in Entry()'s outer loop. That's a real blind spot: a
scheduling delay (the thread taking longer than expected to actually be
resumed) would show up as extra time spent *waiting*, not as extra time
spent *processing*, and every failure investigated this session showed
clean processing stats -- but we had no way to tell if the wait side
looked clean too, since it was never measured at all.
Refactored the ad hoc min/max/mean/stdev/histogram fields and functions
into a reusable TimingStats struct (was about to duplicate all of it a
second time for the wait measurement, which is a clear sign it wanted
to be one thing parametrized by a label rather than two copies). Now
tracks two independent instances: processingStats_ (unchanged
behavior, just moved) and waitStats_ (new -- measures from right before
stopRealTimeWork() to the top of the next loop iteration).
Verified locally: the wait histogram already shows a real, explainable
signal -- most wait cycles on this run cluster right at ~10ms, which
is exactly MacAudioDevice::stopRealTimeWork()'s fastMode timeout
((1000*1024/48000)>>1 = 10 via integer truncation), i.e. the semaphore
is timing out rather than being signaled early most of the time. That's
expected/healthy behavior, and now we have a baseline to compare a
failing run's wait distribution against -- specifically, whether wait
times ever blow past that ~10-21ms designed bound, which would point at
genuine OS-level scheduling starvation rather than slow processing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Fix and extend real-time thread wake-up debt compensation across platforms
WASAPIAudioDevice::stopRealTimeWork() computed extraTimeMs_ (how much
last cycle's total processing+wait overran its nominal period) and
subtracted it from this cycle's target wait duration -- but then passed
the *uncompensated* nominal period to WaitForSingleObject() instead of
the value it had just computed. So the compensation only ever worked in
the extreme case (this cycle's debt already exceeds a full period, skip
the wait entirely); the common case of a small overrun did nothing at
all, silently. Fixed to actually wait for the compensated duration.
PulseAudioDevice's equivalent logic was already correct for comparison
(it builds the actual wait deadline from the compensated value).
MacAudioDevice had no such compensation at all -- stopRealTimeWork()
always waited the full nominal period regardless of how long processing
took, meaning any nonzero processing time made the average loop period
longer than intended every single cycle, drifting later relative to
real time under load instead of self-correcting like the other two
platforms already did. Ported the same pattern (record start time in
startRealTimeWork(), track extraTimeMs_ debt, shave it off the next
wait, floor at zero) to bring it to parity.
Verified locally on macOS: rade_loss still passes (loss 0.082), wait
times remain correctly bounded within the designed timeout window.
Can't locally verify the WASAPI fix (no Windows dev environment here);
next CI round will confirm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* WASAPIAudioDevice: use a high-resolution waitable timer for sub-ms wait precision
WaitForSingleObject's timeout is a DWORD of whole milliseconds, which forced
the wake-up debt compensation added previously to round its compensated wait
duration down to the nearest millisecond before ever calling it -- discarding
sub-millisecond precision on every cycle.
Replace it with WaitForMultipleObjects() on the existing semaphore_ (for early
wake on new audio) plus a new high-resolution waitable timer (CreateWaitableTimerEx
with CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, falling back to a regular waitable
timer on pre-1803 Windows) armed via SetWaitableTimer with a 100ns-resolution
due time. extraTimeMs_ becomes extraTimeHns_ (100ns units) so the debt
bookkeeping itself no longer loses precision to millisecond rounding, not just
the final wait call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* WASAPIAudioDevice: anchor wait compensation to startTime_, not last cycle's debt
The previous compensation shortened this cycle's wait by a debt figure copied
from the *previous* cycle's total overrun -- a one-cycle lag. Under steady
processing time that converges to the same average as compensating directly,
but when processing time is bursty (one long cycle, one short) it over/under-
corrects: it fully discounts the next wait for a spike that cycle may not
actually have, producing a long-then-short cadence oscillation instead of
absorbing the spike within its own cycle.
Compute the compensation directly against startTime_ (recorded by
startRealTimeWork() right before processing begins) instead: elapsed time
since startTime_ is this cycle's own already-known processing cost, so it
can be subtracted immediately with zero lag. waitOvershootHns_ replaces
extraTimeHns_ and narrows to tracking only the wait call itself overshooting
its requested duration -- the one component that genuinely can't be known
until after it happens -- so a systematic scheduling overshoot still can't
silently accumulate into long-term drift.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk
* Add PR #1488 to changelog.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* test: back off rade_reporting_awgn channel noise by 3 dB
rade_reporting_awgn adds AWGN with `ch --No -18` and passes only if RADE
decodes the callsign and FreeDV logs "Reporting callsign ZZ0ZZZ @ SNR".
`ch` fixes the noise density, not the SNR, its AWGN realisation is random
per run, and its input (test.wav) is a live virtual-cable capture whose
level varies run to run. On CI this landed the decode at ~5-6 dB SNR --
right on the RADE decode cliff -- so the test fails intermittently on the
non-sanitizer Linux legs (usually, but not always, absorbed by
`ctest --repeat until-pass:2`).
Move the AWGN case to `--No -21`: ~3 dB of headroom above the cliff,
still a genuinely noisy channel. mpp (`--No -25`, `-txattempts 7`) is
left alone -- it has ample decode retries and isn't flaking.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a
* Increase noise slightly.
* Oops, accidentally deleted the - sign.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
"Generate PGO data" fails intermittently when a VB-Cable / VAC endpoint
isn't enumerable at the moment FreeDV starts a UT: FreeDV pops a fatal
"device cannot be found" message box and aborts.
The old "Start Windows Audio Service" step only polled WMI
Win32_SoundDevice (driver nodes, not the MMDevice endpoints FreeDV
uses), had an operator-precedence bug in its until-condition
((A -and B) -or C), ran once minutes before the test, and never checked
the specific endpoints the job needs.
* ci/Wait-AudioDevices.ps1: restarts AudioEndpointBuilder/audiosrv and
polls "Get-AudioDevice -List" (the active-endpoint surface FreeDV
actually enumerates) until every required playback/recording endpoint
is present, nudging the audio stack again halfway through and dumping
full diagnostics before failing on timeout.
* ci/Invoke-PgoProfileCollection.ps1: runs the endpoint wait +
GeneratePGOProfiles.ps1 and retries the whole thing up to 3x, since an
endpoint can also drop mid-run right after SoX releases the capture
device. Partial .profraw files are cleared between attempts.
* collect-pgo-profile now calls these; the Generate PGO data timeout
goes 10 -> 25 min to cover the retries.
* GeneratePGOProfiles.ps1: best-effort wait for the endpoints before
each of the TX and RX passes.
The identical "Start Windows Audio Service" step in the test job is left
as-is here; this change is scoped to collect-pgo-profile.
Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Match memory ordering to what each atomic actually synchronises.
Two related tidy-ups in how std::atomic orderings are specified. Neither
changes behaviour; both make the code say what it means.
The four FIFO debug counters -- g_infifo1_full, g_outfifo1_empty,
g_infifo2_full and g_outfifo2_empty -- used acquire/release throughout. They
publish nothing: they are incremented in the audio callbacks and read only to
display a count, in the options dialog and in the PTT drain poll. No reader
dereferences memory whose visibility depends on them. Release on those
fetch_add()s advertises a happens-before relationship that nothing consumes,
which is misleading to anyone working out what guards what in the real-time
path. Relaxed is what these actually need, at all 18 sites.
Three accesses were taking the default seq_cst by writing the atomic without
.load()/.store():
* TxRxThread's playback-record predicate read g_recFileFromModulator and
g_sfRecFileFromModulator bare, while the structurally identical predicate a
hundred lines above spells out acquire on g_playFileToMicIn/g_sfPlayFile.
These do publish data -- the opened SNDFILE has to be visible to the RX/TX
thread -- so they stay acquire, just explicitly, and now match their
neighbour. This one runs on every 20 ms frame.
* m_run was read as `while (m_run)` and written as `m_run = false`.
* ongui.cpp sampled g_outfifo1_empty by implicit conversion.
Deliberately left alone: the SNDFILE*/bool publishing pairs, isModemRunning,
ParallelStep's exitingThread, and the endingTx/g_eoo_enqueued handshake, all of
which order access to other memory; and tuneSineWaveSampleNumber and
numRealTimeWorkers_, which are relaxable in principle but are part of the
callback and workgroup protocols and run twice per callback at most.
No performance claim is intended. On arm64 the affected operations do change
instruction (ldaddl -> ldadd, ldar -> ldapr, stlr -> str), but every one of
them runs at most a few times per 20 ms frame, and no atomic in this codebase
sits inside a per-sample loop -- those were hoisted out already. The value here
is that the ordering now documents the actual synchronisation.
Verified with the rade_loss test (PASS, loss 0.099), including a clean
thread start/stop cycle, which is what the m_run change touches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ
* Add PR #1482 to changelog.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Cut waterfall and scalar plot paint cost; fix uninitialised heatmap entry.
On macOS wxGraphicsContext::DrawBitmap(wxBitmap) is far more expensive than the
wxGraphicsBitmap overload: wxBitmapRefData::GetImage() caches nothing, so every
call allocates a fresh NSImage and draws through -[NSImage drawInRect:], where
the wxGraphicsBitmap path goes straight to CGContextDrawImage. The waterfall made
one such call per pixel block -- m_imgHeight/dy of them, ~200 on a 600px tall
plot -- on each of its 10 frames a second.
* Waterfall blocks are now held as {wxBitmap, wxGraphicsBitmap} pairs. The
wxBitmap stays as the StretchBlit target; the renderer-native copy is made once,
when the block is filled, rather than on every frame it spends scrolling down
the screen. Measured 1.648 -> 0.453 ms per paint at 700x600.
* PlotScalar composites its plot area the same way. The conversion is per frame
here since plotArea_ is redrawn each time, but it still beats the NSImage round
trip: 0.381 -> 0.205 ms per paint at 700x200 on the data-only repaint path that
"Frm Mic" and friends actually take.
* Waterfall graticule labels are laid out once per resize instead of per frame.
wxWindowMac::DoGetTextExtent builds and destroys a wxGraphicsContext per call
and drawGraticule() was making ~25 of them a frame for text that only moves when
the control is resized. It also measured every one second tick before checking
whether that tick gets a label, so most of the measuring was discarded.
* Dropped BeginLayer(1.0)/EndLayer from the waterfall, scalar and spectrum plots.
At opacity 1.0 the transparency layer allocates and composites an offscreen
buffer to produce exactly what drawing directly produces.
Separately, the heatmap LUT was filled over 0..254 while plotPixelData() clamps
intensity to 255 and reaches it whenever a bin sits at the top of the current
range -- so the hottest pixels took their colour from indeterminate memory on
essentially every frame carrying signal. Rendering a synthetic spectrum offscreen,
1665 pixels in the peak column came out as RGB(15,0,0), RGB(62,0,0) and
RGB(220,0,0) instead of the intended full-scale red.
Verified by driving draw() offscreen and diffing the output against master over
400 frames, enough to fill the waterfall and exercise block recycling roughly 200
times: the performance changes alone are bit-for-bit identical, and the only
pixels the LUT fix moves are the max-intensity ones described above.
test_rade_loss.sh passes with these changes, though it cannot really speak to
them: it exercises no plotting, and on this machine it fails intermittently on
master too (5 pass / 1 fail on master, 4 pass / 3 fail here across interleaved
runs of the two builds).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ
* Add PR #1481 to changelog.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Equalizer: make sure number of output samples is correct.
There may be a scenario where `EqualizerStep` doesn't properly set the number of output samples. This ensures that we're always setting the number of output samples to a valid value.
* Add PR #1480 to changelog.
* Fix compiler error with Hamlib 5.0.
* Fix typo.
* Bring back old code for Ubuntu 22.04.
* Update CMake to explicitly look for rig_state.h.
* Add PR #1477 to changelog.
* Fix typo.
* Remove deprecated macos-14, test only on macos-26 due to GitHub issues.
* Work around dyld error when enabling sanitizers.
* Readd macOS 14 and 15 to tests.
* Add PR #1478 to changelog.
* Fix waterfall/spectrum filling solid colour during half-duplex TX
The half-duplex-TX "blank the display" path memset() the spectrum
arrays to zero bytes, but this display's dB scale runs 0 (loudest) to
MIN_MAG_DB (quietest) -- zero meant "loudest possible", not silence,
so it painted the waterfall solid instead of blanking it (yellow,
occasionally clamping to red at the top of the colour scale).
Filling with MIN_MAG_DB alone wasn't sufficient either: the
waterfall's colour scale is relative to each row's own peak (peak
minus a fixed 20dB window), so a perfectly flat row -- regardless of
its absolute level -- always reads as "loud relative to itself". This
showed up live as a green-to-red fade as the auto-ranging baseline
(m_max_mag) caught up to the new flat level.
Fixed properly by detecting a row where every bin sits exactly at the
floor (real spectra essentially never do) and rendering it as plain
black directly, bypassing the relative intensity scale and leaving
the auto-ranging baseline untouched, rather than running synthetic
placeholder data through logic designed for real spectra.
Confirmed identical on master and v3.0-dev; tested live against a
real half-duplex TX/RX cycle.
* Add PR #1462 to changelog.
---------
Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
* Fix issue preventing TX thread from sleeping.
* Forgot additional change.
* Try 40ms frames.
* 10ms
* Probably still need to feed zeroes as needed.
* Revert frame duration back to 20ms.
* Add PR #1465 to changelog.
* Enable CCache to make CI builds run more quickly.
* Ensure Hamlib is also part of ccache.
* Try creating symlink.
* Increase ccache max size limit.
* Add ccache for Windows builds.
* Add cache for lint as well.
* Make sure PGO is covered by ccache.
* Dummy change to make sure build is cached.
* macOS: Ensure node 24 is used for ccache
* Windows: use node 24.
* Increase max cache size on macOS to 5G.
* Add Windows debugging for ccache.
* Use CMake compiler launcher to launch ccache.
* Use symlinks for PGO builds.
* Don't cache PGO use steps, we can't afford to with only 10GB available.
* Fix issue causing cache misses.
* Forgot that we need to use single quotes.
* Revert previous whitespace change.
* Enable ccache for Linux builds.
* Fix shell command error.
* Add PR #1328 to changelog.
* Fix FreeDV Reporter column-order repair to detect gaps, not just trailing entries
If a persisted freedvReporterColumnOrder value was missing an index
from the middle of its range (not just missing new indices appended
after a NUM_COLS increase), the old repair logic never noticed --
it only ever appended indices above the current max element. A
column permanently missing from the list means the wxDataViewCtrl
never creates it, so any later getColumnForModelColId_() lookup for
that column asserts/crashes (e.g. while sorting on it as new spots
arrive).
Reproduced and fixed against a real corrupted config
(ColumnOrder missing index 12/SNR_COL from the middle, confirmed to
crash on assert `item != nullptr` in getColumnForModelColId_ during
live FreeDV Reporter use) -- also explains a previously-unexplained
symptom of an extra empty column appearing when dragging the
rightmost column divider.
* Fix column-order gap detection, sentinel-column drag corruption, and Last TX width
Brings this branch's earlier column-order fix up to the same final
state as testing surfaced on v3.0-dev, since all three are general
bugs unrelated to the LAST_RX_MODE_COL difference between the two
branches:
- Validate/repair the persisted column order unconditionally rather
than only when its length looks wrong -- a config value can have
the right length while still containing an out-of-range/duplicate
entry and missing a genuine one, which the old size()-only check
never caught.
- OnColumnReordered's save path assumed the trailing sentinel spacer
column always stays in the last visual position; nothing enforces
that, so a drag could leave it captured as if it were one of the
real NUM_COLS columns, silently dropping a real one and persisting
the sentinel's invalid model ID instead -- the actual cause of an
"empty column" appearing after reordering.
- Keep the Last TX column at least as wide as Last Update's actual
current width (both use the same date/time format), checked
periodically via the dialog's existing timer rather than once at
construction time, since neither column's real autosized width is
known reliably that early.
Confirmed working live on the desktop against a real corrupted
config with a real radio.
* Minor style tweak.
* Add PR #1458 to changelog.
---------
Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
* Enable PGO for Windows builds.
* instrumented prefix wasn't actually being added.
* Port generate_pgo_profiles.sh to PowerShell (thanks Claude).
* Try disabling RX so we can at least debug rest of pipeline.
* Fix profraw path.
* Try enabling RX code again.
* See if we can get some extra debugging.
* Bound process wait.
* Use Reporting sox invocation.
* Redo config file prior to RX run.
* Fix LLVM profile merge error.
* Add PR #1457 to changelog.
* Experiment: Enable LTO/PGO for macOS .app build.
* PGO: Fix tab layout to match new encoding.
* Try disabling LTO during instrumented build.
* Revert "Try disabling LTO during instrumented build."
This reverts commit e6b4e1a191.
* Try disabling temporal profiles.
* Need to run profiling on both x86_64 and arm64.
* Disable universal builds for the PGO instrumented build.
* Revert "Try disabling temporal profiles."
This reverts commit 89036a3ed6.
* Add PR #1456 to changelog.
* Fake change to force rebuild.
* Revert "Fake change to force rebuild."
This reverts commit a2a8886377.
* Add Intel checks for .app.
* No torch on Intel anymore.
* Fix crash on repeat main-window close while shutdown is in progress
topFrame_OnClose() unconditionally dereferenced m_reporterDialog, but a
second wxEVT_CLOSE_WINDOW arriving while the async RX/PTT shutdown from
a first close is still running (e.g. during a slow Hamlib rig disconnect
against an unresponsive radio) re-enters the handler after
m_reporterDialog has already been set to null on the first pass --
SIGSEGV in wxWindowBase::GetPosition(). Guard re-entry via terminating_
and null-check m_reporterDialog, matching the pattern already used in
the destructor and setConfiguration_().
* Don't block on rig disconnect against an unresponsive radio when turning modem off
Dropping the last shared_ptr reference to a rig controller runs its
destructor, which blocks until the rig actually finishes disconnecting.
Against an unresponsive radio (e.g. powered off, connected via rigctld)
this can take far longer than Hamlib's own client-side timeout/retry
settings suggest, since those don't bound however long rigctld itself
waits on the physical radio -- observed over a minute with no feedback
to the user. Move the last reference onto a detached thread for both
rig controllers so that wait can't hold up turning the modem off or
app shutdown.
* Bound the wait for rig-disconnect threads during app-close shutdown
The detached rig PTT/frequency controller disconnect threads added in
the previous commit have no upper bound on app-close: MainFrame::Destroy()
fires right after performFreeDVOff_() returns, with nothing joining those
threads, so an unresponsive rig still mid-disconnect when the process
actually exits gets its thread killed outright -- e.g. a queued ptt(false)
might never reach the radio. Have each detached thread signal completion
via a future, and on the terminating path wait up to 3s (shared deadline
across both) before proceeding to Destroy() -- long enough for a merely
slow rig to finish cleanly, still bounded so a truly unresponsive one
can't hang shutdown.
Also expand the terminating_ guard comment in topFrame_OnClose() to note
it prevents more than the m_reporterDialog crash: m_RxRunning stays true
until deep inside the same async shutdown, so a repeat close request
could otherwise re-enter OnTogBtnOnOff() a second time concurrently with
the shutdown already in progress.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* Add PR #1452 to changelog.
---------
Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
* Fix CMAKE_REQUIRED_DEFINITIONS missing -D prefix in GTK3 wx probe (#1449)
wxWidgets_DEFINITIONS is a list of bare macro tokens (correct for the
COMPILE_DEFINITIONS property UsewxWidgets.cmake sets it via), but
CMAKE_REQUIRED_DEFINITIONS requires full "-DFOO" strings. Without the
prefix, check_cxx_symbol_exists()'s probe compile passes each bare
token as a raw command-line argument, which the compiler treats as a
(nonexistent) input filename and errors out on -- silently failing the
whole check closed (WX_BUILT_FOR_GTK3 always false) even against a
genuinely GTK3-built wxWidgets.
With HAS_GTK3 then never defined, topFrame.cpp's GTK3 guard takes the
non-GTK3 fallback branch and creates a stray 1x1 reference wxWindow
directly on the main frame -- disabling wx's "auto-resize sole child
to fill the frame" behaviour and freezing the entire main window's
layout on resize.
Verified on a real GTK3 wxWidgets 3.3.1 build (Mageia 10): reproduced
the false negative on a completely fresh configure (no stale cache
involved), then confirmed the fix resolves it on the first try.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add PR #1450 to changelog.
* Revert accidental commit.
---------
Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
* Fix stale selection index crash in wxListViewComboPopup
OnMouseMove() deselected the previously hot-tracked row (m_value)
without checking it was still a valid index, so any external
DeleteAllItems() on the underlying list left a stale index that
crashed the next mouse-move over the popup.
* Fix heard-stations callsign combo stuck-highlight and right-click behaviour
OnCloseCallsignList/OnRightClickCallsignList moved keyboard focus away
from the read-only combo synchronously, but the popup window overlaps
the combo's own value area while open and GTK doesn't always repaint
that region on dismiss, leaving it showing a stale "focused/selected"
highlight. Defer the focus change via CallAfter and force a repaint
once the popup's dismissal has fully finished.
Also stop OnRightClickCallsignList from clearing the displayed
callsign text on deselect -- it should only remove the selection,
consistent with how right-click deselect works in the FreeDV Reporter
list.
* Add PR #1448 to changelog.
---------
Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>