From 895e3a4fa1b443cb97655daf385e2cccabb3e3da Mon Sep 17 00:00:00 2001 From: hackerman-kl Date: Sun, 31 May 2026 20:13:25 +0200 Subject: [PATCH] milan-avb: ACMP listener self-heal, CBS-exclusive egress, per-iface MVRP, Milan MaxFrameSize + channel-strict RX --- .../acmp-cmds-resps/acmp-milan-v12.c | 32 ++++++++ src/modules/module-avb/avdecc.c | 14 +--- src/modules/module-avb/es-builder.c | 9 +++ src/modules/module-avb/stream.c | 79 +++++++++++-------- 4 files changed, 91 insertions(+), 43 deletions(-) diff --git a/src/modules/module-avb/acmp-cmds-resps/acmp-milan-v12.c b/src/modules/module-avb/acmp-cmds-resps/acmp-milan-v12.c index b9d4b7ebc..8ead61950 100644 --- a/src/modules/module-avb/acmp-cmds-resps/acmp-milan-v12.c +++ b/src/modules/module-avb/acmp-cmds-resps/acmp-milan-v12.c @@ -2457,6 +2457,38 @@ void acmp_periodic_milan_v12(struct acmp *acmp, uint64_t now) stream_out->last_probe_rx_time = 0; } } + + /* Milan Section 4.3.3.1 / 5.5.3: a settled Listener with no reservation yet + * (SETTLED_NO_RSV) re-evaluates Listener Ready against the Talker Advertise + * registrar each tick. After a bridge convergence delay the TA arrives with no + * fresh ACMP event, so this re-declares Ready and advances to SETTLED_RSV_OK the + * instant the TA is IN — the stall self-heals, no controller re-bind needed. */ + for (uint16_t desc_index = 0; desc_index < UINT16_MAX; desc_index++) { + struct descriptor *desc; + struct aecp_aem_stream_input_state_milan_v12 *si_m; + struct stream_common *common; + bool ta_in; + + desc = server_find_descriptor(acmp->server, AVB_AEM_DESC_STREAM_INPUT, + desc_index); + if (desc == NULL) + break; + + si_m = desc->ptr; + if (si_m->acmp_sta.fsm_acmp_state != FSM_ACMP_STATE_MILAN_V12_SETTLED_NO_RSV) + continue; + + common = &si_m->stream_in_sta.common; + ta_in = common->tastream_attr.mrp != NULL && + avb_mrp_attribute_get_registrar_state(common->tastream_attr.mrp) == AVB_MRP_IN; + common->lstream_attr.param = ta_in + ? AVB_MSRP_LISTENER_PARAM_READY + : AVB_MSRP_LISTENER_PARAM_ASKING_FAILED; + if (common->lstream_attr.mrp != NULL) + avb_mrp_attribute_join(common->lstream_attr.mrp, now, true); + if (ta_in) + si_m->acmp_sta.fsm_acmp_state = FSM_ACMP_STATE_MILAN_V12_SETTLED_RSV_OK; + } } static const char *probing_status_name(uint8_t s) diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c index ba23601d3..41b9322aa 100644 --- a/src/modules/module-avb/avdecc.c +++ b/src/modules/module-avb/avdecc.c @@ -480,23 +480,15 @@ static int raw_stream_setup_socket(struct server *server, struct stream *stream) stream->sock_addr.sll_ifindex = req.ifr_ifindex; if (stream->direction == SPA_DIRECTION_OUTPUT) { - struct sock_txtime txtime_cfg; - + /* CBS/Qav-exclusive: set only the traffic-class priority so the egress + * CBS qdisc shapes this stream. SO_TXTIME (launch-time/ETF) is NOT set -- + * CBS and SO_TXTIME cannot coexist on the same queue. */ res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio, sizeof(stream->prio)); if (res < 0) { pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio); return -errno; } - - txtime_cfg.clockid = CLOCK_TAI; - txtime_cfg.flags = 0; - res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg, - sizeof(txtime_cfg)); - if (res < 0) { - pw_log_error("setsockopt(SO_TXTIME) failed: %m"); - return -errno; - } } else { struct packet_mreq mreq; diff --git a/src/modules/module-avb/es-builder.c b/src/modules/module-avb/es-builder.c index 4eb7222dd..cb1b45be4 100644 --- a/src/modules/module-avb/es-builder.c +++ b/src/modules/module-avb/es-builder.c @@ -122,6 +122,15 @@ static struct descriptor *es_buidler_desc_avb_interface(struct server *server, avb_mrp_attribute_begin(if_ptr->domain_attr.mrp, 0); avb_mrp_attribute_join(if_ptr->domain_attr.mrp, 0, true); + /* milan-avb: declare VID membership (MVRP) once per interface, held for the + * life of the interface like the SR Domain above — NOT per stream, so that + * destroying one stream cannot withdraw the VLAN other streams still need. */ + avb_mvrp_attribute_new(server->mvrp, &if_ptr->vlan_attr, + AVB_MVRP_ATTRIBUTE_TYPE_VID); + if_ptr->vlan_attr.attr.vid.vlan = htons(AVB_DEFAULT_VLAN); + avb_mrp_attribute_begin(if_ptr->vlan_attr.mrp, 0); + avb_mrp_attribute_join(if_ptr->vlan_attr.mrp, 0, true); + return desc; } diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c index 98701206e..872dcc0f4 100644 --- a/src/modules/module-avb/stream.c +++ b/src/modules/module-avb/stream.c @@ -229,34 +229,42 @@ static void on_flush_tick(void *data, uint64_t expirations) { struct stream *stream = data; struct server *server = stream->server; - uint64_t now_ns; + struct timespec ts; + uint64_t now_mono, now_gptp, stamp; int owed; (void)expirations; - now_ns = stream_gptp_now(server); - if (now_ns == 0) { + /* Pace the send rate off CLOCK_MONOTONIC (a stable local 1x clock); use the gPTP + * clock only for the AVTP presentation timestamp. Pacing must not ride the absolute + * PHC interpolation, whose steps during gPTP re-convergence burst the talker past + * its SRP reservation and get the stream policed away by the bridge. */ + clock_gettime(CLOCK_MONOTONIC, &ts); + now_mono = SPA_TIMESPEC_TO_NSEC(&ts); + now_gptp = stream_gptp_now(server); + stamp = now_gptp != 0 ? now_gptp : now_mono; + + if (stream->pdu_period == 0) { return; } - if (stream->flush_last_ns == 0) { - stream->flush_last_ns = now_ns; + stream->flush_last_ns = now_mono; return; } - if (stream->pdu_period == 0) - return; - owed = (int)((now_ns - stream->flush_last_ns) / (uint64_t)stream->pdu_period); - if (owed <= 0) + owed = (int)((now_mono - stream->flush_last_ns) / (uint64_t)stream->pdu_period); + if (owed <= 0) { return; + } stream->flush_last_ns += (uint64_t)owed * (uint64_t)stream->pdu_period; pad_ringbuffer_with_silence(stream, owed); - if (server->avb_mode == AVB_MODE_MILAN_V12) - flush_write_milan_v12(stream, now_ns, owed); - else - flush_write_legacy(stream, now_ns); + if (server->avb_mode == AVB_MODE_MILAN_V12) { + flush_write_milan_v12(stream, stamp, owed); + } else { + flush_write_legacy(stream, stamp); + } } static void on_source_stream_process(void *data) @@ -467,7 +475,7 @@ static int flush_write_milan_v12(struct stream *stream, uint64_t current_time, i ptime = txtime + stream->mtt; while (pdu_count--) { - *(uint64_t*)CMSG_DATA(stream->cmsg) = txtime; + /* CBS-exclusive: no SCM_TXTIME; txtime feeds ptime only */ set_iovec(&stream->ring, stream->buffer_data, @@ -515,7 +523,7 @@ static int flush_write_legacy(struct stream *stream, uint64_t current_time) ptime = txtime + stream->mtt; while (pdu_count--) { - *(uint64_t*)CMSG_DATA(stream->cmsg) = txtime; + /* CBS-exclusive: no SCM_TXTIME; txtime feeds ptime only */ set_iovec(&stream->ring, stream->buffer_data, @@ -671,12 +679,11 @@ static int setup_msg(struct stream *stream) stream->msg.msg_namelen = sizeof(stream->sock_addr); stream->msg.msg_iov = stream->iov; stream->msg.msg_iovlen = 3; - stream->msg.msg_control = stream->control; - stream->msg.msg_controllen = sizeof(stream->control); - stream->cmsg = CMSG_FIRSTHDR(&stream->msg); - stream->cmsg->cmsg_level = SOL_SOCKET; - stream->cmsg->cmsg_type = SCM_TXTIME; - stream->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64)); + /* CBS/Qav-exclusive: no SCM_TXTIME control message -- CBS and SO_TXTIME + * cannot coexist; the egress CBS qdisc paces the stream. */ + stream->msg.msg_control = NULL; + stream->msg.msg_controllen = 0; + stream->cmsg = NULL; return 0; } @@ -855,8 +862,14 @@ struct stream *server_create_stream(struct server *server, struct stream *stream common->tastream_attr.attr.talker.vlan_id = htons(stream->vlan_id); if (server->avb_mode == AVB_MODE_MILAN_V12) + /* Milan v1.2 Section 4.3.3.2 Table 4.4: MaxFrameSize is the AVTPDU + * (header + payload) ONLY, plus 1 byte to account for the PAAD + * sampling clock possibly running slightly fast. The Ethernet header + * and FCS are added separately by the bandwidth rule (F = MaxFrameSize + * + 22), so exclude our avb_frame_header (the L2 header) from pdu_size. */ common->tastream_attr.attr.talker.tspec_max_frame_size = - htons((uint16_t)stream->pdu_size); + htons((uint16_t)(stream->pdu_size - + sizeof(struct avb_frame_header) + 1)); else common->tastream_attr.attr.talker.tspec_max_frame_size = htons((uint16_t)(32 + stream->frames_per_pdu * stream->stride)); @@ -1015,9 +1028,9 @@ static void stream_mc_recover(struct stream *stream, const struct avb_packet_aaf #define AVB_STREAM_SEQ_SETTLE 8 /* Milan v1.2 Section 5.4: the listener supports only the Milan base stream - * format for decode — AAF PCM, 32-bit integer, 48 kHz, non-sparse. Channel - * count is a stream parameter (the ring buffers by data_len), not part of the - * format check, so any Milan channel count passes. */ + * format for decode — AAF PCM, 32-bit integer, 48 kHz, non-sparse. The channel + * count is checked separately by handle_aaf_packet against the stream's + * negotiated channel count (a frame from a mismatched talker is rejected). */ static inline bool aaf_is_milan_format(const struct avb_packet_aaf *p) { return p->subtype == AVB_SUBTYPE_AAF && @@ -1039,13 +1052,15 @@ static void handle_aaf_packet(struct stream *stream, filled = spa_ringbuffer_get_write_index(&stream->ring, &index); n_bytes = ntohs(p->data_len); - /* milan-avb: support only the Milan format. EVERY received frame that is - * not a well-formed Milan AAF PDU — bad length, or subtype/format/sample- - * rate/bit-depth/sparse not the Milan base format — bumps UNSUPPORTED_FORMAT - * and is dropped, per frame: not counted as a valid frame, not media-locked, - * not written. (Channel count is a stream parameter, not part of the format - * check, so a valid 4ch talker like the DS20 is not flagged.) */ - if (n_bytes > (uint32_t)(len - (int)sizeof(*p)) || !aaf_is_milan_format(p)) { + /* milan-avb: accept ONLY frames matching this stream's negotiated format. + * EVERY received frame that is not a well-formed Milan AAF PDU — bad length, + * subtype/format/sample-rate/bit-depth/sparse not the Milan base format, or + * whose channel count differs from this stream's negotiated channel count — + * bumps UNSUPPORTED_FORMAT and is dropped, per frame: not counted as a valid + * frame, not media-locked, not written. The channel check rejects frames from + * a different talker/format sharing the VLAN (Milan Section 5.5.1.2). */ + if (n_bytes > (uint32_t)(len - (int)sizeof(*p)) || !aaf_is_milan_format(p) || + p->chan_per_frame != stream->info.info.raw.channels) { cnt->unsupported_format++; stream_in_mark_counters_dirty(stream); return;