fix: 完善 RK3588 HDMI RX 兼容性

This commit is contained in:
mofeng-git
2026-07-26 22:27:47 +08:00
parent 5963dfa01a
commit 27c8da9a75
4 changed files with 442 additions and 89 deletions

View File

@@ -4,7 +4,7 @@ use std::fs::File;
use std::io;
use std::os::fd::AsFd;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::time::{Duration, Instant};
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
use tracing::{debug, info, warn};
@@ -70,6 +70,8 @@ pub struct CaptureStream {
mappings: Vec<Vec<PlaneMapping>>,
subdev_fd: Option<File>,
bridge_kind: Option<CsiBridgeKind>,
native_hdmirx_state: Option<NativeHdmirxState>,
native_hdmirx_next_state_check: Option<Instant>,
}
impl CaptureStream {
@@ -154,6 +156,7 @@ impl CaptureStream {
let caps_flags = caps.device_caps();
let driver_name = caps.driver.to_string();
let is_csi_bridge = is_csi_bridge_driver(&driver_name);
let is_native_hdmirx = is_native_hdmirx_driver(&driver_name);
// Prefer multi-planar capture when available, as it is required for some
// devices/pixel formats (e.g. NV12 via VIDEO_CAPTURE_MPLANE).
@@ -176,9 +179,14 @@ impl CaptureStream {
width: mode.width,
height: mode.height,
fps: mode.fps,
signature: None,
})
} else if is_csi_bridge {
Some(probe_and_apply_dv_timings(&fd)?)
// The native RK3588 HDMI RX driver already latches detected
// timings while locking the input. S_DV_TIMINGS is unnecessary
// there and rejects some otherwise valid sources whose measured
// porches do not exactly match its CEA table.
Some(probe_dv_timings(&fd, !is_native_hdmirx)?)
} else {
None
};
@@ -208,19 +216,51 @@ impl CaptureStream {
// Prefer the DV-timings-reported geometry for CSI bridges — the
// source, not the user config, dictates what the capture hardware
// will actually deliver.
let (target_w, target_h) = match dv_mode {
Some(DvTimingsMode { width, height, .. }) => (width, height),
let (target_w, target_h) = match dv_mode.as_ref() {
Some(DvTimingsMode { width, height, .. }) => (*width, *height),
None => (resolution.width, resolution.height),
};
fmt.width = target_w;
fmt.height = target_h;
fmt.pixelformat = V4l2rPixelFormat::from(&format.to_fourcc());
let requested_fourcc = V4l2rPixelFormat::from(&format.to_fourcc());
if is_native_hdmirx && fmt.pixelformat != requested_fourcc {
// rk_hdmirx exposes all possible HDMI input encodings through
// ENUM_FMT but can capture only the encoding currently present on
// the wire. Follow G_FMT so a source-side RGB/YUV transition can
// recover even if the saved configuration still names the old
// FourCC. The negotiated format is returned to the caller, which
// rebuilds the encoder when it changed.
info!(
"rk_hdmirx input format changed/requested {:?}, following active {:?}",
requested_fourcc, fmt.pixelformat
);
} else {
fmt.pixelformat = requested_fourcc;
}
let actual_fmt: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt))
.map_err(|e| AppError::VideoError(format!("Failed to set device format: {}", e)))?;
let actual_resolution = Resolution::new(actual_fmt.width, actual_fmt.height);
let actual_format = PixelFormat::from_v4l2r(actual_fmt.pixelformat).unwrap_or(format);
let actual_format = match PixelFormat::from_v4l2r(actual_fmt.pixelformat) {
Some(format) => format,
None if is_native_hdmirx => {
return Err(AppError::VideoError(format!(
"Native HDMI RX input format {:?} is not supported; configure the HDMI source for 8-bit RGB/YUV output",
actual_fmt.pixelformat
)));
}
None => format,
};
let native_hdmirx_state = is_native_hdmirx.then(|| NativeHdmirxState {
width: actual_fmt.width,
height: actual_fmt.height,
pixelformat: actual_fmt.pixelformat,
timings: dv_mode.as_ref().and_then(|mode| mode.signature),
});
let native_hdmirx_next_state_check =
native_hdmirx_state.map(|_| Instant::now() + Duration::from_secs(1));
let stride = actual_fmt
.plane_fmt
@@ -288,6 +328,8 @@ impl CaptureStream {
mappings,
subdev_fd: subdev_fd_opt,
bridge_kind: bridge.kind,
native_hdmirx_state,
native_hdmirx_next_state_check,
};
stream.queue_all_buffers()?;
@@ -376,6 +418,24 @@ impl CaptureStream {
pub fn next_into(&mut self, dst: &mut Vec<u8>) -> io::Result<CaptureMeta> {
self.wait_ready()?;
// Several vendor BSPs update G_FMT/DV timings without making the
// subscribed video fd poll as POLLPRI. Check once per second so a
// genuine source mode change cannot leave us dequeuing buffers with
// stale geometry forever. Transient ioctl failures are ignored here;
// the capture timeout/error path remains responsible for recovery.
if self
.native_hdmirx_next_state_check
.is_some_and(|next| Instant::now() >= next)
{
self.native_hdmirx_next_state_check = Some(Instant::now() + Duration::from_secs(1));
if self.native_hdmirx_state_changed() {
info!(
"Native HDMI RX active format/timings changed without a usable event; requesting stream re-open"
);
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
}
}
let dqbuf: V4l2Buffer = ioctl::dqbuf(&self.fd, self.queue, MemoryType::Mmap)
.map_err(|e| io::Error::other(format!("dqbuf failed: {}", e)))?;
let index = dqbuf.as_v4l2_buffer().index as usize;
@@ -441,6 +501,8 @@ impl CaptureStream {
if self.timeout.is_zero() {
return Ok(());
}
let deadline = Instant::now() + self.timeout;
loop {
// Multiplex video fd (POLLIN for DQBUF, POLLPRI as fallback for
// drivers that deliver events here) and the optional subdev fd
// (POLLPRI only — SOURCE_CHANGE on RK628 / rkcif).
@@ -452,16 +514,25 @@ impl CaptureStream {
if let Some(subdev_fd) = self.subdev_fd.as_ref() {
poll_fds.push(PollFd::new(subdev_fd.as_fd(), PollFlags::POLLPRI));
}
let timeout_ms = self.timeout.as_millis().min(u16::MAX as u128) as u16;
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(io::Error::new(io::ErrorKind::TimedOut, "capture timeout"));
}
// `nix::poll` accepts a u16 millisecond timeout. Round sub-ms
// durations up, and preserve the original deadline if a very long
// timeout needs more than one poll call.
let timeout_ms = remaining.as_millis().clamp(1, u16::MAX as u128) as u16;
let ready = poll(&mut poll_fds, PollTimeout::from(timeout_ms))?;
if ready == 0 {
if Instant::now() >= deadline {
return Err(io::Error::new(io::ErrorKind::TimedOut, "capture timeout"));
}
continue;
}
// Subdev POLLPRI fires first on rkcif/RK628 when the source-side
// HDMI timings changed. Drain all pending events and bubble up
// the `source_changed` marker so the upper layer re-opens with a
// fresh DV_TIMINGS probe.
// HDMI timings changed. Native HDMI RX uses the video node and
// is validated separately below.
if let Some(subdev_fd) = self.subdev_fd.as_ref() {
if let Some(revents) = poll_fds.get(1).and_then(|f| f.revents()) {
if revents.contains(PollFlags::POLLPRI) {
@@ -486,6 +557,16 @@ impl CaptureStream {
}
if revents.contains(PollFlags::POLLPRI) {
let drained = drain_events(&self.fd);
if self.native_hdmirx_state_unchanged() {
debug!(
"Ignoring {} spurious native HDMI RX SOURCE_CHANGE event(s): active format/timings are unchanged",
drained
);
if revents.contains(PollFlags::POLLIN) {
return Ok(());
}
continue;
}
info!(
"Video-node SOURCE_CHANGE detected (drained {} event(s)), \
requesting stream re-open",
@@ -510,7 +591,52 @@ impl CaptureStream {
"capture poll: ready={} but video revents unavailable — requesting stream re-open",
ready
);
Err(io::Error::other(SOURCE_CHANGED_MARKER))
return Err(io::Error::other(SOURCE_CHANGED_MARKER));
}
}
fn native_hdmirx_state_unchanged(&self) -> bool {
let Some(expected) = self.native_hdmirx_state.as_ref() else {
return false;
};
let Ok(current_fmt) = ioctl::g_fmt::<V4l2rFormat>(&self.fd, self.queue) else {
return false;
};
if !expected.format_matches(
current_fmt.width,
current_fmt.height,
current_fmt.pixelformat,
) {
return false;
}
let observed_timings = ioctl::query_dv_timings::<v4l2_dv_timings>(&self.fd)
.ok()
.and_then(|timings| dv_timings_signature(&timings));
expected.timings_match(observed_timings).unwrap_or(false)
}
fn native_hdmirx_state_changed(&self) -> bool {
let Some(expected) = self.native_hdmirx_state.as_ref() else {
return false;
};
let Ok(current_fmt) = ioctl::g_fmt::<V4l2rFormat>(&self.fd, self.queue) else {
return false;
};
if !expected.format_matches(
current_fmt.width,
current_fmt.height,
current_fmt.pixelformat,
) {
return true;
}
let observed_timings = ioctl::query_dv_timings::<v4l2_dv_timings>(&self.fd)
.ok()
.and_then(|timings| dv_timings_signature(&timings));
expected
.timings_match(observed_timings)
.is_some_and(|matches| !matches)
}
fn queue_all_buffers(&mut self) -> Result<()> {
@@ -576,7 +702,11 @@ impl Drop for CaptureStream {
/// `VideoDeviceInfo` at `CaptureStream::open` time.
fn is_csi_bridge_driver(driver: &str) -> bool {
let d = driver.to_ascii_lowercase();
d == "rk_hdmirx" || d == "rkcif" || d == "tc358743" || d.starts_with("rkcif")
is_native_hdmirx_driver(&d) || d == "rkcif" || d == "tc358743" || d.starts_with("rkcif")
}
fn is_native_hdmirx_driver(driver: &str) -> bool {
driver.eq_ignore_ascii_case("rk_hdmirx") || driver.eq_ignore_ascii_case("snps_hdmirx")
}
/// Drain any pending `V4L2_EVENT_*` events on `fd`. Used after POLLPRI to
@@ -602,6 +732,94 @@ struct DvTimingsMode {
height: u32,
#[allow(dead_code)]
fps: Option<f64>,
signature: Option<DvTimingsSignature>,
}
#[derive(Debug, Clone, Copy)]
struct NativeHdmirxState {
width: u32,
height: u32,
pixelformat: V4l2rPixelFormat,
timings: Option<DvTimingsSignature>,
}
impl NativeHdmirxState {
fn format_matches(self, width: u32, height: u32, pixelformat: V4l2rPixelFormat) -> bool {
self.width == width && self.height == height && self.pixelformat == pixelformat
}
/// `None` means the expected state contains timings but the current
/// timings could not be observed. Event handling treats that uncertainty
/// conservatively as changed; periodic fallback probing ignores it so a
/// single transient ioctl failure cannot tear down a healthy stream.
fn timings_match(self, observed: Option<DvTimingsSignature>) -> Option<bool> {
match (self.timings, observed) {
(None, _) => Some(true),
(Some(expected), Some(current)) => Some(current.matches(expected)),
(Some(_), None) => None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct DvTimingsSignature {
width: u32,
height: u32,
total_width: u32,
total_height: u32,
pixelclock: u64,
interlaced: bool,
}
impl DvTimingsSignature {
fn matches(self, other: Self) -> bool {
let self_total = u128::from(self.total_width) * u128::from(self.total_height);
let other_total = u128::from(other.total_width) * u128::from(other.total_height);
// RK3588 BSPs can describe the same active mode with different
// blanking/pixel-clock pairs (observed for 1080p60: 2200×1125 at
// 148.5 MHz and 2752×1125 at 185.448 MHz). Compare the resulting
// frame rates by cross multiplication instead of requiring identical
// totals. A 0.5% tolerance absorbs measurement jitter and 59.94/60,
// while still distinguishing normal 50/60 transitions.
let self_rate = u128::from(self.pixelclock) * other_total;
let other_rate = u128::from(other.pixelclock) * self_total;
let rate_delta = self_rate.abs_diff(other_rate);
let rate_tolerance = (self_rate.max(other_rate) / 200).max(1);
self.width == other.width
&& self.height == other.height
&& self.interlaced == other.interlaced
&& self.pixelclock != 0
&& other.pixelclock != 0
&& self_total != 0
&& other_total != 0
&& rate_delta <= rate_tolerance
}
}
fn dv_timings_signature(timings: &v4l2_dv_timings) -> Option<DvTimingsSignature> {
let timings_type = timings.type_;
if timings_type != V4L2_DV_BT_656_1120 {
return None;
}
let bt = unsafe { timings.__bindgen_anon_1.bt };
let width = bt.width;
let height = bt.height;
let total_width = width
.checked_add(bt.hfrontporch)?
.checked_add(bt.hsync)?
.checked_add(bt.hbackporch)?;
let total_height = height
.checked_add(bt.vfrontporch)?
.checked_add(bt.vsync)?
.checked_add(bt.vbackporch)?;
Some(DvTimingsSignature {
width,
height,
total_width,
total_height,
pixelclock: bt.pixelclock,
interlaced: bt.interlaced != 0,
})
}
/// Probe DV timings from the source and latch them into the driver.
@@ -619,7 +837,7 @@ struct DvTimingsMode {
/// * `ENODATA` → `NoSignal` (driver says "no DV timings support on
/// this input", e.g. EDID not applied yet)
/// * anything else → `NoSignal` (fallback, keeps the retry loop going)
fn probe_and_apply_dv_timings(fd: &File) -> Result<DvTimingsMode> {
fn probe_dv_timings(fd: &File, apply: bool) -> Result<DvTimingsMode> {
let timings: v4l2_dv_timings = match ioctl::query_dv_timings(fd) {
Ok(t) => t,
Err(err) => {
@@ -687,12 +905,14 @@ fn probe_and_apply_dv_timings(fd: &File) -> Result<DvTimingsMode> {
// right pixel clock + blanking. Failure here is *not* fatal on some
// drivers (rkcif doesn't implement S_DV_TIMINGS per-output-device, only
// on the bridging subdev), so degrade to a warning and keep going.
if apply {
if let Err(e) = ioctl::s_dv_timings::<_, v4l2_dv_timings>(fd, timings) {
debug!(
"VIDIOC_S_DV_TIMINGS failed ({}), continuing with queried timings for S_FMT",
e
);
}
}
let fps = dv_timings_fps_from_scalars(
bt_width,
@@ -714,6 +934,7 @@ fn probe_and_apply_dv_timings(fd: &File) -> Result<DvTimingsMode> {
width: bt_width,
height: bt_height,
fps,
signature: dv_timings_signature(&timings),
})
}
@@ -749,3 +970,69 @@ fn set_fps(fd: &File, queue: QueueType, fps: u32) -> std::result::Result<(), ioc
let _actual: v4l2_streamparm = ioctl::s_parm(fd, params)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{is_native_hdmirx_driver, DvTimingsSignature, NativeHdmirxState};
use crate::video::format::PixelFormat;
fn timing(pixelclock: u64) -> DvTimingsSignature {
DvTimingsSignature {
width: 1920,
height: 1080,
total_width: 2200,
total_height: 1125,
pixelclock,
interlaced: false,
}
}
#[test]
fn recognizes_vendor_and_upstream_native_hdmirx_drivers() {
assert!(is_native_hdmirx_driver("rk_hdmirx"));
assert!(is_native_hdmirx_driver("SNPS_HDMIRX"));
assert!(!is_native_hdmirx_driver("rkcif"));
}
#[test]
fn timing_match_tolerates_measurement_jitter_but_not_mode_changes() {
assert!(timing(148_500_000).matches(timing(148_000_000)));
assert!(!timing(148_500_000).matches(timing(120_000_000)));
let mut equivalent_blanking = timing(185_448_000);
equivalent_blanking.total_width = 2752;
assert!(timing(148_500_000).matches(equivalent_blanking));
let mut different_refresh = timing(148_500_000);
different_refresh.total_width = 2640;
assert!(!timing(148_500_000).matches(different_refresh));
let mut interlaced = timing(148_500_000);
interlaced.interlaced = true;
assert!(!timing(148_500_000).matches(interlaced));
}
#[test]
fn native_hdmirx_state_distinguishes_spurious_and_real_changes() {
let bgr24 = PixelFormat::Bgr24.to_v4l2r();
let state = NativeHdmirxState {
width: 1920,
height: 1080,
pixelformat: bgr24,
timings: Some(timing(148_500_000)),
};
assert!(state.format_matches(1920, 1080, bgr24));
assert!(!state.format_matches(1280, 720, bgr24));
assert!(!state.format_matches(1920, 1080, PixelFormat::Nv12.to_v4l2r()));
assert_eq!(state.timings_match(Some(timing(148_000_000))), Some(true));
assert_eq!(state.timings_match(Some(timing(120_000_000))), Some(false));
assert_eq!(state.timings_match(None), None);
let no_timing_state = NativeHdmirxState {
timings: None,
..state
};
assert_eq!(no_timing_state.timings_match(None), Some(true));
}
}

View File

@@ -267,8 +267,8 @@ impl VideoDevice {
(true, None)
};
let mut formats =
if is_rk_hdmirx_driver(&caps.driver, &caps.card) || is_rkcif_driver(&caps.driver) {
let native_hdmirx = is_rk_hdmirx_driver(&caps.driver, &caps.card);
let mut formats = if native_hdmirx || is_rkcif_driver(&caps.driver) {
// CSI/HDMI bridge drivers (rk_hdmirx, rkcif) expose multiple pixel
// formats via ENUM_FMT (e.g. rk_hdmirx: BGR3/NV24/NV16/NV12) but
// `ENUM_FRAMESIZES` is fiction for these drivers (rkcif reports a
@@ -277,7 +277,7 @@ impl VideoDevice {
// resolution is whatever the bridge subdev's DV timings report,
// so we treat the HDMI source mode as the single allowed
// resolution for every pixel format.
self.enumerate_bridge_formats(subdev_hdmi_mode)?
self.enumerate_bridge_formats(subdev_hdmi_mode, native_hdmirx)?
} else {
self.enumerate_formats()?
};
@@ -373,12 +373,13 @@ impl VideoDevice {
/// HDMI source mode.
///
/// Returned formats are sorted by `PixelFormat::priority()` so the
/// higher-level `select_format` picks a sensible default (NV12 > YUYV on
/// rkcif / rk_hdmirx) instead of whatever the driver happens to
/// have stuck as the current active format.
/// higher-level `select_format` picks a sensible default for conversion-
/// capable rkcif paths. Native HDMI RX is reduced to its single current
/// wire format before sorting.
fn enumerate_bridge_formats(
&self,
subdev_hdmi_mode: Option<(u32, u32, Option<f64>)>,
current_format_only: bool,
) -> Result<Vec<FormatInfo>> {
let queue = self.capture_queue_type()?;
let current_fmt = self.get_format().ok();
@@ -432,6 +433,31 @@ impl VideoDevice {
continue;
};
// Native RK3588 HDMI RX does not perform pixel-format conversion.
// ENUM_FMT reports every input encoding the controller can receive,
// but TRY_FMT/S_FMT accept only the FourCC corresponding to the
// source's current AVI InfoFrame (RGB -> BGR3, YUV444 -> NV24,
// YUV422 -> NV16, YUV420 -> NV12). Advertising the full ENUM_FMT
// list makes the higher layer prefer NV12 even for an RGB source,
// and capture then fails with EINVAL. G_FMT is the driver's
// authoritative current-input format.
if current_format_only {
let Some(active) = current_fmt.as_ref() else {
debug!(
"enumerate_bridge_formats: skipping native HDMI RX format {:?} because G_FMT is unavailable",
desc.pixelformat
);
continue;
};
if active.pixelformat != desc.pixelformat {
debug!(
"enumerate_bridge_formats: skipping inactive rk_hdmirx format {:?}; current is {:?}",
desc.pixelformat, active.pixelformat
);
continue;
}
}
let resolutions = hdmi_mode.clone().into_iter().collect();
formats.push(FormatInfo {
@@ -685,6 +711,7 @@ impl VideoDevice {
"uvc",
"rkcif",
"rk_hdmirx",
"snps_hdmirx",
];
// Check card/driver names
@@ -1158,6 +1185,7 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
"grabber",
"rkcif",
"rk_hdmirx",
"snps_hdmirx",
];
if capture_hints.iter().any(|hint| sysfs_name.contains(hint)) {
maybe_capture = true;
@@ -1167,6 +1195,7 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
|| driver.contains("tc358743")
|| driver.contains("rkcif")
|| driver.contains("rk_hdmirx")
|| driver.contains("snps_hdmirx")
{
maybe_capture = true;
}

View File

@@ -20,7 +20,9 @@ pub mod bridge;
pub mod bridge;
pub(crate) fn is_rk_hdmirx_driver(driver: &str, card: &str) -> bool {
driver.eq_ignore_ascii_case("rk_hdmirx") || card.eq_ignore_ascii_case("rk_hdmirx")
[driver, card].iter().any(|name| {
name.eq_ignore_ascii_case("rk_hdmirx") || name.eq_ignore_ascii_case("snps_hdmirx")
})
}
pub(crate) fn is_rk_hdmirx_device(device: &VideoDeviceInfo) -> bool {
@@ -37,5 +39,18 @@ pub(crate) fn is_csi_hdmi_bridge(device: &VideoDeviceInfo) -> bool {
is_rk_hdmirx_device(device) || is_rkcif_driver(&device.driver)
}
#[cfg(test)]
mod tests {
use super::is_rk_hdmirx_driver;
#[test]
fn recognizes_vendor_and_upstream_native_hdmirx_names() {
assert!(is_rk_hdmirx_driver("rk_hdmirx", "rk_hdmirx"));
assert!(is_rk_hdmirx_driver("snps_hdmirx", "Synopsys HDMI RX"));
assert!(is_rk_hdmirx_driver("other", "SNPS_HDMIRX"));
assert!(!is_rk_hdmirx_driver("rkcif", "stream_cif_mipi_id0"));
}
}
#[cfg(unix)]
pub(crate) use linux::parse_bridge_kind;

View File

@@ -616,6 +616,17 @@ impl Streamer {
return Ok(());
}
// A no-signal/source-change recovery keeps the existing capture thread
// alive while it closes and re-opens the V4L2 stream. HTTP clients may
// reconnect while that thread is still probing. Do not spawn a second
// capture thread here: it would contend for the same video node and
// overwrite `direct_handle`, making the original thread impossible to
// join from `stop()`.
if self.direct_active.load(Ordering::SeqCst) {
debug!("Capture thread is already active; waiting for its recovery loop");
return Ok(());
}
if state == StreamerState::Uninitialized {
// Auto-initialize if not done
self.init_auto().await?;
@@ -994,7 +1005,13 @@ impl Streamer {
);
let buffer_pool = Arc::new(FrameBufferPool::new(BUFFER_COUNT.max(4) as usize));
let mut signal_present = true;
// Preserve the no-signal state across an outer-loop re-open. This
// makes the first recovered frame transition the handler back
// online and publish Streaming instead of silently inheriting the
// previous offline state.
let mut signal_present = !handle
.block_on(async { self.state().await })
.is_no_signal_like();
let mut idle_since: Option<std::time::Instant> = None;
let mut fps_frame_count: u64 = 0;
@@ -1168,6 +1185,11 @@ impl Streamer {
no_signal_since = None;
no_signal_restart_count = 0;
set_retry(0);
// Signal-loss handling marks the MJPEG handler offline so
// stale HTTP responses close cleanly. Re-enable it on the
// first recovered frame so a reconnect can remain attached
// to this (still single) capture thread.
self.mjpeg_handler.set_online();
set_state(StreamerState::Streaming);
let fps_val = config.fps;