Consume the new connector detect()/mode_valid() and plane damage-clip
bindings, and finish the KMS feature set the driver previously stubbed:
- Two display heads. probe() builds an independent CRTC + primary/cursor
plane + encoder + connector per head (see HEADS), each pinned to its
own video endpoint (VIDEO_EPS: head 0 -> EP08, head 1 -> EP0b) via a
head index carried in VinoPlane. The scanout and cursor CP messages
route by that head; per-head EDID uses a per-head connector pointer
array. The CP mode-set has no decoded head field, so a head is
conveyed on the wire only by which endpoint its frames go to.
- 90/270 plane rotation. The rotation property now advertises all four
90-degree rotations plus reflection; the scanout maps each output
pixel back to its source pixel with rot_src(), and src_dims() swaps
the source/output dimensions for 90/270 (rot_src itself was already
correct given source dimensions -- only its callers were feeding it
output dims).
- Damage clips. The RLE scanout takes the old+new plane state, merges
the client's damage clips (RawPlaneState::damage_merged) into one
rectangle, and re-converts only that region into the shadow the
encoder diffs against (identity rotation only; the WHT keyframe path
is unaffected).
- Connector detect()/mode_valid(). A head reports connected once its
downstream EDID arrives (no phantom output for an unpopulated head),
and modes above MAX_HEAD_CLOCK_KHZ (~4K@60) are pruned.
Still inert until the dock engages CP (see docs/BLOCKER.md).
Signed-off-by: Mike Lothian <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
drivers/gpu/drm/vino/drm_sink.rs | 426 +++++++++++++++++++++----------
drivers/gpu/drm/vino/vino.rs | 8 +-
2 files changed, 292 insertions(+), 142 deletions(-)
diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
index 898cfae8fd4f..ee04a5af9f7d 100644
--- a/drivers/gpu/drm/vino/drm_sink.rs
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -2,31 +2,38 @@
//! DRM/KMS sink: register a real `struct drm_device` with an atomic
mode-setting
//! pipeline so the dock appears to userspace as a `card`/`renderD` node that
can be
-//! `drmModeSetCrtc`'d. One CRTC driven by a single primary plane
-//! ([`VinoPlane::atomic_update`] -> EP08 scanout), a virtual encoder, and a
virtual
-//! connector whose mode list comes from the dock's real EDID (falling back to
1080p),
-//! with GEM-shmem dumb buffers and `drm_gem_fb_create` framebuffers.
+//! `drmModeSetCrtc`'d. Two independent display heads (see [`HEADS`]), each a
CRTC driven by a
+//! primary plane ([`VinoPlane::atomic_update`] -> per-head video endpoint
scanout), a cursor plane,
+//! a virtual encoder, and a virtual connector whose mode list comes from the
dock's real EDID
+//! (falling back to 1080p), with GEM-shmem dumb buffers and
`drm_gem_fb_create` framebuffers.
//!
//! Built on the safe KMS mode-object layer (`kernel::drm::kms`), not the raw
//! `bindings::drm_*` C API: `VinoDrmDriver` implements `drm::kms::KmsDriver`,
and each
//! mode object (`VinoCrtc`/`VinoPlane`/`VinoConnector`/`VinoEncoder`)
implements the
//! matching `Driver*` trait rather than hand-assembling a C vtable.
//!
-//! Wired onto the safe KMS layer: primary plane (EP08 scanout), a
`Type::Cursor` plane (bitmap +
-//! position forwarded via `cp::cursor_{create,image,move}`), a 256-entry CRTC
`GAMMA_LUT` (applied
-//! in the scanout), a primary-plane rotation property (0/180 + reflect,
applied per source pixel
-//! via `rot_src`), and a DDC/CI virtual I2C adapter ([`VinoI2c`], tunnelling
monitor-control writes
-//! to the dock over CP -- brightness/contrast/etc. via `ddcutil`), alongside
the DPMS-power VCP the
-//! CRTC hooks already send.
+//! Wired onto the safe KMS layer:
+//! - Per-head primary plane scanout to that head's video endpoint
([`VIDEO_EPS`]) and a
+//! `Type::Cursor` plane (bitmap + position forwarded via
`cp::cursor_{create,image,move}` with
+//! the head as the CP `head` field).
+//! - A 256-entry CRTC `GAMMA_LUT` (applied in the scanout) and a full plane
rotation property
+//! (all four 90-degree rotations plus X/Y reflection), applied per source
pixel via `rot_src`
+//! (90/270 swap the source/output dimensions -- see [`src_dims`]).
+//! - Frame-damage clips: the RLE scanout re-converts only the client's
changed rectangles
+//! (`RawPlaneState::for_each_damage_clip`) for identity rotation.
+//! - Connector `detect()` (connected once the head's EDID arrives) and
`mode_valid()` (prune modes
+//! above [`MAX_HEAD_CLOCK_KHZ`]).
+//! - A DDC/CI virtual I2C adapter ([`VinoI2c`], tunnelling monitor-control
writes to the dock over
+//! CP -- brightness/contrast/etc. via `ddcutil`), alongside the DPMS-power
VCP the CRTC hooks
+//! send.
//!
-//! Not yet ported (needs `kernel::drm::kms` extension points that don't exist
yet; not fabricated):
-//! - A second display head (the DL3 protocol supports up to 4; one is wired
here).
-//! `VinoPlane`/`VinoCrtc` hold their state inline, so a head is a second
`probe()` call away.
-//! - 90/270 rotation (swaps width/height, unlike the dimension-preserving
rotations above).
+//! Not yet done (needs hardware capture past the CP wall; not fabricated):
+//! - Per-head EDID reads (the bring-up reads head 0's EDID only) and per-head
mode-set / DDC
+//! differentiation on the wire -- the CP mode-set (`id=0x48`) has no
decoded head/stream field,
+//! so the head is conveyed only by which video endpoint its frames go to.
+//! - The WHT keyframe codec ignores damage clips (strip-based); damage
applies to the RLE path.
//! - DDC/CI *reads* (Get-VCP) -- need the dock's CP reply path; and
brightness/contrast as
-//! *connector properties* (the I2C adapter above is the interface for now).
-//! - The `.detect`/`mode_valid` connector hooks -- `DriverConnector` only
exposes `get_modes`.
-//! - Damage-clip bounded conversion (always converts the full frame).
+//! *connector properties* (the I2C adapter is the interface for now).
//!
//! None of this is reachable on real hardware yet regardless: the dock never
engages
//! its content-protection channel for vino (see `docs/BLOCKER.md`), so
`atomic_update`
@@ -36,9 +43,10 @@
bindings, drm,
drm::kms::{
self,
- connector::{self, ConnectorGuard},
+ connector::{self, Connector, ConnectorGuard, ModeStatus, Status},
crtc::{self, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
encoder,
+ modes::DisplayMode,
plane::{self, PlaneAtomicCommit, RawPlaneState as _},
KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _,
NewKmsDevice, Probing,
},
@@ -65,16 +73,24 @@
/// Cursor-plane format list.
static CURSOR_FORMATS: [u32; 1] = [DRM_FORMAT_ARGB8888];
-/// Per-mode pixel-clock ceiling (kHz) -- about 4K@60 (CEA 594 MHz). With only
one head wired
-/// there is no combined-heads budget to enforce (see the module doc); a real
ceiling still
-/// needs the connector `mode_valid` hook this port doesn't expose yet, so
this constant is
-/// currently unused wiring for when it does.
-#[allow(dead_code)]
+/// Per-mode pixel-clock ceiling (kHz) -- about 4K@60 (CEA 594 MHz). Modes
above this are pruned
+/// by the connector `mode_valid` hook ([`VinoConnector::mode_valid`]).
const MAX_HEAD_CLOCK_KHZ: i32 = 600_000;
-/// The one wired display head's video bulk-OUT endpoint (see the module doc
-- only
-/// head 0 is wired for now).
-const VIDEO_EP: u8 = 0x08;
+/// Number of display heads wired. The DL3 protocol supports up to 4; two are
wired here (the
+/// common dual-monitor dock), each scanned out to its own video endpoint
([`VIDEO_EPS`]).
+const HEADS: usize = 2;
+
+/// Per-head video bulk-OUT endpoint: head 0 -> EP08, head 1 -> EP0b (captured
from DLM driving a
+/// two-monitor dock). The endpoint is the head selector for scanout; the
cursor uses the CP `head`
+/// field (see [`VinoPlane::atomic_update`]).
+const VIDEO_EPS: [u8; HEADS] = [0x08, 0x0b];
+
+/// Maximum number of individual frame-damage rectangles re-converted per flip
before they are
+/// collapsed into a single bounding box. Bounds the stack array used on the
atomic-commit path
+/// (no per-flip allocation); a compositor that reports more clips than this
just gets a coarser
+/// (still correct) repaint.
+const MAX_DAMAGE_CLIPS: usize = 16;
/// The DRM driver marker type.
pub(super) struct VinoDrmDriver;
@@ -123,10 +139,11 @@ pub(super) struct VinoDrmData {
intf: ARef<super::usb::Interface>,
#[pin]
cp_link: Mutex<Option<CpLink>>,
- /// The device's one connector, stashed by [`KmsDriver::probe`] so
[`VinoDrmData::set_edid`]
- /// can reach its cached-EDID slot without needing DRM's mode-object list.
Written once,
+ /// The device's per-head connectors, stashed by [`KmsDriver::probe`] so
+ /// [`VinoDrmData::set_edid`] can reach a head's cached-EDID slot without
needing DRM's
+ /// mode-object list. Written once,
/// during single-threaded probe, before the device is registered;
read-only thereafter.
- connector: core::sync::atomic::AtomicPtr<VinoConnector>,
+ connectors: [core::sync::atomic::AtomicPtr<VinoConnector>; HEADS],
/// The CRTC's gamma ramp cached from the atomic hook as three 256-entry
8-bit LUTs
/// (`[r; 256] ++ [g; 256] ++ [b; 256]`), or `None` for identity. Cached
here (not read from
/// the CRTC state) because scanout runs in the plane path; it is `Copy`,
so the scanout
@@ -140,7 +157,9 @@ pub(super) fn new(intf: ARef<super::usb::Interface>) ->
impl PinInit<Self, Error
try_pin_init!(Self {
intf,
cp_link <- new_mutex!(Option::<CpLink>::None),
- connector:
core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
+ connectors: core::array::from_fn(|_| {
+ core::sync::atomic::AtomicPtr::new(core::ptr::null_mut())
+ }),
gamma <- new_mutex!(None),
})
}
@@ -210,12 +229,14 @@ pub(super) fn set_vcp(&self, vcp: u8, value: u16) ->
Result {
self.send_cp(0x15, 0, |ctr| super::cp::ddc_set_vcp(ctr, vcp, value))
}
- /// Cache the dock's EDID (read during probe) for the connector's
`get_modes` to install,
- /// then fire a hotplug so the compositor re-probes the connector. Only
the connector
- /// itself holds the cached blob (see [`VinoConnector::cached_edid`]);
this just forwards
- /// it there via the pointer [`KmsDriver::probe`] stashed in
`self.connector`.
- pub(super) fn set_edid(&self, dev: &VinoDrmDevice, blob: KVec<u8>) {
- let ptr = self.connector.load(core::sync::atomic::Ordering::Acquire);
+ /// Cache a head's downstream EDID (read during probe) for that
connector's `get_modes` to
+ /// install, then fire a hotplug so the compositor re-probes it. Only the
connector itself
+ /// holds the cached blob (see [`VinoConnector::cached_edid`]); this
forwards it there via the
+ /// pointer [`KmsDriver::probe`] stashed in `self.connectors[head]`.
Out-of-range `head` is a
+ /// no-op.
+ pub(super) fn set_edid(&self, dev: &VinoDrmDevice, head: usize, blob:
KVec<u8>) {
+ let Some(slot) = self.connectors.get(head) else { return };
+ let ptr = slot.load(core::sync::atomic::Ordering::Acquire);
let Some(connector) = (unsafe { ptr.as_ref() }) else { return };
*connector.cached_edid.lock() = Some(blob);
dev.hotplug_event();
@@ -319,69 +340,77 @@ fn mode_config_info(_dev: &drm::Device<Self,
drm::Uninit>) -> Result<ModeConfigI
}
fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
- // Order matters: `possible_crtcs` for the plane/encoder is a bitmask
of CRTC
- // *indices*, which only exist once `UnregisteredCrtc::new` runs --
but planes
- // must exist before the CRTC that references them. With exactly one
CRTC ever
- // created here, its index is always 0, so `possible_crtcs = 1` is
correct by
- // construction rather than needing the CRTC up front.
- let primary = plane::UnregisteredPlane::<VinoPlane>::new(
- dev,
- 1,
- &PRIMARY_FORMATS,
- None,
- plane::Type::Primary,
- None,
- false,
- )?;
- // Advertise the rotations vino's re-encode handles 1:1
(dimension-preserving): the
- // scanout applies them per source pixel via `rot_src`. 90/270 (which
swap width/height)
- // are intentionally omitted.
- primary.create_rotation_property(
- bindings::DRM_MODE_ROTATE_0,
- bindings::DRM_MODE_ROTATE_0
- | bindings::DRM_MODE_ROTATE_180
- | bindings::DRM_MODE_REFLECT_X
- | bindings::DRM_MODE_REFLECT_Y,
- )?;
- let cursor = plane::UnregisteredPlane::<VinoPlane>::new(
- dev,
- 1,
- &CURSOR_FORMATS,
- None,
- plane::Type::Cursor,
- None,
- true,
- )?;
- let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
- dev,
- primary,
- Some(&cursor),
- None,
- (),
- )?;
- // Advertise a 256-entry GAMMA_LUT; the scanout applies it (cached via
the CRTC hooks).
- crtc_obj.enable_gamma(256);
- let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
- dev,
- encoder::Type::Virtual,
- crtc_obj.mask(),
- 0,
- None,
- (),
- )?;
- let conn =
- connector::UnregisteredConnector::<VinoConnector>::new(dev,
connector::Type::Virtual, ())?;
- conn.attach_encoder(&*enc)?;
- // Stash a pointer to our own connector data (not the wrapping
`Connector<T>`, which
- // this crate has no public way to reconstruct from a reference) so
- // `VinoDrmData::set_edid` can reach it later without walking DRM's
mode-object list.
- // `conn` outlives the device (destroyed only alongside it), so this
is valid for as
- // long as `dev.connector` is read.
let data: &VinoDrmData = dev;
- data.connector.store(
- &**conn as *const VinoConnector as *mut VinoConnector,
- core::sync::atomic::Ordering::Release,
- );
+ // Build one independent head (CRTC + primary/cursor plane + encoder +
connector) per
+ // wired display, each pinned to its own video endpoint via its head
index.
+ for head in 0..HEADS {
+ // `possible_crtcs` for the plane/encoder is a bitmask of CRTC
*indices*, which only
+ // exist once `UnregisteredCrtc::new` runs -- but planes must
exist before the CRTC
+ // that references them. CRTCs are created here one per head in
order, so this head's
+ // CRTC index is `head` and its mask is `1 << head`.
+ let crtc_mask = 1u32 << head;
+ let primary = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ crtc_mask,
+ &PRIMARY_FORMATS,
+ None,
+ plane::Type::Primary,
+ None,
+ PlaneArgs { head: head as u8, is_cursor: false },
+ )?;
+ // Advertise every rotation vino's re-encode can produce by
remapping source pixels
+ // (`rot_src`): the four 90-degree rotations plus the two
reflections.
+ primary.create_rotation_property(
+ bindings::DRM_MODE_ROTATE_0,
+ bindings::DRM_MODE_ROTATE_0
+ | bindings::DRM_MODE_ROTATE_90
+ | bindings::DRM_MODE_ROTATE_180
+ | bindings::DRM_MODE_ROTATE_270
+ | bindings::DRM_MODE_REFLECT_X
+ | bindings::DRM_MODE_REFLECT_Y,
+ )?;
+ let cursor = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ crtc_mask,
+ &CURSOR_FORMATS,
+ None,
+ plane::Type::Cursor,
+ None,
+ PlaneArgs { head: head as u8, is_cursor: true },
+ )?;
+ let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
+ dev,
+ primary,
+ Some(&cursor),
+ None,
+ head as u8,
+ )?;
+ // Advertise a 256-entry GAMMA_LUT; the scanout applies it (cached
via the CRTC hooks).
+ crtc_obj.enable_gamma(256);
+ let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
+ dev,
+ encoder::Type::Virtual,
+ crtc_obj.mask(),
+ 0,
+ None,
+ (),
+ )?;
+ let conn = connector::UnregisteredConnector::<VinoConnector>::new(
+ dev,
+ connector::Type::Virtual,
+ (),
+ )?;
+ conn.attach_encoder(&*enc)?;
+ // Stash a pointer to our own connector data (not the wrapping
`Connector<T>`, which
+ // this crate has no public way to reconstruct from a reference) so
+ // `VinoDrmData::set_edid` can reach it later without walking
DRM's mode-object list.
+ // `conn` outlives the device (destroyed only alongside it), so
this is valid for as
+ // long as `dev.connectors[head]` is read.
+ data.connectors[head].store(
+ &**conn as *const VinoConnector as *mut VinoConnector,
+ core::sync::atomic::Ordering::Release,
+ );
+ }
Ok(())
}
}
@@ -389,7 +418,11 @@ fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
// ---- CRTC -----------------------------------------------------------------
#[pin_data]
-pub(super) struct VinoCrtc;
+pub(super) struct VinoCrtc {
+ /// Which display head (0-based) this CRTC drives. Used for diagnostics;
the mode-set/DDC CP
+ /// messages this CRTC sends are not yet head-differentiated on the wire
(see the module doc).
+ head: u8,
+}
#[derive(Clone, Default)]
pub(super) struct VinoCrtcState;
@@ -400,26 +433,32 @@ impl crtc::DriverCrtcState for VinoCrtcState {
#[vtable]
impl crtc::DriverCrtc for VinoCrtc {
- type Args = ();
+ type Args = u8;
type Driver = VinoDrmDriver;
type State = VinoCrtcState;
type VblankImpl = core::marker::PhantomData<Self>;
- fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: &()) ->
impl PinInit<Self, Error> {
- try_pin_init!(VinoCrtc {})
+ fn new(
+ _device: &drm::Device<Self::Driver, drm::Uninit>,
+ head: &u8,
+ ) -> impl PinInit<Self, Error> {
+ try_pin_init!(VinoCrtc { head: *head })
}
/// The display is turning on (scanout begins). Pushes a live mode-set CP
message for the
/// negotiated mode and brings the monitor out of DPMS standby -- both
no-ops until CP
/// engages (the wall).
fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
- let data: &VinoDrmData = commit.crtc().drm_dev();
+ let crtc = commit.crtc();
+ let head = crtc.head;
+ let data: &VinoDrmData = crtc.drm_dev();
let new = commit.take_new_state();
// Cache the gamma ramp for the scanout to apply.
data.update_gamma(new.gamma_lut());
let timing = super::cp::timing_from_drm_mode(new.mode());
pr_info!(
- "vino: KMS CRTC enable -- display ON, mode {}x{}@{} (scanout
begins)\n",
+ "vino: KMS CRTC enable -- head {} display ON, mode {}x{}@{}
(scanout begins)\n",
+ head,
timing.hactive,
timing.vactive,
timing.refresh_hz
@@ -435,10 +474,12 @@ fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
/// against a shadow the dock may have dropped, and blanks the monitor via
DDC/CI -- a
/// no-op until CP engages.
fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
- let data: &VinoDrmData = commit.crtc().drm_dev();
+ let crtc = commit.crtc();
+ let head = crtc.head;
+ let data: &VinoDrmData = crtc.drm_dev();
data.update_gamma(None);
let _ = data.set_vcp(super::cp::VCP_POWER_MODE, super::cp::POWER_OFF);
- pr_info!("vino: KMS CRTC disable -- display OFF (scanout stopped)\n");
+ pr_info!("vino: KMS CRTC disable -- head {head} display OFF (scanout
stopped)\n");
}
}
@@ -447,10 +488,21 @@ fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
// The safe KMS layer allows one `DriverPlane` type per driver, so `VinoPlane`
serves both the
// primary and cursor planes, told apart by `is_cursor` (from the plane's
`Args`).
+/// Constructor arguments for a [`VinoPlane`]: which head it belongs to and
whether it is that
+/// head's cursor plane (vs. its primary scanout plane).
+#[derive(Clone, Copy)]
+pub(super) struct PlaneArgs {
+ head: u8,
+ is_cursor: bool,
+}
+
#[pin_data]
pub(super) struct VinoPlane {
#[pin]
scanout: Mutex<ScanoutState>,
+ /// Which display head (0-based) this plane belongs to. Selects the
scanout video endpoint
+ /// ([`VIDEO_EPS`]) and the cursor CP `head` field.
+ head: u8,
/// Whether this is the cursor plane (vs. the primary scanout plane).
is_cursor: bool,
/// The framebuffer last uploaded to the dock as the cursor bitmap (raw
address, `0` = none),
@@ -467,11 +519,14 @@ impl plane::DriverPlaneState for VinoPlaneState {
#[vtable]
impl plane::DriverPlane for VinoPlane {
- type Args = bool;
+ type Args = PlaneArgs;
type Driver = VinoDrmDriver;
type State = VinoPlaneState;
- fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, is_cursor: bool)
-> impl PinInit<Self, Error> {
+ fn new(
+ _device: &drm::Device<Self::Driver, drm::Uninit>,
+ args: PlaneArgs,
+ ) -> impl PinInit<Self, Error> {
try_pin_init!(VinoPlane {
scanout <- new_mutex!(ScanoutState {
enc: None,
@@ -480,7 +535,8 @@ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>,
is_cursor: bool) -> imp
dims: (0, 0),
hint: 0,
}),
- is_cursor,
+ head: args.head,
+ is_cursor: args.is_cursor,
cursor_last: core::sync::atomic::AtomicUsize::new(0),
})
}
@@ -498,14 +554,14 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
return;
}
let plane = commit.plane();
+ let head = plane.head;
let data: &VinoDrmData = plane.drm_dev();
- let new = commit.take_new_state();
// Cursor plane: forward the cursor bitmap/position to the dock over
CP (id=0x1b create,
// 0x401c image, 0x1a move -- see `cp::cursor_*`). A no-op until CP
engages, like scanout.
if plane.is_cursor {
use core::sync::atomic::Ordering::Relaxed;
- const HEAD: u8 = 0;
+ let new = commit.take_new_state();
match new.framebuffer::<VinoDrmDriver>() {
Some(fb) => {
let w = fb.width() as u16;
@@ -514,15 +570,17 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
// position (a bare cursor move must not re-send the whole
image).
let key = fb as *const _ as usize;
if plane.cursor_last.swap(key, Relaxed) != key {
- let _ = data.send_cp(0x1b, 0, |ctr|
super::cp::cursor_create(ctr, HEAD, w, h));
+ let _ =
+ data.send_cp(0x1b, 0, |ctr|
super::cp::cursor_create(ctr, head, w, h));
if let Ok(bgra) = read_cursor_bgra(fb, w as usize, h
as usize) {
- let _ = data
- .send_cp(0x401c, 0, |ctr|
super::cp::cursor_image(ctr, HEAD, w, h, &bgra));
+ let _ = data.send_cp(0x401c, 0, |ctr| {
+ super::cp::cursor_image(ctr, head, w, h, &bgra)
+ });
}
}
let x = new.crtc_x().max(0) as u16;
let y = new.crtc_y().max(0) as u16;
- let _ = data.send_cp(0x1a, 0, |ctr|
super::cp::cursor_move(ctr, HEAD, x, y));
+ let _ = data.send_cp(0x1a, 0, |ctr|
super::cp::cursor_move(ctr, head, x, y));
}
// Cursor disabled: forget the last bitmap so it re-uploads if
it comes back.
None => plane.cursor_last.store(0, Relaxed),
@@ -530,6 +588,8 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
return;
}
+ // Primary plane: take both old and new state so the frame-damage
clips can be merged.
+ let (old, new) = commit.take_old_new_state();
let Some(fb) = new.framebuffer::<VinoDrmDriver>() else { return };
// The plane's destination geometry mirrors the negotiated mode (the
compositor sizes the
// primary plane 1:1 with the virtual output), so this drives the
dynamic scanout
@@ -537,6 +597,37 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
let (w, h) = (new.crtc_w() as usize, new.crtc_h() as usize);
// Plane rotation/reflection (identity unless the compositor set the
rotation property).
let rotation = new.rotation();
+ // Collect the client's individual frame-damage clips (the rectangles
that
+ // `damage_merged()` would collapse into one bounding box), each
clamped to the output, so
+ // only the genuinely changed rectangles are re-converted from the
source rather than their
+ // whole enclosing box. Only for identity rotation (the clips are in
un-rotated source
+ // space; mapping them through 90/270 is not worth it for the
throttled fallback path), and
+ // never on the WHT keyframe path -- see `encode_and_send`. A fixed
stack array keeps the
+ // atomic-commit path allocation-free; on overflow the clips collapse
into one bounding box.
+ // An empty list means "convert the whole output" (used for the
rotated/reflected case).
+ let mut clips = [(0usize, 0usize, 0usize, 0usize); MAX_DAMAGE_CLIPS];
+ let mut nclips = 0usize;
+ if rotation & bindings::DRM_MODE_ROTATE_MASK ==
bindings::DRM_MODE_ROTATE_0
+ && rotation & (bindings::DRM_MODE_REFLECT_X |
bindings::DRM_MODE_REFLECT_Y) == 0
+ {
+ new.for_each_damage_clip(old, |r| {
+ let c = (
+ (r.x1.max(0) as usize).min(w),
+ (r.y1.max(0) as usize).min(h),
+ (r.x2.max(0) as usize).min(w),
+ (r.y2.max(0) as usize).min(h),
+ );
+ if nclips < MAX_DAMAGE_CLIPS {
+ clips[nclips] = c;
+ nclips += 1;
+ } else {
+ // Overflow: collapse everything so far (and `c`) into
clips[0]'s bounding box.
+ let b = clips[0];
+ clips[0] = (b.0.min(c.0), b.1.min(c.1), b.2.max(c.2),
b.3.max(c.3));
+ nclips = 1;
+ }
+ });
+ }
use core::sync::atomic::Ordering::Relaxed;
// Throttle: while scanout is failing (dock NAKing because CP isn't
engaged), skip the
@@ -547,7 +638,7 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
super::SCANOUT_SKIP.store(skip - 1, Relaxed);
return;
}
- match scanout_one(data, plane, fb, rotation, w, h) {
+ match scanout_one(data, plane, fb, rotation, &clips[..nclips], w, h) {
Ok(()) => {
let n = super::SCANOUT_FAILS.swap(0, Relaxed);
super::SCANOUT_SKIP.store(0, Relaxed);
@@ -608,12 +699,27 @@ fn read_cursor_bgra(
Ok(out)
}
-/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be
used.
+/// Source (framebuffer) dimensions for an output of `ow`x`oh` pixels under
plane `rotation`.
+/// The 90/270 rotations swap width and height between the framebuffer and the
displayed output;
+/// the others preserve them.
+fn src_dims(rotation: u32, ow: usize, oh: usize) -> (usize, usize) {
+ let rot = rotation & bindings::DRM_MODE_ROTATE_MASK;
+ if rot == bindings::DRM_MODE_ROTATE_90 || rot ==
bindings::DRM_MODE_ROTATE_270 {
+ (oh, ow)
+ } else {
+ (ow, oh)
+ }
+}
+
+/// vmap `fb`, encode it, and push one video frame to the head's endpoint.
Split out so `?` can be
+/// used. `w`/`h` are the OUTPUT (displayed) dimensions; `clips` are the
client's changed
+/// rectangles in output space (empty = repaint the whole output).
fn scanout_one(
data: &VinoDrmData,
plane: &plane::Plane<VinoPlane>,
fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
rotation: u32,
+ clips: &[(usize, usize, usize, usize)],
w: usize,
h: usize,
) -> Result {
@@ -626,7 +732,7 @@ fn scanout_one(
// The real source stride: GEM dumb buffers pad the pitch (alignment), so
it is not necessarily
// `w * 4` -- read it from the framebuffer rather than assuming.
let pitch = fb.pitch(0) as usize;
- encode_and_send(data, plane, vmap.as_ptr(), pitch, rotation, w, h)
+ encode_and_send(data, plane, vmap.as_ptr(), pitch, rotation, clips, w, h)
}
/// Encode the mapped frame with the byte-exact Vino WHT **colour** codec and
bulk-write the
@@ -646,11 +752,13 @@ fn encode_and_send_wht(
) -> Result {
let seq0 = plane.scanout.lock().seq;
let gamma = data.gamma_snapshot();
+ // Source dimensions (swapped from the output for 90/270 rotation).
+ let (sw, sh) = src_dims(rotation, w, h);
let (frames, next_seq) = super::video::wht::colour_frame_ep08(w, h, seq0,
|dx, dy| {
// Map the output pixel back to its source pixel under the plane
rotation/reflection.
- let (sx, sy) = rot_src(rotation, dx, dy, w, h);
- // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source
framebuffer (`pitch*h` bytes);
- // `rot_src` returns `sx < w <= pitch/4`, `sy < h`.
+ let (sx, sy) = rot_src(rotation, dx, dy, sw, sh);
+ // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source
framebuffer (`pitch*sh`
+ // bytes); `rot_src` returns `sx < sw <= pitch/4`, `sy < sh`.
let px = unsafe { (vaddr.add(sy * pitch + sx * 4) as *const
u32).read_unaligned() };
apply_gamma(
&gamma,
@@ -665,8 +773,9 @@ fn encode_and_send_wht(
// in `disconnect()`, which first unplugs the DRM device, so it is bound
for this push.
let dev = unsafe { data.intf.as_bound() };
prime_video_eps(dev);
+ let ep = VIDEO_EPS[plane.head as usize];
for frame in frames.iter() {
- dev.bulk_send(VIDEO_EP, frame, super::timeout(), GFP_KERNEL)?;
+ dev.bulk_send(ep, frame, super::timeout(), GFP_KERNEL)?;
}
Ok(())
}
@@ -679,13 +788,14 @@ fn encode_and_send(
vaddr: *const u8,
pitch: usize,
rotation: u32,
+ clips: &[(usize, usize, usize, usize)],
w: usize,
h: usize,
) -> Result {
// WHT colour codec path (default off): the byte-exact,
bandwidth-efficient DLM-quality
// codec. Requires a 64x16-aligned mode; for non-aligned geometry (e.g.
1080p, height
// 1080 % 16 = 8) fall through to the RLE path until partial-strip edge
handling is
- // captured.
+ // captured. The keyframe codec ignores damage clips.
if super::EP08_WHT_CODEC
&& w % super::video::wht::STRIP_W == 0
&& h % super::video::wht::STRIP_H == 0
@@ -693,6 +803,8 @@ fn encode_and_send(
return encode_and_send_wht(data, plane, vaddr, pitch, rotation, w, h);
}
let gamma = data.gamma_snapshot();
+ // Source dimensions (swapped from the output for 90/270 rotation).
+ let (sw, sh) = src_dims(rotation, w, h);
let frame = {
let mut st = plane.scanout.lock();
// On the first frame `cur` is freshly zeroed, so the whole buffer
must be filled.
@@ -708,21 +820,31 @@ fn encode_and_send(
st.hint = 0;
}
let ScanoutState { enc, cur, seq, hint, dims: _ } = &mut *st;
- for dy in 0..h {
- for dx in 0..w {
- // Map the output pixel back to its source pixel under the
plane rotation.
- let (sx, sy) = rot_src(rotation, dx, dy, w, h);
- // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source
framebuffer
- // (`pitch*h` bytes); `rot_src` returns `sx < w <= pitch/4`,
`sy < h`.
- let px = unsafe { (vaddr.add(sy * pitch + sx * 4) as *const
u32).read_unaligned() };
- let (r, g, b) = apply_gamma(
- &gamma,
- ((px >> 16) & 0xff) as u8,
- ((px >> 8) & 0xff) as u8,
- (px & 0xff) as u8,
- );
- let (r, g, b) = (r as u32, g as u32, b as u32);
- cur[dy * w + dx] = (((r >> 3) << 11) | ((g >> 2) << 5) | (b >>
3)) as u16;
+ // Re-convert only the client's changed rectangles into `cur`; the
rest already holds the
+ // previous frame, which the encoder diffs against. On the first frame
there is no valid
+ // previous frame (and on a rotated output there are no usable clips),
so convert the whole
+ // output. `clips` are already clamped to the output in
`atomic_update`.
+ let full = [(0usize, 0usize, w, h)];
+ let regions: &[(usize, usize, usize, usize)] =
+ if first || clips.is_empty() { &full } else { clips };
+ for &(x0, y0, x1, y1) in regions {
+ for dy in y0..y1 {
+ for dx in x0..x1 {
+ // Map the output pixel back to its source pixel under the
plane rotation.
+ let (sx, sy) = rot_src(rotation, dx, dy, sw, sh);
+ // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped
source framebuffer
+ // (`pitch*sh` bytes); `rot_src` returns `sx < sw <=
pitch/4`, `sy < sh`.
+ let px =
+ unsafe { (vaddr.add(sy * pitch + sx * 4) as *const
u32).read_unaligned() };
+ let (r, g, b) = apply_gamma(
+ &gamma,
+ ((px >> 16) & 0xff) as u8,
+ ((px >> 8) & 0xff) as u8,
+ (px & 0xff) as u8,
+ );
+ let (r, g, b) = (r as u32, g as u32, b as u32);
+ cur[dy * w + dx] = (((r >> 3) << 11) | ((g >> 2) << 5) |
(b >> 3)) as u16;
+ }
}
}
let s = *seq;
@@ -746,7 +868,7 @@ fn encode_and_send(
// DRM device, so it is bound for the duration of this push.
let dev = unsafe { data.intf.as_bound() };
prime_video_eps(dev);
- dev.bulk_send(VIDEO_EP, &frame, super::timeout(), GFP_KERNEL)?;
+ dev.bulk_send(VIDEO_EPS[plane.head as usize], &frame, super::timeout(),
GFP_KERNEL)?;
Ok(())
}
@@ -769,7 +891,9 @@ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>,
_args: ()) -> impl PinI
#[pin_data]
pub(super) struct VinoConnector {
- /// This connector's downstream-monitor EDID (`None` until the CP channel
delivers it).
+ /// This connector's downstream-monitor EDID (`None` until the CP channel
delivers it). Which
+ /// head a connector belongs to is tracked by its slot in
[`VinoDrmData::connectors`], so the
+ /// connector itself needs no head field.
#[pin]
cached_edid: Mutex<Option<KVec<u8>>>,
}
@@ -814,6 +938,28 @@ fn get_modes<'a>(
connector.set_preferred_mode((FALLBACK_W as u32, FALLBACK_H as u32));
n
}
+
+ /// Report the head connected once the dock has delivered this head's
downstream EDID (a real
+ /// monitor is attached and described), disconnected until then. A head
with no monitor (e.g.
+ /// the second head of a single-monitor dock) stays disconnected rather
than advertising a
+ /// phantom output.
+ fn detect(connector: &Connector<Self>, _force: bool) -> Status {
+ if connector.cached_edid.lock().is_some() {
+ Status::Connected
+ } else {
+ Status::Disconnected
+ }
+ }
+
+ /// Prune modes whose pixel clock exceeds a single head's bandwidth ceiling
+ /// ([`MAX_HEAD_CLOCK_KHZ`], ~4K@60).
+ fn mode_valid(_connector: &Connector<Self>, mode: &DisplayMode) ->
ModeStatus {
+ if mode.clock() > MAX_HEAD_CLOCK_KHZ {
+ ModeStatus::ClockHigh
+ } else {
+ ModeStatus::Ok
+ }
+ }
}
// ---- DDC/CI I2C adapter
-----------------------------------------------------
diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
index fd8d351dd1d9..049e8c4cb0f7 100644
--- a/drivers/gpu/drm/vino/vino.rs
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -372,8 +372,12 @@ fn run(this: Arc<BringUp>) {
let n = blob.len();
let dev: &drm_sink::VinoDrmDevice = d;
let data: &drm_sink::VinoDrmData = dev;
- data.set_edid(dev, blob);
- dev_info!(cdev, "vino: cached dock EDID for
connector ({n} bytes)\n");
+ // The bring-up reads the dock's primary
downstream EDID; head 0.
+ data.set_edid(dev, 0, blob);
+ dev_info!(
+ cdev,
+ "vino: cached dock EDID for head 0 connector
({n} bytes)\n"
+ );
}
}
Err(e) => dev_info!(cdev, "vino: HDCP AKE incomplete
({e:?}) -- WIP\n"),
--
2.55.0