RTL8812AU: ground-station uplink starves the video receiver (unrecoverable loss / colour blocks)
Symptom
On an RTL8812AU receiver, current master shows persistent video corruption: the top
~1/3 of the frame decodes, the rest is large pink/blue blocks. It is not intermittent — it is
present continuously, at any signal strength.
The picture is what an H.26* decoder produces from a partially delivered frame: the macroblock
rows it received are drawn, and the rest of the output buffer is never written.
Measured on a Samsung Galaxy S22 (SM-S901U1, Android 16) and Google Pixel 1 (), RTL8812AU (0bda:881a), 5 GHz
|
Ground station A |
Ground station B |
| Device |
Samsung Galaxy S22 (SM-S901U1) |
Google Pixel (1st gen, sailfish) |
| SoC |
Snapdragon 8 Gen 1 (taro) |
Snapdragon 821 (msm8996) |
| Android |
16 (SDK 36) |
10 (SDK 29) |
| ABI |
arm64-v8a |
arm64-v8a |
channel 161, 20 MHz, at ~60 dB SNR.
Root cause
The ground-station uplink transmit is starving the receiver. The adapter is half duplex, so
every uplink frame is a receive blackout.
WfbngLink.cpp configures the uplink as k=1, n=5 — every message is put on the air five
times — and Transmitter::sendPacket emits a block's FEC fragments in a tight loop with no
spacing. Combined with the ~10 Hz adaptive-link cadence plus mavlink, the measured result is:
~102 uplink frames/second, in bursts of 10-12 frames inside 1-2 ms
From logcat (07:40:30, one adaptive-link period):
07:40:30.007 message 1785249630:2000:2000:13:11:2000:60.956657:0:-1:5:phbe
07:40:30.007 Packet sent successfully (x8 <- 8 frames inside one millisecond)
07:40:30.008 Packet sent successfully (x4)
07:40:30.108 message 1785249630:... <- next period, 100 ms later
07:40:30.108 Packet sent successfully (x8)
07:40:30.109 Packet sent successfully (x2)
Because the burst is contiguous, the video fragments lost are consecutive — the one loss
pattern the downlink FEC cannot repair. The link reports FEC already bumped to its maximum
setting (5) and still failing.
Why this only appeared recently
This is not a devourer regression, though a devourer change is what exposed it.
Bisecting the devourer submodule (14468b6 … 73f1cb4, 163 commits) against a fixed PixelPilot
tree landed on devourer 5f099ee — "tx: programmatic runtime TX-mode API; drop
DEVOURER_TX_HT_MCS gate" (2026-06-27). That commit made send_packet honour the radiotap
HT-MCS index unconditionally:
- static const bool ht_mcs_enabled = std::getenv("DEVOURER_TX_HT_MCS") != nullptr;
- if (ht_mcs_enabled && (mcs_known & IEEE80211_RADIOTAP_MCS_HAVE_MCS)) {
+ if (mcs_known & IEEE80211_RADIOTAP_MCS_HAVE_MCS) {
Before it, fixed_rate stayed at the MGN_1M default. CCK is a 2.4 GHz-only modulation, and
this link runs on 5 GHz channel 161 — so the chip never actually transmitted those frames.
The uplink was silently dead. The radio was effectively receive-only, which is why older builds
show no loss. 5f099ee made the uplink real for the first time, and 102 frames/s of transmit
started costing the receiver its packets.
The corollary is worth stating plainly: on release_0_23 and earlier, adaptive-link feedback
and the mavlink uplink were not working at all on a 5 GHz channel. Pinning devourer back
"fixes" the colour blocks by silencing the uplink again.
Evidence
Same RF environment, same session, metric is lost_last_second from the adaptive-link telemetry
(WfbngLink.cpp) — video fragments the downlink FEC could not recover.
| Build |
SNR |
lost/s |
zero-loss samples |
master as-is (n=5, MCS0, back-to-back) |
59.8 dB |
15.82 |
4.9 % |
| n=5, fragments spaced 8 ms apart |
61.3 dB |
10.42 |
9.0 % |
| n=2 |
53.3 dB |
2.15 |
62 % |
| n=2 + uplink MCS3 |
57.0 dB |
0.12 |
94 % |
| uplink muted entirely (control) |
56.3 dB |
0.00 |
100 % |
devourer pinned pre-5f099ee (control) |
60.0 dB |
0.26 |
93 % |
The muted-uplink control is the decisive one: with RtlJaguarDevice::send_packet returning
early and nothing else changed, loss goes to exactly zero and the colour blocks disappear.
Spacing the burst makes it worse, not better
Spreading the five fragments over 40 ms lowered total lost fragments (15.8 → 10.4) but made the
artefacts visibly more frequent. The same damage is spread across more downlink FEC blocks,
and one damaged block is one visible glitch. The lever that works is less airtime, not
smoother airtime — worth knowing before anyone reaches for pacing.
Ruled out
Each of these was tested on S22:
- SIMD/NEON FEC (
ZFEX_USE_ARM_NEON etc.) — a differential harness compiling zfex.c twice
into one binary, scalar vs NEON, ran 60 000 randomised encode/decode trials on the target SoC
with byte-identical results.
- Concurrent bulk-IN URBs (devourer
7701ec5) — measured 0.02 lost/s.
- Jaguar1 async URB RX queue (
bad37a8), RX URB infinite timeout (d31f915).
- RX aggregation thresholds and RX buffer size (
bbc7385 reduced both; measured 0.00 lost/s).
- Renderer / decoder path — the AI-feature branch on a
release_0_23 base shows no blocks.
wfb-ng submodule — identical in both good and bad configurations.
Environment
- Samsung Galaxy S22 (SM-S901U1), Google Pixel 1
- RTL8812AU,
0bda:881a, USB 2.0 high speed
- PixelPilot
34837ed, devourer 73f1cb4, wfb-ng 0da5279
- 5 GHz channel 161, 20 MHz
RTL8812AU: ground-station uplink starves the video receiver (unrecoverable loss / colour blocks)
Symptom
On an RTL8812AU receiver, current
mastershows persistent video corruption: the top~1/3 of the frame decodes, the rest is large pink/blue blocks. It is not intermittent — it is
present continuously, at any signal strength.
The picture is what an H.26* decoder produces from a partially delivered frame: the macroblock
rows it received are drawn, and the rest of the output buffer is never written.
Measured on a Samsung Galaxy S22 (SM-S901U1, Android 16) and Google Pixel 1 (), RTL8812AU (
0bda:881a), 5 GHzsailfish)taro)msm8996)channel 161, 20 MHz, at ~60 dB SNR.
Root cause
The ground-station uplink transmit is starving the receiver. The adapter is half duplex, so
every uplink frame is a receive blackout.
WfbngLink.cppconfigures the uplink ask=1, n=5— every message is put on the air fivetimes — and
Transmitter::sendPacketemits a block's FEC fragments in a tight loop with nospacing. Combined with the ~10 Hz adaptive-link cadence plus mavlink, the measured result is:
From logcat (
07:40:30, one adaptive-link period):Because the burst is contiguous, the video fragments lost are consecutive — the one loss
pattern the downlink FEC cannot repair. The link reports FEC already bumped to its maximum
setting (5) and still failing.
Why this only appeared recently
This is not a devourer regression, though a devourer change is what exposed it.
Bisecting the devourer submodule (14468b6 … 73f1cb4, 163 commits) against a fixed PixelPilot
tree landed on devourer
5f099ee— "tx: programmatic runtime TX-mode API; dropDEVOURER_TX_HT_MCS gate" (2026-06-27). That commit made
send_packethonour the radiotapHT-MCS index unconditionally:
Before it,
fixed_ratestayed at theMGN_1Mdefault. CCK is a 2.4 GHz-only modulation, andthis link runs on 5 GHz channel 161 — so the chip never actually transmitted those frames.
The uplink was silently dead. The radio was effectively receive-only, which is why older builds
show no loss.
5f099eemade the uplink real for the first time, and 102 frames/s of transmitstarted costing the receiver its packets.
The corollary is worth stating plainly: on
release_0_23and earlier, adaptive-link feedbackand the mavlink uplink were not working at all on a 5 GHz channel. Pinning devourer back
"fixes" the colour blocks by silencing the uplink again.
Evidence
Same RF environment, same session, metric is
lost_last_secondfrom the adaptive-link telemetry(
WfbngLink.cpp) — video fragments the downlink FEC could not recover.masteras-is (n=5, MCS0, back-to-back)5f099ee(control)The muted-uplink control is the decisive one: with
RtlJaguarDevice::send_packetreturningearly and nothing else changed, loss goes to exactly zero and the colour blocks disappear.
Spacing the burst makes it worse, not better
Spreading the five fragments over 40 ms lowered total lost fragments (15.8 → 10.4) but made the
artefacts visibly more frequent. The same damage is spread across more downlink FEC blocks,
and one damaged block is one visible glitch. The lever that works is less airtime, not
smoother airtime — worth knowing before anyone reaches for pacing.
Ruled out
Each of these was tested on S22:
ZFEX_USE_ARM_NEONetc.) — a differential harness compilingzfex.ctwiceinto one binary, scalar vs NEON, ran 60 000 randomised encode/decode trials on the target SoC
with byte-identical results.
7701ec5) — measured 0.02 lost/s.bad37a8), RX URB infinite timeout (d31f915).bbc7385reduced both; measured 0.00 lost/s).release_0_23base shows no blocks.wfb-ngsubmodule — identical in both good and bad configurations.Environment
0bda:881a, USB 2.0 high speed34837ed, devourer73f1cb4, wfb-ng0da5279