perf(wdsp): build the minimum-phase workspace only when minimum phase is on - #5697
Conversation
There was a problem hiding this comment.
Issue fit
No fixes/closes reference and no linked issue — I checked the PR body and searched; this stands on its own stated intent, which is a performance change to a vendored file plus the test that makes the new lazy path safe. Per GOVERNANCE.md that's fine: it is not an architecture change, a new dependency, or a UX change, and it is a local patch to an already-enumerated vendor patch set. The change does what it says: plan_fircore() builds the minimum-phase workspace only at mp != 0, calc_fircore() builds it on first use, deplan_fircore() tolerates the null, and AETHERSDR-PATCHES.md records it as patch 5 with a drop condition.
Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
third_party/wdsp/upstream/firmin.c |
The patch itself: conditional build in plan_fircore(), new static ensure_minphase(), guard in calc_fircore(), null-tolerant deplan_fircore() |
Yes | In scope |
third_party/wdsp/AETHERSDR-PATCHES.md |
Four→five, patch 5 entry with rationale, counts and drop condition | Yes | In scope — this is the required provenance record (Principle IV) |
tests/wdsp_channel_test.cpp |
runMinimumPhaseWorkspaceTest, registered in main()'s runLeakChecked chain |
Yes | In scope |
Everything in the diff is explained by the stated change. No new public surface, no settings, no UI, no CHANGELOG entry (correct — do not add one). No deleted guard: the only - lines in firmin.c are the unconditional create_minphase and the unconditional destroy_minphase, both replaced by strictly more defensive forms. No sibling copies — firmin.c is the only definition of plan_fircore/deplan_fircore in the tree, and pminphase is read in exactly the four places this diff touches (grep pminphase third_party/wdsp/upstream/ returns only firmin.c and the firmin.h struct member).
CI on e3235a35 is green across build / check-macos / check-windows / Static checks / Sanitizer option configures, against base 85e4a1ff which is current main.
Blockers
None.
Nits (non-blocking)
- The ~66.9 MB figure doesn't reconcile with the patch note's own inventory (
AETHERSDR-PATCHES.md:121, inline). By the note's own numbers — 104 bytes/element and lengths 131072×3, 65536, 32768×8, 8192×5, 4096 — I get 765,952 elements × 104 = 79.7 MB (76.0 MiB), not 66.9. It's an understatement rather than an oversell, and the note correctly labels it "arithmetic on those counts, not a measurement", but it would be good to show the working or correct it. The allocation counts in the same paragraph (1300→1204 = 96 = 12×8; 1518→1374 = 144 = 18×8; mp==1 at 1252 = 1204+48 = 6×8) are all internally consistent and I could not fault any of them. - The "plan-construction time first" framing understates the existing wisdom cache (
AETHERSDR-PATCHES.md:98, inline).WdspChannel::open()callsloadWisdomOnce()before the first plan andexportWisdomNow()after (src/core/dsp/WdspChannel.cpp:881,:919), so on a warm cache FFTW_PATIENT does not re-measure and the planner cost is largely already amortised. The durable win is the one the counts prove: 12 fewer live objects and tens of MB of resident workspace per RX channel, on every open, on every backend that opens aWdspChannel. That's a good enough case on its own. AETHERSDR-PATCHES.md:5now reads "five fixes", but patch 5 is a performance change, not a correctness fix — the closing paragraph already draws that distinction ("changes no filter output"). "five local changes" or similar would read truer.
What I tried to break
Findings below are reasoned from the code in /tmp/aetherclaude/pr-5697; I have no build or test run here, so nothing was reproduced at runtime.
- Leak on a
plan_fircore()that runs with a live workspace already attached. Every caller ofplan_fircore()is eithercreate_fircore()(on amalloc0struct, sopminphase == 0) or is immediately preceded bydeplan_fircore()—setBuffers_fircore(firmin.c:471),setSize_fircore(:479),setNc_fircore(:493). Since the patcheddeplan_fircore()now nulls the pointer after destroying it,plan_fircore()never overwrites a liveMINPHASE.plan_fircoreis declared infirmin.hbut has no caller outsidefirmin.c. Survives. mpflipped to 1 on a path that never reachescalc_fircore()— that would leavemp_imp_exec()dereferencing null on the next call. Every write to aFIRCORE'smpis eithercreate_fircore()'s initialiser orsetMp_fircore()(:506), which unconditionally callscalc_fircore(). The module-levela->mp = mpwrites inbandpass.c,nbp.c,emph.c,fmd.c,fmmod.c,fmsq.care on the module struct and each is immediately followed bysetMp_fircore()/setMp_nbp(). No path reachesmp_imp_exec()without passingensure_minphase()first. Survives.- A stale workspace sized for the wrong
nc.ensure_minphase()is a no-op whenpminphase != 0, so ifnccould change without adeplan/plancycle you'd getmp_imp_exec()with a mismatchedN.nconly changes insetNc_fircore(), which deplans (→ null) and re-plans;pfactorhas no setter in this tree. Staleness properties are identical to upstream. Survives. - Double teardown. Upstream's
deplan_fircore()calleddestroy_minphase(a->pminphase)unconditionally, so a doubledeplanwas a double free; the patched form nulls it and is now the only member of that function that is safe to run twice. Strictly an improvement, not a regression. (crev,accum,fftoutetc. are still left dangling — pre-existing, not this PR's problem.) - New FFTW planning on a thread that didn't do any before.
create_minphase()builds fourFFTW_PATIENTplans, and planning is not thread-safe. Moving it intocalc_fircore()meanssetMp_fircore()can now plan, which it never could before. In this tree the only caller ofRXASetMPisWdspChannel::open()(WdspChannel.cpp:908), which holdsg_setupMutex— so no new concurrent-planning site, andsetNc_fircore()already planned from the same place. Survives, but it is a new property ofsetMp_fircore()worth remembering if anything ever starts toggling minimum phase at runtime. - The test asserting the implementation back to itself. The allocation assertion is the one that has teeth: the body reports that before the patch
mp == 1andmp == 0both held 1300, sominimumAllocations <= linearAllocationsfails on the unpatched library — the test does not pass against the code it is meant to guard. The audio assertion walks the lazy path for real, becauseopen()callsRXASetNC()at line 907 beforeRXASetMP()at 908, so all six cores are planned atmp == 0and only then flipped, which is exactly thepminphase == 0caseensure_minphase()exists for.RXASetMPfans out to six setters (RXA.c:1056) andConfig::minimumPhasehas no other setter anywhere insrc/, so the claim that nothing else exercisesmp == 1holds. - The "12 of 13 are
mp == 0, the exception iseqp" claim.create_fircorecall sites confirm the shape:dexp.c:163is the only other hardcodedmp = 1, andcreate_dexpis instantiated in neitherRXA.cnorTXA.c.eq.c:282passesa->mpandRXA.c:266creates it — consistent. I could not independently count all 13 without running the instrumented build; the allocation deltas quoted are self-consistent with it. - Test runtime.
wdsp_channel_testhas noTIMEOUTproperty intests.cmake:603-605(default 1500 s) and is noted elsewhere in that file at ~22 s. The new case adds two 8192-tap opens but the patch also removes 18create_minphaseplan-builds from every other open in the file, so I expect this to be net-neutral or faster. I cannot measure it; CI passing at this SHA is the only evidence either way. - Socket tests: none added, modified or removed.
tests/wdsp_channel_test.cppcontains no socket or fake-peer code; it is a socket-free CTest, which is the correct layer for DSP perAGENTS.md's test-layer boundary. Notests.cmakeregistration change was needed — the target already exists.
Recommendation
Approve with nits. This is a tightly scoped vendor patch with the right shape: the removal is provably unobservable at mp == 0, the one new null it introduces is closed by ensure_minphase() at the single site that dereferences it, and the accompanying test is the first thing in the tree to enter the mp == 1 branch — it would fail both against the unpatched library (the allocation relation) and against a botched lazy build (the audio assertion). The three nits are all in AETHERSDR-PATCHES.md and none of them change the code. Next step: fix or show the working for the MB figure, and a maintainer should confirm they're happy carrying a performance divergence in the vendored tree alongside the four correctness ones — that's a policy call, not a code defect, and the note's drop condition is already written.
Nice work on the provenance section — counting the cores rather than assuming them, and separating the measured counts from the derived bytes, is what made this reviewable without a build.
🤖 aethersdr-agent · cost: $7.9734 · model: claude-opus-5
|
Antares Detector — candidate vulnerable file(s)
The ssdr loader reads packet lengths from untrusted network input and uses them to allocate or copy memory (e.g., buildVitaTxPacket in src/core/AudioEngine.cpp) without validating the length field, enabling buffer-overrun vulnerabilities. Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting. 🤖 aethersdr-agent · cost: $8.1307 · model: claude-opus-5 |
…cold case Review nits on aethersdr#5697. Documentation only -- `firmin.c` and `tests/wdsp_channel_test.cpp` are untouched. THE MB FIGURE DID NOT RECONCILE, AND THE REVIEWER'S SUM WAS RIGHT. Summing the note's own list of lengths gives 765,952 elements, ~79.7 MB, not the ~66.9 MB the note quoted. Both are real; they are different quantities, and the note named neither. 765,952 is BUILT: the 18 dead `create_minphase()` calls across one RX open. 643,072 is RESIDENT: what the 12 dead cores still hold once the open finishes. The 122,880-element gap is the six cores `RXASetNC()` re-plans -- `setNc_fircore()` calls `deplan_fircore()` BEFORE `plan_fircore()`, so `nbp0`, `bpsnba`'s inner `nbp` and `bp1` are each built at 32768, freed, and rebuilt at 131072, and `fmsq`, `fmd->pde` and `fmd->paud` each at 8192 then 32768. 3x32768 + 3x8192 = 122,880. Those six count twice in the built list and once in the resident one. Re-tallied from the instrumented log: 831,488 elements across all 19 calls, less the 65,536 of the one live `mp == 1` core, `rxa[].eqp`. The note now states the RESIDENT figure, with the subtraction shown as a table so the next reader does not have to redo it. Resident is also the peak concurrent figure, because the frees precede the allocations and every core only grows; ~79.7 MB is allocation churn across the open, never memory held at once. The two coincide at ~28.5 MB at WDSP's `max(2048, dsp_size)` default, where `RXASetNC(2048)` changes no `nc` and re-plans nothing. PLAN-CONSTRUCTION TIME IS DEMOTED TO THE COLD-CACHE CASE. `WdspChannel::open()` calls `loadWisdomOnce()` before the first plan and exports after, and FFTW wisdom is keyed on the (transform kind, size) pair, so a warm cache imports these geometries instead of re-measuring them. The standing cost is the resident workspace -- allocated on every open, held for the life of the channel, never read. Plan time still bites on a cold cache (first run, cleared cache, CI container, a session whose export never ran) and the note now says that is what it is. No timing of either was taken; the aethersdr#5456 attribution remains a hypothesis. "FIVE FIXES" -> "five local changes ... four correctness fixes and, in patch 5, one performance change". Patch 5 also gains an explicit drop condition, since being the only performance divergence in the vendored tree makes it the one a maintainer may decline to carry on policy rather than on any defect. Every byte figure remains arithmetic on the counted inventory and `create_minphase()`'s own allocation sizes. The allocation COUNTS are measured (`wdspPortOutstandingAllocations()`); no memory figure was. Nothing here touched hardware and nothing keyed a transmitter. CTEST: 444 of 445 ran, 443 passed. `tgxl_docked_parity_test` fails, now 5 of 5 runs, on a `QFontMetrics` caption-fit assertion with Qt reporting a missing "Sans Serif" family under `QT_QPA_PLATFORM=offscreen`; its target links neither `libaether_wdsp.a` nor `libaethercore.a`, and the file's only commit is aethersdr#5676's. `vkamp_connection_test` passed this run, confirming the earlier failure was the socket-timing flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…uld bite The reviewer on aethersdr#5697 noticed that moving the workspace build into calc_fircore gives setMp_fircore a property it never had: it can build FFTW plans, and FFTW planning is not thread-safe. It is harmless today because the only RXASetMP caller holds the setup mutex and setNc_fircore already planned from there. Recorded in the patch note rather than left in a review thread, because a thread is the weakest place to keep a caveat that only matters to whoever next makes minimum phase runtime-togglable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
The PR body's own framing is the right one: this is a self-standing performance change to a vendored file, plus the test that makes the new lazy path safe. Per GOVERNANCE.md that needs no prior RFC — it is not an architecture change, a new dependency, or a UX change.
The change does what it says, and I verified it rather than taking it on faith. plan_fircore() builds the workspace only at mp != 0, calc_fircore() builds it on first use, deplan_fircore() tolerates the null, and AETHERSDR-PATCHES.md records it as patch 5 with a drop condition. The C change itself is sound — see "What held up" below for the attacks it survived.
But the PR is currently linked to close #5456, which its own body says in bold it does not fix. That is blocker 1, and it is the one that does real damage at merge.
Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
third_party/wdsp/upstream/firmin.c (+27/-2) |
Conditional build in plan_fircore(), new static ensure_minphase(), guard in calc_fircore(), null-tolerant deplan_fircore() |
Yes | In scope |
third_party/wdsp/AETHERSDR-PATCHES.md (+113/-5) |
four→five, patch 5 entry, reconciled MB tables, thread-safety caveat, rewritten refresh instruction | Yes | In scope (Principle IV provenance record) — but the rewritten refresh instruction introduces a factual error, blocker 3 |
tests/wdsp_channel_test.cpp (+114/-0) |
runMinimumPhaseWorkspaceTest, registered in main()'s runLeakChecked chain |
Yes | In scope |
All three commits are authored 2026-09-14 (authoredDate); no date outlier, no commit unrelated to the stated change. No new public surface, no settings key, no UI, no CHANGELOG entry (correct — do not add one).
No deleted guard. The only - lines in firmin.c are the unconditional create_minphase and the unconditional destroy_minphase, both replaced by strictly more defensive forms. pminphase is read in exactly the four places this diff touches, and firmin.c holds the only definition of plan_fircore/deplan_fircore in the tree — there is no sibling copy left unfixed.
What is missing from the diff is the finding. This PR changes the count of local divergences from four to five, and two other documents still state four. That is blocker 2.
Blockers
1. The PR is set to auto-close #5456, which the body explicitly disclaims.
The body says, in bold: "This does not fix #5456. It is a candidate mechanism for the cold-connect times recorded there, and connecting the two needs a cold-wisdom connect-path timing that nobody has taken. I am not claiming it."
That restraint is exactly right, and I agree with it. But GitHub disagrees:
$ gh api graphql -f query='{repository(owner:"aethersdr",name:"AetherSDR"){
pullRequest(number:5697){closingIssuesReferences(first:10){nodes{number title state}}}}}'
{"data":{"repository":{"pullRequest":{"closingIssuesReferences":{"nodes":[
{"number":5456,
"title":"docs/HERMES.md §22.3's cold FFTW planning figures do not reproduce: ...",
"state":"OPEN"}]}}}}}
There is no fixes/closes keyword in the body, so this was linked manually in the Development sidebar. Either way it is the metadata GitHub acts on: merging this PR closes #5456.
#5456 is an open, maintainer-review-labelled documentation issue about docs/HERMES.md §22.3's cold-planning magnitudes being 5–19× out, measured against a documented 20.2 s cold connect that reproduced at 257.86 s. This PR touches no documentation in docs/ at all, and by its own account does not establish the causal link. Merging it would silently retire an unresolved measurement discrepancy.
Principle XI frames a Fixes #NNNN as "the implementing agent's hypothesis". The prose here correctly declines to make that hypothesis; the metadata makes it anyway. Unlink #5456 from the Development sidebar (a plain refs #5456 mention in the body carries the context without the auto-close), and the PR's stated position and its behaviour will agree.
2. Two documents still say the snapshot carries four local changes.
The diff updates AETHERSDR-PATCHES.md:5 to "five local changes", but leaves both other places that state the count:
third_party/wdsp/README.md:23-24— "upstream/matches that source snapshot except for the four fixes recorded inAETHERSDR-PATCHES.md— three teardown corrections and one use-after-free". After this PR there are five, and the fifth is neither a teardown correction nor a use-after-free.docs/architecture/wdsp-integration.md:50— "The source snapshot has four documented corrections."
This is not cosmetic. README.md is the boundary statement a maintainer reads at refresh time — the document whose whole job is to enumerate what diverges from upstream. Someone refreshing WDSP reads "four", verifies four, and drops the fifth on the floor. That is precisely the failure the AETHERSDR-PATCHES.md procedure exists to prevent, and this PR is the change that invalidates the claim. Both lines need the same edit as AETHERSDR-PATCHES.md:5. Inline comment on that line.
3. The rewritten refresh instruction mischaracterises patch 3, and the old text did not.
AETHERSDR-PATCHES.md:197-198 (added by this diff) now reads "the equivalent frees for patches 1-3". Patch 3 is not a free. This same document, at lines 12-31, describes it as a->pfcimp = build_fcimp (...) — a discarded return pointer causing a use-after-free on a live path, fixed by an assignment, and reported as TAPR/OpenHPSDR-wdsp#2. A future refresher grepping a new upstream release for an added _aligned_free will conclude the fix is absent when it has landed, or reapply a patch upstream already carries.
The text this replaced ("first check whether upstream contains equivalent frees") was vague; the new text is specific and wrong, which is worse. Suggestion inline.
Nits (all explicitly non-blocking)
- The PR body still leads with the argument the patch note retracts. The body's second heading is "## The cost is plan construction, not bytes" — "that is where the seconds go, and it is why this is worth more than its allocation figure suggests." Commit
c6da5cc9cthen added, atAETHERSDR-PATCHES.md:109: "The standing cost is the resident workspace, not the planning. An earlier draft of this note led with plan-construction time; that overstated it ... on a warm cacheFFTW_PATIENTimports instead of re-measuring." The note was corrected; the body was not. Since this is a discretionary vendor divergence with its own drop condition, the maintainer deciding whether to carry it reads the body — and gets the superseded case as the headline. Worth the same edit the note received. setMp_fircore()can now plan on a channel that is running, not just outside a mutex. The note (AETHERSDR-PATCHES.md:88-95) records the thread-safety half of this. The other half is thatRXASetNC()brackets its work inSetChannelState(channel, 0, 1)(RXA.c:1045,1052) andRXASetMP()(RXA.c:1056-1064) does not. Today that is harmless —WdspChannel::open()callsSetChannelState(m_channelId, 1, 0)at line 926, afterRXASetMP()at 908 — but a future runtime toggle would build fourFFTW_PATIENTplans at up to 131072 points with audio flowing, i.e. a cold-cache stall mid-stream, not only a data race. Same line to revisit; worth one clause in the same paragraph. Inline.- Teardown is not symmetric with construction.
setMp_fircore(a, 0)leavesa->pminphaseallocated until the nextdeplan_fircore(). No effect today (the only caller passes the config value once, at open, when nothing has been built yet), but it means the saving is one-way if minimum phase ever becomes togglable. Worth a sentence in the drop-condition paragraph. - The test cannot tell "minimum phase applied" from "workspace built, output discarded." It asserts both modes produce energy > 1e-4 and that minimum phase costs strictly more allocations. Neither fires if
mp_imp_exec()runs and its result is silently ignored — the allocation relation proves objects were created, not used. A minimum-phase filter has markedly different group delay, so the two outputs must differ;maximumDifference()already exists attests/wdsp_channel_test.cpp:47and both buffers are in hand. One assertion closes it. Inline. AetherSDR patch 5is hardcoded in five places (firmin.c:320,331,347,390andtests/wdsp_channel_test.cpp:413) while the body itself notes the number becomes 7 if #5628 lands first. Merge order alone would make five in-tree comments cite the wrong registry entry. Anchoring on the change instead — "AetherSDR: minimum-phase workspace is built lazily — see AETHERSDR-PATCHES.md" — drops the ordering dependency. Inline.toneRmsreceives a sum, not an RMS (tests/wdsp_channel_test.cpp:488):energy += rms(outputLeft)over 40 blocks, then*toneRms = energy. Compared against a fixed 1e-4 floor, so harmless now, but it scales with the block count.toneEnergymatches the arithmetic. Inline.- The
#5628conflict is on two files, not one. The body flagsAETHERSDR-PATCHES.md. #5628 also inserts afterrunNotchAttenuationTest()attests/wdsp_channel_test.cpp:404and appends to the samemain()runLeakCheckedchain — both PRs collide there too, and both claim the number "5". Worth naming in the body so whoever rebases second knows the full surface. - No commit subject cites a principle, which
AGENTS.md:95-96asks for ("Commit messages cite the most-load-bearing principle asPrinciple <N>.at the end of the subject line"). Principle VIII would fit. Genuinely minor: the squash-merge subject is the maintainer's to compose, andmain's own history is inconsistent here — #5686, the WDSP 2.10 refresh this patch sits on, carries no principle either.
What held up
I tried to break this and mostly could not. The claims I tested:
- "It goes red with the patch reverted (
linear=2069 minimum=2069)" — reproduced exactly. Revertedfirmin.calone to the merge base, rebuilt, ran:FAIL: minimum phase cost no extra WDSP allocations (linear=2069 minimum=2069). Byte-for-byte the figure in the body. The regression test pins something real. - "Getting the laziness wrong is a null dereference inside
mp_imp_exec()" — reproduced. I neuteredensure_minphase()to a no-op on the patched tree;wdsp_channel_testexits 139 (SIGSEGV). So the new test genuinely walks the lazy-construction path, and the laziness is load-bearing exactly as claimed. - The counted inventory reproduces on a second architecture. The body's figures are macOS arm64. On Linux x86-64 (Arch, RelWithDebInfo) the same test yields patched
linear=1973 minimum=2021against reverted2069/2069— i.e. 96 fewer allocations held atmp == 0(12 cores × 8) and 48 fewer atmp == 1(6 × 8). That is the body's "exactly 12 fewer objects held" and "mp == 1holds 1204+48", arrived at independently on different hardware. - ASan on Linux, which the body says has not signed this off — it does now.
-DAETHERSDR_SANITIZER=address,detect_leaks=1: no new finding on the minimum-phase path. The run does report 1120 bytes in 10 allocations, but I built the merge base with the same 3 files reverted and it reports the identical leak at the identical site (createWaitable←create_calcc←create_txa,calcc.c:1079), reached from the pre-existingrunVectortest. Pre-existing, not this PR, and worth its own issue. - Every
plan_fircore()caller checked for a stale or null workspace. All four (create_fircore,setBuffers_fircore,setSize_fircore,setNc_fircore) are deplan → plan → calc, andcreate_fircoreassignsa->mpbeforeplan_fircore(a).setNc_fircoredeplans before mutatinga->nc, soensure_minphase()can never size against a stalenc. I could not construct a path that reachesmp_imp_exec()with a null or wrongly-sizedpminphase. - One refutation, in the PR's favour. I checked whether the note's TX figures describe an unreachable configuration, since
WdspChannel::open()'s TX branch never callsTXASetNC/TXASetMPand nothing in the tree setsConfig::minimumPhase. They are reachable:TXA.cbuilds its bandpass cores throughcreate_bandpass→create_fircore(..., a->mp, 16, impulse)with the flag fixed at construction, so the ~16.2 MB saving lands on every TX open regardless. The note is right. - Considered and rejected: collapsing the ternary.
a->pminphase = 0;inplan_fircore()would be exactly equivalent, since all four callers reachcalc_fircore()before anything reads the pointer. I think the author'sa->mp ? create_minphase(...) : 0is the better call anyway: it keeps themp == 1path byte-identical to upstream, which is the right instinct in a vendored file.
Not driven through the app. This is a headless DSP change with no user-visible surface — nothing in the tree sets Config::minimumPhase, so there is no control to click and no pixel to grab. A bridge session against the demo backend would have added nothing the allocation counters and the two mutations did not already settle. Windows ASan not run (no Windows box here); AETHERSDR-PATCHES.md asks for every supported platform, and macOS + Linux are now covered.
CI is green on the current head 079f7463 across all five checks (build, check-macos, check-windows, Static checks, Sanitizer option configures).
The three blockers are four lines of edits between them and none of them is in the C. The patch is good work — the counted inventory, the mutation-proof test, and the drop condition are all the right shape, and the note's own self-correction on plan time versus resident memory is the kind of thing most PRs would have quietly left standing.
🤖 Generated with Claude Code
…cold case Review nits on aethersdr#5697. Documentation only -- `firmin.c` and `tests/wdsp_channel_test.cpp` are untouched. THE MB FIGURE DID NOT RECONCILE, AND THE REVIEWER'S SUM WAS RIGHT. Summing the note's own list of lengths gives 765,952 elements, ~79.7 MB, not the ~66.9 MB the note quoted. Both are real; they are different quantities, and the note named neither. 765,952 is BUILT: the 18 dead `create_minphase()` calls across one RX open. 643,072 is RESIDENT: what the 12 dead cores still hold once the open finishes. The 122,880-element gap is the six cores `RXASetNC()` re-plans -- `setNc_fircore()` calls `deplan_fircore()` BEFORE `plan_fircore()`, so `nbp0`, `bpsnba`'s inner `nbp` and `bp1` are each built at 32768, freed, and rebuilt at 131072, and `fmsq`, `fmd->pde` and `fmd->paud` each at 8192 then 32768. 3x32768 + 3x8192 = 122,880. Those six count twice in the built list and once in the resident one. Re-tallied from the instrumented log: 831,488 elements across all 19 calls, less the 65,536 of the one live `mp == 1` core, `rxa[].eqp`. The note now states the RESIDENT figure, with the subtraction shown as a table so the next reader does not have to redo it. Resident is also the peak concurrent figure, because the frees precede the allocations and every core only grows; ~79.7 MB is allocation churn across the open, never memory held at once. The two coincide at ~28.5 MB at WDSP's `max(2048, dsp_size)` default, where `RXASetNC(2048)` changes no `nc` and re-plans nothing. PLAN-CONSTRUCTION TIME IS DEMOTED TO THE COLD-CACHE CASE. `WdspChannel::open()` calls `loadWisdomOnce()` before the first plan and exports after, and FFTW wisdom is keyed on the (transform kind, size) pair, so a warm cache imports these geometries instead of re-measuring them. The standing cost is the resident workspace -- allocated on every open, held for the life of the channel, never read. Plan time still bites on a cold cache (first run, cleared cache, CI container, a session whose export never ran) and the note now says that is what it is. No timing of either was taken; the aethersdr#5456 attribution remains a hypothesis. "FIVE FIXES" -> "five local changes ... four correctness fixes and, in patch 5, one performance change". Patch 5 also gains an explicit drop condition, since being the only performance divergence in the vendored tree makes it the one a maintainer may decline to carry on policy rather than on any defect. Every byte figure remains arithmetic on the counted inventory and `create_minphase()`'s own allocation sizes. The allocation COUNTS are measured (`wdspPortOutstandingAllocations()`); no memory figure was. Nothing here touched hardware and nothing keyed a transmitter. CTEST: 444 of 445 ran, 443 passed. `tgxl_docked_parity_test` fails, now 5 of 5 runs, on a `QFontMetrics` caption-fit assertion with Qt reporting a missing "Sans Serif" family under `QT_QPA_PLATFORM=offscreen`; its target links neither `libaether_wdsp.a` nor `libaethercore.a`, and the file's only commit is aethersdr#5676's. `vkamp_connection_test` passed this run, confirming the earlier failure was the socket-timing flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…uld bite The reviewer on aethersdr#5697 noticed that moving the workspace build into calc_fircore gives setMp_fircore a property it never had: it can build FFTW plans, and FFTW planning is not thread-safe. It is harmless today because the only RXASetMP caller holds the setup mutex and setNc_fircore already planned from there. Recorded in the patch note rather than left in a review thread, because a thread is the weakest place to keep a caveat that only matters to whoever next makes minimum phase runtime-togglable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Principle IV. `third_party/wdsp/README.md` is the boundary statement a maintainer reads at refresh time, and `docs/architecture/wdsp-integration.md` repeats the count. Both were stale: README said six, the architecture doc said five, while `AETHERSDR-PATCHES.md` now enumerates seven. Someone refreshing WDSP reads the boundary statement, verifies that many, and drops the rest on the floor -- the failure the registry exists to prevent. The architecture doc was already one short on `main` before this branch: aethersdr#5687 added two entries and bumped README four->six, but wdsp-integration four->five. That off-by-one is corrected here too. Raised as blocker 2 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…iple IV. The line this replaces said "the equivalent frees for patches 1-3". Patch 3 is not a free: 2.10 discards the pointer `build_fcimp()` returns, and the fix is `a->pfcimp = build_fcimp (...)` -- a use-after-free closed by capturing a return value (TAPR/OpenHPSDR-wdsp#2). A refresher grepping a new release for an added free concludes the fix is absent when it has landed, or reapplies a patch upstream already carries. The text before it was vague; that version was specific and wrong, which is the worse failure mode for a refresh instruction. Patches 4-7 are not frees either -- an exit handshake, an accessor set, a null guard and a conditional construction -- so the paragraph now names the shape to grep for, one per patch, instead of one verb for all of them. Raised as blocker 3 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
079f746 to
ec340e0
Compare
…cold case Review nits on aethersdr#5697. Documentation only -- `firmin.c` and `tests/wdsp_channel_test.cpp` are untouched. THE MB FIGURE DID NOT RECONCILE, AND THE REVIEWER'S SUM WAS RIGHT. Summing the note's own list of lengths gives 765,952 elements, ~79.7 MB, not the ~66.9 MB the note quoted. Both are real; they are different quantities, and the note named neither. 765,952 is BUILT: the 18 dead `create_minphase()` calls across one RX open. 643,072 is RESIDENT: what the 12 dead cores still hold once the open finishes. The 122,880-element gap is the six cores `RXASetNC()` re-plans -- `setNc_fircore()` calls `deplan_fircore()` BEFORE `plan_fircore()`, so `nbp0`, `bpsnba`'s inner `nbp` and `bp1` are each built at 32768, freed, and rebuilt at 131072, and `fmsq`, `fmd->pde` and `fmd->paud` each at 8192 then 32768. 3x32768 + 3x8192 = 122,880. Those six count twice in the built list and once in the resident one. Re-tallied from the instrumented log: 831,488 elements across all 19 calls, less the 65,536 of the one live `mp == 1` core, `rxa[].eqp`. The note now states the RESIDENT figure, with the subtraction shown as a table so the next reader does not have to redo it. Resident is also the peak concurrent figure, because the frees precede the allocations and every core only grows; ~79.7 MB is allocation churn across the open, never memory held at once. The two coincide at ~28.5 MB at WDSP's `max(2048, dsp_size)` default, where `RXASetNC(2048)` changes no `nc` and re-plans nothing. PLAN-CONSTRUCTION TIME IS DEMOTED TO THE COLD-CACHE CASE. `WdspChannel::open()` calls `loadWisdomOnce()` before the first plan and exports after, and FFTW wisdom is keyed on the (transform kind, size) pair, so a warm cache imports these geometries instead of re-measuring them. The standing cost is the resident workspace -- allocated on every open, held for the life of the channel, never read. Plan time still bites on a cold cache (first run, cleared cache, CI container, a session whose export never ran) and the note now says that is what it is. No timing of either was taken; the aethersdr#5456 attribution remains a hypothesis. "FIVE FIXES" -> "five local changes ... four correctness fixes and, in patch 5, one performance change". Patch 5 also gains an explicit drop condition, since being the only performance divergence in the vendored tree makes it the one a maintainer may decline to carry on policy rather than on any defect. Every byte figure remains arithmetic on the counted inventory and `create_minphase()`'s own allocation sizes. The allocation COUNTS are measured (`wdspPortOutstandingAllocations()`); no memory figure was. Nothing here touched hardware and nothing keyed a transmitter. CTEST: 444 of 445 ran, 443 passed. `tgxl_docked_parity_test` fails, now 5 of 5 runs, on a `QFontMetrics` caption-fit assertion with Qt reporting a missing "Sans Serif" family under `QT_QPA_PLATFORM=offscreen`; its target links neither `libaether_wdsp.a` nor `libaethercore.a`, and the file's only commit is aethersdr#5676's. `vkamp_connection_test` passed this run, confirming the earlier failure was the socket-timing flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…uld bite The reviewer on aethersdr#5697 noticed that moving the workspace build into calc_fircore gives setMp_fircore a property it never had: it can build FFTW plans, and FFTW planning is not thread-safe. It is harmless today because the only RXASetMP caller holds the setup mutex and setNc_fircore already planned from there. Recorded in the patch note rather than left in a review thread, because a thread is the weakest place to keep a caveat that only matters to whoever next makes minimum phase runtime-togglable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Principle IV. `third_party/wdsp/README.md` is the boundary statement a maintainer reads at refresh time, and `docs/architecture/wdsp-integration.md` repeats the count. Both were stale: README said six, the architecture doc said five, while `AETHERSDR-PATCHES.md` now enumerates seven. Someone refreshing WDSP reads the boundary statement, verifies that many, and drops the rest on the floor -- the failure the registry exists to prevent. The architecture doc was already one short on `main` before this branch: aethersdr#5687 added two entries and bumped README four->six, but wdsp-integration four->five. That off-by-one is corrected here too. Raised as blocker 2 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…iple IV. The line this replaces said "the equivalent frees for patches 1-3". Patch 3 is not a free: 2.10 discards the pointer `build_fcimp()` returns, and the fix is `a->pfcimp = build_fcimp (...)` -- a use-after-free closed by capturing a return value (TAPR/OpenHPSDR-wdsp#2). A refresher grepping a new release for an added free concludes the fix is absent when it has landed, or reapplies a patch upstream already carries. The text before it was vague; that version was specific and wrong, which is the worse failure mode for a refresh instruction. Patches 4-7 are not frees either -- an exit handshake, an accessor set, a null guard and a conditional construction -- so the paragraph now names the shape to grep for, one per patch, instead of one verb for all of them. Raised as blocker 3 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
ec340e0 to
aa3e916
Compare
|
Three blockers, all correct, all cleared. Rebased onto Blocker 1 — your finding is right, but the cause is not the one in your review, and that matters. There was never a Development-sidebar link. Two proofs: the PR timeline contains no The link came from our own disclaimer. The body read "This does not fix" immediately followed by the issue number. GitHub's closing-keyword parser does not read negation — it matched the keyword against the number that followed it and set the PR to close on merge. The sentence disclaiming the link is the sentence that created it. I am flagging this rather than just fixing it because your remedy — unlink it in the sidebar — would have sent the next person to a sidebar with nothing in it, and the auto-close would have survived. The fix is to keep the reference away from Blocker 2 — fixed, and the count was wrong in one more place than you found. All three statements now say seven. Blocker 3 — fixed, and wider than patch 3. You are right that "the equivalent frees for patches 1-3" is specific and wrong, which is worse than vague. It was also wrong for 4 through 7 — an exit handshake, an accessor set, a null guard and a conditional construction are none of them frees. The paragraph is now a shape-per-patch list, with patch 3 named as the Your nit predicted the renumber, so I have taken it. This entry is patch 7 now, not 5 — #5687 took 5 and 6 while the branch was in review. You wrote that merge order alone would leave five in-tree comments citing the wrong registry entry; four hours later merge order did exactly that. The five markers in An observation, not a request. Four separate conflict waves hit our branches today and every one landed in one of two files. Measured: You have been on both sides of this in one day: blocker 2 warned about a stale count, and #5687 moved it. That is the system working on you, not you working badly. Full
A build break I hit on the way, now filed as #5706: Not measured: nothing was timed, nothing touched hardware, nothing keyed a transmitter. Byte figures are still arithmetic; allocation counts are still macOS arm64 only. Your Linux x86-64 reproduction and your Linux ASan run remain the only Linux evidence, and thank you for both — the SIGSEGV mutation in particular is the check I could not run myself. 🤖 Generated with Claude Code |
|
Cross-reference: this PR and #5628 edit the same two files — The grouping, and two open issues that sit in the same area, are laid out on #5628 rather than repeated here. 🤖 Generated with Claude Code |
|
@ten9876 — the blocker is answered and two cosmetic threads are still open; I 🤖 Generated with Claude Code |
… is on `plan_fircore()` in the vendored WDSP ends with an unconditional `a->pminphase = create_minphase (a->nc, a->pfactor)`, with no reference to `a->mp`. The only consumer is the `if (a->mp)` branch of `calc_fircore()`. `WdspChannel::Config::minimumPhase` is false and nothing in this tree sets it, so `RXASetMP()` passes 0 and every one of those workspaces is built on the channel-open path and then never executed. THIS IS SHARED DSP AND EVERY BACKEND PAYS IT. Any backend that opens a `WdspChannel` — HL2, ANAN, RTL — builds them; `Hl2RxDsp::kRxFilterTaps` = 8192 only decides how big they are. That is the argument for the patch, not the HL2. THE HEADLINE IS PLAN-CONSTRUCTION TIME, NOT BYTES. `create_minphase()` (`fir.c`) builds FOUR `FFTW_PATIENT` plans of `nc * pfactor` points. `FFTW_PATIENT` measures many transform strategies at plan time, and `WdspChannel.cpp`'s own wisdom comment records ~220 planner calls per RX open. At `nc` 8192 and the `pfactor` 16 that `nbp.c` and `bandpass.c` pass, that is a 131072-point geometry per core. COUNTED, NOT ASSUMED. Instrumenting `create_fircore()` and `create_minphase()` and opening one channel: an RX channel builds 13 fircores, 12 of them `mp == 0`. The exception is `rxa[].eqp`, which upstream creates with the minimum-phase flag set; its workspace is real and this patch keeps it. `RXASetNC()` re-plans six of the twelve, so one RX open at 8192 taps makes 19 `create_minphase()` calls, 18 of them dead — 72 dead `FFTW_PATIENT` plans. A TX channel builds 7 fircores, 6 of them `mp == 0`. MEASURED, via WDSP's own port allocation counters on macOS arm64: one RX open at 8192 taps drops from 1300 to 1204 live allocations and from 1518 to 1374 allocations made. `create_minphase()` makes 8 allocations, so that is exactly 12 fewer held and 18 fewer built — the counted inventory, arrived at independently. Before the patch `mp == 1` and `mp == 0` hold the identical 1300; after it, `mp == 1` holds 1252. ARITHMETIC, NOT MEASURED: ~66.9 MB of never-executed workspace per RX channel at 8192 taps, ~28.5 MB at WDSP's `max(2048, dsp_size)` default, ~16.2 MB per TX channel. The delta the tap bump alone introduced is ~38 MB per receiver. WHAT THIS DOES NOT CLAIM. It does not fix aethersdr#5456. Removing 72 `FFTW_PATIENT` plan constructions from the connect path is a candidate mechanism for the cold-connect time — a plausible one, since plan time is where `FFTW_PATIENT` spends itself — but connecting the two needs a cold-wisdom connect-path timing that nobody has taken, here or upstream. No timing figure in this commit is a measurement. Nothing here touched hardware and nothing keyed a transmitter. THE PATCH. `plan_fircore()` builds the workspace only when `a->mp` is set; `calc_fircore()` builds it on first use through `ensure_minphase()`, so `setMp_fircore()` can still turn minimum phase on at any time; and `deplan_fircore()` tolerates its absence. Recorded as patch 5 in `third_party/wdsp/AETHERSDR-PATCHES.md`, following the procedure there. THE COVER. `runMinimumPhaseWorkspaceTest` in `tests/wdsp_channel_test.cpp` is the first test in this tree to set `Config::minimumPhase`, and because `WdspChannel::open()` calls `RXASetNC()` before `RXASetMP()` it is the case that walks the lazy build — the path that is a null dereference inside `mp_imp_exec()` if the laziness is got wrong. It asserts audio out of both phase modes and that minimum phase costs strictly more live WDSP allocations than linear phase, stated as a relation so it does not re-hardcode WDSP's fircore inventory. It goes red with this patch reverted. An ASan build of the vendored library alone, cycling open / `RXASetNC` / `RXASetMP(1)` / `RXASetNC` / `RXASetMP(0)` / close twice plus a TX open, reported nothing on macOS arm64. CTEST: 443 of 445 run, 441 passed. Two failures, neither owned here — neither target links `libaether_wdsp.a` or `libaethercore.a`, so a change confined to `firmin.c` cannot reach either. `tgxl_docked_parity_test` fails 4 of 4 runs on a `QFontMetrics` caption-fit assertion with Qt reporting a missing "Sans Serif" family under `QT_QPA_PLATFORM=offscreen`; the file was introduced by aethersdr#5676 and has no other commits. `vkamp_connection_test` failed once in the full run and passed 3 of 3 reruns — a socket-timing flake on "commands after a disconnect are dropped, not queued". `crdv_quarantined_test` and `weather_radar_texture_gl_test` were skipped by their own guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…cold case Review nits on aethersdr#5697. Documentation only -- `firmin.c` and `tests/wdsp_channel_test.cpp` are untouched. THE MB FIGURE DID NOT RECONCILE, AND THE REVIEWER'S SUM WAS RIGHT. Summing the note's own list of lengths gives 765,952 elements, ~79.7 MB, not the ~66.9 MB the note quoted. Both are real; they are different quantities, and the note named neither. 765,952 is BUILT: the 18 dead `create_minphase()` calls across one RX open. 643,072 is RESIDENT: what the 12 dead cores still hold once the open finishes. The 122,880-element gap is the six cores `RXASetNC()` re-plans -- `setNc_fircore()` calls `deplan_fircore()` BEFORE `plan_fircore()`, so `nbp0`, `bpsnba`'s inner `nbp` and `bp1` are each built at 32768, freed, and rebuilt at 131072, and `fmsq`, `fmd->pde` and `fmd->paud` each at 8192 then 32768. 3x32768 + 3x8192 = 122,880. Those six count twice in the built list and once in the resident one. Re-tallied from the instrumented log: 831,488 elements across all 19 calls, less the 65,536 of the one live `mp == 1` core, `rxa[].eqp`. The note now states the RESIDENT figure, with the subtraction shown as a table so the next reader does not have to redo it. Resident is also the peak concurrent figure, because the frees precede the allocations and every core only grows; ~79.7 MB is allocation churn across the open, never memory held at once. The two coincide at ~28.5 MB at WDSP's `max(2048, dsp_size)` default, where `RXASetNC(2048)` changes no `nc` and re-plans nothing. PLAN-CONSTRUCTION TIME IS DEMOTED TO THE COLD-CACHE CASE. `WdspChannel::open()` calls `loadWisdomOnce()` before the first plan and exports after, and FFTW wisdom is keyed on the (transform kind, size) pair, so a warm cache imports these geometries instead of re-measuring them. The standing cost is the resident workspace -- allocated on every open, held for the life of the channel, never read. Plan time still bites on a cold cache (first run, cleared cache, CI container, a session whose export never ran) and the note now says that is what it is. No timing of either was taken; the aethersdr#5456 attribution remains a hypothesis. "FIVE FIXES" -> "five local changes ... four correctness fixes and, in patch 5, one performance change". Patch 5 also gains an explicit drop condition, since being the only performance divergence in the vendored tree makes it the one a maintainer may decline to carry on policy rather than on any defect. Every byte figure remains arithmetic on the counted inventory and `create_minphase()`'s own allocation sizes. The allocation COUNTS are measured (`wdspPortOutstandingAllocations()`); no memory figure was. Nothing here touched hardware and nothing keyed a transmitter. CTEST: 444 of 445 ran, 443 passed. `tgxl_docked_parity_test` fails, now 5 of 5 runs, on a `QFontMetrics` caption-fit assertion with Qt reporting a missing "Sans Serif" family under `QT_QPA_PLATFORM=offscreen`; its target links neither `libaether_wdsp.a` nor `libaethercore.a`, and the file's only commit is aethersdr#5676's. `vkamp_connection_test` passed this run, confirming the earlier failure was the socket-timing flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…uld bite The reviewer on aethersdr#5697 noticed that moving the workspace build into calc_fircore gives setMp_fircore a property it never had: it can build FFTW plans, and FFTW planning is not thread-safe. It is harmless today because the only RXASetMP caller holds the setup mutex and setNc_fircore already planned from there. Recorded in the patch note rather than left in a review thread, because a thread is the weakest place to keep a caveat that only matters to whoever next makes minimum phase runtime-togglable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Principle IV. `third_party/wdsp/README.md` is the boundary statement a maintainer reads at refresh time, and `docs/architecture/wdsp-integration.md` repeats the count. Both were stale: README said six, the architecture doc said five, while `AETHERSDR-PATCHES.md` now enumerates seven. Someone refreshing WDSP reads the boundary statement, verifies that many, and drops the rest on the floor -- the failure the registry exists to prevent. The architecture doc was already one short on `main` before this branch: aethersdr#5687 added two entries and bumped README four->six, but wdsp-integration four->five. That off-by-one is corrected here too. Raised as blocker 2 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…iple IV. The line this replaces said "the equivalent frees for patches 1-3". Patch 3 is not a free: 2.10 discards the pointer `build_fcimp()` returns, and the fix is `a->pfcimp = build_fcimp (...)` -- a use-after-free closed by capturing a return value (TAPR/OpenHPSDR-wdsp#2). A refresher grepping a new release for an added free concludes the fix is absent when it has landed, or reapplies a patch upstream already carries. The text before it was vague; that version was specific and wrong, which is the worse failure mode for a refresh instruction. Patches 4-7 are not frees either -- an exit handshake, an accessor set, a null guard and a conditional construction -- so the paragraph now names the shape to grep for, one per patch, instead of one verb for all of them. Raised as blocker 3 by the reviewer on aethersdr#5697. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
Rebase correction, not new work. The commit above this one stated the count as seven in all three places; that was right when this branch was cut and is wrong now. aethersdr#5628 landed three more vendored patches (the two SetChannelState fixes and the quiesce_flush move), so the minimum-phase patch is the TENTH local change, not the seventh, and the category breakdown gains the two channel-state fixes. AETHERSDR-PATCHES.md was reconciled in the conflict resolutions themselves; these two files applied cleanly with the stale number in them, which is exactly the kind of drift a clean apply hides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
aa3e916 to
fd49a4d
Compare
|
@ten9876 — this one is answered and rebased, and the only thing holding it is a review state no action of ours reaches. All twelve unresolved threads end with our reply, posted 2026-09-14. The two you raised last round are both taken: the Rebased onto
The marker convention is the one exception I did not resolve: #5628 established Not measured: nothing on hardware. No rush on my side — flagging it because the review state, not the code, is what it is waiting on. 🤖 Generated with Claude Code |
jensenpat
left a comment
There was a problem hiding this comment.
Issue fit
Self-standing WDSP performance patch. No closing-keyword link; closingIssuesReferences is empty. Related mention of #5456 is correctly kept away from fix/close/resolve. Scope is the lazy create_minphase() path plus registry/docs/test. Shared DSP; default Config::minimumPhase remains false, so live backends keep linear-phase audio.
| Group | Role |
|---|---|
third_party/wdsp/upstream/firmin.c |
Guard plan_fircore; ensure_minphase in calc_fircore; null-safe deplan_fircore |
tests/wdsp_channel_test.cpp |
First minimumPhase=true case; allocation relation + energy floor |
AETHERSDR-PATCHES.md / README / wdsp-integration.md |
Patch 10 registry; count ten |
Findings
No blockers.
Prior ten9876 blockers (auto-close wording, stale patch counts, ordinal-in-comments) are addressed on this SHA. Remaining items from that review are nits: toneRms is still a 40-block energy sum; the test does not assert the two streams differ; setMp_fircore(a,0) does not release the workspace. None of those reintroduce the dead workspace on the default path.
Verification
- Socket preflight:
wdsp_channel_testhas no Qt sockets / fake radio peers. Registered intests/tests.cmake. - Local RelWithDebInfo Ninja at
fd49a4dd:wdsp_channel_testpassed (7.29 s, then 7.24 s after restore). - Mutation: unconditional
create_minphaseinplan_fircore→FAIL: ... (linear=2069 minimum=2069)— same figure as the PR body. Restored; test passed again. - CI on
fd49a4dd: build, check-macos, check-windows, Static checks, Sanitizer option configures — all success. - App not built (DSP-only target). No hardware / no TX.
Verdict: Approve. Merge authorized if live gates pass.
Addressed on fd49a4d: closing-keyword trap, patch count, and ordinal comments. Remaining items are nits; CODEOWNER approved current head.
plan_fircore()ends with an unconditionala->pminphase = create_minphase(a->nc, a->pfactor), with no reference toa->mp.calc_fircore()only uses that workspace underif (a->mp). So every fircore in the tree builds it, and almost none of them ever reads it.This is shared DSP. Every backend pays it — Flex, Icom, ANAN, RTL-SDR and the HL2 alike, and every one of them for a code path that does not execute.
The cost is plan construction, not bytes
create_minphase()builds fourFFTW_PATIENTplans per instance.FFTW_PATIENTmeasures many transform strategies at plan time; that is where the seconds go, and it is why this is worth more than its allocation figure suggests.One RX channel open at 8192 taps makes 19
create_minphase()calls, 18 of them dead — 72 never-executedFFTW_PATIENTplans, at lengths 131072 (×3), 65536, 32768 (×8), 8192 (×5) and 4096.Related: #5456 — this PR does not establish that link. It is a candidate mechanism for the cold-connect times recorded there, and connecting the two needs a cold-wisdom connect-path timing that nobody has taken. I am not claiming it.
(Worded this way deliberately. The sentence that stood here read "This does not fix" followed directly by the issue number — and GitHub's closing-keyword parser does not read negation. It matched the keyword against the number that followed it and set this PR to close that issue on merge. The disclaimer created the link it disclaimed. Keeping the reference is fine; keeping it away from
fix/close/resolveis the part that matters.)The inventory, counted rather than assumed
Counted by instrumenting
create_fircore()andcreate_minphase()and opening one channel, then confirmed by reading the call sites.An RX channel builds 13 fircores; 12 are
mp == 0.RXASetNC:rxa[].nbp0, thenbpinsiderxa[].bpsnba,rxa[].bp1,rxa[].fmsq,fmd->pde,fmd->paud.ncand passmpas a literal0at their call site:rxa[].doublepole,rxa[].matched,rxa[].gaussian, andrxa[].wbfm'spfil0_15,pfil19andpfil23_53.rxa[].eqp, which is created with the minimum-phase flag set (nc16384,pfactor4). Its workspace is real and the patch keeps it.RXASetMPnever touches it.A TX channel builds 7 fircores, 6 of them
mp == 0.Figures
Arithmetic on a code reading, not measurements. 643,072 elements × 104 B (six complex at 16 B plus one real at 8 B):
What was measured, through WDSP's own
wdspPortOutstandingAllocations()/wdspPortAllocationSequence(), macOS arm64, one RX open at 8192 taps withmp == 0: live allocations 1300 → 1204, allocations made during open 1518 → 1374.create_minphase()makes 8 allocations, so that is exactly 12 fewer held and 18 fewer built — matching the counted inventory independently.The patch
plan_fircore()builds the workspace only whena->mpis set.calc_fircore()builds it on first use through a new staticensure_minphase().deplan_fircore()tolerates its absence.The laziness is load-bearing, not tidiness.
setMp_fircore()can turnmpon at any time, andWdspChannel::open()callsRXASetNC()beforeRXASetMP()— so with minimum phase requested, the six re-planned cores are planned whilemp == 0and flipped on afterwards. Building only inplan_fircore()would be a null dereference inmp_imp_exec().Test
tests/wdsp_channel_test.cppgainsrunMinimumPhaseWorkspaceTest— the first test in this tree to setConfig::minimumPhase. It asserts audio out of both phase modes, and that minimum phase costs strictly more live WDSP allocations than linear. A relation, not a hardcoded count.It goes red with the patch reverted (
linear=2069 minimum=2069), which is the only thing that stops a no-op passing.Full
cteston the rebased branch (macOS arm64, RelWithDebInfo,-j 4): 449 tests, 447 ran, 446 passed, 1 failed. Two skipped by their own guards (crdv_quarantined_test,weather_radar_texture_gl_test).wdsp_channel_testpasses, 3.51 s. Ownership of the failure established from the target's own link line and sources rather than assumed:tgxl_docked_parity_test— deterministic, 7 of 7 (both full runs plus 5 of 5 solo reruns). It fails theQFontMetrics(drawn).horizontalAdvance(btn->text()) <= btn->width() - 6check intests/tgxl_docked_parity_test.cpp, with Qt logging a missing"Sans Serif"family underQT_QPA_PLATFORM=offscreenandThemeManagerfalling back to compiled-in defaults. Its link line carries neitherlibaethercore.anorthird_party/wdsp/libaether_wdsp.a, the file contains no reference to WDSP,fircoreor minimum phase, and its only commit is43687413(feat(tuner): TGXL front-panel presentation, tune abort, and tuner alerts #5676). Not this PR's.vkamp_connection_testalso failed once, in an earlier full run, and passed the second full run and 4 of 5 solo reruns — 2 failures in 7 observations. The check that fails isamp.commands.isEmpty()after a 400 msspinFor()intests/vkamp_connection_test.cpp: a time-boxed socket assertion, which is the shape of a timing flake rather than a regression. Same link line — noaethercore, no WDSP. Last touched by13ba5790(#4919).AddressSanitizer
AETHERSDR-PATCHES.mdasks for the lifecycle test under ASan on every supported platform. This patch is the only one of the seven that makes a pointer legitimately null, so ASan is exactly its gate.An ASan build of the vendored library alone (all 78 translation units,
-fsanitize=address), cycling open /RXASetNC/RXASetMP(1)/RXASetNC/RXASetMP(0)/ close twice plus a TX open: clean, on macOS arm64. The Linuxsanitizers.ymllane has not signed this off, and that is the same lane whose applicability to the 2.10 refresh is still open on #5686.Numbering
This is patch 7. It was written as patch 5 against a
mainthat listed four. #5687 landed while this branch was in review and added two —upstream/nnr.c/upstream/nnr.handupstream/nnet.c— taking 5 and 6, so the entry is renumbered on the rebase.The in-source markers no longer carry the ordinal at all. Per the reviewer's nit, the five comments in
firmin.candtests/wdsp_channel_test.cppnow name the change and point atAETHERSDR-PATCHES.mdinstead of citing a number, so the ordinal appears in exactly one place: the registry entry.grep AetherSDRstill finds all five.#5628 is still open and also touches
AETHERSDR-PATCHES.mdandtests/wdsp_channel_test.cpp(it inserts afterrunNotchAttenuationTest()and appends to the samerunLeakCheckedchain inmain()). Whichever of the two lands second conflicts in both files; the ordinal is no longer part of that conflict.Not measured
Nothing was timed. Nothing touched hardware and nothing keyed a transmitter. The byte figures are arithmetic; the allocation deltas are measured on one platform.
🤖 Generated with Claude Code