Skip to main content

mx_remote/rx/
svd.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Short Video Descriptors and the detailed signal report that names one.
5
6use core::fmt;
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10use crate::event::Event;
11use crate::state::State;
12use crate::types::BaySignalDetails;
13use crate::wire::{BayStatus, BayUid, MxrSignalType};
14
15use super::handlers::{u16_at, u32_at};
16use super::Rx;
17
18/// The CTA-861 short video descriptor table, one line per descriptor.
19const SVD_TABLE: &str = include_str!("../svd.csv");
20
21/// A Short Video Descriptor: one standard video resolution and timing.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct Svd {
24    /// The descriptor's CTA-861 id.
25    pub id: u16,
26    /// Picture aspect ratio code.
27    pub picture_aspect: u16,
28    /// Pixel aspect ratio code.
29    pub pixel_aspect: u16,
30    /// Active pixels per line.
31    pub horizontal_active: u16,
32    /// Total pixels per line, including blanking.
33    pub horizontal_total: u16,
34    /// Active lines per frame.
35    pub vertical_active: u16,
36    /// Total lines per frame, including blanking.
37    pub vertical_total: u16,
38    /// Refresh rate in Hz.
39    pub refresh: u16,
40    /// Whether the format is interlaced.
41    pub interlaced: bool,
42    /// Pixel clock multiplier.
43    pub multiplier: u16,
44}
45
46impl fmt::Display for Svd {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(
49            f,
50            "{}x{}@{}Hz",
51            self.horizontal_active, self.vertical_active, self.refresh
52        )
53    }
54}
55
56fn table() -> &'static HashMap<u16, Svd> {
57    static TABLE: OnceLock<HashMap<u16, Svd>> = OnceLock::new();
58    TABLE.get_or_init(|| {
59        let mut map = HashMap::new();
60        for line in SVD_TABLE.lines() {
61            let fields: Vec<u16> = line
62                .trim()
63                .split(';')
64                .filter_map(|f| f.trim().parse().ok())
65                .collect();
66            let Ok(n) = <[u16; 10]>::try_from(fields.as_slice()) else {
67                continue;
68            };
69            map.insert(
70                n[0],
71                Svd {
72                    id: n[0],
73                    picture_aspect: n[1],
74                    pixel_aspect: n[2],
75                    horizontal_active: n[3],
76                    horizontal_total: n[4],
77                    vertical_active: n[5],
78                    vertical_total: n[6],
79                    refresh: n[7],
80                    interlaced: n[8] == 1,
81                    multiplier: n[9],
82                },
83            );
84        }
85        map
86    })
87}
88
89/// Looks up the Short Video Descriptor with the given id.
90pub fn lookup_svd(id: u16) -> Option<Svd> {
91    table().get(&id).copied()
92}
93
94/// Names the colour space a signal report carries.
95fn colour_space(v: u8) -> &'static str {
96    match v {
97        0 => "RGB",
98        1 => "4:4:4",
99        2 => "4:2:2",
100        3 => "4:2:0",
101        _ => "unknown",
102    }
103}
104
105/// `av_details` wire layout, packed:
106///
107/// ```text
108/// 0..8     header
109/// 8..24    AVI infoframe
110/// 24..40   audio
111/// 40..56   video
112/// 56..88   vsync
113/// 88..100  HDMI link errors
114/// 100..112 bay
115/// ```
116const AV_DETAILS_SIZE: usize = 112;
117
118/// Bits of the stream-flags byte.
119const STREAM_INTERLACED: u8 = 1 << 1;
120const STREAM_NON_INTEGER_CLOCK: u8 = 1 << 3;
121const STREAM_HDR: u8 = 1 << 4;
122
123/// The support-flags bit that says the stream block holds a real signal.
124const SUPPORT_STREAM_VALID: u8 = 1 << 1;
125
126/// Decodes a detailed AV signal report.
127///
128/// A report is answered one packet per bay: the port number in the bay block
129/// at the tail is what names the reporting bay, so demultiplex on it. Because
130/// that block sits behind the vsync and link-error tail, a report shorter than
131/// the full struct cannot be attributed to a bay at all and is dropped, as the
132/// firmware does.
133///
134/// An empty payload is a broadcast request for every device to report, and a
135/// 16-byte payload requests a report from the one unit it addresses.
136pub(super) fn signal_status(state: &mut State, rx: &Rx<'_>, ev: &mut Vec<Event>) {
137    let p = rx.frame.payload();
138    if p.len() < AV_DETAILS_SIZE {
139        return;
140    }
141    let support_flags = p[2];
142    let stream_flags = p[3];
143    let stream_valid = support_flags & SUPPORT_STREAM_VALID != 0;
144
145    let bay_block = &p[100..112];
146    let port = u16_at(bay_block, 0);
147    let bay = BayUid::new(rx.sender(), port);
148    if state.bay(bay).is_none() {
149        return;
150    }
151
152    let video = &p[40..56];
153    let svd_id = u16::from(video[0]);
154    let mut frame_rate = f64::from(u16_at(video, 8));
155    if stream_flags & STREAM_NON_INTEGER_CLOCK != 0 {
156        frame_rate = (frame_rate * 1000.0 / 1001.0 * 100.0).round() / 100.0;
157    }
158
159    let signal_type = match lookup_svd(svd_id) {
160        Some(svd) if stream_valid && svd_id != 0 => {
161            let mut description = format!(
162                "{}x{} / {} / {}bpp",
163                svd.horizontal_active,
164                svd.vertical_active,
165                colour_space(video[1]),
166                video[2]
167            );
168            if stream_flags & STREAM_INTERLACED != 0 {
169                description.push_str(" interlaced");
170            }
171            if stream_flags & STREAM_HDR != 0 {
172                description.push_str(" HDR");
173            }
174            description.push_str(&format!(" / {frame_rate}Hz"));
175            description
176        }
177        _ => "No Signal".to_owned(),
178    };
179
180    let details = BaySignalDetails {
181        frame_rate,
182        tmds_clock: u32_at(video, 10),
183        status: BayStatus::from_bits(u32_at(bay_block, 2)),
184        scaling: MxrSignalType::from_wire(u16_at(bay_block, 6)),
185        clock_rate: u32_at(bay_block, 8),
186    };
187
188    if let Some(bay) = state.bay_mut(bay) {
189        bay.set_signal_details(details);
190        bay.apply_signal_status(stream_valid, Some(signal_type), ev);
191    }
192}