Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ of new `Dipsatch2` and `GlobalDispatch2` traits. These will replace `Dispatch` a

- ExtBackgroundEffect protocol is now available in `smithay::wayland::background_effect` module.

- `DrmDevice::tile_info` exposes the parsed DRM `TILE` connector property as a new `backend::drm::TileInfo` struct, allowing compositors to identify connectors that belong to the same DisplayID-tiled monitor.

`crate::input::dnd` was introduced to enable implementation of Drag&Drop operations on custom types.
Internally the same types and traits are used to implement `wayland::data_device` dnd-operations and XDND
operations (see below).
Expand Down
171 changes: 171 additions & 0 deletions src/backend/drm/compositor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,177 @@ where
span: tracing::Span,
}

/// One CRTC's contribution to a tiled [`DrmCompositor`].
///
/// A tiled compositor drives several CRTCs as a single logical output, each
/// scanning out a sub-rect of that output. A `TilePlacement` pairs one
/// [`DrmSurface`] (one CRTC) with the planes it may use and the sub-rect it
/// covers. See [`DrmCompositor::new_tiled`].
#[derive(Debug)]
pub struct TilePlacement {
/// The surface (exactly one CRTC) backing this tile.
pub surface: DrmSurface,
/// Planes this tile may use for direct scan-out, with the same meaning as
/// the `planes` argument of [`DrmCompositor::new`]: `None` uses all planes
/// reported by [`DrmSurface::planes`]. Planes belong to a CRTC, so they are
/// configured per tile.
pub planes: Option<Planes>,
/// The sub-rect of the logical output this tile scans out, in the output's
/// untransformed (mode) coordinate space. The offset positions the tile
/// within the logical framebuffer; the size is expected to match
/// `surface`'s scan-out mode.
pub region: Rectangle<i32, Physical>,
}

/// Reason a set of tile regions does not form a valid logical output.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TileConfigError {
/// There must be at least one tile.
#[error("a tiled output must contain at least one tile")]
Empty,
/// A tile region has a non-positive width or height.
#[error("tile region {0:?} has a non-positive size")]
NonPositiveRegion(Rectangle<i32, Physical>),
/// Two tile regions overlap.
#[error("tile regions {0:?} and {1:?} overlap")]
OverlappingRegions(Rectangle<i32, Physical>, Rectangle<i32, Physical>),
/// The tile regions leave a gap or are not anchored at the origin, so they
/// do not cover a single rectangular logical output.
#[error("tile regions do not cover the logical output without gaps")]
NotContiguous,
}

/// Validate that `regions` exactly tile a rectangle anchored at `(0, 0)` with no
/// gaps or overlaps, returning that rectangle's size.
///
/// Pure geometry, independent of any DRM state, so it is unit-testable.
// First non-test caller is `new_tiled`, added in a following commit.
#[allow(dead_code)]
fn validate_tile_regions(
regions: impl IntoIterator<Item = Rectangle<i32, Physical>>,
) -> Result<Size<i32, Physical>, TileConfigError> {
let regions: SmallVec<[_; 4]> = regions.into_iter().collect();
let Some(first) = regions.first() else {
return Err(TileConfigError::Empty);
};

// Bounding box of all regions, rejecting any non-positive tile on the way.
let mut bbox = *first;
let mut covered: i64 = 0;
for region in &regions {
if region.size.w <= 0 || region.size.h <= 0 {
return Err(TileConfigError::NonPositiveRegion(*region));
}
bbox = bbox.merge(*region);
covered += i64::from(region.size.w) * i64::from(region.size.h);
}

// The logical output starts at the origin.
if bbox.loc != Point::from((0, 0)) {
Comment thread
algesten marked this conversation as resolved.
Outdated
return Err(TileConfigError::NotContiguous);
}

// No two tiles may overlap (O(n²); tile counts are tiny). `overlaps` is
// exclusive, so tiles sharing an edge are adjacent, not overlapping.
for (i, a) in regions.iter().enumerate() {
for b in &regions[i + 1..] {
if a.overlaps(*b) {
return Err(TileConfigError::OverlappingRegions(*a, *b));
}
}
}

// With overlaps ruled out, the tiles cover the bounding box exactly iff
// their areas sum to the box area — otherwise there is a gap.
let bbox_area = i64::from(bbox.size.w) * i64::from(bbox.size.h);
if covered != bbox_area {
return Err(TileConfigError::NotContiguous);
}

Ok(bbox.size)
}

#[cfg(test)]
mod tiled_tests {
use super::{TileConfigError, validate_tile_regions};
use crate::utils::{Physical, Point, Rectangle, Size};

fn rect(x: i32, y: i32, w: i32, h: i32) -> Rectangle<i32, Physical> {
Rectangle::new(Point::from((x, y)), Size::from((w, h)))
}

#[test]
fn single_tile_is_the_whole_output() {
assert_eq!(
validate_tile_regions([rect(0, 0, 5120, 2880)]),
Ok(Size::from((5120, 2880)))
);
}

#[test]
fn two_tiles_side_by_side() {
// LG UltraFine 5K: two 2560×2880 halves.
assert_eq!(
validate_tile_regions([rect(0, 0, 2560, 2880), rect(2560, 0, 2560, 2880)]),
Ok(Size::from((5120, 2880)))
);
}

#[test]
fn two_by_two_grid() {
assert_eq!(
validate_tile_regions([
rect(0, 0, 1920, 1080),
rect(1920, 0, 1920, 1080),
rect(0, 1080, 1920, 1080),
rect(1920, 1080, 1920, 1080),
]),
Ok(Size::from((3840, 2160)))
);
}

#[test]
fn empty_is_rejected() {
assert_eq!(validate_tile_regions([]), Err(TileConfigError::Empty));
}

#[test]
fn overlapping_tiles_are_rejected() {
let a = rect(0, 0, 2600, 2880);
let b = rect(2560, 0, 2560, 2880);
assert_eq!(
validate_tile_regions([a, b]),
Err(TileConfigError::OverlappingRegions(a, b))
);
}

#[test]
fn gap_between_tiles_is_rejected() {
// 40px gap between the two halves.
assert_eq!(
validate_tile_regions([rect(0, 0, 2560, 2880), rect(2600, 0, 2560, 2880)]),
Err(TileConfigError::NotContiguous)
);
}

#[test]
fn not_anchored_at_origin_is_rejected() {
assert_eq!(
validate_tile_regions([rect(10, 0, 2560, 2880), rect(2570, 0, 2560, 2880)]),
Err(TileConfigError::NotContiguous)
);
}

#[test]
fn non_positive_region_is_rejected() {
let bad = rect(0, 0, 0, 2880);
assert_eq!(
validate_tile_regions([bad]),
Err(TileConfigError::NonPositiveRegion(bad))
);
}
}

impl<A, F, U, G> DrmCompositor<A, F, U, G>
where
A: Allocator,
Expand Down
10 changes: 9 additions & 1 deletion src/backend/drm/device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::utils::{Buffer, DevPath, Size};

use super::error::AccessError;
use super::surface::{DrmSurface, DrmSurfaceInternal, atomic::AtomicDrmSurface, legacy::LegacyDrmSurface};
use super::{Planes, error::Error, planes};
use super::{Planes, TileInfo, error::Error, planes, tile_info};
use atomic::AtomicDrmDevice;
use legacy::LegacyDrmDevice;

Expand Down Expand Up @@ -276,6 +276,14 @@ impl DrmDevice {
planes(self, crtc, self.has_universal_planes)
}

/// Returns the parsed `TILE` property of a connector, if set.
///
/// Tiled monitors (DisplayID Tile Topology) expose multiple connectors
/// that share a [`TileInfo::group_id`].
pub fn tile_info(&self, conn: connector::Handle) -> Result<Option<TileInfo>, Error> {
tile_info(self, conn)
}

/// Claim a plane so that it won't be used by a different crtc
///
/// Returns `None` if the plane could not be claimed
Expand Down
161 changes: 160 additions & 1 deletion src/backend/drm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub use surface::{DrmSurface, PlaneConfig, PlaneDamageClips, PlaneState, VrrSupp

use drm::{
DriverCapability,
control::{Device as ControlDevice, PlaneType, crtc, framebuffer, plane},
control::{Device as ControlDevice, PlaneType, connector, crtc, framebuffer, plane},
};
use tracing::trace;

Expand Down Expand Up @@ -515,3 +515,162 @@ fn plane_size_hints(
}
Ok(None)
}

/// Information from a connector's DRM `TILE` property
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TileInfo {
/// Tile-group identifier shared by all connectors of the same monitor
pub group_id: u32,
/// Total horizontal tile count in the group
pub num_h_tiles: u8,
/// Total vertical tile count in the group
pub num_v_tiles: u8,
/// Horizontal location of this tile, `0..num_h_tiles`
pub loc_h: u8,
/// Vertical location of this tile, `0..num_v_tiles`
pub loc_v: u8,
/// Native horizontal pixel size of this tile
pub tile_w: u16,
/// Native vertical pixel size of this tile
pub tile_h: u16,
}

fn tile_info(
dev: &(impl ControlDevice + DevPath),
conn: connector::Handle,
) -> Result<Option<TileInfo>, DrmError> {
let props = dev.get_properties(conn).map_err(|source| {
DrmError::Access(AccessError {
errmsg: "Failed to get properties of connector",
dev: dev.dev_path(),
source,
})
})?;
let (ids, vals) = props.as_props_and_values();
for (&id, &val) in ids.iter().zip(vals.iter()) {
let info = dev.get_property(id).map_err(|source| {
DrmError::Access(AccessError {
errmsg: "Failed to get property info",
dev: dev.dev_path(),
source,
})
})?;
if info.name().to_str().map(|x| x == "TILE").unwrap_or(false) {
let tile_info =
if let drm::control::property::Value::Blob(blob_id) = info.value_type().convert_value(val) {
// Value 0 means "no blob set" — the kernel uses this for
// non-tiled connectors that nonetheless expose the property.
if blob_id == 0 {
return Ok(None);
}
let data = dev.get_property_blob(blob_id).map_err(|source| {
DrmError::Access(AccessError {
errmsg: "Failed to read TILE property blob",
dev: dev.dev_path(),
source,
})
})?;
parse_tile_blob(&data)
} else {
tracing::debug!(?conn, "TILE property has wrong value type");
None
};
return Ok(tile_info);
}
}
Ok(None)
}

// Format written by the kernel in drm_connector_set_tile_property: eight
// unsigned decimal fields separated by ':', in the order
// group_id:flags:num_h_tiles:num_v_tiles:loc_h:loc_v:tile_w:tile_h.
fn parse_tile_blob(data: &[u8]) -> Option<TileInfo> {
// The blob is a NUL-terminated C string; trim trailing NULs before parsing.
let s = std::str::from_utf8(data).ok()?.trim_end_matches('\0').trim();

let mut parts = s.split(':');
let group_id: u32 = parts.next()?.parse().ok()?;
// Field 2 is the kernel ABI "flags" placeholder; currently always 0 and unused.
let _flags: u32 = parts.next()?.parse().ok()?;
let num_h_tiles: u8 = parts.next()?.parse().ok()?;
let num_v_tiles: u8 = parts.next()?.parse().ok()?;
let loc_h: u8 = parts.next()?.parse().ok()?;
let loc_v: u8 = parts.next()?.parse().ok()?;
let tile_w: u16 = parts.next()?.parse().ok()?;
let tile_h: u16 = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}

// group_id == 0 is the kernel sentinel for "no tile info".
if group_id == 0 || num_h_tiles == 0 || num_v_tiles == 0 {
return None;
}
if loc_h >= num_h_tiles || loc_v >= num_v_tiles {
return None;
}

Some(TileInfo {
group_id,
num_h_tiles,
num_v_tiles,
loc_h,
loc_v,
tile_w,
tile_h,
})
}

#[cfg(test)]
mod test {
use super::{TileInfo, parse_tile_blob};

#[test]
fn parse_tile_blob_kernel_format() {
let blob = b"305419896:0:2:1:1:0:2560:2880";
assert_eq!(
parse_tile_blob(blob),
Some(TileInfo {
group_id: 0x12345678,
num_h_tiles: 2,
num_v_tiles: 1,
loc_h: 1,
loc_v: 0,
tile_w: 2560,
tile_h: 2880,
})
);
}

#[test]
fn parse_tile_blob_tolerates_nul_padding() {
let info = parse_tile_blob(b"42:0:2:1:0:0:2560:2880\0\0\0").unwrap();
assert_eq!(info.group_id, 42);
}

#[test]
fn parse_tile_blob_rejects_zero_group_id() {
assert_eq!(parse_tile_blob(b"0:0:2:1:0:0:2560:2880"), None);
}

#[test]
fn parse_tile_blob_rejects_zero_tile_count() {
assert_eq!(parse_tile_blob(b"1:0:0:1:0:0:2560:2880"), None);
assert_eq!(parse_tile_blob(b"1:0:2:0:0:0:2560:2880"), None);
}

#[test]
fn parse_tile_blob_rejects_out_of_range_location() {
assert_eq!(parse_tile_blob(b"1:0:2:1:2:0:2560:2880"), None);
assert_eq!(parse_tile_blob(b"1:0:2:1:0:1:2560:2880"), None);
}

#[test]
fn parse_tile_blob_rejects_malformed() {
assert_eq!(parse_tile_blob(b""), None);
assert_eq!(parse_tile_blob(b"not:a:tile:blob"), None);
assert_eq!(parse_tile_blob(b"1:0:2:1:0:0:2560"), None);
assert_eq!(parse_tile_blob(b"1:0:2:1:0:0:2560:2880:99"), None);
assert_eq!(parse_tile_blob(b"1,0,2,1,0,0,2560,2880"), None);
}
}