Skip to content

Commit ab03be0

Browse files
Ozy311rfoust
andauthored
feat(core): add shared-capture RF admission policy (#5472)
## Summary RTL RFC #5468 requires every receiver's complete guarded RF passband to fit the shared capture before tuning, filter/mode changes or sample-rate changes are accepted. Add a pure `SharedCapturePolicy` helper that selects the nearest legal quantized tuner center, validates coherent readback and produces a deterministic restore plan within the current capture. The helper validates asymmetric filter/usable margins, signed carrier/BFO translation, transition guards, disjoint hardware domains, bounded numeric inputs, stable slot IDs and caller-supplied receiver capacity. It keeps an already legal center, refuses an impossible desired set atomically, and restores fitting entries in ascending stable-ID order without recentering or renumbering. Conservative floating-point arithmetic rejects unrepresentable boundary fringes rather than widening acceptance with a tolerance. This is F2 of #5468 and is independent of P01 #5471. The helper compiles into the static `aethercore` library; the registered test compiles that same implementation. No live backend calls it yet. USB/DSP/audio/viewport/UI/persistence integration and supported receiver counts remain later RFC phases. The new contract is documented in `docs/shared-capture-policy.md`. This PR does not close the umbrella RFC. ## Review follow-up An additional regression rejects a center grid whose 1e-5 Hz step is smaller than the double spacing at 2^39 Hz while its grid indices remain within the independent index bound. The capture and slice otherwise fit. Disabling only the resolution guard makes this new check fail, closing the reported mutation-coverage gap. The final readback validation remains, with its defense-in-depth purpose made explicit. The contract now distinguishes adapter-declared logical readback from measured physical RF frequency. In [Osmocom librtlsdr v2.0.2](https://github.com/osmocom/rtl-sdr/blob/v2.0.2/src/librtlsdr.c#L884-L913) and [RTL-SDR Blog commit aed0ea19](https://github.com/rtlsdrblog/rtl-sdr-blog/blob/aed0ea19f3a273370a13c9009b96313c75d54c7b/src/librtlsdr.c#L887-L936), `rtlsdr_get_center_freq()` returns the cached requested frequency stored by successful tuning; it does not measure achieved PLL frequency. Other versions and forks need verification. Actual readback must still satisfy the declared domains and complete-set containment checks. ## Constitution principles honored Principle VII — invalid numeric descriptors, impossible intervals, malformed identities, oversized collections and incoherent readback fail at the policy boundary. Principle II — a selection is only a proposal; the integration layer must validate coherent capture state before publication. Logical library readback does not establish physical RF accuracy. ## Validation - Follow-up on macOS ARM64, Apple clang 21.0.0, standalone C++20 `-O2`: **260 checks, 0 failures**. The test is socket-free and requires no Qt event loop, hardware, DSP or settings. - Follow-up mutation in a temporary source copy: disabling only `stepHz < resolution` produces **260 checks, 1 failure**, specifically the new unresolved-grid assertion. Production guards remain unchanged. - `git diff --check` passes for the follow-up. - Prior reviewed head `5cd78ad10bf3ce86c404cddf7c2aea9b4215c844`: Linux normal engine policy object and registered CTest built with GCC 16.2.1; all 259 then-existing assertions passed. The independent rational oracle matched all 2,000 cases, upper-transition-guard and generation mutations failed as expected, and the exact restored source passed. Strict engine-boundary, test-registration, frozen CI-gate and touchpoint checks passed on that head. Those are recorded prior-head results, not a new Linux run of this follow-up. - Linux follow-up: normal engine policy object and registered CTest rebuilt; **260 checks pass**. Removing only the resolution guard compiled and failed the new assertion. Exact source restoration, rebuild and final CTest pass. Strict engine-boundary, registration, frozen CI gate, touchpoint manifest and whitespace checks pass. - Signed follow-up head `bec2815c605442864fc425133ddd887cb200680d`; full source matches validation base `e60c3bea2ac8e5b762be9a3c6f52aa1bab8fb9ad` plus six-file digest `b2237190f1a875addf40557d39d9e5e8c01fcc171906908861a1c51ac46c4cf5`. All seven PR checks passed on this follow-up head. - Review follow-up `beb054c384c9bb1334c68ab6ecb8bc8ec96ebd93` corrects only the Osmocom source anchor; implementation and tests are unchanged. Independent review of `bec2815c` merged with main `f0ed969b` passed all 260 checks in native macOS ARM64 optimized and ASan/UBSan builds. Grid-resolution, upper-guard, generation and restore-capacity mutations all failed as expected. Engine-boundary, registration, frozen CI-gate, touchpoint and whitespace checks passed. See the live Checks tab for the documentation follow-up status. No live-radio or performance claim is made for this unintegrated policy. The test is registered in the default graph without expanding the frozen per-PR allow-list. No dependencies, threads, settings owner, socket-owning test, UI controls or CHANGELOG entry are added. Full application behavior, physical RF accuracy and supported receiver capacity remain integration gates. --- 73, Ozy **K6OZY** · GPT-6 Astra-Ultra --------- Co-authored-by: Robbie Foust <rfoust@gmail.com>
1 parent f0ed969 commit ab03be0

6 files changed

Lines changed: 1108 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ set(CORE_SOURCES
757757
src/core/AudioDeviceNegotiator.cpp
758758
src/core/AudioOutputRouter.cpp
759759
src/core/AetherDspModePolicy.cpp
760+
src/core/SharedCapturePolicy.cpp
760761
src/core/AudioEngine.cpp
761762
src/core/TxCaptureBuffer.cpp
762763
src/core/TxMicChannelNormalizer.cpp

docs/shared-capture-policy.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Shared capture policy foundation
2+
3+
`src/core/SharedCapturePolicy.{h,cpp}` implements the pure RF admission and
4+
restore policy from approved RTL RFC #5468. It is compiled into `aethercore`
5+
and exercised directly by `shared_capture_policy_test`. No backend invokes it
6+
yet. It does not change USB, DSP, audio, viewport, UI, settings or slice lifecycle.
7+
8+
## Descriptor contract
9+
10+
All frequency and rate fields use finite `double` Hz with magnitude at most
11+
2^40 Hz. This numerical ceiling is not a tuner range or usable-bandwidth claim.
12+
Occupied RF must be nonnegative and remain below that ceiling. Empty/inverted
13+
filters, negative guards, zero achieved rate and unresolved numerical intervals
14+
are refused. No mode defaults, filter presets or measured margins are inferred.
15+
16+
A slice's complete occupied interval is:
17+
18+
```
19+
[carrier + translation + filterLow - guardLow,
20+
carrier + translation + filterHigh + guardHigh]
21+
```
22+
23+
The adapter supplies the signed carrier/BFO translation so both filter edges
24+
refer to RF in the same coordinate system. Filter edges may be asymmetric or
25+
on one side of the carrier. `occupiedInterval` validates geometry and a
26+
nonnegative stable ID; the set operations also validate the caller's slot bound.
27+
28+
A capture descriptor carries one endpoint ID, a target generation, its center,
29+
achieved sample rate and independently supplied left/right usable extents.
30+
Extents are nonnegative, not both zero, and each must fit within half the achieved
31+
rate. One-sided usable intervals are supported. Usable bandwidth is separate
32+
from nominal rate and viewport. A legal capture may include unused negative RF;
33+
that does not disqualify positive occupied RF inside it.
34+
35+
`ReceiverLimits` separates addressable slots from concurrent capacity. The caller
36+
must supply `slotCount` matching its published `maxSlices`/addressable range;
37+
valid IDs are `[0, slotCount)`. `capacity` may be lower because fewer resources
38+
are available, but cannot exceed `slotCount` or bypass a smaller architectural
39+
limit. Slot count is positive; zero capacity is valid. The 4096-entry/slot and
40+
256-domain ceilings only bound validation work and allocations; they do not
41+
advertise supported receiver counts. Architecture limits require integrated
42+
measurement in later RFC phases.
43+
44+
Hardware center domains are an explicit union of inclusive intervals with
45+
independent grids. They may be disjoint, overlapping or supplied in any order.
46+
The representable hardware request at grid index `n` is
47+
`std::fma(n, stepHz, gridOriginHz)`. The adapter must describe its actual tuning
48+
quantization this way; the helper does not infer it from a nominal sample rate.
49+
Indices are exact integers bounded to +/-2^52. A domain must contain a realizable
50+
point and its step must be at least the greatest double spacing at its upper
51+
edge/origin. Unresolvable grids fail validation instead of hanging or rounding
52+
onto unrelated centers.
53+
54+
Arithmetic uses IEEE round-to-nearest double operations. TwoSum residuals round
55+
occupied edges outward and usable capture edges inward. There is no tolerance
56+
that widens acceptance and no fixed fractional-Hz lattice. Conservative rounding
57+
can refuse a mathematically fitting fringe that cannot be represented safely.
58+
The helper must not be compiled with unsafe floating-point reassociation or
59+
fast-math. Ordinary decimal rates, guards and measured margins are accepted.
60+
61+
## Selection and readback
62+
63+
`selectCenter` receives the complete desired receiver set. It validates every
64+
entry and the caller's limits before returning a proposal. From occupied
65+
intervals `[L_i,H_i]` and usable extents `U_L,U_R`, the continuous center bound is:
66+
67+
```
68+
max(H_i - U_R) <= center <= min(L_i + U_L)
69+
```
70+
71+
It intersects that bound with each legal hardware domain, examines neighboring
72+
grid points around the current center and feasible endpoints, then checks full
73+
containment at the actual returned double center. It preserves a legal current
74+
center, otherwise chooses the nearest realizable center; exact ties choose lower
75+
Hz regardless of domain order. Distance residuals distinguish close candidates
76+
whose large distances would otherwise round to the same double.
77+
78+
All add/tune/filter/guard/mode-translation/rate proposals use this same operation
79+
with their full desired set and proposed achieved capture descriptor. No input
80+
is mutated; one invalid or impossible entry refuses the whole proposal. Removing
81+
entries preserves the center when still legal.
82+
83+
A selection is a proposal, not confirmed hardware state. `validateReadback`
84+
requires coherent proposed and actual endpoint IDs and target generations, checks
85+
both descriptors and the entire desired set, and validates actual center/rate/
86+
usable margins. Actual values may differ if the full set still fits. The caller
87+
assigns the target generation for a reconfiguration and assembles one coherent
88+
readback with that generation. Hardware transactions, rollback, asynchronous
89+
ownership and publication remain the integration layer's responsibility.
90+
91+
Center domains and returned centers must use the same adapter-declared
92+
representation. The adapter must document whether readback is logical library
93+
state or a measured hardware quantity. In
94+
[Osmocom librtlsdr v2.0.2](https://github.com/osmocom/rtl-sdr/blob/v2.0.2/src/librtlsdr.c#L884-L913)
95+
and [RTL-SDR Blog commit aed0ea19](https://github.com/rtlsdrblog/rtl-sdr-blog/blob/aed0ea19f3a273370a13c9009b96313c75d54c7b/src/librtlsdr.c#L887-L936),
96+
`rtlsdr_get_center_freq()` returns the cached requested frequency stored after
97+
successful tuning; it does not measure the achieved PLL frequency. Verify other
98+
library versions or forks before relying on that behavior. This policy's
99+
validation does not establish physical RF accuracy, which needs hardware evidence.
100+
101+
## Fixed-capture restore
102+
103+
`restoreFixedCapture` validates the actual established capture and all input
104+
entries before returning a complete plan. It never recenters or changes capture
105+
bandwidth. Among valid individually fitting entries it accepts ascending stable
106+
IDs up to capacity, preserving every field and ID. All occurrences of duplicated
107+
nonnegative IDs are rejected, including otherwise-invalid occurrences, so input
108+
order cannot choose a winner. Other per-entry reasons distinguish invalid IDs,
109+
numeric/interval failures, outside-capture intervals and capacity refusal.
110+
Rejections carry the original input index and ID in original input order.
111+
A global descriptor/limits/collection failure returns no partial plan.
112+
113+
Saved slice descriptors contain only RF configuration and stable slot IDs, no
114+
session capture identity or generation. Consequently reconnecting into a new
115+
capture generation does not invalidate remembered configuration. Future lifecycle
116+
code owns slot occupant generations; future persistence code owns parsing,
117+
logging each refusal and atomically applying/persisting accepted state. If no
118+
slice is accepted, the caller retains the session's valid initial state.
119+
120+
The focused test has no sockets, settings, hardware, Qt event loop or DSP. It
121+
uses explicit RF edges, exhaustive small integer hardware-center enumeration,
122+
nextafter boundary cases and malformed bounded input. Live RTL integration,
123+
measured margins, architecture capacity, RF reception and GUI convergence remain
124+
later gates.

0 commit comments

Comments
 (0)