Files
RemoteDesk/agent/agent-core/src/multi_display.rs
T
曾志威 19a8e03a83
ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
Document all-Rust migration and extend native media stack
2026-08-14 14:31:57 +08:00

1215 lines
39 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use remotedesk_protocol::{
DisplayDescriptor, DisplayId, DisplayLayout, DisplaySelection, DisplaySelectionError,
MemoryPathStatus,
};
use super::{
AdapterId, CaptureMemoryPath, ConfigurationError, DesktopBackend, ImportProbeResult,
PipelineState, Resolution, TransferMode, ZeroCopyPolicy, validate_capture_for_backend,
};
/// Capture information that is local to the Linux Agent and not sent as
/// display-topology metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisplayCaptureSource {
pub id: DisplayId,
pub name: String,
pub capture_path: CaptureMemoryPath,
}
impl DisplayCaptureSource {
/// Associates a protocol display identity with an observed capture path.
///
/// # Errors
///
/// Returns [`MultiDisplayPlanError::EmptyDisplayName`] for an empty name.
pub fn new(
id: DisplayId,
name: impl Into<String>,
capture_path: CaptureMemoryPath,
) -> Result<Self, MultiDisplayPlanError> {
let name = name.into();
if name.trim().is_empty() {
return Err(MultiDisplayPlanError::EmptyDisplayName { id });
}
Ok(Self {
id,
name,
capture_path,
})
}
}
/// The union of all selected display rectangles.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DesktopBounds {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
/// A point normalized to the top-left of [`DesktopBounds`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NormalizedPoint {
pub x: u32,
pub y: u32,
}
impl DesktopBounds {
/// Converts a native desktop point to non-negative stream coordinates.
#[must_use]
pub fn normalize(self, x: i32, y: i32) -> Option<NormalizedPoint> {
let normalized_x = i64::from(x) - i64::from(self.x);
let normalized_y = i64::from(y) - i64::from(self.y);
if normalized_x < 0
|| normalized_y < 0
|| normalized_x >= i64::from(self.width)
|| normalized_y >= i64::from(self.height)
{
return None;
}
Some(NormalizedPoint {
x: u32::try_from(normalized_x).ok()?,
y: u32::try_from(normalized_y).ok()?,
})
}
/// Converts a stream point back to native Linux desktop coordinates.
#[must_use]
pub fn denormalize(self, point: NormalizedPoint) -> Option<(i32, i32)> {
if point.x >= self.width || point.y >= self.height {
return None;
}
let x = i64::from(self.x) + i64::from(point.x);
let y = i64::from(self.y) + i64::from(point.y);
Some((i32::try_from(x).ok()?, i32::try_from(y).ok()?))
}
}
/// One selected protocol descriptor paired with its Agent capture source.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SelectedDisplaySource {
pub descriptor: DisplayDescriptor,
pub capture: DisplayCaptureSource,
}
/// A protocol-generation-bound selection with computed stream coordinates.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SelectedDisplayLayout {
generation: u64,
sources: Vec<SelectedDisplaySource>,
bounds: DesktopBounds,
}
impl SelectedDisplayLayout {
/// Resolves Agent capture sources against a validated protocol selection.
///
/// The protocol layout owns display identity, geometry, scale, primary
/// status, selection ordering, and stale-generation rejection. This layer
/// only joins those descriptors to locally observed capture memory paths.
///
/// # Errors
///
/// Rejects invalid protocol selections, duplicate local source IDs,
/// missing selected sources, and unrepresentable combined desktop bounds.
pub fn resolve(
topology: &DisplayLayout,
capture_sources: &[DisplayCaptureSource],
selection: &DisplaySelection,
) -> Result<Self, MultiDisplayPlanError> {
let descriptors = topology
.selected_displays(selection)
.map_err(MultiDisplayPlanError::Selection)?;
let mut capture_by_id = BTreeMap::new();
for source in capture_sources {
if capture_by_id.insert(source.id, source).is_some() {
return Err(MultiDisplayPlanError::DuplicateCaptureSource { id: source.id });
}
}
let sources = descriptors
.into_iter()
.map(|descriptor| {
let capture = capture_by_id
.get(&descriptor.id)
.copied()
.ok_or(MultiDisplayPlanError::MissingCaptureSource { id: descriptor.id })?;
Ok(SelectedDisplaySource {
descriptor: descriptor.clone(),
capture: capture.clone(),
})
})
.collect::<Result<Vec<_>, MultiDisplayPlanError>>()?;
let bounds = desktop_bounds(&sources)?;
Ok(Self {
generation: topology.generation(),
sources,
bounds,
})
}
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub fn sources(&self) -> &[SelectedDisplaySource] {
&self.sources
}
#[must_use]
pub const fn bounds(&self) -> DesktopBounds {
self.bounds
}
/// Returns the selected display's origin relative to the stream desktop.
#[must_use]
pub fn normalized_origin(&self, id: DisplayId) -> Option<NormalizedPoint> {
let source = self
.sources
.iter()
.find(|source| source.descriptor.id == id)?;
self.bounds
.normalize(source.descriptor.rect.x, source.descriptor.rect.y)
}
}
fn desktop_bounds(
sources: &[SelectedDisplaySource],
) -> Result<DesktopBounds, MultiDisplayPlanError> {
let left = sources
.iter()
.map(|source| source.descriptor.rect.x)
.min()
.ok_or(MultiDisplayPlanError::NoSelectedDisplays)?;
let top = sources
.iter()
.map(|source| source.descriptor.rect.y)
.min()
.ok_or(MultiDisplayPlanError::NoSelectedDisplays)?;
let right = sources
.iter()
.filter_map(|source| source.descriptor.rect.right())
.max()
.ok_or(MultiDisplayPlanError::DesktopBoundsOverflow)?;
let bottom = sources
.iter()
.filter_map(|source| source.descriptor.rect.bottom())
.max()
.ok_or(MultiDisplayPlanError::DesktopBoundsOverflow)?;
let width = u32::try_from(i64::from(right) - i64::from(left))
.map_err(|_| MultiDisplayPlanError::DesktopBoundsOverflow)?;
let height = u32::try_from(i64::from(bottom) - i64::from(top))
.map_err(|_| MultiDisplayPlanError::DesktopBoundsOverflow)?;
Ok(DesktopBounds {
x: left,
y: top,
width,
height,
})
}
/// Import result for one display on one target encoder.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisplayImportProbe {
pub display_id: DisplayId,
pub result: ImportProbeResult,
}
/// One target encoder with independent import evidence for every display.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiDisplayGpuCandidate {
adapter: AdapterId,
encoder_name: String,
source_probes: Vec<DisplayImportProbe>,
}
impl MultiDisplayGpuCandidate {
/// Creates a target encoder from per-display import probes.
///
/// # Errors
///
/// Uses the same encoder-name validation as [`super::GpuCandidate`] and
/// rejects duplicate probe IDs because their evidence would be ambiguous.
pub fn new(
adapter: AdapterId,
encoder_name: impl Into<String>,
source_probes: Vec<DisplayImportProbe>,
) -> Result<Self, MultiDisplayPlanError> {
let encoder_name = encoder_name.into();
if encoder_name.trim().is_empty() {
return Err(MultiDisplayPlanError::Configuration(
ConfigurationError::EmptyEncoderName,
));
}
let mut ids = BTreeSet::new();
for probe in &source_probes {
if !ids.insert(probe.display_id) {
return Err(MultiDisplayPlanError::DuplicateImportProbe {
id: probe.display_id,
});
}
}
Ok(Self {
adapter,
encoder_name,
source_probes,
})
}
#[must_use]
pub fn adapter(&self) -> &AdapterId {
&self.adapter
}
#[must_use]
pub fn encoder_name(&self) -> &str {
&self.encoder_name
}
#[must_use]
pub fn source_probes(&self) -> &[DisplayImportProbe] {
&self.source_probes
}
}
/// Capture-to-encode behavior selected for one display.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisplayPathPlan {
pub display_id: DisplayId,
pub capture_status: MemoryPathStatus,
pub transfer_mode: TransferMode,
}
impl DisplayPathPlan {
#[must_use]
pub const fn is_strict_zero_copy(&self) -> bool {
self.capture_status.satisfies_strict_zero_copy()
&& self.transfer_mode.is_verified_zero_copy()
}
}
/// Whether every selected display is strict or compatibility fallback is used.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MultiDisplayCompliance {
StrictZeroCopy,
CompatibilityDegraded,
}
/// A plan that uses one encoder target for every selected display.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiDisplayCapturePlan {
pub encoder_adapter: AdapterId,
pub encoder_name: String,
pub layout: SelectedDisplayLayout,
pub paths: Vec<DisplayPathPlan>,
pub compliance: MultiDisplayCompliance,
}
impl MultiDisplayCapturePlan {
/// Selects a single encoder that can import all selected display sources.
///
/// Strict policy rejects CPU, opaque, and cross-adapter paths. Compatibility
/// mode chooses the candidate with the least severe worst path and reports
/// every display's actual fallback in [`Self::paths`].
///
/// # Errors
///
/// Returns [`MultiDisplayPlanError`] when protocol selection, source/path
/// compatibility, strict policy, or common encoder requirements fail.
pub fn create(
backend: DesktopBackend,
policy: ZeroCopyPolicy,
topology: &DisplayLayout,
capture_sources: &[DisplayCaptureSource],
selection: &DisplaySelection,
candidates: &[MultiDisplayGpuCandidate],
) -> Result<Self, MultiDisplayPlanError> {
if !backend.carries_video() {
return Err(MultiDisplayPlanError::TerminalHasNoVideoPipeline);
}
let layout = SelectedDisplayLayout::resolve(topology, capture_sources, selection)?;
for selected in layout.sources() {
let source = &selected.capture;
validate_capture_for_backend(backend, &source.capture_path).map_err(|_| {
MultiDisplayPlanError::BackendCaptureMismatch {
display_id: source.id,
backend,
}
})?;
if policy == ZeroCopyPolicy::RequiredEndToEnd
&& !source
.capture_path
.protocol_status()
.satisfies_strict_zero_copy()
{
return Err(MultiDisplayPlanError::StrictSourceRejected {
display_id: source.id,
status: source.capture_path.protocol_status(),
});
}
}
let mut plans = candidates
.iter()
.filter_map(|candidate| plan_candidate(&layout, candidate))
.filter(|(_, paths)| {
policy != ZeroCopyPolicy::RequiredEndToEnd
|| paths.iter().all(DisplayPathPlan::is_strict_zero_copy)
})
.collect::<Vec<_>>();
plans.sort_by_key(|(_, paths)| paths.iter().map(path_severity).max().unwrap_or(u8::MAX));
let Some((candidate, paths)) = plans.into_iter().next() else {
return Err(if policy == ZeroCopyPolicy::RequiredEndToEnd {
MultiDisplayPlanError::NoVerifiedSingleAdapterEncoder
} else {
MultiDisplayPlanError::NoCommonEncoder
});
};
let compliance = if paths.iter().all(DisplayPathPlan::is_strict_zero_copy) {
MultiDisplayCompliance::StrictZeroCopy
} else {
MultiDisplayCompliance::CompatibilityDegraded
};
Ok(Self {
encoder_adapter: candidate.adapter.clone(),
encoder_name: candidate.encoder_name.clone(),
layout,
paths,
compliance,
})
}
}
fn plan_candidate<'a>(
layout: &SelectedDisplayLayout,
candidate: &'a MultiDisplayGpuCandidate,
) -> Option<(&'a MultiDisplayGpuCandidate, Vec<DisplayPathPlan>)> {
let probes = candidate
.source_probes
.iter()
.map(|probe| (probe.display_id, &probe.result))
.collect::<BTreeMap<_, _>>();
let paths = layout
.sources()
.iter()
.map(|selected| {
let source = &selected.capture;
let result = probes.get(&source.id)?;
let transfer_mode = transfer_mode_for_probe(source, candidate, result)?;
Some(DisplayPathPlan {
display_id: source.id,
capture_status: source.capture_path.protocol_status(),
transfer_mode,
})
})
.collect::<Option<Vec<_>>>()?;
Some((candidate, paths))
}
fn transfer_mode_for_probe(
source: &DisplayCaptureSource,
candidate: &MultiDisplayGpuCandidate,
result: &ImportProbeResult,
) -> Option<TransferMode> {
match result {
ImportProbeResult::SameAdapterZeroCopy
if matches!(
&source.capture_path,
CaptureMemoryPath::DmaBuf { adapter } if *adapter == candidate.adapter
) =>
{
Some(TransferMode::SameAdapterZeroCopy)
}
ImportProbeResult::CrossAdapterCopy { source_adapter }
if source.capture_path.adapter() == Some(source_adapter)
&& candidate.adapter != *source_adapter =>
{
Some(TransferMode::CrossAdapterCopy)
}
ImportProbeResult::CpuUpload => Some(TransferMode::CpuUpload),
ImportProbeResult::Software => Some(TransferMode::Software),
ImportProbeResult::OpaqueGpuPath => Some(TransferMode::OpaqueGpuPath),
ImportProbeResult::SameAdapterZeroCopy
| ImportProbeResult::CrossAdapterCopy { .. }
| ImportProbeResult::Unsupported { .. } => None,
}
}
const fn path_severity(path: &DisplayPathPlan) -> u8 {
let transfer = match path.transfer_mode {
TransferMode::SameAdapterZeroCopy => 0,
TransferMode::CrossAdapterCopy => 1,
TransferMode::CpuUpload => 2,
TransferMode::OpaqueGpuPath => 3,
TransferMode::Software => 4,
};
if path.capture_status.satisfies_strict_zero_copy() {
transfer
} else {
4
}
}
/// Revalidation state for a multi-display stream.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultiDisplayPipeline {
policy: ZeroCopyPolicy,
resolution: Resolution,
layout: SelectedDisplayLayout,
epoch: u64,
state: PipelineState,
plan: Option<MultiDisplayCapturePlan>,
}
impl MultiDisplayPipeline {
/// Creates an active pipeline from a policy-compliant plan.
///
/// # Errors
///
/// Rejects a compatibility-degraded plan under strict policy.
pub fn active(
policy: ZeroCopyPolicy,
resolution: Resolution,
plan: MultiDisplayCapturePlan,
) -> Result<Self, MultiDisplayPipelineError> {
validate_plan_for_policy(policy, &plan)?;
Ok(Self {
policy,
resolution,
layout: plan.layout.clone(),
epoch: 0,
state: PipelineState::Active,
plan: Some(plan),
})
}
#[must_use]
pub const fn epoch(&self) -> u64 {
self.epoch
}
#[must_use]
pub const fn state(&self) -> PipelineState {
self.state
}
#[must_use]
pub const fn resolution(&self) -> Resolution {
self.resolution
}
#[must_use]
pub fn layout(&self) -> &SelectedDisplayLayout {
&self.layout
}
#[must_use]
pub fn plan(&self) -> Option<&MultiDisplayCapturePlan> {
self.plan.as_ref()
}
/// Applies output resolution or selected-layout changes. Any real change
/// invalidates all per-display import evidence and increments the epoch.
///
/// # Errors
///
/// Returns [`MultiDisplayPipelineError::GenerationOverflow`] instead of
/// applying a change that cannot receive a fresh epoch.
pub fn reconfigure(
&mut self,
resolution: Resolution,
layout: SelectedDisplayLayout,
) -> Result<bool, MultiDisplayPipelineError> {
if self.resolution == resolution && self.layout == layout {
return Ok(false);
}
let next_epoch = self
.epoch
.checked_add(1)
.ok_or(MultiDisplayPipelineError::GenerationOverflow)?;
self.resolution = resolution;
self.layout = layout;
self.epoch = next_epoch;
self.state = PipelineState::Revalidating;
self.plan = None;
Ok(true)
}
/// Activates a freshly probed plan for the current epoch.
///
/// # Errors
///
/// Rejects invalid state, stale layout evidence, and a plan that violates
/// the pipeline's zero-copy policy.
pub fn finish_revalidation(
&mut self,
plan: MultiDisplayCapturePlan,
) -> Result<(), MultiDisplayPipelineError> {
if self.state != PipelineState::Revalidating {
return Err(MultiDisplayPipelineError::InvalidState {
expected: PipelineState::Revalidating,
actual: self.state,
});
}
if self.layout != plan.layout {
return Err(MultiDisplayPipelineError::StaleLayout);
}
validate_plan_for_policy(self.policy, &plan)?;
self.plan = Some(plan);
self.state = PipelineState::Active;
Ok(())
}
}
fn validate_plan_for_policy(
policy: ZeroCopyPolicy,
plan: &MultiDisplayCapturePlan,
) -> Result<(), MultiDisplayPipelineError> {
if policy == ZeroCopyPolicy::RequiredEndToEnd
&& plan.compliance != MultiDisplayCompliance::StrictZeroCopy
{
return Err(MultiDisplayPipelineError::PlanRejectedByPolicy);
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MultiDisplayPlanError {
EmptyDisplayName {
id: DisplayId,
},
DesktopBoundsOverflow,
NoSelectedDisplays,
DuplicateCaptureSource {
id: DisplayId,
},
MissingCaptureSource {
id: DisplayId,
},
DuplicateImportProbe {
id: DisplayId,
},
Selection(DisplaySelectionError),
Configuration(ConfigurationError),
TerminalHasNoVideoPipeline,
BackendCaptureMismatch {
display_id: DisplayId,
backend: DesktopBackend,
},
StrictSourceRejected {
display_id: DisplayId,
status: MemoryPathStatus,
},
NoVerifiedSingleAdapterEncoder,
NoCommonEncoder,
}
impl fmt::Display for MultiDisplayPlanError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyDisplayName { id } => {
write!(formatter, "display {id:?} name must not be empty")
}
Self::DesktopBoundsOverflow => {
formatter.write_str("combined desktop bounds exceed supported dimensions")
}
Self::NoSelectedDisplays => formatter.write_str("no displays were selected"),
Self::DuplicateCaptureSource { id } => {
write!(formatter, "capture source id {id:?} is duplicated")
}
Self::MissingCaptureSource { id } => {
write!(formatter, "selected display {id:?} has no capture source")
}
Self::DuplicateImportProbe { id } => {
write!(
formatter,
"encoder has duplicate import probe for display {id:?}"
)
}
Self::Selection(error) => error.fmt(formatter),
Self::Configuration(error) => error.fmt(formatter),
Self::TerminalHasNoVideoPipeline => {
formatter.write_str("terminal sessions do not use a video pipeline")
}
Self::BackendCaptureMismatch {
display_id,
backend,
} => write!(
formatter,
"display {display_id:?} capture path does not match backend {backend:?}"
),
Self::StrictSourceRejected { display_id, status } => write!(
formatter,
"strict policy rejected display {display_id:?} capture status {status:?}"
),
Self::NoVerifiedSingleAdapterEncoder => formatter.write_str(
"no single encoder adapter has verified zero-copy imports for every display",
),
Self::NoCommonEncoder => {
formatter.write_str("no single encoder can import every selected display")
}
}
}
}
impl std::error::Error for MultiDisplayPlanError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MultiDisplayPipelineError {
InvalidState {
expected: PipelineState,
actual: PipelineState,
},
StaleLayout,
PlanRejectedByPolicy,
GenerationOverflow,
}
impl fmt::Display for MultiDisplayPipelineError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidState { expected, actual } => write!(
formatter,
"pipeline transition requires {expected:?}, but current state is {actual:?}"
),
Self::StaleLayout => {
formatter.write_str("capture plan belongs to a stale display layout")
}
Self::PlanRejectedByPolicy => {
formatter.write_str("strict zero-copy policy rejected the multi-display plan")
}
Self::GenerationOverflow => {
formatter.write_str("multi-display pipeline generation overflowed")
}
}
}
}
impl std::error::Error for MultiDisplayPipelineError {}
#[cfg(test)]
mod tests {
use remotedesk_protocol::{DisplayRect, DisplayScale};
use super::*;
fn adapter(value: &str) -> AdapterId {
AdapterId::new(value).unwrap()
}
fn id(value: u8) -> DisplayId {
DisplayId::from_bytes([value; 16])
}
fn descriptor(
value: u8,
x: i32,
y: i32,
width: u32,
height: u32,
primary: bool,
) -> DisplayDescriptor {
DisplayDescriptor {
id: id(value),
rect: DisplayRect {
x,
y,
width,
height,
},
scale: DisplayScale::ONE,
is_primary: primary,
}
}
fn topology(generation: u64, descriptors: Vec<DisplayDescriptor>) -> DisplayLayout {
DisplayLayout::new(generation, descriptors).unwrap()
}
fn source(value: u8, adapter_name: &str) -> DisplayCaptureSource {
DisplayCaptureSource::new(
id(value),
format!("display-{value}"),
CaptureMemoryPath::DmaBuf {
adapter: adapter(adapter_name),
},
)
.unwrap()
}
fn probe(value: u8, result: ImportProbeResult) -> DisplayImportProbe {
DisplayImportProbe {
display_id: id(value),
result,
}
}
fn candidate(adapter_name: &str, probes: Vec<DisplayImportProbe>) -> MultiDisplayGpuCandidate {
MultiDisplayGpuCandidate::new(adapter(adapter_name), "vaapi-h264", probes).unwrap()
}
fn two_display_topology(generation: u64) -> DisplayLayout {
topology(
generation,
vec![
descriptor(1, -1920, -120, 1920, 1440, false),
descriptor(2, 0, 0, 2560, 1440, true),
],
)
}
fn strict_plan(
topology: &DisplayLayout,
sources: &[DisplayCaptureSource],
) -> MultiDisplayCapturePlan {
let probes = sources
.iter()
.map(|source| DisplayImportProbe {
display_id: source.id,
result: ImportProbeResult::SameAdapterZeroCopy,
})
.collect();
MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
topology,
sources,
&DisplaySelection::all(topology.generation()),
&[candidate("gpu-0", probes)],
)
.unwrap()
}
#[test]
fn protocol_selection_and_negative_coordinates_map_consistently() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let layout = SelectedDisplayLayout::resolve(
&topology,
&sources,
&DisplaySelection::all(topology.generation()),
)
.unwrap();
assert_eq!(layout.generation(), 7);
assert_eq!(
layout.bounds(),
DesktopBounds {
x: -1920,
y: -120,
width: 4480,
height: 1560,
}
);
assert_eq!(
layout.normalized_origin(id(1)),
Some(NormalizedPoint { x: 0, y: 0 })
);
assert_eq!(
layout.normalized_origin(id(2)),
Some(NormalizedPoint { x: 1920, y: 120 })
);
let point = layout.bounds().normalize(-1, 0).unwrap();
assert_eq!(layout.bounds().denormalize(point), Some((-1, 0)));
}
#[test]
fn all_single_and_custom_selection_reuse_protocol_ordering() {
let topology = topology(
9,
vec![
descriptor(1, -1920, 0, 1920, 1080, false),
descriptor(2, 0, 0, 1920, 1080, true),
descriptor(3, 1920, 0, 1920, 1080, false),
],
);
let sources = [source(1, "gpu-0"), source(2, "gpu-0"), source(3, "gpu-0")];
let all =
SelectedDisplayLayout::resolve(&topology, &sources, &DisplaySelection::all(9)).unwrap();
assert_eq!(
all.sources()
.iter()
.map(|source| source.descriptor.id)
.collect::<Vec<_>>(),
vec![id(1), id(2), id(3)]
);
let single = SelectedDisplayLayout::resolve(
&topology,
&sources,
&DisplaySelection::single(9, id(2)),
)
.unwrap();
assert_eq!(single.sources().len(), 1);
assert_eq!(single.bounds().width, 1920);
let custom = SelectedDisplayLayout::resolve(
&topology,
&sources,
&DisplaySelection::custom(9, vec![id(3), id(2)]),
)
.unwrap();
assert_eq!(custom.sources()[0].descriptor.id, id(3));
assert_eq!(custom.sources()[1].descriptor.id, id(2));
assert_eq!(custom.bounds().x, 0);
assert_eq!(custom.bounds().width, 3840);
}
#[test]
fn stale_empty_duplicate_and_unknown_protocol_selections_fail_closed() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let cases = [
(
DisplaySelection::all(6),
DisplaySelectionError::LayoutGenerationMismatch {
expected: 7,
actual: 6,
},
),
(
DisplaySelection::custom(7, Vec::new()),
DisplaySelectionError::EmptyCustomSelection,
),
(
DisplaySelection::custom(7, vec![id(1), id(1)]),
DisplaySelectionError::DuplicateDisplay(id(1)),
),
(
DisplaySelection::single(7, id(99)),
DisplaySelectionError::UnknownDisplay(id(99)),
),
];
for (selection, expected) in cases {
assert_eq!(
SelectedDisplayLayout::resolve(&topology, &sources, &selection),
Err(MultiDisplayPlanError::Selection(expected))
);
}
}
#[test]
fn duplicate_or_missing_agent_capture_source_is_rejected() {
let topology = two_display_topology(7);
let duplicate = [source(1, "gpu-0"), source(1, "gpu-0")];
assert_eq!(
SelectedDisplayLayout::resolve(&topology, &duplicate, &DisplaySelection::all(7)),
Err(MultiDisplayPlanError::DuplicateCaptureSource { id: id(1) })
);
assert_eq!(
SelectedDisplayLayout::resolve(
&topology,
&[source(1, "gpu-0")],
&DisplaySelection::all(7),
),
Err(MultiDisplayPlanError::MissingCaptureSource { id: id(2) })
);
}
#[test]
fn strict_plan_uses_one_gpu_for_every_display() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let plan = strict_plan(&topology, &sources);
assert_eq!(plan.encoder_adapter, adapter("gpu-0"));
assert_eq!(plan.paths.len(), 2);
assert_eq!(plan.compliance, MultiDisplayCompliance::StrictZeroCopy);
assert!(plan.paths.iter().all(DisplayPathPlan::is_strict_zero_copy));
}
#[test]
fn strict_plan_rejects_displays_captured_on_different_adapters() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-1")];
let candidates = [
candidate(
"gpu-0",
vec![
probe(1, ImportProbeResult::SameAdapterZeroCopy),
probe(
2,
ImportProbeResult::CrossAdapterCopy {
source_adapter: adapter("gpu-1"),
},
),
],
),
candidate(
"gpu-1",
vec![
probe(
1,
ImportProbeResult::CrossAdapterCopy {
source_adapter: adapter("gpu-0"),
},
),
probe(2, ImportProbeResult::SameAdapterZeroCopy),
],
),
];
assert_eq!(
MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&sources,
&DisplaySelection::all(7),
&candidates,
),
Err(MultiDisplayPlanError::NoVerifiedSingleAdapterEncoder)
);
}
#[test]
fn compatibility_reports_each_cross_adapter_downgrade() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-1")];
let candidate = candidate(
"gpu-0",
vec![
probe(1, ImportProbeResult::SameAdapterZeroCopy),
probe(
2,
ImportProbeResult::CrossAdapterCopy {
source_adapter: adapter("gpu-1"),
},
),
],
);
let plan = MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&sources,
&DisplaySelection::all(7),
&[candidate],
)
.unwrap();
assert_eq!(
plan.compliance,
MultiDisplayCompliance::CompatibilityDegraded
);
assert_eq!(
plan.paths[0].transfer_mode,
TransferMode::SameAdapterZeroCopy
);
assert_eq!(plan.paths[1].transfer_mode, TransferMode::CrossAdapterCopy);
assert_eq!(
plan.paths[1].transfer_mode.protocol_status(),
MemoryPathStatus::CrossAdapterCopy
);
}
#[test]
fn strict_rejects_cpu_and_opaque_capture_sources() {
let topology = topology(7, vec![descriptor(1, 0, 0, 1920, 1080, true)]);
for capture_path in [
CaptureMemoryPath::CpuMemory,
CaptureMemoryPath::OpaqueGpu {
adapter: Some(adapter("gpu-0")),
},
] {
let source = DisplayCaptureSource::new(id(1), "primary", capture_path.clone()).unwrap();
let result = MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&[source],
&DisplaySelection::all(7),
&[candidate(
"gpu-0",
vec![probe(1, ImportProbeResult::OpaqueGpuPath)],
)],
);
assert_eq!(
result,
Err(MultiDisplayPlanError::StrictSourceRejected {
display_id: id(1),
status: capture_path.protocol_status(),
})
);
}
}
#[test]
fn strict_rejects_degraded_imports_while_compatibility_reports_them() {
let topology = topology(7, vec![descriptor(1, 0, 0, 1920, 1080, true)]);
let sources = [source(1, "gpu-0")];
for (probe_result, expected_mode) in [
(ImportProbeResult::CpuUpload, TransferMode::CpuUpload),
(ImportProbeResult::Software, TransferMode::Software),
(
ImportProbeResult::OpaqueGpuPath,
TransferMode::OpaqueGpuPath,
),
] {
let candidate = candidate("gpu-0", vec![probe(1, probe_result)]);
assert_eq!(
MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&sources,
&DisplaySelection::all(7),
std::slice::from_ref(&candidate),
),
Err(MultiDisplayPlanError::NoVerifiedSingleAdapterEncoder)
);
let plan = MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&sources,
&DisplaySelection::all(7),
&[candidate],
)
.unwrap();
assert_eq!(
plan.compliance,
MultiDisplayCompliance::CompatibilityDegraded
);
assert_eq!(plan.paths[0].transfer_mode, expected_mode);
}
}
#[test]
fn missing_probe_means_candidate_cannot_cover_selection() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let incomplete = candidate(
"gpu-0",
vec![probe(1, ImportProbeResult::SameAdapterZeroCopy)],
);
assert_eq!(
MultiDisplayCapturePlan::create(
DesktopBackend::WaylandPipeWire,
ZeroCopyPolicy::RequiredEndToEnd,
&topology,
&sources,
&DisplaySelection::all(7),
&[incomplete],
),
Err(MultiDisplayPlanError::NoCommonEncoder)
);
}
#[test]
fn resolution_and_protocol_layout_generation_force_revalidation() {
let initial_topology = two_display_topology(7);
let initial_sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let initial_plan = strict_plan(&initial_topology, &initial_sources);
let mut pipeline = MultiDisplayPipeline::active(
ZeroCopyPolicy::RequiredEndToEnd,
Resolution::new(4480, 1440).unwrap(),
initial_plan.clone(),
)
.unwrap();
assert_eq!(
pipeline.reconfigure(
Resolution::new(4480, 1440).unwrap(),
initial_plan.layout.clone()
),
Ok(false)
);
assert_eq!(
pipeline.reconfigure(
Resolution::new(2240, 720).unwrap(),
initial_plan.layout.clone()
),
Ok(true)
);
assert_eq!(pipeline.epoch(), 1);
assert_eq!(pipeline.state(), PipelineState::Revalidating);
pipeline.finish_revalidation(initial_plan).unwrap();
let changed_topology = topology(
8,
vec![
descriptor(1, -2560, 0, 2560, 1440, false),
descriptor(2, 0, 0, 2560, 1440, true),
],
);
let changed_plan = strict_plan(&changed_topology, &initial_sources);
assert_eq!(
pipeline.reconfigure(
Resolution::new(2240, 720).unwrap(),
changed_plan.layout.clone()
),
Ok(true)
);
assert_eq!(pipeline.epoch(), 2);
assert_eq!(pipeline.layout().generation(), 8);
assert!(pipeline.plan().is_none());
pipeline.finish_revalidation(changed_plan).unwrap();
assert_eq!(pipeline.state(), PipelineState::Active);
}
#[test]
fn stale_layout_proof_cannot_finish_revalidation() {
let old_topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let old_plan = strict_plan(&old_topology, &sources);
let mut pipeline = MultiDisplayPipeline::active(
ZeroCopyPolicy::RequiredEndToEnd,
Resolution::new(4480, 1440).unwrap(),
old_plan.clone(),
)
.unwrap();
let new_topology = topology(
8,
vec![
descriptor(1, -2560, 0, 2560, 1440, false),
descriptor(2, 0, 0, 2560, 1440, true),
],
);
let new_plan = strict_plan(&new_topology, &sources);
assert_eq!(
pipeline.reconfigure(Resolution::new(4480, 1440).unwrap(), new_plan.layout),
Ok(true)
);
assert_eq!(
pipeline.finish_revalidation(old_plan),
Err(MultiDisplayPipelineError::StaleLayout)
);
assert_eq!(pipeline.state(), PipelineState::Revalidating);
}
#[test]
fn generation_overflow_fails_closed_without_mutating_pipeline() {
let topology = two_display_topology(7);
let sources = [source(1, "gpu-0"), source(2, "gpu-0")];
let plan = strict_plan(&topology, &sources);
let mut pipeline = MultiDisplayPipeline::active(
ZeroCopyPolicy::RequiredEndToEnd,
Resolution::new(4480, 1440).unwrap(),
plan,
)
.unwrap();
pipeline.epoch = u64::MAX;
let original = pipeline.clone();
assert_eq!(
pipeline.reconfigure(Resolution::new(2240, 720).unwrap(), original.layout.clone()),
Err(MultiDisplayPipelineError::GenerationOverflow)
);
assert_eq!(pipeline, original);
}
}