|
| 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