[RFC PATCH v2 10/10] drm/vino: hrtimer-driven software vblank
From: Mike Lothian
Date: Thu Jul 02 2026 - 23:13:38 EST
The atomic helpers were completing every page-flip immediately via
drm_atomic_helper_fake_vblank(), because the CRTC had no vblank support --
so a compositor got no refresh-rate pacing and updates arrived in bursts.
Implement VblankSupport for VinoCrtc backed by a per-CRTC hrtimer that
fires once per frame (from the mode's framedur_ns) and drives
drm_crtc_handle_vblank(). The CRTC now enables/disables vblank around
scanout (drm_crtc_vblank_on/off) and arms the page-flip completion event
to the next vblank in atomic_flush (drm_crtc_arm_vblank_event via the safe
PendingVblankEvent::arm()), so flips are paced to the display's refresh
rate. The timer free-runs once started and is cancelled when the CRTC is
dropped at teardown.
Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
drivers/gpu/drm/vino/drm_sink.rs | 146 +++++++++++++++++++++++++++++--
1 file changed, 139 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
index ee04a5af9f7d..ce04c8b65461 100644
--- a/drivers/gpu/drm/vino/drm_sink.rs
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -39,21 +39,32 @@
//! its content-protection channel for vino (see `docs/BLOCKER.md`), so `atomic_update`
//! never gets past the first `bulk_send`.
+use core::sync::atomic::{AtomicBool, AtomicI64, AtomicPtr, Ordering};
use kernel::{
bindings, drm,
drm::kms::{
self,
connector::{self, Connector, ConnectorGuard, ModeStatus, Status},
- crtc::{self, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
+ crtc::{self, AsRawCrtc as _, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
encoder,
modes::DisplayMode,
plane::{self, PlaneAtomicCommit, RawPlaneState as _},
+ vblank::{RawVblankCrtcState as _, VblankGuard, VblankSupport, VblankTimestamp},
KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _, NewKmsDevice, Probing,
},
i2c,
error::code::EINVAL,
+ impl_has_hr_timer,
+ interrupt::LocalInterruptDisabled,
prelude::*,
- sync::{aref::ARef, new_mutex, Mutex},
+ sync::{aref::ARef, new_mutex, new_spinlock, Arc, ArcBorrow, Mutex, SpinLock},
+ time::{
+ hrtimer::{
+ ArcHrTimerHandle, HrTimer, HrTimerCallback, HrTimerCallbackContext, HrTimerPointer,
+ HrTimerRestart, RelativeMode,
+ },
+ Delta, Monotonic,
+ },
types::ForLt,
};
@@ -417,11 +428,69 @@ fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
// ---- CRTC -----------------------------------------------------------------
+/// A software vblank source: an hrtimer that fires once per frame and drives
+/// `drm_crtc_handle_vblank()`, so the atomic helpers pace page-flips against a real vblank
+/// (via `drm_crtc_arm_vblank_event()` in [`VinoCrtc::atomic_flush`]) instead of completing them
+/// immediately with a fake vblank. The timer free-runs once started; `enabled` gates delivery so
+/// DPMS off/on is a flag flip. Cancelled when the owning [`VinoCrtc`] is dropped at teardown.
+#[pin_data]
+pub(super) struct VblankTimer {
+ #[pin]
+ timer: HrTimer<Self>,
+ /// The `drm_crtc` to deliver vblanks to (set when vblank is first enabled).
+ crtc: AtomicPtr<bindings::drm_crtc>,
+ /// One scanout frame in nanoseconds (from the mode's `framedur_ns`).
+ interval_ns: AtomicI64,
+ /// Whether vblanks should currently be delivered (toggled by enable/disable_vblank).
+ enabled: AtomicBool,
+ /// Whether the free-running timer has been started yet.
+ started: AtomicBool,
+}
+
+impl VblankTimer {
+ fn new() -> impl PinInit<Self> {
+ pin_init!(VblankTimer {
+ timer <- HrTimer::new(),
+ crtc: AtomicPtr::new(core::ptr::null_mut()),
+ interval_ns: AtomicI64::new(16_666_666), // ~60 Hz until a mode sets it
+ enabled: AtomicBool::new(false),
+ started: AtomicBool::new(false),
+ })
+ }
+}
+
+impl HrTimerCallback for VblankTimer {
+ type Pointer<'a> = Arc<Self>;
+
+ fn run(this: ArcBorrow<'_, Self>, mut ctx: HrTimerCallbackContext<'_, Self>) -> HrTimerRestart {
+ let crtc = this.crtc.load(Ordering::Relaxed);
+ if !crtc.is_null() && this.enabled.load(Ordering::Relaxed) {
+ // SAFETY: `crtc` is the `drm_crtc` stored in `enable_vblank` while the device is live;
+ // the timer is cancelled (its handle dropped) before the crtc is freed at teardown.
+ unsafe { bindings::drm_crtc_handle_vblank(crtc) };
+ }
+ let interval = this.interval_ns.load(Ordering::Relaxed).max(1_000_000);
+ ctx.forward_now(Delta::from_nanos(interval));
+ HrTimerRestart::Restart
+ }
+}
+
+impl_has_hr_timer! {
+ impl HasHrTimer<Self> for VblankTimer {
+ mode: RelativeMode<Monotonic>, field: self.timer
+ }
+}
+
#[pin_data]
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,
+ /// The software vblank source for this CRTC.
+ vblank: Arc<VblankTimer>,
+ /// Keeps the timer running; dropping it (at CRTC teardown) cancels the timer.
+ #[pin]
+ vblank_handle: SpinLock<Option<ArcHrTimerHandle<VblankTimer>>>,
}
#[derive(Clone, Default)]
@@ -436,20 +505,25 @@ impl crtc::DriverCrtc for VinoCrtc {
type Args = u8;
type Driver = VinoDrmDriver;
type State = VinoCrtcState;
- type VblankImpl = core::marker::PhantomData<Self>;
+ type VblankImpl = Self;
fn new(
_device: &drm::Device<Self::Driver, drm::Uninit>,
head: &u8,
) -> impl PinInit<Self, Error> {
- try_pin_init!(VinoCrtc { head: *head })
+ try_pin_init!(VinoCrtc {
+ head: *head,
+ vblank: Arc::pin_init(VblankTimer::new(), GFP_KERNEL)?,
+ vblank_handle <- new_spinlock!(None),
+ })
}
- /// 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).
+ /// The display is turning on (scanout begins). Enables vblank pacing, pushes a live mode-set CP
+ /// message for the negotiated mode and brings the monitor out of DPMS standby -- the latter two
+ /// no-ops until CP engages (the wall).
fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
let crtc = commit.crtc();
+ crtc.vblank_on();
let head = crtc.head;
let data: &VinoDrmData = crtc.drm_dev();
let new = commit.take_new_state();
@@ -475,12 +549,70 @@ fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
/// no-op until CP engages.
fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
let crtc = commit.crtc();
+ crtc.vblank_off();
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 -- head {head} display OFF (scanout stopped)\n");
}
+
+ /// Arm the page-flip completion event to be sent by the next vblank tick, so userspace is paced
+ /// to the refresh rate rather than signalled immediately.
+ fn atomic_flush(commit: CrtcAtomicCommit<'_, Self>) {
+ let crtc = commit.crtc();
+ let mut new = commit.take_new_state();
+ if let Some(pending) = new.get_pending_vblank_event() {
+ match crtc.vblank_get() {
+ Ok(vbl_ref) => pending.arm(vbl_ref),
+ // Vblank couldn't be enabled (e.g. mid-teardown): fall back to sending now.
+ Err(_) => pending.send(),
+ }
+ }
+ }
+}
+
+impl VblankSupport for VinoCrtc {
+ type Crtc = VinoCrtc;
+
+ fn enable_vblank(
+ crtc: &crtc::Crtc<Self::Crtc>,
+ vblank_guard: &VblankGuard<'_, Self::Crtc>,
+ _irq: &LocalInterruptDisabled,
+ ) -> Result {
+ let data: &VinoCrtc = crtc;
+ // Track the mode's real frame duration so the tick matches the negotiated refresh rate.
+ let fd = vblank_guard.frame_duration();
+ if fd > 0 {
+ data.vblank.interval_ns.store(fd as i64, Ordering::Relaxed);
+ }
+ data.vblank.crtc.store(crtc.as_raw(), Ordering::Relaxed);
+ data.vblank.enabled.store(true, Ordering::Relaxed);
+ // Start the free-running timer the first time vblank is enabled.
+ if !data.vblank.started.swap(true, Ordering::Relaxed) {
+ let interval = data.vblank.interval_ns.load(Ordering::Relaxed);
+ let handle = data.vblank.clone().start(Delta::from_nanos(interval));
+ *data.vblank_handle.lock() = Some(handle);
+ }
+ Ok(())
+ }
+
+ fn disable_vblank(
+ crtc: &crtc::Crtc<Self::Crtc>,
+ _vblank_guard: &VblankGuard<'_, Self::Crtc>,
+ _irq: &LocalInterruptDisabled,
+ ) {
+ let data: &VinoCrtc = crtc;
+ data.vblank.enabled.store(false, Ordering::Relaxed);
+ }
+
+ fn get_vblank_timestamp(
+ _crtc: &crtc::Crtc<Self::Crtc>,
+ _in_vblank_irq: bool,
+ ) -> Option<VblankTimestamp> {
+ // Let DRM estimate the timestamp from the mode timings.
+ None
+ }
}
// ---- Planes: primary (scanout) + cursor -------------------------------------
--
2.55.0