Extend output profiles with monitor reconciliation and mirroring
This commit is contained in:
1 parent
ac184576f6
commit
d14cd3cbd2
23 files changed
+3614
-474
No files matched your search
+633
-350
File diff suppressed because it is too large.
Load diff
@@ -30,7 +30,7 @@ pub(super) fn capture_output(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||
pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &'a mut Option<GlesTexture>,
|
||||
output: &Output,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Resume reads are asynchronous; newer switch events win over old replies.
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -63,8 +63,51 @@ impl LidState {
|
||||
}
|
||||
}
|
||||
|
||||
struct InhibitorLease<T> {
|
||||
active: bool,
|
||||
generation: u64,
|
||||
descriptor: Option<T>,
|
||||
suspend: bool,
|
||||
}
|
||||
|
||||
impl<T> Default for InhibitorLease<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
generation: 0,
|
||||
descriptor: None,
|
||||
suspend: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> InhibitorLease<T> {
|
||||
fn activate(&mut self, active: bool) -> u64 {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
self.active = active;
|
||||
self.suspend = false;
|
||||
// Closing here releases logind immediately, independently of worker progress.
|
||||
self.descriptor = None;
|
||||
self.generation
|
||||
}
|
||||
|
||||
fn accept(&mut self, generation: u64, descriptor: T) {
|
||||
if self.active && self.generation == generation {
|
||||
self.descriptor = Some(descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PolicyRequest {
|
||||
Activate(u64),
|
||||
Suspend,
|
||||
}
|
||||
|
||||
pub(super) struct Reader {
|
||||
requests: mpsc::Sender<Probe>,
|
||||
suspend: mpsc::Sender<PolicyRequest>,
|
||||
lease: Arc<Mutex<InhibitorLease<zbus::zvariant::OwnedFd>>>,
|
||||
suspend_requested: bool,
|
||||
pub deadline: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
@@ -99,7 +142,94 @@ fn read_acpi() -> Option<bool> {
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
pub fn set_active(&mut self, active: bool) {
|
||||
let generation = self.lease.lock().unwrap().activate(active);
|
||||
self.suspend_requested = false;
|
||||
if active {
|
||||
let _ = self.suspend.send(PolicyRequest::Activate(generation));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(state: &Ferese) -> Result<Self, Box<dyn Error>> {
|
||||
let (suspend, suspend_requests) = mpsc::channel::<PolicyRequest>();
|
||||
let lease = Arc::new(Mutex::new(InhibitorLease::default()));
|
||||
let worker_lease = lease.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-lid-policy".into())
|
||||
.spawn(move || {
|
||||
let connection = zbus::blocking::connection::Builder::system()
|
||||
.and_then(|builder| builder.method_timeout(Duration::from_secs(5)).build());
|
||||
let Ok(connection) = connection else { return };
|
||||
while let Ok(request) = suspend_requests.recv() {
|
||||
if let PolicyRequest::Activate(generation) = request {
|
||||
let current = worker_lease.lock().unwrap();
|
||||
if !current.active || current.generation != generation {
|
||||
continue;
|
||||
}
|
||||
drop(current);
|
||||
let inhibitor = connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Inhibit",
|
||||
&(
|
||||
"handle-lid-switch",
|
||||
"Ferese",
|
||||
"Reconcile monitors before lid action",
|
||||
"block",
|
||||
),
|
||||
)
|
||||
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedFd>());
|
||||
match inhibitor {
|
||||
Ok(descriptor) => worker_lease.lock().unwrap().accept(generation, descriptor),
|
||||
Err(error) => tracing::warn!(%error, "could not own logind lid policy"),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let current = worker_lease.lock().unwrap();
|
||||
if !current.active || !current.suspend {
|
||||
continue;
|
||||
}
|
||||
drop(current);
|
||||
if read_logind(&connection).ok() != Some(true) {
|
||||
continue;
|
||||
}
|
||||
let allowed = connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.DBus.Properties"),
|
||||
"Get",
|
||||
&("org.freedesktop.login1.Manager", "BlockInhibited"),
|
||||
)
|
||||
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedValue>())
|
||||
.and_then(|value| String::try_from(value).map_err(Into::into));
|
||||
if allowed
|
||||
.as_ref()
|
||||
.is_ok_and(|inhibitors| inhibitors.split(':').any(|kind| kind == "sleep"))
|
||||
{
|
||||
tracing::debug!("lid suspend prevented by sleep inhibitor");
|
||||
continue;
|
||||
}
|
||||
let current = worker_lease.lock().unwrap();
|
||||
let requested = current.active && current.suspend;
|
||||
drop(current);
|
||||
if !requested {
|
||||
continue;
|
||||
}
|
||||
if let Err(error) = connection.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Suspend",
|
||||
&(false,),
|
||||
) {
|
||||
tracing::warn!(%error, "logind rejected lid suspend");
|
||||
}
|
||||
}
|
||||
worker_lease.lock().unwrap().activate(false);
|
||||
})?;
|
||||
let (requests, receiver) = mpsc::channel::<Probe>();
|
||||
let (replies, events) = smithay::reexports::calloop::channel::channel();
|
||||
state.loop_handle.insert_source(events, |event, _, state| {
|
||||
@@ -126,10 +256,15 @@ impl Reader {
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
let mut reader = Self {
|
||||
requests,
|
||||
suspend,
|
||||
lease,
|
||||
suspend_requested: false,
|
||||
deadline: None,
|
||||
})
|
||||
};
|
||||
reader.set_active(true);
|
||||
Ok(reader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +287,8 @@ pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
|
||||
cancel_output_timers(&state.loop_handle, output);
|
||||
}
|
||||
}
|
||||
backend.lid_reader.suspend_requested = false;
|
||||
backend.lid_reader.lease.lock().unwrap().suspend = false;
|
||||
let probe = backend.lid.begin(reactivate);
|
||||
let sent = backend.lid_reader.requests.send(probe).is_ok();
|
||||
let timer = state
|
||||
@@ -191,6 +328,7 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
|
||||
|
||||
// Waking a locked session may request redraws. Hold them until the fresh
|
||||
// device/connector snapshot has been reconciled.
|
||||
backend.lid_reader.suspend_requested = false;
|
||||
backend.topology.reconciling = true;
|
||||
state.reset_animation_clock();
|
||||
state.lock_input_activity();
|
||||
@@ -198,10 +336,40 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
|
||||
reconcile_outputs(state, reactivate);
|
||||
}
|
||||
|
||||
/// Only logind requests suspend. Inactive events wait for fresh resume topology.
|
||||
pub(super) fn apply_suspend_policy(state: &mut Ferese) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let requested = backend.active && backend.desired_outputs.suspend && backend.lid.closed();
|
||||
backend.lid_reader.lease.lock().unwrap().suspend = requested;
|
||||
if backend.lid_reader.suspend_requested != requested {
|
||||
backend.lid_reader.suspend_requested = requested;
|
||||
let _ = backend.lid_reader.suspend.send(PolicyRequest::Suspend);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pause_releases_inhibitor_and_rejects_delayed_acquisition() {
|
||||
let mut lease = InhibitorLease::default();
|
||||
let first = lease.activate(true);
|
||||
lease.accept(first, "fd");
|
||||
assert_eq!(lease.descriptor, Some("fd"));
|
||||
lease.activate(false);
|
||||
assert_eq!(lease.descriptor, None);
|
||||
lease.accept(first, "late fd");
|
||||
assert_eq!(lease.descriptor, None);
|
||||
let next = lease.activate(true);
|
||||
lease.accept(first, "old session fd");
|
||||
assert_eq!(lease.descriptor, None);
|
||||
lease.accept(next, "new session fd");
|
||||
assert_eq!(lease.descriptor, Some("new session fd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires dbus-daemon and private test sockets"]
|
||||
fn logind_reads_are_fresh_without_property_change_signals() {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//! A target displays a composition of the source, with no second workspace or input domain.
|
||||
//! Source alone releases client callbacks/presentation feedback. Targets have independent KMS clocks.
|
||||
use super::*;
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle};
|
||||
|
||||
/// Fit the complete source into the transformed target; black letterbox, never crop.
|
||||
fn fit(source: (i32, i32), target: (i32, i32)) -> Rectangle<i32, Physical> {
|
||||
let scale = (target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64);
|
||||
let width = (source.0 as f64 * scale).round().max(1.0) as i32;
|
||||
let height = (source.1 as f64 * scale).round().max(1.0) as i32;
|
||||
Rectangle::new(
|
||||
((target.0 - width) / 2, (target.1 - height) / 2).into(),
|
||||
(width, height).into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
source: &Output,
|
||||
frame: &crate::state::FrameScene,
|
||||
target: &mut DirectOutput,
|
||||
) -> Result<Vec<crate::render::AnimatedWindowRenderElement>, smithay::backend::renderer::gles::GlesError> {
|
||||
let elements = sampled_output_elements(state, renderer, source, true, frame);
|
||||
// Target renderer owns this texture, including when source and target use different GPUs.
|
||||
let canvas = target.mirror_canvas.get_or_insert_with(|| {
|
||||
Output::new(
|
||||
"mirror-canvas".into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "Ferese".into(),
|
||||
model: "Mirror composition".into(),
|
||||
},
|
||||
)
|
||||
});
|
||||
compose(renderer, &mut target.mirror_texture, canvas, source, &elements)?;
|
||||
let texture = target
|
||||
.mirror_texture
|
||||
.as_ref()
|
||||
.expect("source composition has a texture")
|
||||
.clone();
|
||||
target.mirror_commit.increment();
|
||||
Ok(vec![
|
||||
fitted_texture(texture, &target.output, target.mirror_id.clone(), target.mirror_commit).into(),
|
||||
])
|
||||
}
|
||||
|
||||
fn compose(
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &mut Option<GlesTexture>,
|
||||
canvas: &Output,
|
||||
source: &Output,
|
||||
elements: &[crate::render::AnimatedWindowRenderElement],
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
let source_mode = source.current_mode().expect("source has a mode");
|
||||
canvas.change_current_state(
|
||||
Some(OutputMode {
|
||||
size: source.current_transform().transform_size(source_mode.size),
|
||||
refresh: source_mode.refresh,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(source.current_scale()),
|
||||
None,
|
||||
);
|
||||
let framebuffer = capture::render_capture(renderer, texture, canvas, elements)?;
|
||||
drop(framebuffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fitted_texture(
|
||||
texture: GlesTexture,
|
||||
target: &Output,
|
||||
id: smithay::backend::renderer::element::Id,
|
||||
commit: CommitCounter,
|
||||
) -> crate::presentation::NativeTextureElement {
|
||||
let source_size = texture.size();
|
||||
let size = target
|
||||
.current_transform()
|
||||
.transform_size(target.current_mode().expect("target has a mode").size);
|
||||
crate::presentation::NativeTextureElement {
|
||||
id,
|
||||
commit,
|
||||
source: Rectangle::<f64, Buffer>::from_size(source_size.to_f64()),
|
||||
texture,
|
||||
geometry: fit((source_size.w, source_size.h), (size.w, size.h)),
|
||||
alpha: 1.0,
|
||||
program: None,
|
||||
uniforms: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn different_native_sizes_fit_without_cropping() {
|
||||
assert_eq!(
|
||||
fit((1920, 1080), (1280, 1024)),
|
||||
Rectangle::new((0, 152).into(), (1280, 720).into())
|
||||
);
|
||||
assert_eq!(
|
||||
fit((1920, 1080), (1080, 1920)),
|
||||
Rectangle::new((0, 656).into(), (1080, 608).into())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn mirrored_pixels_fit_rotated_source_and_target_without_cropping() {
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::backend::renderer::{Color32F, ExportMem};
|
||||
let device = EGLDevice::enumerate().unwrap().last().expect("EGL device");
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let output = |name: &str| {
|
||||
Output::new(
|
||||
name.into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
)
|
||||
};
|
||||
let source = output("source");
|
||||
source.change_current_state(
|
||||
Some(OutputMode {
|
||||
size: (64, 32).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::_90),
|
||||
Some(Scale::Fractional(2.0)),
|
||||
None,
|
||||
);
|
||||
let canvas = output("canvas");
|
||||
let colors: Vec<crate::render::AnimatedWindowRenderElement> = [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])]
|
||||
.into_iter()
|
||||
.map(|(y, color)| {
|
||||
SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
Rectangle::new((0, y).into(), (32, 32).into()),
|
||||
CommitCounter::default(),
|
||||
Color32F::from(color),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
let mut texture = None;
|
||||
compose(&mut renderer, &mut texture, &canvas, &source, &colors).unwrap();
|
||||
assert_eq!(texture.as_ref().unwrap().size(), (32, 64).into());
|
||||
let target = output("target");
|
||||
for (transform, colored_area) in [(Transform::Normal, 2048), (Transform::_90, 4608)] {
|
||||
target.change_current_state(
|
||||
Some(OutputMode {
|
||||
size: (96, 64).into(),
|
||||
refresh: 120_000,
|
||||
}),
|
||||
Some(transform),
|
||||
Some(Scale::Fractional(1.5)),
|
||||
None,
|
||||
);
|
||||
let element = fitted_texture(
|
||||
texture.as_ref().unwrap().clone(),
|
||||
&target,
|
||||
Id::new(),
|
||||
CommitCounter::default(),
|
||||
);
|
||||
let mut target_texture = None;
|
||||
let framebuffer = capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&framebuffer, Rectangle::from_size((96, 64).into()), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||
let red = pixels
|
||||
.chunks_exact(4)
|
||||
.filter(|pixel| pixel[0] > 100 && pixel[0] > pixel[2])
|
||||
.count();
|
||||
let blue = pixels
|
||||
.chunks_exact(4)
|
||||
.filter(|pixel| pixel[2] > 100 && pixel[2] > pixel[0])
|
||||
.count();
|
||||
assert_eq!(red, colored_area / 2, "{transform:?}");
|
||||
assert_eq!(blue, colored_area / 2, "{transform:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,8 @@ pub(super) struct Topology {
|
||||
pub reconciling: bool,
|
||||
pub dirty: bool,
|
||||
pub retry_timer: Option<RegistrationToken>,
|
||||
pub settle_timer: Option<RegistrationToken>,
|
||||
pub coalescer: crate::output_policy::Debouncer,
|
||||
}
|
||||
|
||||
impl Topology {
|
||||
@@ -93,73 +95,6 @@ impl Topology {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() {
|
||||
let mut topology = Topology::default();
|
||||
assert!(!topology.request(false));
|
||||
assert!(!topology.request(false));
|
||||
assert!(topology.dirty);
|
||||
assert!(topology.request(true));
|
||||
topology.reconciling = true;
|
||||
assert!(!topology.request(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replug_matches_hardware_even_when_card_numbers_change() {
|
||||
let original = Path::new("/dev/dri/card0");
|
||||
let identity = PathBuf::from("/sys/devices/pci0000:00/0000:00:02.0");
|
||||
let mut slot = DeviceState {
|
||||
identity: Some(identity.clone()),
|
||||
..DeviceState::default()
|
||||
};
|
||||
slot.failed(Instant::now());
|
||||
slot.missing();
|
||||
let observed = vec![
|
||||
(
|
||||
PathBuf::from("/dev/dri/card0"),
|
||||
Some(PathBuf::from("/sys/devices/other-gpu")),
|
||||
),
|
||||
(PathBuf::from("/dev/dri/card2"), Some(identity)),
|
||||
];
|
||||
assert_eq!(
|
||||
slot.current_path(original, &observed),
|
||||
Some(Path::new("/dev/dri/card2"))
|
||||
);
|
||||
assert!(slot.current_path(original, &observed[..1]).is_none());
|
||||
assert!(slot.current_path(original, &[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_devices_retry_independently_with_capped_backoff() {
|
||||
let now = Instant::now();
|
||||
let mut topology = Topology::default();
|
||||
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
|
||||
let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap();
|
||||
let mut second = DeviceState::default();
|
||||
second.ready(healthy);
|
||||
topology.devices.insert("/dev/dri/card1".into(), second);
|
||||
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap();
|
||||
device.failed(now);
|
||||
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
|
||||
for _ in 0..20 {
|
||||
device.failed(now);
|
||||
}
|
||||
assert_eq!(device.retry_at, Some(now + Duration::from_secs(32)));
|
||||
assert_eq!(
|
||||
topology.devices[Path::new("/dev/dri/card1")].availability,
|
||||
Availability::Ready(healthy)
|
||||
);
|
||||
assert!(topology.retry_deadline(false).is_none());
|
||||
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
|
||||
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
|
||||
assert!(topology.retry_deadline(true).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn slot_mut<'a>(state: &'a mut Ferese, path: &Path) -> &'a mut DeviceState {
|
||||
state
|
||||
.direct_backend
|
||||
@@ -180,8 +115,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(token) = backend.topology.settle_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
}
|
||||
backend.topology.coalescer.clear();
|
||||
backend.topology.reconciling = true;
|
||||
backend.topology.dirty = false;
|
||||
backend.output_error = None;
|
||||
if reactivate {
|
||||
backend
|
||||
.presentation
|
||||
@@ -191,7 +131,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
if let Some(token) = backend.topology.retry_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
}
|
||||
let paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
|
||||
let mut paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
|
||||
let invalidated = std::mem::take(&mut backend.topology.invalidated);
|
||||
let observed = all_gpus(backend.session.seat()).map(|paths| {
|
||||
paths
|
||||
@@ -209,6 +149,23 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
backend.low_power =
|
||||
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent());
|
||||
|
||||
if let Ok(observed) = &observed {
|
||||
for (path, identity) in observed {
|
||||
if !backend
|
||||
.topology
|
||||
.devices
|
||||
.values()
|
||||
.any(|slot| slot.identity == *identity && identity.is_some())
|
||||
&& !backend.topology.devices.contains_key(path)
|
||||
{
|
||||
backend.topology.devices.insert(path.clone(), DeviceState::known(path));
|
||||
paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
paths.sort();
|
||||
let mut ready = Vec::new();
|
||||
|
||||
for node in invalidated {
|
||||
remove_device(state, node);
|
||||
}
|
||||
@@ -301,7 +258,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
.remove(&crtc)
|
||||
.unwrap();
|
||||
cancel_output_timers(&state.loop_handle, &mut output);
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
if let Some(global) = output.global.take() {
|
||||
state.display_handle.disable_global::<Ferese>(global);
|
||||
}
|
||||
state.unregister_output(&output.output);
|
||||
state
|
||||
.direct_backend
|
||||
@@ -311,28 +270,65 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
.remove(&(node, crtc));
|
||||
}
|
||||
|
||||
let complete = rescan_device(state, node);
|
||||
if complete.is_err() {
|
||||
remove_device(state, node);
|
||||
ready.push((path, node));
|
||||
}
|
||||
|
||||
let known_good = state.direct_backend.as_ref().unwrap().applied_outputs.clone();
|
||||
if let Err(error) = refresh_monitor_policy(state).and_then(|()| {
|
||||
transaction::retire_disconnected(state);
|
||||
validate_desired_outputs(state)
|
||||
}) {
|
||||
tracing::warn!(%error, "monitor policy validation failed; retaining applied outputs");
|
||||
state.direct_backend.as_mut().unwrap().output_error = Some(error);
|
||||
for (path, _) in &ready {
|
||||
slot_mut(state, path).failed(Instant::now());
|
||||
}
|
||||
let entry = slot_mut(state, &path);
|
||||
if complete == Ok(true) {
|
||||
entry.ready(node);
|
||||
} else {
|
||||
entry.failed(Instant::now());
|
||||
// A partial connector failure must not take down working outputs.
|
||||
if complete == Ok(false) {
|
||||
entry.availability = Availability::Degraded(node);
|
||||
} else {
|
||||
// Activate destinations before disabling former owners, including across GPUs.
|
||||
ready.sort_by_key(|(_, node)| {
|
||||
let backend = state.direct_backend.as_ref().unwrap();
|
||||
let prefix = format!("{}/", drm_device_key(&backend.devices[node].drm));
|
||||
let desired = backend
|
||||
.desired_outputs
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| output.settings.enabled && output.key.starts_with(&prefix));
|
||||
let logical_sources = desired.clone().filter(|output| output.mirror_source.is_none()).count();
|
||||
std::cmp::Reverse((logical_sources, desired.count()))
|
||||
});
|
||||
let mut succeeded = Vec::new();
|
||||
for (path, node) in &ready {
|
||||
let complete = rescan_device(state, *node);
|
||||
let entry = slot_mut(state, path);
|
||||
if complete == Ok(true) {
|
||||
entry.ready(*node);
|
||||
succeeded.push(*node);
|
||||
} else {
|
||||
entry.failed(Instant::now());
|
||||
entry.availability = Availability::Degraded(*node);
|
||||
// Device boundaries are not cross-GPU atomic. Compensate already
|
||||
// applied devices using their last actual hardware settings.
|
||||
if !known_good.outputs.is_empty() {
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
let requested = backend.desired_outputs.clone();
|
||||
backend.desired_outputs =
|
||||
crate::output_policy::restore_configuration(&known_good, &backend.monitors);
|
||||
for previous in succeeded {
|
||||
let _ = rescan_device(state, previous);
|
||||
}
|
||||
state.direct_backend.as_mut().unwrap().desired_outputs = requested;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if state.direct_backend.as_ref().unwrap().output_error.is_none() {
|
||||
remember_applied_configuration(state);
|
||||
}
|
||||
}
|
||||
|
||||
transaction::recover_last_output(state);
|
||||
refresh_connected_info(state);
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
backend.connected_outputs = backend
|
||||
.devices
|
||||
.values()
|
||||
.flat_map(|device| device.connected_outputs.clone())
|
||||
.collect();
|
||||
backend.topology.reconciling = false;
|
||||
let retry = backend.topology.retry_deadline(backend.active);
|
||||
if let Some(deadline) = retry {
|
||||
@@ -355,4 +351,110 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
state.restore_output_focus();
|
||||
state.relayout();
|
||||
render_all(state);
|
||||
lid::apply_suspend_policy(state);
|
||||
}
|
||||
|
||||
const SETTLE: Duration = Duration::from_millis(150);
|
||||
|
||||
/// Trailing-edge coalescing; inactive events retain dirtiness without arming a timer.
|
||||
pub(super) fn hotplug(state: &mut Ferese) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
backend.topology.dirty = true;
|
||||
if let Some(token) = backend.topology.settle_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
}
|
||||
let deadline = backend
|
||||
.topology
|
||||
.coalescer
|
||||
.request(monotonic_now(), backend.active && !backend.lid.pending());
|
||||
if deadline.is_none() {
|
||||
return;
|
||||
}
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(SETTLE), |_, _, state| {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.topology.settle_timer = None;
|
||||
if !backend.topology.coalescer.take_due(monotonic_now()) {
|
||||
return TimeoutAction::Drop;
|
||||
}
|
||||
}
|
||||
reconcile_outputs(state, false);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
Ok(token) => state.direct_backend.as_mut().unwrap().topology.settle_timer = Some(token),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not debounce output events");
|
||||
reconcile_outputs(state, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() {
|
||||
let mut topology = Topology::default();
|
||||
assert!(!topology.request(false));
|
||||
assert!(!topology.request(false));
|
||||
assert!(topology.dirty);
|
||||
assert!(topology.request(true));
|
||||
topology.reconciling = true;
|
||||
assert!(!topology.request(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replug_matches_hardware_even_when_card_numbers_change() {
|
||||
let original = Path::new("/dev/dri/card0");
|
||||
let identity = PathBuf::from("/sys/devices/pci0000:00/0000:00:02.0");
|
||||
let mut slot = DeviceState {
|
||||
identity: Some(identity.clone()),
|
||||
..DeviceState::default()
|
||||
};
|
||||
slot.failed(Instant::now());
|
||||
slot.missing();
|
||||
let observed = vec![
|
||||
(
|
||||
PathBuf::from("/dev/dri/card0"),
|
||||
Some(PathBuf::from("/sys/devices/other-gpu")),
|
||||
),
|
||||
(PathBuf::from("/dev/dri/card2"), Some(identity)),
|
||||
];
|
||||
assert_eq!(
|
||||
slot.current_path(original, &observed),
|
||||
Some(Path::new("/dev/dri/card2"))
|
||||
);
|
||||
assert!(slot.current_path(original, &observed[..1]).is_none());
|
||||
assert!(slot.current_path(original, &[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_devices_retry_independently_with_capped_backoff() {
|
||||
let now = Instant::now();
|
||||
let mut topology = Topology::default();
|
||||
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
|
||||
let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap();
|
||||
let mut second = DeviceState::default();
|
||||
second.ready(healthy);
|
||||
topology.devices.insert("/dev/dri/card1".into(), second);
|
||||
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap();
|
||||
device.failed(now);
|
||||
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
|
||||
for _ in 0..20 {
|
||||
device.failed(now);
|
||||
}
|
||||
assert_eq!(device.retry_at, Some(now + Duration::from_secs(32)));
|
||||
assert_eq!(
|
||||
topology.devices[Path::new("/dev/dri/card1")].availability,
|
||||
Availability::Ready(healthy)
|
||||
);
|
||||
assert!(topology.retry_deadline(false).is_none());
|
||||
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
|
||||
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
|
||||
assert!(topology.retry_deadline(true).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
//! Validate a device-wide desired topology, then use Smithay for every real commit.
|
||||
//! Smithay exposes TEST_ONLY per surface, but not a combined CRTC request. The
|
||||
//! one raw request here is validation only; mode/plane ownership stays in Smithay.
|
||||
use super::*;
|
||||
use smithay::backend::allocator::Allocator;
|
||||
use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer};
|
||||
use smithay::reexports::drm::control::{AtomicCommitFlags, ResourceHandle, atomic::AtomicModeReq};
|
||||
|
||||
fn property<D: ControlDevice, H: ResourceHandle>(drm: &D, handle: H, name: &str) -> io::Result<property::Handle> {
|
||||
for (property, _) in drm.get_properties(handle)?.iter() {
|
||||
if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() {
|
||||
return Ok(*property);
|
||||
}
|
||||
}
|
||||
Err(io::Error::other(format!("missing DRM property {name}")))
|
||||
}
|
||||
|
||||
fn add<H: ResourceHandle>(
|
||||
drm: &DrmDevice,
|
||||
request: &mut AtomicModeReq,
|
||||
handle: H,
|
||||
name: &str,
|
||||
value: u64,
|
||||
) -> io::Result<()> {
|
||||
request.add_raw_property(handle.into(), property(drm, handle, name)?, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_secondary_planes(
|
||||
drm: &DrmDevice,
|
||||
request: &mut AtomicModeReq,
|
||||
crtc: crtc::Handle,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let planes = drm.planes(&crtc)?;
|
||||
for plane in planes.cursor.iter().chain(&planes.overlay) {
|
||||
let properties = drm.get_properties(plane.handle)?;
|
||||
let owned = properties.iter().any(|(handle, value)| {
|
||||
drm.get_property(*handle)
|
||||
.is_ok_and(|info| info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64)
|
||||
});
|
||||
if owned {
|
||||
add(drm, request, plane.handle, "FB_ID", 0)?;
|
||||
add(drm, request, plane.handle, "CRTC_ID", 0)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Blobs {
|
||||
fd: DrmDeviceFd,
|
||||
ids: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Drop for Blobs {
|
||||
fn drop(&mut self) {
|
||||
for id in &self.ids {
|
||||
let _ = self.fd.destroy_property_blob(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Result<(), Box<dyn Error>> {
|
||||
if scan.selections.iter().all(|selection| {
|
||||
device
|
||||
.outputs
|
||||
.get(&selection.crtc)
|
||||
.is_some_and(|output| output.connector == selection.connector.handle() && output.mode == selection.mode)
|
||||
}) && scan.selections.len() == device.outputs.len()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// Legacy KMS has no atomic test. Smithay validates its surface/mode as far as
|
||||
// the API permits; commit failures still go through the restoration path.
|
||||
if !device.drm.is_atomic() {
|
||||
return Ok(());
|
||||
}
|
||||
let drm = &device.drm;
|
||||
let mut request = AtomicModeReq::new();
|
||||
let mut blobs = Blobs {
|
||||
fd: drm.device_fd().clone(),
|
||||
ids: Vec::new(),
|
||||
};
|
||||
let mut buffers = Vec::new();
|
||||
let mut framebuffers = Vec::new();
|
||||
let mut allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||
let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node));
|
||||
let mut used_planes = HashSet::new();
|
||||
let desired_crtcs = scan
|
||||
.selections
|
||||
.iter()
|
||||
.map(|selection| selection.crtc)
|
||||
.collect::<HashSet<_>>();
|
||||
let desired_connectors = scan
|
||||
.selections
|
||||
.iter()
|
||||
.map(|selection| selection.connector.handle())
|
||||
.collect::<HashSet<_>>();
|
||||
for output in device.outputs.values() {
|
||||
if !desired_connectors.contains(&output.connector) {
|
||||
add(drm, &mut request, output.connector, "CRTC_ID", 0)?;
|
||||
}
|
||||
if !desired_crtcs.contains(&output.surface.crtc()) {
|
||||
clear_secondary_planes(drm, &mut request, output.surface.crtc())?;
|
||||
add(drm, &mut request, output.surface.crtc(), "ACTIVE", 0)?;
|
||||
add(drm, &mut request, output.surface.crtc(), "MODE_ID", 0)?;
|
||||
add(drm, &mut request, output.surface.plane(), "FB_ID", 0)?;
|
||||
add(drm, &mut request, output.surface.plane(), "CRTC_ID", 0)?;
|
||||
}
|
||||
}
|
||||
for selection in &scan.selections {
|
||||
clear_secondary_planes(drm, &mut request, selection.crtc)?;
|
||||
let planes = drm.planes(&selection.crtc)?;
|
||||
let primary = device
|
||||
.outputs
|
||||
.get(&selection.crtc)
|
||||
.map(|output| output.surface.plane())
|
||||
.or_else(|| {
|
||||
planes
|
||||
.primary
|
||||
.iter()
|
||||
.find(|plane| !used_planes.contains(&plane.handle))
|
||||
.map(|plane| plane.handle)
|
||||
})
|
||||
.ok_or("no usable primary plane")?;
|
||||
used_planes.insert(primary);
|
||||
if let Ok(rotation) = property(drm, primary, "rotation") {
|
||||
request.add_raw_property(primary.into(), rotation, 1);
|
||||
}
|
||||
let blob: u64 = drm.create_property_blob(&selection.mode)?.into();
|
||||
blobs.ids.push(blob);
|
||||
add(drm, &mut request, selection.crtc, "MODE_ID", blob)?;
|
||||
add(drm, &mut request, selection.crtc, "ACTIVE", 1)?;
|
||||
add(
|
||||
drm,
|
||||
&mut request,
|
||||
selection.connector.handle(),
|
||||
"CRTC_ID",
|
||||
u32::from(selection.crtc) as u64,
|
||||
)?;
|
||||
let (width, height) = selection.mode.size();
|
||||
let formats = &planes
|
||||
.primary
|
||||
.iter()
|
||||
.find(|plane| plane.handle == primary)
|
||||
.ok_or("primary plane disappeared")?
|
||||
.formats;
|
||||
let mut allocated = None;
|
||||
for format in formats.iter().filter(|format| {
|
||||
matches!(
|
||||
format.code,
|
||||
Fourcc::Argb8888 | Fourcc::Abgr8888 | Fourcc::Xrgb8888 | Fourcc::Xbgr8888
|
||||
)
|
||||
}) {
|
||||
if let Ok(buffer) = allocator.create_buffer(width.into(), height.into(), format.code, &[format.modifier])
|
||||
&& let Ok(Some(fb)) = exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false)
|
||||
{
|
||||
allocated = Some((buffer, fb));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (buffer, fb) = allocated.ok_or("unable to allocate a DRM validation buffer")?;
|
||||
add(drm, &mut request, primary, "FB_ID", u32::from(*fb.as_ref()) as u64)?;
|
||||
add(drm, &mut request, primary, "CRTC_ID", u32::from(selection.crtc) as u64)?;
|
||||
for (name, value) in [
|
||||
("SRC_X", 0),
|
||||
("SRC_Y", 0),
|
||||
("SRC_W", (width as u64) << 16),
|
||||
("SRC_H", (height as u64) << 16),
|
||||
("CRTC_X", 0),
|
||||
("CRTC_Y", 0),
|
||||
("CRTC_W", width as u64),
|
||||
("CRTC_H", height as u64),
|
||||
] {
|
||||
add(drm, &mut request, primary, name, value)?;
|
||||
}
|
||||
buffers.push(buffer);
|
||||
framebuffers.push(fb);
|
||||
}
|
||||
drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(
|
||||
surface: &mut OutputCompositor,
|
||||
renderer: &mut GlesRenderer,
|
||||
mode: DrmMode,
|
||||
settings: &OutputSettings,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
surface.use_mode(mode)?;
|
||||
surface.set_output_mode_source(OutputModeSource::Static {
|
||||
size: OutputMode::from(mode).size,
|
||||
scale: settings.scale.into(),
|
||||
transform: output_transform(settings.transform),
|
||||
});
|
||||
surface.reset_buffer_ages();
|
||||
let elements: Vec<crate::render::AnimatedWindowRenderElement> = Vec::new();
|
||||
let result = surface.render_frame(
|
||||
renderer,
|
||||
&elements,
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
smithay::backend::drm::compositor::FrameFlags::empty(),
|
||||
)?;
|
||||
if let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element {
|
||||
primary.sync.wait()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retire a logical output only after its hardware is unavailable or confirmed disabled.
|
||||
fn retire(state: &mut Ferese, output: &mut DirectOutput, node: DrmNode, crtc: crtc::Handle) {
|
||||
cancel_output_timers(&state.loop_handle, output);
|
||||
if let Some(global) = output.global.take() {
|
||||
state.display_handle.disable_global::<Ferese>(global);
|
||||
}
|
||||
state.session_lock.output_removed(&output.output);
|
||||
state.unregister_output(&output.output);
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.presentation
|
||||
.remove(&(node, crtc));
|
||||
}
|
||||
|
||||
struct DeviceTransaction<'a> {
|
||||
state: &'a mut Ferese,
|
||||
device: &'a mut DirectDevice,
|
||||
selections: &'a HashMap<crtc::Handle, OutputSelection>,
|
||||
staged: HashMap<crtc::Handle, DirectOutput>,
|
||||
replaced: HashSet<crtc::Handle>,
|
||||
}
|
||||
|
||||
impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
|
||||
type Key = crtc::Handle;
|
||||
|
||||
fn stage(&mut self, crtc: crtc::Handle) -> Result<(), String> {
|
||||
let selection = &self.selections[&crtc];
|
||||
if self
|
||||
.device
|
||||
.outputs
|
||||
.get(&crtc)
|
||||
.is_none_or(|output| output.connector != selection.connector.handle())
|
||||
{
|
||||
let output = create_direct_output(
|
||||
self.state,
|
||||
&mut self.device.drm,
|
||||
&self.device.gbm,
|
||||
&self.device.renderer,
|
||||
selection.clone(),
|
||||
false,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
self.staged.insert(crtc, output);
|
||||
}
|
||||
let output = self
|
||||
.staged
|
||||
.get_mut(&crtc)
|
||||
.or_else(|| self.device.outputs.get_mut(&crtc))
|
||||
.unwrap();
|
||||
output
|
||||
.surface
|
||||
.set_connectors(&[selection.connector.handle()])
|
||||
.map_err(|error| error.to_string())?;
|
||||
prepare(
|
||||
&mut output.surface,
|
||||
&mut self.device.renderer,
|
||||
selection.mode,
|
||||
&selection.settings,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn commit(&mut self, crtc: crtc::Handle) -> Result<(), String> {
|
||||
if self.staged.contains_key(&crtc) && self.device.outputs.contains_key(&crtc) {
|
||||
// Connector and mode cannot be changed together through use_mode /
|
||||
// set_connectors: each tests against the other's old value. Stage a
|
||||
// replacement surface, retire the old one *before* its first commit
|
||||
// (Smithay clears the CRTC on surface drop), then publish metadata.
|
||||
let replacement = self.staged.remove(&crtc).unwrap();
|
||||
let previous = self.device.outputs.get_mut(&crtc).unwrap();
|
||||
previous.surface = replacement.surface;
|
||||
self.replaced.insert(crtc);
|
||||
}
|
||||
self.staged
|
||||
.get_mut(&crtc)
|
||||
.or_else(|| self.device.outputs.get_mut(&crtc))
|
||||
.unwrap()
|
||||
.surface
|
||||
.commit_frame()
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn disable(&mut self, crtc: crtc::Handle) -> Result<(), String> {
|
||||
self.device
|
||||
.outputs
|
||||
.get_mut(&crtc)
|
||||
.unwrap()
|
||||
.surface
|
||||
.clear()
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn abort_staged(&mut self) {
|
||||
for output in self.staged.values_mut() {
|
||||
let _ = output.surface.clear();
|
||||
}
|
||||
self.staged.clear();
|
||||
}
|
||||
|
||||
fn restore(&mut self, crtc: crtc::Handle) -> Result<(), String> {
|
||||
if self.replaced.contains(&crtc) {
|
||||
let output = &self.device.outputs[&crtc];
|
||||
let connector = self
|
||||
.device
|
||||
.drm
|
||||
.get_connector(output.connector, true)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if connector.state() != connector::State::Connected {
|
||||
return Err("previous connector is no longer available".into());
|
||||
}
|
||||
let selection = OutputSelection {
|
||||
connector,
|
||||
crtc,
|
||||
mode: output.mode,
|
||||
settings: output.settings.clone(),
|
||||
identity: output.identity.clone(),
|
||||
mirror_source: output.mirror_source.clone(),
|
||||
};
|
||||
let mut replacement = create_direct_output(
|
||||
self.state,
|
||||
&mut self.device.drm,
|
||||
&self.device.gbm,
|
||||
&self.device.renderer,
|
||||
selection,
|
||||
false,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
prepare(
|
||||
&mut replacement.surface,
|
||||
&mut self.device.renderer,
|
||||
replacement.mode,
|
||||
&replacement.settings,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let previous = self.device.outputs.get_mut(&crtc).unwrap();
|
||||
previous.surface = replacement.surface;
|
||||
previous.surface.commit_frame().map_err(|error| error.to_string())?;
|
||||
}
|
||||
let Some(output) = self.device.outputs.get_mut(&crtc) else {
|
||||
return Ok(());
|
||||
};
|
||||
output.surface.reset_state().map_err(|error| error.to_string())?;
|
||||
let restored = output
|
||||
.surface
|
||||
.set_connectors(&[output.connector])
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|()| {
|
||||
prepare(
|
||||
&mut output.surface,
|
||||
&mut self.device.renderer,
|
||||
output.mode,
|
||||
&output.settings,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.and_then(|()| output.surface.commit_frame().map_err(|error| error.to_string()));
|
||||
self.state.session_lock.output_added(&output.output);
|
||||
output.lock_frame_pending = false;
|
||||
output.surface.set_output_mode_source((&output.output).into());
|
||||
cancel_output_timers(&self.state.loop_handle, output);
|
||||
output.frame_pending = false;
|
||||
restored
|
||||
}
|
||||
}
|
||||
|
||||
/// All allocations/tests precede commits. Logical publication follows every
|
||||
/// successful commit on this device; other devices are separately rollbackable.
|
||||
pub(super) fn apply_device(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
device: &mut DirectDevice,
|
||||
scan: OutputScan,
|
||||
) -> Result<(), String> {
|
||||
let selections = scan
|
||||
.selections
|
||||
.into_iter()
|
||||
.map(|selection| (selection.crtc, selection))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut changed = selections
|
||||
.iter()
|
||||
.filter_map(|(crtc, selection)| {
|
||||
let same = device.outputs.get(crtc).is_some_and(|output| {
|
||||
output.connector == selection.connector.handle()
|
||||
&& output.identity == selection.identity
|
||||
&& output.mode == selection.mode
|
||||
&& output.settings.same_configuration(&selection.settings)
|
||||
&& output.mirror_source == selection.mirror_source
|
||||
});
|
||||
(!same).then_some(*crtc)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
changed.sort_by_key(|crtc| (selections[crtc].mirror_source.is_some(), u32::from(*crtc)));
|
||||
let removals = device
|
||||
.outputs
|
||||
.keys()
|
||||
.filter(|crtc| !selections.contains_key(crtc))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let hardware_changes = changed
|
||||
.iter()
|
||||
.filter(|crtc| {
|
||||
!device.outputs.get(crtc).is_some_and(|output| {
|
||||
let selection = &selections[crtc];
|
||||
output.connector == selection.connector.handle() && output.mode == selection.mode
|
||||
})
|
||||
})
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let mut driver = DeviceTransaction {
|
||||
state,
|
||||
device,
|
||||
selections: &selections,
|
||||
staged: HashMap::new(),
|
||||
replaced: HashSet::new(),
|
||||
};
|
||||
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &removals);
|
||||
let mut staged = std::mem::take(&mut driver.staged);
|
||||
if let Err(failure) = result {
|
||||
for crtc in failure.unavailable {
|
||||
if let Some(mut output) = device.outputs.remove(&crtc) {
|
||||
retire(state, &mut output, node, crtc);
|
||||
}
|
||||
}
|
||||
return Err(failure.error);
|
||||
}
|
||||
for crtc in &changed {
|
||||
let selection = &selections[crtc];
|
||||
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap();
|
||||
let old_geometry = state.space.output_geometry(&output.output);
|
||||
let old_id = state.output_id(&output.output);
|
||||
if output.connector != selection.connector.handle() || output.identity != selection.identity {
|
||||
retire(state, &mut output, node, *crtc);
|
||||
let (handle, _) = create_output(
|
||||
state,
|
||||
&selection.connector,
|
||||
selection.mode,
|
||||
selection.identity.clone(),
|
||||
&selection.settings,
|
||||
false,
|
||||
);
|
||||
output.output = handle;
|
||||
}
|
||||
if selection.mirror_source.is_some() && output.global.is_some() {
|
||||
retire(state, &mut output, node, *crtc);
|
||||
}
|
||||
output.connector = selection.connector.handle();
|
||||
output.internal = internal_connector(selection.connector.interface());
|
||||
output.identity = selection.identity.clone();
|
||||
output.settings = selection.settings.clone();
|
||||
output.mode = selection.mode;
|
||||
output.mirror_source = selection.mirror_source.clone();
|
||||
let position = selection.settings.position.unwrap_or_else(|| {
|
||||
old_geometry
|
||||
.map(|geometry| [geometry.loc.x, geometry.loc.y])
|
||||
.unwrap_or_else(|| {
|
||||
[
|
||||
state
|
||||
.space
|
||||
.outputs()
|
||||
.filter_map(|output| state.space.output_geometry(output))
|
||||
.map(|geometry| geometry.loc.x + geometry.size.w)
|
||||
.max()
|
||||
.unwrap_or(0),
|
||||
0,
|
||||
]
|
||||
})
|
||||
});
|
||||
output.output.change_current_state(
|
||||
Some(OutputMode::from(output.mode)),
|
||||
Some(output_transform(output.settings.transform)),
|
||||
Some(Scale::Fractional(output.settings.scale)),
|
||||
Some((position[0], position[1]).into()),
|
||||
);
|
||||
if output.mirror_source.is_none() {
|
||||
if output.global.is_none() {
|
||||
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle));
|
||||
}
|
||||
state.space.map_output(&output.output, (position[0], position[1]));
|
||||
if state.output_id(&output.output).is_none() {
|
||||
state.register_output(&output.output, output.identity.clone());
|
||||
}
|
||||
if let Some(id) = state.output_id(&output.output)
|
||||
&& let Some(geometry) = state.space.output_geometry(&output.output)
|
||||
{
|
||||
state.output_workspaces.update_geometry(
|
||||
id,
|
||||
ferese_core::OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h),
|
||||
);
|
||||
if old_id == Some(id)
|
||||
&& let Some(old) = old_geometry
|
||||
{
|
||||
state.reposition_output_floats(
|
||||
id,
|
||||
ferese_layout::Rect::new(
|
||||
old.loc.x as f64,
|
||||
old.loc.y as f64,
|
||||
old.size.w as f64,
|
||||
old.size.h as f64,
|
||||
),
|
||||
ferese_layout::Rect::new(
|
||||
geometry.loc.x as f64,
|
||||
geometry.loc.y as f64,
|
||||
geometry.size.w as f64,
|
||||
geometry.size.h as f64,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.session_lock.output_added(&output.output);
|
||||
output.lock_frame_pending = false;
|
||||
output.surface.set_output_mode_source((&output.output).into());
|
||||
cancel_output_timers(&state.loop_handle, &mut output);
|
||||
output.frame_pending = false;
|
||||
output.primary_commit = None;
|
||||
output.capture_texture = None;
|
||||
output.power.suspended();
|
||||
let clock = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.presentation
|
||||
.entry((node, *crtc))
|
||||
.or_default();
|
||||
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
||||
clock.reset_timing();
|
||||
device.outputs.insert(*crtc, output);
|
||||
}
|
||||
// Add/register destinations before removing former owners, reusing evacuation.
|
||||
for crtc in removals {
|
||||
let mut output = device.outputs.remove(&crtc).unwrap();
|
||||
retire(state, &mut output, node, crtc);
|
||||
}
|
||||
device.connected_outputs = scan.connected_outputs;
|
||||
update_applied_info(device);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn update_applied_info(device: &mut DirectDevice) {
|
||||
for info in &mut device.connected_outputs {
|
||||
info.enabled = false;
|
||||
info.current_mode = None;
|
||||
if let Some(output) = device.outputs.values().find(|output| output.identity == info.identity) {
|
||||
info.enabled = true;
|
||||
info.current_mode = Some(connected_mode_info(output.mode));
|
||||
info.scale = output.settings.scale;
|
||||
info.transform = output.settings.transform;
|
||||
info.configured_position = output.settings.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnected hardware cannot remain logically live, even if a new policy fails validation.
|
||||
pub(super) fn retire_disconnected(state: &mut Ferese) {
|
||||
let missing = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.devices
|
||||
.iter()
|
||||
.flat_map(|(node, device)| {
|
||||
device.outputs.iter().filter_map(|(crtc, output)| {
|
||||
device
|
||||
.drm
|
||||
.get_connector(output.connector, true)
|
||||
.ok()
|
||||
.filter(|connector| connector.state() != connector::State::Connected)
|
||||
.map(|_| (*node, *crtc))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (node, crtc) in missing {
|
||||
if let Some(mut output) = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.devices
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.outputs
|
||||
.remove(&crtc)
|
||||
{
|
||||
let _ = output.surface.clear();
|
||||
retire(state, &mut output, node, crtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If initial activation or rollback leaves no usable desktop, try a preferred
|
||||
/// mode on each physical monitor independently. Never disable a surviving output.
|
||||
pub(super) fn recover_last_output(state: &mut Ferese) {
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.devices
|
||||
.values()
|
||||
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let backend = state.direct_backend.as_ref().unwrap();
|
||||
let requested = backend.desired_outputs.clone();
|
||||
let original_error = backend.output_error.clone();
|
||||
let mut candidates = backend
|
||||
.monitors
|
||||
.iter()
|
||||
.filter(|monitor| monitor.usable)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
candidates.sort_by_key(|monitor| (!monitor.internal, monitor.key.clone()));
|
||||
for candidate in candidates {
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
backend.desired_outputs = DesiredOutputConfiguration {
|
||||
profile: None,
|
||||
suspend: requested.suspend,
|
||||
outputs: backend
|
||||
.monitors
|
||||
.iter()
|
||||
.map(|monitor| {
|
||||
let mut settings = crate::output_policy::defaults(monitor.identity.clone());
|
||||
settings.enabled = monitor.key == candidate.key;
|
||||
crate::output_policy::DesiredOutput {
|
||||
key: monitor.key.clone(),
|
||||
settings,
|
||||
mirror_source: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let nodes = backend
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|(_, device)| candidate.key.starts_with(&format!("{}/", drm_device_key(&device.drm))))
|
||||
.map(|(node, _)| *node)
|
||||
.collect::<Vec<_>>();
|
||||
if validate_desired_outputs(state).is_ok() {
|
||||
for node in nodes {
|
||||
let _ = rescan_device(state, node);
|
||||
}
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.devices
|
||||
.values()
|
||||
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
|
||||
{
|
||||
remember_applied_configuration(state);
|
||||
state.direct_backend.as_mut().unwrap().output_error = original_error;
|
||||
tracing::warn!(identity = %candidate.identity, "recovered last usable output with preferred mode");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
backend.desired_outputs = requested;
|
||||
backend.output_error = original_error;
|
||||
}
|
||||
Reference in new issue
Block a user