rx: gate PHY-status parsing on the RX-descriptor PHYST bit (Jaguar1/2/3) - #409
Conversation
f83c2b0 to
51d743b
Compare
PR Summary by QodoGate Jaguar3 PHY-status parsing on RX descriptor PHYST
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
josephnef
left a comment
There was a problem hiding this comment.
Reviewed with verification passes (every finding below was independently re-verified against the working tree; one candidate — the alleged doc duplication across the four new comment blocks — was checked and refuted, the copies are house-convention self-contained headers with location-specific scope).
The core gate itself checks out: DW0 bit 26 matches the vendor halmac layout, the selftest's byte encoding is right, Packet p{} is per-frame zero-initialized, and the new selftest builds and passes locally. The inline comments cover the in-diff issues; five more findings land on files this PR doesn't touch but its own premise implicates:
1. The identical bug is left live on jaguar2 — and it steers a control loop there (src/jaguar2/RtlJaguar2Device.cpp:622). The loop parses phy-status on only !is_c2h && f.drvinfo_size >= 28; Rx8822bFrame never decodes PHYST even though GET_RX_DESC_PHYST_8822B (same DW0 bit 26) sits unused at FrameParserJaguar2.h:89 and REG_RX_DRVINFO_SZ=4 is global (HalJaguar2.cpp:2578). On 8822BU/8821CU A-MPDU RX, subframes' reserved-but-unwritten drvinfo bytes are decoded as rssi/snr/evm/cfo_tail, and with cfo_track on, _cfo.add(p.RxAtrib.cfo_tail) (lines 631-632) steers the closed-loop XtalCap crystal trim from garbage. The PR's own comments describe the mechanism as generic; landing the fix at per-chip altitude leaves the worst instance of it in the tree.
2. Jaguar1 has a plausible cousin of the same gap (src/jaguar1/FrameParser.cpp:237). pattrib.physt is decoded at line 88 but the phy-status memcpy is gated on drvinfo_sz alone. The vendor driver passes pattrib->physt ? pbuf+RXDESC_OFFSET : NULL (rtl8812au usb_ops_linux.c:179); the same one-line gate is already parsed and sitting unused here. Affects per-path RSSI/SNR/EVM on rx.path/GetActiveRxPaths — including 8814AU 4-chain spatial-diversity measurements.
3. Demo-side consumers are not gated on the new flag (examples/rx/main.cpp:911-913, 920-925, 1136-1146). The hopset per-slot accumulators, the g_rxagg energy/LinkHealth aggregate, and DEVOURER_RX_ALLPATHS rx.path rows all read rssi[0]/snr[0]/evm[0] unconditionally, so jaguar3 A-MPDU subframes now inject deterministic rssi=0/snr=0/evm=0 samples. Pre-PR these frames carried random stale garbage; the PR converts that into a systematic directional bias (per-dwell means scale down with aggregation depth, snr_min pins to 0, FHSS exclusion scores a more-aggregating channel as worse, LinkHealth reads a strong link as weak). Cheap to close now that RxAtrib.physt exists to gate on.
4. rx.txhit's proof-of-what-flew contract weakens (examples/rx/main.cpp:1076). ldpc/stbc/bw come only from parse_phy_sts_jgr3, so physt=0 subframes now deterministically report ldpc=0/stbc=0 — indistinguishable from a real BCC fallback — and the event doesn't emit physt for consumers to tell the difference. In an encoding-truth-table run against an aggregating TX, a sampled txhit on a non-first subframe records an LDPC-qualification pass as BCC fallback. Emitting physt in the event (or skipping ldpc/stbc when unset) closes it.
5. physt now carries three per-generation semantics in a shared struct (src/RxPacket.h:25, no doc comment pinning one): raw descriptor bit (jaguar1 FrameParser.cpp:88, rtl8733b Rtl8733bDevice.cpp:355), bit AND drvinfo>=28 AND page-0/1-parsed (jaguar3, this PR), and never-written/always-false (jaguar2, kestrel). A cross-generation consumer keying on it as "hardware wrote a report" sees false on a jaguar3 PHYST=1 frame with an unrecognized page; one keying on it as "signal fields valid" discards every jaguar2/kestrel frame whose fields ARE populated. The rtl8733b shape is the clean one: raw bit into the field, parse-success in a local for the gate — plus one documented contract at the declaration.
Items 1 and 2 don't have to land in this PR, but item 1 at minimum deserves a tracked issue before merge — the CFO-trim exposure is a live correctness problem this PR's analysis proves exists.
|
Pushed 40a2439 addressing the review. Every finding was checked against the working tree before acting; here is where each landed. In-diff (all four inline threads answered on the threads themselves): the mutate-then-return-false contract, the stale CCK comment, the CCK page feeding Items 1 and 2 — both confirmed, both fixed here rather than tracked. Item 1 (jaguar2): verified end to end — Item 2 (jaguar1): also confirmed — The PR title and description now cover all three generations. Item 5 — taken, plus a fourth semantics you did not list. Item 3 — one correction before it becomes a follow-up. Gating the demo consumers on One thing the CCK finding turned up that was not in the review. Chasing where the zero SNR actually lands:
🤖 Generated with Claude Code |
The jgr3 RX loop parsed the drvinfo area as a PHY-status report whenever it was long enough (>= 28 bytes). But drvinfo space is reserved on *every* frame — RX_DRVINFO_SZ is a global register — while the PHY writes a report only into the frames whose descriptor PHYST bit is set. On all-but-one subframe of an A-MPDU the area therefore holds stale bytes, and the page nibble in byte0 can alias a valid page number (0 = CCK, 1 = OFDM type1), so the parse silently folded garbage RSSI/SNR/EVM into the RF EMAs behind GetRxQuality() / GetActiveRxPaths() / the rx.path event. Decode DW0 bit 26 into Rx8822cFrame.physt and require it before parsing. This brings jaguar3 in line with jaguar1 (FrameParser.cpp:88) and rtl8733b (FrameParser8733b.h:77), which already decode the same bit into rx_pkt_attrib::physt — jaguar3 was the generation leaving it unset. parse_phy_sts_jgr3 now returns whether it recognised the page layout (page 0 CCK or page 1 OFDM type1) and actually filled the attrib's signal fields, rather than returning void after silently ignoring an unknown page number. The internal EMA folds (_rxq, _rxpaths, _cfo) gate on that result, so an unparsed page no longer contributes a zero-valued sample. tests/rx_physt_selftest.cpp is a headless guard on the bit position and the plumbing (ctest: rx_physt_bit, built with DEVOURER_JAGUAR3) so a descriptor-layout regression fails the suite instead of quietly poisoning the RF averages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FN9qETmHFJr6ekfkViZv8
Review follow-up to the Jaguar3 gate: the same defect is present on all three generations, and the shared `physt` field needed one meaning. REG_RX_DRVINFO_SZ is a GLOBAL register on every generation here — Jaguar1 _InitDriverInfoSize_8812A(4), Jaguar2 0x060F=4, Jaguar3 0x060F=4 — so the drvinfo space is reserved on every frame while the PHY writes a report only into frames whose RX-descriptor PHYST bit (DW0 bit 26) is set. Gating on drvinfo size alone decodes bytes left by an earlier frame, notably on all-but-one subframe of an A-MPDU. - Jaguar2 (RtlJaguar2Device.cpp): decode PHYST into Rx8822bFrame (the GET_RX_DESC_PHYST_8822B macro already existed, unused) and require it. This was the worst instance: the garbage cfo_tail fed _cfo.add(), which steers the closed-loop XtalCap crystal trim. - Jaguar1 (FrameParser.cpp): gate the phy-status memcpy on pattrib.physt, already decoded at the top of the same function and named in the comment as the vendor's gate; gate the EMA feed on it too. - Jaguar3: keep the gate, and assign the RAW descriptor bit to RxAtrib.physt rather than the parse outcome, so the shared field means the same thing on every generation that sets it. parse_phy_sts_jgr2/jgr3 now return PhyStsFill (None/Power/Full) instead of void/bool: which fields they filled, not whether they liked the buffer. A bool could not express the two real tiers, and the previous bool was wrong in both directions — it returned false for OFDM pages 2..6 after filling valid per-path RSSI from the shared common header (dropping a real measurement, and freezing GetRxQuality/GetActiveRxPaths with no diagnostic if the BB page selector ever left type1), and returned true for the CCK page, whose layout has no SNR/EVM/CFO at all. The CFO trackers now take only a Full fill. RxQualityAccumulator::add folded SNR unconditionally while its own doc comment promised it folded only when present, so CCK and non-type1 frames dragged snr_mean toward zero and pinned snr_min to 0 on every generation. SNR now carries its own sample count like EVM and the noise-floor term, surfaced as RxQuality::snr_valid. Also drop the stale comment claiming CCK frames carry no report and short drvinfo — neither is true, and parse_phy_sts_jgr3 decodes the CCK page. tests/rx_physt_selftest.cpp now covers both generations' descriptor-bit decode and the PhyStsFill tiers (short buffer, CCK page, OFDM type1, other OFDM page), so a regression in the return plumbing fails ctest instead of silently stopping all OFDM SNR/EVM accounting; rx_quality_selftest covers the mixed and CCK-only SNR windows. ctest 55/55 (la_csi_math skipped); JAGUAR1-only, JAGUAR2-only and JAGUAR3-only configs all build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JiAS32956Z3cmTbnmcXYkT
40a2439 to
ca61f2d
Compare
josephnef
left a comment
There was a problem hiding this comment.
All review findings verified fixed at ca61f2d — approving.
Re-verified against the branch, not just the replies: the jaguar2 PHYST gate (including the PhyStsFill::Full requirement on the XtalCap _cfo.add feed), the jaguar1 memcpy gate, the three-tier PhyStsFill return (pages 2..6 as Power is a better shape than either option I offered — the common-header RSSI stays a measurement and the page-selector freeze scenario degrades to RSSI-only), the raw-bit physt contract documented at the RxPacket.h declaration, the corrected CCK/drvinfo comment, and the widened selftest (build + full ctest green locally, 56/56, including the new rx_physt_bit tiers and the rx_quality mixed/CCK-only windows).
The RxQualityAccumulator SNR find deserves its own credit: the unconditional snr_sum_ += snr_raw contradicting the accumulator's own doc comment was live on every generation, and snr_valid is the honest surface for a CCK-only window.
The two demo-side items from the review body (gating the examples/rx accumulators on the now-available RxAtrib.physt, and emitting physt on rx.txhit) plus the same ungated-SNR shape in chanscout and cell::UeRxAttribution are follow-up-sized and tracked separately rather than holding this fix.
Problem
REG_RX_DRVINFO_SZis a global register on every generation that carries per-frame drvinfo — Jaguar1_InitDriverInfoSize_8812A(4)(HalModule.cpp:268), Jaguar20x060F=4(HalJaguar2.cpp:2578), Jaguar30x060F=4(HalJaguar3.cpp:485). The drvinfo space is therefore reserved on every frame, while the PHY writes a report only into frames whose RX-descriptor PHYST bit (DW0 bit 26) is set.All three RX loops gated the phy-status parse on the drvinfo size alone. On all-but-one subframe of an A-MPDU the area holds bytes left by an earlier frame, and the page nibble can alias a valid page number, so the parse succeeded on garbage and folded it into the RF running averages behind
GetRxQuality(),GetActiveRxPaths()and therx.pathevent — contaminated RSSI/SNR/EVM tails that track A-MPDU density rather than the link.On Jaguar2 it went further than telemetry: the garbage
cfo_tailfed_cfo.add(), which steers the closed-loop XtalCap crystal trim.Change
The gate, on all three generations. Decode the descriptor PHYST bit and require it before parsing.
FrameParser.cpp:237drvinfo_szonly — the comment there already namedpattrib->phystas the vendor's gate it was approximatingRtlJaguar2Device.cpp:622GET_RX_DESC_PHYST_8822Bpresent but unused; bit never decoded intoRx8822bFrameRtlJaguar3Device.cpp:330RxAtrib.phystgets one cross-generation meaning, documented at its declaration (src/RxPacket.h): the raw descriptor bit — "the PHY wrote a report into this frame's drvinfo" — explicitly not "the report parsed" and not "the signal fields are valid". Kestrel never sets it, and the comment says so and why (its PHY status arrives as its own PPDU-status frame). This resolves the open question the first revision raised: the parse outcome lives in a local instead.parse_phy_sts_jgr2/parse_phy_sts_jgr3returnPhyStsFill(None/Power/Full) — which fields they filled, not whether they liked the buffer. A bool could not express the two real tiers, and was wrong in both directions:ldpc,stbcandbwfrom the common header those pages share with type1 — dropping a real measurement, and leavingGetRxQuality/GetActiveRxPathsto freeze with no diagnostic if the BB page selector ever left type1 (devourer's BB table pins0x8C0[25:22]=1, but the vendor auto-switch and the debug page helper do not restore it)The CFO trackers now take only a
Fullfill on Jaguar2 and Jaguar3.RxQualityAccumulatorfolded SNR unconditionally (src/RxQuality.h) while its own doc comment three lines above claimed it folded only when present. EVM and the noise-floor term did filter; SNR did not. Not a Jaguar3 bug — Jaguar1, Jaguar2 and the RTL8733B all feed CCK frames through the same accumulator, sosnr_min_dbhas been pinned to 0 on any 2.4 GHz channel with CCK traffic on all of them, andsnr_mean_dbdragged toward zero in proportion to CCK density. SNR now carries its own sample count like EVM, surfaced asRxQuality::snr_validso a CCK-only window reports "no SNR" rather than a fabricated 0.RxPathActivityAccumulatoralready filtered per-metric and is unchanged.Stale comment dropped (
RtlJaguar3Device.cpp): it claimed CCK frames have short drvinfo and carry no report. Both false — the register write is global, andparse_phy_sts_jgr3decodes the CCK page-0 report explicitly.Testing
tests/rx_physt_selftest.cpp(ctestrx_physt_bit) exercises both the bit decode and the return contract, on both generations: descriptor bit set/clear throughparse_rx_8822candparse_rx_8822b; null buffer and a 27-byte report →Nonewith the attrib verified untouched; CCK page →Powerwithsnr/evm/cfo_tailasserted still 0; OFDM page 1 →Fullwith rssi/snr/evm/cfo/ldpc/bw checked; OFDM page 5 →Powerwith common-header RSSI present and the type1-only fields still 0.rx_quality_selftestgained the mixed-window and CCK-only-window SNR cases.ctest: 55 passed, 1 skipped of 56 (la_csi_mathskips, needs numpy), 0 failures — rebased ontomasterat 1413b14.JAGUAR1-only,JAGUAR2_8822B-only,JAGUAR3_8822C-only. The selftest is registered underDEVOURER_JAGUAR3 OR DEVOURER_JAGUAR2_8822B OR DEVOURER_JAGUAR2_8821C— verified absent in the Jaguar1-only config and passing in the other two.Deliberately not included
examples/rx/main.cpp): the hopset per-slot accumulators, theg_rxaggenergy/LinkHealth aggregate and theDEVOURER_RX_ALLPATHSrx.pathrows readrssi[0]/snr[0]/evm[0]unconditionally. One correction to how this was framed in review: gating them onRxAtrib.phystwould drop every sample on Kestrel, which never sets it. The generation-neutral gate isrssi[0] > 0— the same testRxQualityAccumulatoralready applies. Follow-up.rx.txhitemittingphystso an encoding-truth-table run can tell a real BCC fallback from a subframe that carried no report. Follow-up.usb.rx_zerocopydefault flip that rode the same working branch.🤖 Generated with Claude Code
https://claude.ai/code/session_01JiAS32956Z3cmTbnmcXYkT