Skip to content

Commit 55fb6b0

Browse files
Ozy311rfoust
andauthored
Add bounded RTL receiver lifecycle foundation. Principle VII. (#5590)
Implement RFC #5468 capture/processing lifecycle foundation with stable receiver identities, bounded asynchronous preparation, sample-boundary publication and acknowledged off-callback retirement. Reserve complete banks from the shared WDSP pool. Validate derived DSP buffer sizes and exchange ratios before preparation; reject configurations that would truncate to zero or produce incorrect exchange lengths. Preserve active state on refusal and demonstrate rejection with mutation-backed socket-free coverage. Co-authored-by: Ozy311 <Ozy311@users.noreply.github.com> Co-authored-by: Robbie Foust <rfoust@gmail.com>
1 parent 796008b commit 55fb6b0

10 files changed

Lines changed: 1959 additions & 16 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ set(CORE_SOURCES
747747
src/core/backends/icom/IcomSettings.cpp # owned config, "Icom" root key (Principle V)
748748
src/core/backends/icom/IcomCredentials.cpp # password -> keychain, NEVER settings
749749
src/core/dsp/WdspChannel.cpp
750+
src/core/backends/rtl/RtlReceiverRegistry.cpp # RFC #5468 prepared receiver ownership; no USB dependency
750751
${AETHER_SETTINGS_SOURCES}
751752
src/core/RadioStateMemory.cpp # RFC #4603 client-side radio memory
752753
src/core/RtlSliceSettings.cpp # RFC #5468 persistence foundation

docs/architecture/aetherd-rtl-sdr-backend-design.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ There is no intermediate ring buffer: each librtlsdr callback converts and
3535
processes its block on the reader thread. This bounds memory use but means DSP
3636
must keep pace with USB input.
3737

38+
The compiled [prepared receiver registry](rtl-receiver-registry.md) implements
39+
RFC #5468 F4's bounded multi-receiver ownership, asynchronous WDSP preparation,
40+
sample-boundary publication and acknowledged retirement. The live backend does
41+
not select it yet; RF extraction and audio integration belong to M1, so the
42+
current DDC and receiver count remain unchanged.
43+
3844
## Connection and state
3945

4046
RTL-SDR is discovered as a local device; it is deliberately not accepted by
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# RTL prepared receiver ownership (RFC #5468 F4)
2+
3+
`RtlReceiverRegistry` is a compiled engine foundation for the
4+
[approved RTL multi-RX RFC](https://github.com/aethersdr/AetherSDR/issues/5468).
5+
F4 is the local plan ID for the RFC's Foundation-stage
6+
"testable capture/processing lifecycle" bullet.
7+
Its default preparation function creates real `WdspChannel` receivers and
8+
preallocates their output buffers. It represents multiple receivers from the
9+
start and enforces their admission, session identity, publication and retirement.
10+
11+
The live `RtlSdrBackend` still selects `RtlSdrDdc`, with its existing single
12+
receiver, WFM behavior and audio rate. F4 does not enable another receiver or
13+
replace the current demodulator. M1 supplies RF extraction, paired I/Q conversion,
14+
fixed-block accumulation and aligned mixing before selecting this foundation;
15+
the A-series audio changes must precede a new 48 kHz producer. Viewport plans
16+
and filters also remain with their later feature. This boundary avoids shipping
17+
a partially converted receive path. There is no persistence-owner cutover,
18+
new UI, new dependency or new Qt/acquisition thread per slice here. Each
19+
`WdspChannel` still owns WDSP's existing internal execution threads.
20+
21+
## Owners and identity
22+
23+
Each registry owns one capture endpoint per session. `beginSession()` returns
24+
a nonzero process-unique session token. The control side reserves stable UI
25+
slots through `reserveSlot()`, optionally naming a particular slot. A handle
26+
contains three distinct fields: session token, slot number and slot-instance
27+
generation. WDSP's process-global channel number is private to `WdspChannel`
28+
and is never a UI or settings key.
29+
30+
Capture identity and capture generation remain the F2 descriptor fields.
31+
A capture generation identifies the complete center/rate/usable-interval
32+
readback, not an independently mutable collection of controls. Reusing a
33+
generation with different metadata is refused. Slot/session counter exhaustion
34+
refuses allocation rather than wrapping identities.
35+
36+
A prepared target can precede M1's hardware transaction. A failed future-capture
37+
preparation therefore does not invalidate the capture still in use: an older
38+
descriptor is accepted only when it exactly matches an `Active` bank in the
39+
same session. Every other older or modified descriptor is refused. The newest
40+
requested generation remains the high-water mark even when that exception is
41+
used. M1 remains responsible for actual hardware readback and transaction order;
42+
preparing a target is not evidence that hardware has adopted it.
43+
44+
A complete desired set is submitted as bounded `ReceiverSpec` values. Each
45+
spec carries its reserved handle, F2 guarded RF passband and WDSP configuration.
46+
Pending and live reservations together respect the receiver capacity, even
47+
when the addressable UI slot range is larger. A removed live handle remains
48+
charged until its bank is actually retired and destroyed, so failed-removal
49+
retries retain the complete still-active set.
50+
Admission validates all handles, distinct slots, bounded DSP buffers and
51+
parameters, and whole-set passband containment within the fixed capture using
52+
the production `SharedCapturePolicy` helper. Its center domain is deliberately
53+
the singleton supplied center; this does not validate tuner range or grid
54+
legality. It never recenters a capture or adjusts a sibling.
55+
Derived DSP input/output block sizes must be positive and integral, and the
56+
exchange input/output rates must have an integral ratio. This prevents WDSP
57+
integer truncation from creating zero-sized internal buffers or incorrect
58+
exchange lengths.
59+
WDSP blocking-output mode is refused because these receivers run in the
60+
acquisition context.
61+
62+
An omitted handle remains valid while an old bank references it. This matters
63+
when preparing a removal fails: the old bank and its complete set of handles
64+
remain usable for a retry. An omitted slot can be reused only after its last
65+
pending/preparing/offered/active/retiring reference is gone. Reuse advances its
66+
instance generation; siblings keep their handles. `cancelReservation()` is only
67+
for an unused reservation. Removing a live receiver is a new complete desired
68+
set, not destruction through that method.
69+
70+
## Bounded process-wide preparation
71+
72+
All registries share one executor over the existing Qt global worker pool.
73+
It retains at most one executing preparation/destruction operation and one
74+
coalesced pending desired set. A newer request from the pending set's owner
75+
replaces that set. A different owner receives `Busy`, preserving the other
76+
capture's work. Independent sessions are never silently coalesced together.
77+
Callers retain their desired request and retry a `Busy` refusal through their
78+
control event loop; the registry does not create an unbounded retry queue.
79+
80+
The executor retains at most four registry contexts, including closed owners
81+
whose jobs or sample readers have not acknowledged retirement. A fifth
82+
constructor returns an invalid registry. This is deliberate backpressure under
83+
repeated family swaps or stalled readers. There is no detached-task list that
84+
grows each time a backend is destroyed.
85+
86+
A job owns its state and preparation callable; it never captures a backend
87+
QObject or accesses a `QPointer` across threads. Destroying the registry or
88+
canceling a session invalidates pending/results immediately and returns without
89+
waiting for FFTW. An already running FFTW call is allowed to finish, then its
90+
obsolete result is destroyed on the worker pool. The same executor survives
91+
replacement registry objects, even when endpoint metadata is identical.
92+
93+
A preparation callable that throws a standard or non-standard exception reports
94+
`PreparationFailed`. Its partial bank and reservations are retired off the
95+
callback, while the active bank remains usable and later requests can proceed.
96+
97+
Each registry has three fixed bank positions: preparing/offered, active, and
98+
retiring. It cannot accumulate another prepared bank behind an unconsumed
99+
offer. With a reader attached, the next block acknowledges the stale offer;
100+
without a reader, the control path may retire it directly. Thus startup may
101+
wait for the newest prepared result before acquisition begins. `service()`
102+
on the model/control event loop reaps acknowledgment pressure and advances
103+
pending work. No callback posts an event or schedules a task.
104+
105+
## Resource accounting
106+
107+
Limits are constructor inputs: addressable slots, receiver capacity, and total
108+
resident receiver capacity. The latter includes preparing, offered, active,
109+
retired and currently destroying receivers. Slots do not become free merely
110+
because destruction was queued. Replacing an active bank requires sufficient
111+
capacity for old plus new; refusal preserves the old bank. For example, a
112+
capacity of four and a resident budget of eight permit a complete four-channel
113+
replacement after earlier retirements have actually drained.
114+
115+
Eight UI slots and four retained registry contexts are representation ceilings,
116+
not advertised receiver counts or performance claims. Architecture-specific
117+
limits still require the RFC's measured integrated evidence. Buffer/rate/filter
118+
ceilings also bound individual receiver allocations; they do not qualify every
119+
accepted DSP configuration for real-time operation.
120+
121+
`WdspChannel::reserveChannels()` atomically reserves the entire new bank from
122+
the **same 32-slot allocator** used by ordinary `WdspChannel::create()`.
123+
It performs no FFTW planning. Other radio backends and TX channels continue to
124+
compete for those actual slots. A failed batch takes none; unconsumed slots
125+
return by RAII; successful reserved creation transfers one slot into the
126+
channel's existing lifetime. Destruction returns that slot only after the
127+
channel has really closed. There is no independent RTL pool counter that can
128+
overcommit WDSP. Local resident-limit and shared-pool failures are distinct
129+
observable preparation errors.
130+
131+
This is a deliberate public API addition to the shared DSP class. A caller can
132+
reserve all 32 slots without creating channels, and the reservation has no
133+
timeout or per-backend quota. Callers must release reservations promptly when
134+
preparation is canceled or fails; other backends receive pool exhaustion while
135+
those slots are held. Maintainer review must explicitly consider that API scope.
136+
137+
The normal WDSP setup lock, production wisdom import and atomic export remain
138+
unchanged. F4 sets no planner time limit. The repository's existing isolated,
139+
bounded test-planner setup applies to the registered correctness tests; those
140+
results are not production cold-start measurements.
141+
142+
## Sample publication and retirement
143+
144+
`attachReader()` creates the sole acquisition-side reader before acquisition
145+
starts. It can outlive the registry object. `processBlock()` consumes a borrowed
146+
normalized complex-float IQ span with an explicit session token, full capture
147+
descriptor, first-sample position and discontinuity flag. The source captures
148+
these tokens with the acquisition configuration; it must never relabel old IQ
149+
using the registry's latest session or capture values.
150+
151+
Block delivery validates the session, every descriptor field, bounded nonempty
152+
length, sample-position arithmetic and finite I/Q values. A prepared bank can
153+
be adopted only at entry to a valid block whose descriptor matches exactly.
154+
A bank for a future capture generation waits while the old bank continues
155+
processing matching old-capture blocks. After adoption, old-capture blocks
156+
are refused. First delivery, bank changes, explicit discontinuity, position
157+
gaps and refused input all mark the next delivery discontinuous; M1 uses this
158+
to reset extraction/accumulation histories and align output.
159+
160+
The synchronous `BlockProcessor` receives the block plus borrowed receiver
161+
views. A view may be used only during that call and never by another thread.
162+
`Receiver::processIq()` accepts the prepared WDSP configuration's exact planar
163+
input block size and returns borrowed preallocated L/R output spans. F4 does
164+
not mistake full-rate RTL IQ for already extracted WDSP input; M1's extraction
165+
step belongs between the block and that processor.
166+
167+
The sample method uses fixed stack storage and lock-free atomic state only.
168+
It does not allocate, take a mutex, copy/release a shared pointer, plan, change
169+
filters, invoke a preparation callable or destroy a receiver. Preparing a
170+
replacement cannot mutate the bank a callback currently borrows.
171+
Finite-value validation performs an O(N) read of up to 65,536 complex samples
172+
after the metadata checks, even if no active bank or usable offer remains.
173+
M1's extraction will read that IQ again; integrated callback-budget measurements
174+
must include both passes. Allocation freedom alone does not establish headroom.
175+
176+
On the next sample boundary, the reader adopts the complete ready bank and
177+
marks the previous bank `Retired` only after its preceding synchronous call
178+
has returned. The pool observes that release/acquire acknowledgment before
179+
moving the old bank to `Destroying`. Its actual destructors run off the sample
180+
path, and only then does the position become `Free`.
181+
182+
If no next callback will arrive, the acquisition owner must stop/join its
183+
sample context and call `SampleReader::stop()` outside the callback. `stop()`
184+
acknowledges the active and offered banks and cancels the session, including
185+
pending/running preparation results. The reader's destructor follows this same
186+
path. Reader moves, stop and destruction must not race `processBlock()`; control
187+
methods may run concurrently with processing but must never be called from the
188+
processor. A stopped or destroyed reader cannot silently leave a bank waiting
189+
for a nonexistent next block. A still-running reader must never be destroyed
190+
as a timeout workaround, just as a live librtlsdr handle must never be closed.
191+
192+
## Validation and integration obligations
193+
194+
The default graph registers two socket-free targets even in an RTL-disabled
195+
build:
196+
197+
- `wdsp_channel_reservation_test`: atomic batch refusal, moves, rollback,
198+
ordinary allocator competition, real reserved/ordinary channel creation,
199+
and exact slot return.
200+
- `rtl_receiver_registry_test`: injected delayed/failing preparation, immutable
201+
held sample views, stable slot reuse, failed-removal retry, session/capture
202+
rejection, coalescing and independent-owner refusal, no-reader startup,
203+
publication pressure, resident/pool bounds, owner destruction/recreation,
204+
same-owner reconnect, stopped acquisition, retained-context ceiling, and
205+
actual default WDSP preparation/processing, and recovery after standard and
206+
non-standard preparation exceptions. CTest bounds each target to 120 seconds.
207+
208+
The registry tests establish these ownership contracts, not reception quality,
209+
GUI convergence or race freedom on their own. Run the production units under
210+
the sanitizer lane; TSan needs the project's instrumented Qt build. M1 must
211+
add production caller/wiring tests, actual RF extraction and variable-block
212+
DSP tests, normalized lifecycle/error routing, USB readback transactions,
213+
audio integration and the approved hardware/bridge/radiocert gates. Native
214+
Linux aarch64 runtime/performance and per-architecture advertised limits remain
215+
independent release requirements.
216+
217+
M1 must validate requested and returned capture centers against the actual
218+
tuner's supported center domains and grid before supplying descriptors here.
219+
The registry's singleton center domain is not hardware-domain validation or
220+
evidence of a successful tune; actual USB readback and capture transactions
221+
remain the integration owner's responsibility.

0 commit comments

Comments
 (0)