mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
//! CSI/HDMI bridge helpers: subdev discovery, DV probe, RK628 "fake VGA" filter (must run before `S_FMT` / `STREAMON` on capture — see RK628 driver).
|
//! CSI/HDMI bridge helpers: subdev discovery, DV probe, RK628 "fake VGA" filter (must run before `S_FMT` / `STREAMON` on capture — see RK628 driver).
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -97,43 +98,391 @@ impl std::fmt::Debug for DvTimingsMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Heuristic: scan `/sys/class/video4linux/v4l-subdev*` names for rk628 / hdmirx / tc358743.
|
const SYSFS_VIDEO4LINUX: &str = "/sys/class/video4linux";
|
||||||
pub fn discover_subdev_for_video(video_path: &Path) -> Option<(PathBuf, CsiBridgeKind)> {
|
const DEV_ROOT: &str = "/dev";
|
||||||
let sysfs_base = Path::new("/sys/class/video4linux");
|
const MEDIA_ENT_ID_FLAG_NEXT: u32 = 1 << 31;
|
||||||
let entries = std::fs::read_dir(sysfs_base).ok()?;
|
const MEDIA_LNK_FL_ENABLED: u32 = 1 << 0;
|
||||||
|
const MEDIA_LNK_FL_LINK_TYPE: u32 = 0xf << 28;
|
||||||
|
const MEDIA_LNK_FL_DATA_LINK: u32 = 0 << 28;
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
#[repr(C)]
|
||||||
let name = entry.file_name();
|
#[derive(Clone, Copy)]
|
||||||
let name_str = name.to_string_lossy();
|
struct MediaEntityDesc {
|
||||||
if !name_str.starts_with("v4l-subdev") {
|
id: u32,
|
||||||
continue;
|
name: [u8; 32],
|
||||||
|
type_: u32,
|
||||||
|
revision: u32,
|
||||||
|
flags: u32,
|
||||||
|
group_id: u32,
|
||||||
|
pads: u16,
|
||||||
|
links: u16,
|
||||||
|
reserved: [u32; 4],
|
||||||
|
info: MediaEntityInfo,
|
||||||
}
|
}
|
||||||
let Some(kind) = read_sysfs_name(&entry.path())
|
|
||||||
.as_deref()
|
#[repr(C)]
|
||||||
.and_then(CsiBridgeKind::from_subdev_name)
|
#[derive(Clone, Copy)]
|
||||||
else {
|
struct MediaDeviceNode {
|
||||||
continue;
|
major: u32,
|
||||||
};
|
minor: u32,
|
||||||
let dev_path = PathBuf::from("/dev").join(&*name_str);
|
}
|
||||||
if dev_path.exists() {
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
union MediaEntityInfo {
|
||||||
|
dev: MediaDeviceNode,
|
||||||
|
_raw: [u8; 184],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaEntityDesc {
|
||||||
|
fn default() -> Self {
|
||||||
|
// This mirrors the zero-initialization required by the media UAPI.
|
||||||
|
unsafe { std::mem::zeroed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct MediaPadDesc {
|
||||||
|
entity: u32,
|
||||||
|
index: u16,
|
||||||
|
flags: u32,
|
||||||
|
reserved: [u32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct MediaLinkDesc {
|
||||||
|
source: MediaPadDesc,
|
||||||
|
sink: MediaPadDesc,
|
||||||
|
flags: u32,
|
||||||
|
reserved: [u32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct MediaLinksEnum {
|
||||||
|
entity: u32,
|
||||||
|
pads: *mut MediaPadDesc,
|
||||||
|
links: *mut MediaLinkDesc,
|
||||||
|
reserved: [u32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
nix::ioctl_readwrite!(media_ioc_enum_entities, b'|', 0x01, MediaEntityDesc);
|
||||||
|
nix::ioctl_readwrite!(media_ioc_enum_links, b'|', 0x02, MediaLinksEnum);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct MediaGraphEntity {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
major: u32,
|
||||||
|
minor: u32,
|
||||||
|
pads: u16,
|
||||||
|
links: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct MediaGraphLink {
|
||||||
|
source: u32,
|
||||||
|
sink: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MediaGraph {
|
||||||
|
path: PathBuf,
|
||||||
|
entities: Vec<MediaGraphEntity>,
|
||||||
|
links: Vec<MediaGraphLink>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the CSI/HDMI bridge that is connected to `video_path` in the same
|
||||||
|
/// media-controller graph. Name-only global scans are deliberately avoided:
|
||||||
|
/// boards can expose RK628, native HDMI RX and USB capture at the same time.
|
||||||
|
pub fn discover_subdev_for_video(video_path: &Path) -> Option<(PathBuf, CsiBridgeKind)> {
|
||||||
|
match discover_subdev_for_video_inner(video_path) {
|
||||||
|
Ok(Some((path, kind, media_path))) => {
|
||||||
info!(
|
info!(
|
||||||
"Discovered CSI bridge subdev for {:?}: {:?} ({:?})",
|
"Discovered CSI bridge subdev for {:?}: {:?} ({:?}) via {:?}",
|
||||||
video_path, dev_path, kind
|
video_path, path, kind, media_path
|
||||||
);
|
);
|
||||||
return Some((dev_path, kind));
|
Some((path, kind))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Ok(None) => {
|
||||||
debug!(
|
debug!(
|
||||||
"No CSI bridge subdev found in /sys/class/video4linux for {:?}",
|
"No connected CSI bridge subdev found in media topology for {:?}",
|
||||||
video_path
|
video_path
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
Err(error) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to inspect media topology for {:?}: {}",
|
||||||
|
video_path, error
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_sysfs_name(subdev_sysfs: &Path) -> Option<String> {
|
fn discover_subdev_for_video_inner(
|
||||||
std::fs::read_to_string(subdev_sysfs.join("name"))
|
video_path: &Path,
|
||||||
.ok()
|
) -> io::Result<Option<(PathBuf, CsiBridgeKind, PathBuf)>> {
|
||||||
.map(|s| s.trim().to_string())
|
let video_device = device_numbers(video_path)?;
|
||||||
|
let mut graphs = Vec::new();
|
||||||
|
let mut first_error = None;
|
||||||
|
|
||||||
|
// A physical device may expose more than one media controller. Inspect
|
||||||
|
// every controller and use the graph that actually contains video_path;
|
||||||
|
// choosing the first mediaN node merely moves the old global-scan bug.
|
||||||
|
for media_path in media_device_paths()? {
|
||||||
|
let graph = File::open(&media_path).and_then(|media| {
|
||||||
|
read_media_graph(&media).map(|(entities, links)| MediaGraph {
|
||||||
|
path: media_path.clone(),
|
||||||
|
entities,
|
||||||
|
links,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
match graph {
|
||||||
|
Ok(graph) => graphs.push(graph),
|
||||||
|
Err(error) => {
|
||||||
|
debug!(
|
||||||
|
"Failed to inspect media controller {:?}: {}",
|
||||||
|
media_path, error
|
||||||
|
);
|
||||||
|
first_error.get_or_insert(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let graph_contains_video = graphs.iter().any(|graph| {
|
||||||
|
graph
|
||||||
|
.entities
|
||||||
|
.iter()
|
||||||
|
.any(|entity| (entity.major, entity.minor) == video_device)
|
||||||
|
});
|
||||||
|
let Some((media_path, entity, kind)) = connected_bridge_in_media_graphs(&graphs, video_device)
|
||||||
|
else {
|
||||||
|
if !graph_contains_video {
|
||||||
|
if let Some(error) = first_error {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(subdev_path) = video4linux_devnode((entity.major, entity.minor))? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !subdev_path
|
||||||
|
.file_name()
|
||||||
|
.is_some_and(|name| name.to_string_lossy().starts_with("v4l-subdev"))
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some((subdev_path, kind, media_path.to_path_buf())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_numbers(path: &Path) -> io::Result<(u32, u32)> {
|
||||||
|
let rdev = std::fs::metadata(path)?.rdev();
|
||||||
|
let major = nix::sys::stat::major(rdev);
|
||||||
|
let minor = nix::sys::stat::minor(rdev);
|
||||||
|
Ok((major as u32, minor as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_device_numbers(value: &str) -> Option<(u32, u32)> {
|
||||||
|
let (major, minor) = value.trim().split_once(':')?;
|
||||||
|
Some((major.parse().ok()?, minor.parse().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn video4linux_class_entry(device: (u32, u32)) -> io::Result<Option<PathBuf>> {
|
||||||
|
for entry in std::fs::read_dir(SYSFS_VIDEO4LINUX)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let dev = match std::fs::read_to_string(entry.path().join("dev")) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if parse_device_numbers(&dev) == Some(device) {
|
||||||
|
return Ok(Some(entry.path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn video4linux_devnode(device: (u32, u32)) -> io::Result<Option<PathBuf>> {
|
||||||
|
let Some(class_entry) = video4linux_class_entry(device)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(name) = class_entry.file_name() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let path = Path::new(DEV_ROOT).join(name);
|
||||||
|
Ok(path.exists().then_some(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn media_device_paths() -> io::Result<Vec<PathBuf>> {
|
||||||
|
let mut media_nodes = std::fs::read_dir(DEV_ROOT)?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_str()?;
|
||||||
|
let index = media_device_index(name)?;
|
||||||
|
Some((index, entry.path()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
media_nodes.sort_by(|(left_index, left_path), (right_index, right_path)| {
|
||||||
|
left_index
|
||||||
|
.cmp(right_index)
|
||||||
|
.then_with(|| left_path.cmp(right_path))
|
||||||
|
});
|
||||||
|
Ok(media_nodes.into_iter().map(|(_, path)| path).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn media_device_index(name: &str) -> Option<u32> {
|
||||||
|
let suffix = name.strip_prefix("media")?;
|
||||||
|
if suffix.is_empty() || !suffix.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
suffix.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_media_graph(media: &File) -> io::Result<(Vec<MediaGraphEntity>, Vec<MediaGraphLink>)> {
|
||||||
|
let mut entities = Vec::new();
|
||||||
|
let mut previous_id = 0u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut desc = MediaEntityDesc {
|
||||||
|
id: previous_id | MEDIA_ENT_ID_FLAG_NEXT,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// SAFETY: `desc` has the exact media_entity_desc UAPI layout and is
|
||||||
|
// writable for the duration of the ioctl.
|
||||||
|
match unsafe { media_ioc_enum_entities(media.as_raw_fd(), &mut desc) } {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(Errno::EINVAL) => break,
|
||||||
|
Err(error) => return Err(io::Error::from_raw_os_error(error as i32)),
|
||||||
|
}
|
||||||
|
if desc.id == previous_id || entities.len() >= 4096 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"media entity enumeration did not advance",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
previous_id = desc.id;
|
||||||
|
|
||||||
|
let nul = desc
|
||||||
|
.name
|
||||||
|
.iter()
|
||||||
|
.position(|byte| *byte == 0)
|
||||||
|
.unwrap_or(desc.name.len());
|
||||||
|
let name = String::from_utf8_lossy(&desc.name[..nul]).into_owned();
|
||||||
|
// SAFETY: the kernel filled the `dev` member of the media UAPI union
|
||||||
|
// for V4L2 devnode entities. Non-devnode entities report zeroes.
|
||||||
|
let device = unsafe { desc.info.dev };
|
||||||
|
entities.push(MediaGraphEntity {
|
||||||
|
id: desc.id,
|
||||||
|
name,
|
||||||
|
major: device.major,
|
||||||
|
minor: device.minor,
|
||||||
|
pads: desc.pads,
|
||||||
|
links: desc.links,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut graph_links = HashSet::new();
|
||||||
|
for entity in &entities {
|
||||||
|
let mut pads = vec![MediaPadDesc::default(); entity.pads as usize];
|
||||||
|
let mut links = vec![MediaLinkDesc::default(); entity.links as usize];
|
||||||
|
let mut request = MediaLinksEnum {
|
||||||
|
entity: entity.id,
|
||||||
|
pads: if pads.is_empty() {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
} else {
|
||||||
|
pads.as_mut_ptr()
|
||||||
|
},
|
||||||
|
links: if links.is_empty() {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
} else {
|
||||||
|
links.as_mut_ptr()
|
||||||
|
},
|
||||||
|
reserved: [0; 4],
|
||||||
|
};
|
||||||
|
// SAFETY: the vectors provide the number of entries reported by the
|
||||||
|
// entity descriptor and remain alive while the kernel fills them.
|
||||||
|
unsafe { media_ioc_enum_links(media.as_raw_fd(), &mut request) }
|
||||||
|
.map_err(|error| io::Error::from_raw_os_error(error as i32))?;
|
||||||
|
|
||||||
|
for link in links {
|
||||||
|
if link.flags & MEDIA_LNK_FL_ENABLED == 0
|
||||||
|
|| link.flags & MEDIA_LNK_FL_LINK_TYPE != MEDIA_LNK_FL_DATA_LINK
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
graph_links.insert((link.source.entity, link.sink.entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut links = graph_links
|
||||||
|
.into_iter()
|
||||||
|
.map(|(source, sink)| MediaGraphLink { source, sink })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
links.sort_by_key(|link| (link.source, link.sink));
|
||||||
|
Ok((entities, links))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connected_bridge_entity<'a>(
|
||||||
|
entities: &'a [MediaGraphEntity],
|
||||||
|
links: &[MediaGraphLink],
|
||||||
|
video_device: (u32, u32),
|
||||||
|
) -> Option<(&'a MediaGraphEntity, CsiBridgeKind)> {
|
||||||
|
let start = entities
|
||||||
|
.iter()
|
||||||
|
.find(|entity| (entity.major, entity.minor) == video_device)?;
|
||||||
|
let by_id = entities
|
||||||
|
.iter()
|
||||||
|
.map(|entity| (entity.id, entity))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
let mut upstream = HashMap::<u32, Vec<u32>>::new();
|
||||||
|
for link in links {
|
||||||
|
// Media data flows source -> sink. Starting at a capture video node,
|
||||||
|
// only sink -> source traversal can lead to its real input bridge.
|
||||||
|
upstream.entry(link.sink).or_default().push(link.source);
|
||||||
|
}
|
||||||
|
for neighbors in upstream.values_mut() {
|
||||||
|
neighbors.sort_unstable();
|
||||||
|
neighbors.dedup();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visited = HashSet::from([start.id]);
|
||||||
|
let mut queue = VecDeque::from([start.id]);
|
||||||
|
while let Some(id) = queue.pop_front() {
|
||||||
|
if id != start.id {
|
||||||
|
if let Some(entity) = by_id.get(&id) {
|
||||||
|
if entity.major != 0 || entity.minor != 0 {
|
||||||
|
if let Some(kind) = CsiBridgeKind::from_subdev_name(&entity.name) {
|
||||||
|
return Some((entity, kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(neighbors) = upstream.get(&id) {
|
||||||
|
for neighbor in neighbors {
|
||||||
|
if visited.insert(*neighbor) {
|
||||||
|
queue.push_back(*neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connected_bridge_in_media_graphs(
|
||||||
|
graphs: &[MediaGraph],
|
||||||
|
video_device: (u32, u32),
|
||||||
|
) -> Option<(&Path, &MediaGraphEntity, CsiBridgeKind)> {
|
||||||
|
graphs.iter().find_map(|graph| {
|
||||||
|
connected_bridge_entity(&graph.entities, &graph.links, video_device)
|
||||||
|
.map(|(entity, kind)| (graph.path.as_path(), entity, kind))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_subdev(path: &Path) -> io::Result<File> {
|
pub fn open_subdev(path: &Path) -> io::Result<File> {
|
||||||
@@ -339,6 +688,53 @@ pub fn wait_source_change(subdev_fd: &File, timeout: Duration) -> io::Result<boo
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn media_graph(
|
||||||
|
path: &str,
|
||||||
|
entities: Vec<MediaGraphEntity>,
|
||||||
|
links: Vec<MediaGraphLink>,
|
||||||
|
) -> MediaGraph {
|
||||||
|
MediaGraph {
|
||||||
|
path: PathBuf::from(path),
|
||||||
|
entities,
|
||||||
|
links,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_entity(id: u32, name: &str, device: (u32, u32)) -> MediaGraphEntity {
|
||||||
|
MediaGraphEntity {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
major: device.0,
|
||||||
|
minor: device.1,
|
||||||
|
pads: 0,
|
||||||
|
links: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_link(source: u32, sink: u32) -> MediaGraphLink {
|
||||||
|
MediaGraphLink { source, sink }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_uapi_layout_matches_linux_legacy_api() {
|
||||||
|
assert_eq!(std::mem::size_of::<MediaEntityDesc>(), 256);
|
||||||
|
assert_eq!(std::mem::size_of::<MediaPadDesc>(), 20);
|
||||||
|
assert_eq!(std::mem::size_of::<MediaLinkDesc>(), 52);
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
assert_eq!(std::mem::size_of::<MediaLinksEnum>(), 40);
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
assert_eq!(std::mem::size_of::<MediaLinksEnum>(), 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_device_names_require_a_numeric_suffix() {
|
||||||
|
assert_eq!(media_device_index("media0"), Some(0));
|
||||||
|
assert_eq!(media_device_index("media12"), Some(12));
|
||||||
|
assert_eq!(media_device_index("media"), None);
|
||||||
|
assert_eq!(media_device_index("media-controller"), None);
|
||||||
|
assert_eq!(media_device_index("video0"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn subdevice_handles_are_non_blocking() {
|
fn subdevice_handles_are_non_blocking() {
|
||||||
let file = tempfile::NamedTempFile::new().unwrap();
|
let file = tempfile::NamedTempFile::new().unwrap();
|
||||||
@@ -348,6 +744,88 @@ mod tests {
|
|||||||
assert_ne!(flags & libc::O_NONBLOCK, 0);
|
assert_ne!(flags & libc::O_NONBLOCK, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_graph_finds_only_the_connected_rk628() {
|
||||||
|
// Captured shape of the RK3588 RKCIF graph:
|
||||||
|
// video0 <- mipi-csi2 <- dphy <- RK628. A second RK628 entity is
|
||||||
|
// present in the same synthetic topology but is not connected.
|
||||||
|
let entities = vec![
|
||||||
|
graph_entity(1, "stream_cif_mipi_id0", (81, 0)),
|
||||||
|
graph_entity(45, "rockchip-mipi-csi2", (0, 0)),
|
||||||
|
graph_entity(58, "rockchip-csi2-dphy0", (0, 0)),
|
||||||
|
graph_entity(63, "m00_b_rk628-csi 3-0050", (81, 16)),
|
||||||
|
graph_entity(90, "other-rk628-csi 7-0050", (81, 19)),
|
||||||
|
];
|
||||||
|
let links = vec![graph_link(63, 58), graph_link(58, 45), graph_link(45, 1)];
|
||||||
|
|
||||||
|
let (entity, kind) = connected_bridge_entity(&entities, &links, (81, 0)).unwrap();
|
||||||
|
assert_eq!(entity.id, 63);
|
||||||
|
assert_eq!(kind, CsiBridgeKind::Rk628);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_graph_search_only_walks_towards_link_sources() {
|
||||||
|
let entities = vec![
|
||||||
|
graph_entity(1, "stream_cif_mipi_id0", (81, 0)),
|
||||||
|
graph_entity(20, "rockchip-mipi-csi2", (0, 0)),
|
||||||
|
graph_entity(63, "unrelated-rk628-csi 7-0050", (81, 19)),
|
||||||
|
];
|
||||||
|
// Entity 20 is upstream of the capture node. Entity 63 is downstream
|
||||||
|
// of 20 and must not be reached while tracing the capture input.
|
||||||
|
let links = vec![graph_link(20, 1), graph_link(20, 63)];
|
||||||
|
|
||||||
|
assert!(connected_bridge_entity(&entities, &links, (81, 0)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_graphs_select_the_controller_containing_the_video_node() {
|
||||||
|
let graphs = vec![
|
||||||
|
media_graph(
|
||||||
|
"/dev/media0",
|
||||||
|
vec![
|
||||||
|
graph_entity(1, "other-video", (81, 4)),
|
||||||
|
graph_entity(63, "wrong-rk628-csi", (81, 16)),
|
||||||
|
],
|
||||||
|
vec![graph_link(63, 1)],
|
||||||
|
),
|
||||||
|
media_graph(
|
||||||
|
"/dev/media1",
|
||||||
|
vec![
|
||||||
|
graph_entity(10, "stream_cif_mipi_id0", (81, 0)),
|
||||||
|
graph_entity(75, "tc358743 2-000f", (81, 20)),
|
||||||
|
],
|
||||||
|
vec![graph_link(75, 10)],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let (path, entity, kind) = connected_bridge_in_media_graphs(&graphs, (81, 0)).unwrap();
|
||||||
|
assert_eq!(path, Path::new("/dev/media1"));
|
||||||
|
assert_eq!(entity.id, 75);
|
||||||
|
assert_eq!(kind, CsiBridgeKind::Tc358743);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_graph_does_not_attach_an_unrelated_rk628_to_native_hdmirx() {
|
||||||
|
let entities = vec![
|
||||||
|
graph_entity(1, "rk_hdmirx", (81, 11)),
|
||||||
|
graph_entity(63, "m00_b_rk628-csi 3-0050", (81, 16)),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(connected_bridge_entity(&entities, &[], (81, 11)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_graph_leaves_usb_capture_without_a_csi_bridge() {
|
||||||
|
let entities = vec![
|
||||||
|
graph_entity(1, "USB Video: USB Video", (81, 12)),
|
||||||
|
graph_entity(8, "Processing 2", (0, 0)),
|
||||||
|
graph_entity(11, "Input 1", (0, 0)),
|
||||||
|
];
|
||||||
|
let links = vec![graph_link(11, 8), graph_link(8, 1)];
|
||||||
|
|
||||||
|
assert!(connected_bridge_entity(&entities, &links, (81, 12)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rk628_fingerprint_matches_vga() {
|
fn rk628_fingerprint_matches_vga() {
|
||||||
let mut bt: v4l2_bt_timings = unsafe { std::mem::zeroed() };
|
let mut bt: v4l2_bt_timings = unsafe { std::mem::zeroed() };
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ pub fn resolve_video_input_config(
|
|||||||
requested_resolution: Resolution,
|
requested_resolution: Resolution,
|
||||||
requested_fps: u32,
|
requested_fps: u32,
|
||||||
) -> ResolvedVideoInputConfig {
|
) -> ResolvedVideoInputConfig {
|
||||||
|
let mut resolved = ResolvedVideoInputConfig {
|
||||||
|
format: requested_format,
|
||||||
|
resolution: requested_resolution,
|
||||||
|
fps: requested_fps,
|
||||||
|
};
|
||||||
|
|
||||||
if device.control_mode == VideoControlMode::SourceFollowing {
|
if device.control_mode == VideoControlMode::SourceFollowing {
|
||||||
if let VideoInputStatus {
|
if let VideoInputStatus {
|
||||||
state: VideoInputState::Locked,
|
state: VideoInputState::Locked,
|
||||||
@@ -135,26 +141,39 @@ pub fn resolve_video_input_config(
|
|||||||
} = &device.input_status
|
} = &device.input_status
|
||||||
{
|
{
|
||||||
if let Ok(format) = format.parse::<PixelFormat>() {
|
if let Ok(format) = format.parse::<PixelFormat>() {
|
||||||
return ResolvedVideoInputConfig {
|
resolved = ResolvedVideoInputConfig {
|
||||||
format,
|
format,
|
||||||
resolution: Resolution::new(*width, *height),
|
resolution: Resolution::new(*width, *height),
|
||||||
fps: fps.round().clamp(1.0, 120.0) as u32,
|
fps: fps.round().clamp(1.0, 120.0) as u32,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source-following devices do not allow One-KVM to choose the HDMI
|
||||||
|
// resolution or frame rate, but their pixel format still has to be one
|
||||||
|
// of the formats enumerated by the capture node. In particular, rkcif
|
||||||
|
// commonly exposes NV12 but One-KVM's default is MJPEG. Passing that
|
||||||
|
// unsupported default to S_FMT leaves the pipeline in an invalid state.
|
||||||
|
if !device.formats.is_empty()
|
||||||
|
&& !device
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.any(|format| format.format == resolved.format)
|
||||||
|
{
|
||||||
|
resolved.format = device.formats[0].format;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedVideoInputConfig {
|
resolved
|
||||||
format: requested_format,
|
|
||||||
resolution: requested_resolution,
|
|
||||||
fps: requested_fps,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use super::linux::FormatInfo;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn device(control_mode: VideoControlMode, input_status: VideoInputStatus) -> VideoDeviceInfo {
|
fn device(control_mode: VideoControlMode, input_status: VideoInputStatus) -> VideoDeviceInfo {
|
||||||
VideoDeviceInfo {
|
VideoDeviceInfo {
|
||||||
@@ -175,6 +194,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn format(format: PixelFormat) -> FormatInfo {
|
||||||
|
FormatInfo {
|
||||||
|
format,
|
||||||
|
resolutions: Vec::new(),
|
||||||
|
description: format.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recognizes_vendor_and_upstream_native_hdmirx_names() {
|
fn recognizes_vendor_and_upstream_native_hdmirx_names() {
|
||||||
assert!(is_rk_hdmirx_driver("rk_hdmirx", "rk_hdmirx"));
|
assert!(is_rk_hdmirx_driver("rk_hdmirx", "rk_hdmirx"));
|
||||||
@@ -246,6 +274,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn source_following_replaces_unenumerated_default_format_without_signal() {
|
||||||
|
let mut device = device(
|
||||||
|
VideoControlMode::SourceFollowing,
|
||||||
|
VideoInputStatus::no_signal(),
|
||||||
|
);
|
||||||
|
device.formats = vec![format(PixelFormat::Nv12), format(PixelFormat::Yuyv)];
|
||||||
|
|
||||||
|
let resolved = resolve_video_input_config(
|
||||||
|
&device,
|
||||||
|
PixelFormat::Mjpeg,
|
||||||
|
Resolution::new(1920, 1080),
|
||||||
|
30,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(resolved.format, PixelFormat::Nv12);
|
||||||
|
assert_eq!(resolved.resolution, Resolution::new(1920, 1080));
|
||||||
|
assert_eq!(resolved.fps, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn source_following_replaces_stale_active_format_but_keeps_input_mode() {
|
||||||
|
let mut device = device(
|
||||||
|
VideoControlMode::SourceFollowing,
|
||||||
|
VideoInputStatus::locked(PixelFormat::Mjpeg, 1280, 720, 59.94),
|
||||||
|
);
|
||||||
|
device.formats = vec![format(PixelFormat::Nv12), format(PixelFormat::Yuyv)];
|
||||||
|
|
||||||
|
let resolved = resolve_video_input_config(
|
||||||
|
&device,
|
||||||
|
PixelFormat::Mjpeg,
|
||||||
|
Resolution::new(1920, 1080),
|
||||||
|
30,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(resolved.format, PixelFormat::Nv12);
|
||||||
|
assert_eq!(resolved.resolution, Resolution::new(1280, 720));
|
||||||
|
assert_eq!(resolved.fps, 60);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_signal_and_unavailable_never_expose_stale_mode_fields() {
|
fn no_signal_and_unavailable_never_expose_stale_mode_fields() {
|
||||||
for status in [
|
for status in [
|
||||||
|
|||||||
Reference in New Issue
Block a user