Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

File diff suppressed because it is too large. Load diff
+78 -35
View File
@@ -17,31 +17,32 @@ pub(crate) fn touch_cache(state: &mut Ferese) {
if state.capture_last_used.replace(Instant::now()).is_some() {
return;
}
if let Err(error) = state
.loop_handle
.insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| {
let Some(last_used) = state.capture_last_used else {
return TimeoutAction::Drop;
};
let idle = last_used.elapsed();
if idle < CACHE_IDLE {
return TimeoutAction::ToDuration(CACHE_IDLE - idle);
}
// A stream can pause or fail without another repaint. Release its
// offscreen targets and filtered-scene caches from the event loop.
state.capture_last_used = None;
state.capture_render = Default::default();
state.nested_capture_texture = None;
if let Some(backend) = state.direct_backend.as_mut() {
for device in backend.devices.values_mut() {
for output in device.outputs.values_mut() {
output.capture_texture = None;
output.mirror_capture_texture = None;
if let Err(error) =
state
.loop_handle
.insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| {
let Some(last_used) = state.capture_last_used else {
return TimeoutAction::Drop;
};
let idle = last_used.elapsed();
if idle < CACHE_IDLE {
return TimeoutAction::ToDuration(CACHE_IDLE - idle);
}
// A stream can pause or fail without another repaint. Release its
// offscreen targets and filtered-scene caches from the event loop.
state.capture_last_used = None;
state.capture_render = Default::default();
state.nested_capture_texture = None;
if let Some(backend) = state.direct_backend.as_mut() {
for device in backend.devices.values_mut() {
for output in device.outputs.values_mut() {
output.capture_texture = None;
output.mirror_capture_texture = None;
}
}
}
}
TimeoutAction::Drop
})
TimeoutAction::Drop
})
{
state.capture_last_used = None;
tracing::warn!(%error, "could not schedule capture cache cleanup");
@@ -85,9 +86,18 @@ pub(super) fn capture_mirror(
}
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
// Never sample mirror_texture: it contains the unfiltered display.
let canvas = output.mirror_canvas.as_ref().expect("mirror display composed");
let canvas = output
.mirror_canvas
.as_ref()
.expect("mirror display composed");
let Some(()) = capture_request(state, &output.output, include_cursor, || {
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements)
super::mirror::compose(
renderer,
&mut output.mirror_capture_texture,
canvas,
source,
&elements,
)
}) else {
continue;
};
@@ -98,7 +108,12 @@ pub(super) fn capture_mirror(
Default::default(),
);
let Some(target) = capture_request(state, &output.output, include_cursor, || {
render_capture(renderer, &mut output.capture_texture, &output.output, &[element])
render_capture(
renderer,
&mut output.capture_texture,
&output.output,
&[element],
)
}) else {
continue;
};
@@ -168,7 +183,10 @@ mod tests {
#[test]
#[ignore = "requires an EGL rendering device"]
fn capture_recomposes_cursor_without_touching_the_display_target() {
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
let device = EGLDevice::enumerate()
.unwrap()
.last()
.expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
@@ -223,18 +241,32 @@ mod tests {
] {
let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap();
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
.copy_framebuffer(
&target,
Rectangle::from_size((32, 24).into()),
Fourcc::Abgr8888,
)
.unwrap();
let pixels = renderer.map_texture(&mapping).unwrap();
assert_eq!(&pixels[..4], &expected);
assert_eq!(&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], &[255, 0, 0, 255]);
assert_eq!(
&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4],
&[255, 0, 0, 255]
);
drop(target);
let target = renderer.bind(displayed.as_mut().unwrap()).unwrap();
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
.copy_framebuffer(
&target,
Rectangle::from_size((32, 24).into()),
Fourcc::Abgr8888,
)
.unwrap();
assert_eq!(&renderer.map_texture(&mapping).unwrap()[..4], &[255, 0, 0, 255]);
assert_eq!(
&renderer.map_texture(&mapping).unwrap()[..4],
&[255, 0, 0, 255]
);
}
output.change_current_state(
@@ -246,7 +278,9 @@ mod tests {
None,
None,
);
let _ = render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[]).unwrap();
let _ =
render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[])
.unwrap();
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
}
}
@@ -318,7 +352,10 @@ mod failure_tests {
Ok::<(), ()>(())
};
after_queue(queued, || {
assert!(submitted.get(), "capture must run after the display queue succeeds");
assert!(
submitted.get(),
"capture must run after the display queue succeeds"
);
assert!(
capture_request(&mut state, &output, false, || Err::<(), _>(
"injected allocation failure"
@@ -328,14 +365,20 @@ mod failure_tests {
})
.unwrap();
assert!(submitted.get(), "capture failure must not undo queued display work");
assert!(
submitted.get(),
"capture failure must not undo queued display work"
);
assert_eq!(state.pending_screencopies.len(), 1);
assert_eq!(state.pending_screencopies[0].output, other_output);
let outcome = receiver.try_recv().unwrap();
assert_eq!(outcome.request, 41);
assert_eq!(outcome.part, 0);
assert!(outcome.result.is_err());
assert!(receiver.try_recv().is_err(), "request should fail exactly once");
assert!(
receiver.try_recv().is_err(),
"request should fail exactly once"
);
assert!(
other_receiver.try_recv().is_err(),
"other output's request must stay pending"
@@ -130,17 +130,31 @@ mod tests {
#[test]
fn every_connector_order_finds_a_complete_matching() {
let graph = [&[1, 2][..], &[1, 3][..], &[1, 3][..]];
for order in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] {
for order in [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let requests = order.map(|index| candidates(None, None, graph[index]));
let result = assign(&requests);
assert_eq!(result.iter().flatten().count(), 3, "order {order:?}");
assert_eq!(assign(&requests), result, "deterministic for the same inventory");
assert_eq!(
assign(&requests),
result,
"deterministic for the same inventory"
);
}
}
#[test]
fn equal_preferences_use_connector_order() {
let result = assign(&[candidates(Some(1), None, &[1, 2]), candidates(Some(1), None, &[1, 2])]);
let result = assign(&[
candidates(Some(1), None, &[1, 2]),
candidates(Some(1), None, &[1, 2]),
]);
assert_eq!(result, vec![Some(crtc(1)), Some(crtc(2))]);
}
@@ -159,18 +173,29 @@ mod tests {
#[test]
fn maximum_cardinality_and_preference_objective_match_exhaustive_oracle() {
fn score(requests: &[Candidates], assignment: &[Option<crtc::Handle>]) -> (usize, usize, usize) {
fn score(
requests: &[Candidates],
assignment: &[Option<crtc::Handle>],
) -> (usize, usize, usize) {
let retained = [true, false].map(|applied| {
requests
.iter()
.zip(assignment)
.filter(|(request, actual)| {
let preferred = if applied { request.applied } else { request.kernel };
let preferred = if applied {
request.applied
} else {
request.kernel
};
preferred.is_some() && preferred == **actual
})
.count()
});
(assignment.iter().flatten().count(), retained[0], retained[1])
(
assignment.iter().flatten().count(),
retained[0],
retained[1],
)
}
fn enumerate(
@@ -204,7 +229,8 @@ mod tests {
.filter(|crtc| graph & (1 << (index * 3 + crtc - 1)) != 0)
.collect::<Vec<_>>();
let applied = ((seed + index) % 4 != 0).then_some((seed + index) % 4);
let kernel = ((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4);
let kernel =
((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4);
candidates(applied, kernel, &compatible)
})
.collect::<Vec<_>>();
@@ -213,13 +239,14 @@ mod tests {
let actual = assign(&requests);
let used = actual.iter().flatten().collect::<HashSet<_>>();
assert_eq!(used.len(), actual.iter().flatten().count());
assert!(
requests
.iter()
.zip(&actual)
.all(|(request, crtc)| crtc.is_none_or(|crtc| request.compatible.contains(&crtc)))
assert!(requests.iter().zip(&actual).all(|(request, crtc)| {
crtc.is_none_or(|crtc| request.compatible.contains(&crtc))
}));
assert_eq!(
score(&requests, &actual),
best,
"graph {graph}, preferences {seed}"
);
assert_eq!(score(&requests, &actual), best, "graph {graph}, preferences {seed}");
}
}
}
@@ -250,10 +277,16 @@ mod tests {
// The internal panel appears first in the DRM connector inventory.
let internal = candidates(None, None, &[151, 270]);
let external = candidates(Some(151), Some(151), &[151, 270]);
assert_eq!(assign(&[internal, external]), vec![Some(crtc(270)), Some(crtc(151))]);
assert_eq!(
assign(&[internal, external]),
vec![Some(crtc(270)), Some(crtc(151))]
);
let internal = candidates(None, None, &[151, 270]);
let external = candidates(Some(151), Some(151), &[151, 270]);
assert_eq!(assign(&[external, internal]), vec![Some(crtc(151)), Some(crtc(270))]);
assert_eq!(
assign(&[external, internal]),
vec![Some(crtc(151)), Some(crtc(270))]
);
}
#[test]
@@ -304,7 +337,10 @@ mod tests {
#[test]
fn restricted_connector_gets_its_only_free_crtc() {
assert_eq!(
assign(&[candidates(None, None, &[151, 270]), candidates(None, None, &[151])]),
assign(&[
candidates(None, None, &[151, 270]),
candidates(None, None, &[151])
]),
vec![Some(crtc(270)), Some(crtc(151))]
);
}
@@ -312,7 +348,10 @@ mod tests {
#[test]
fn exhausted_crtcs_do_not_displace_surviving_output_or_assign_twice() {
assert_eq!(
assign(&[candidates(None, None, &[151]), candidates(Some(151), Some(151), &[151])]),
assign(&[
candidates(None, None, &[151]),
candidates(Some(151), Some(151), &[151])
]),
vec![None, Some(crtc(151))]
);
assert!(assign(&[]).is_empty());
+20 -5
View File
@@ -54,7 +54,10 @@ pub(super) struct Diagnostics {
impl Diagnostics {
pub fn from_environment(output: String) -> Self {
Self::new(output, std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some())
Self::new(
output,
std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(),
)
}
fn new(output: String, enabled: bool) -> Self {
@@ -191,9 +194,18 @@ mod tests {
#[test]
fn primary_classification_does_not_infer_scanout_from_zero_damage() {
assert_eq!(PrimaryOutcome::classify(true, false), PrimaryOutcome::DirectScanout);
assert_eq!(PrimaryOutcome::classify(false, true), PrimaryOutcome::Composed);
assert_eq!(PrimaryOutcome::classify(false, false), PrimaryOutcome::ReusedSwapchain);
assert_eq!(
PrimaryOutcome::classify(true, false),
PrimaryOutcome::DirectScanout
);
assert_eq!(
PrimaryOutcome::classify(false, true),
PrimaryOutcome::Composed
);
assert_eq!(
PrimaryOutcome::classify(false, false),
PrimaryOutcome::ReusedSwapchain
);
}
#[test]
@@ -224,7 +236,10 @@ mod tests {
let mut d = Diagnostics::new("test".into(), true);
let queued = Err::<(), _>("queue failed");
d.queue_result(&queued, PrimaryOutcome::DirectScanout, true, true);
assert!(super::super::capture::after_queue(queued, || panic!("failed queues must not capture")).is_err());
assert!(
super::super::capture::after_queue(queued, || panic!("failed queues must not capture"))
.is_err()
);
assert_eq!(d.counters.submissions, 0);
assert_eq!(d.counters.scanout_submissions, 0);
assert_eq!(d.counters.cursor_plane_submissions, 0);
+79 -23
View File
@@ -72,17 +72,27 @@ impl OutputFeedback {
// Restrict the preference to the observed primary format/modifier:
// without ALLOW_PRIMARY_PLANE_SCANOUT_ANY Smithay compares the full
// format with the swapchain. Renderer-only formats remain in fallback.
match DmabufFeedbackBuilder::new(self.render_device, self.render_formats.iter().copied())
.add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6)
.build()
match DmabufFeedbackBuilder::new(
self.render_device,
self.render_formats.iter().copied(),
)
.add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6)
.build()
{
Ok(feedback) => self.preferred = Some(feedback),
Err(error) => tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback"),
Err(error) => {
tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback")
}
}
}
}
pub fn observed_frame(&mut self, format: Option<Format>, states: &RenderElementStates, candidates: HashSet<Id>) {
pub fn observed_frame(
&mut self,
format: Option<Format>,
states: &RenderElementStates,
candidates: HashSet<Id>,
) {
self.observe_format(format);
self.visible = states
.states
@@ -203,13 +213,17 @@ pub(crate) fn refresh(state: &mut Ferese) {
.filter(|id| {
state
.current_window_presentation(*id)
.is_some_and(|presentation| presentation.alpha() == 1.0 && presentation.dim == 0.0)
.is_some_and(|presentation| {
presentation.alpha() == 1.0 && presentation.dim == 0.0
})
})
.and_then(|id| state.windows.window(id))
.and_then(|window| window.toplevel())
.filter(|toplevel| {
with_renderer_surface_state(toplevel.wl_surface(), |state| state.buffer().is_some())
.unwrap_or(false)
with_renderer_surface_state(toplevel.wl_surface(), |state| {
state.buffer().is_some()
})
.unwrap_or(false)
})
.map(|toplevel| Id::from(toplevel.wl_surface()))
})
@@ -251,8 +265,10 @@ mod tests {
let feedback = DmabufFeedbackBuilder::new(10, [format(Fourcc::Argb8888, Modifier::Linear)])
.build()
.unwrap();
let global = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
let other = state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
let global =
state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
let other =
state.create_global_with_default_feedback::<Ferese>(&display.handle(), &feedback);
(display, global, other)
}
fn visible(output: &mut OutputFeedback, id: &Id, f: Format) {
@@ -273,11 +289,31 @@ mod tests {
let linear = format(Fourcc::Argb8888, Modifier::Linear);
let implicit = format(Fourcc::Argb8888, Modifier::Invalid);
let opaque = format(Fourcc::Xrgb8888, Modifier::Linear);
assert!(compatible(&formats(&[linear, implicit]), &formats(&[linear]), linear));
assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), linear));
assert!(!compatible(&formats(&[linear]), &formats(&[opaque]), linear));
assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), implicit));
assert!(!compatible(&formats(&[opaque]), &formats(&[linear]), linear));
assert!(compatible(
&formats(&[linear, implicit]),
&formats(&[linear]),
linear
));
assert!(!compatible(
&formats(&[linear]),
&formats(&[implicit]),
linear
));
assert!(!compatible(
&formats(&[linear]),
&formats(&[opaque]),
linear
));
assert!(!compatible(
&formats(&[linear]),
&formats(&[implicit]),
implicit
));
assert!(!compatible(
&formats(&[opaque]),
&formats(&[linear]),
linear
));
}
#[test]
@@ -307,7 +343,11 @@ mod tests {
let mut b = output(&[f], &[f]);
visible(&mut a, &id, f);
visible(&mut b, &id, f);
fn view(global: DmabufGlobal, feedback: &OutputFeedback, eligible: Option<Id>) -> OutputView<'_> {
fn view(
global: DmabufGlobal,
feedback: &OutputFeedback,
eligible: Option<Id>,
) -> OutputView<'_> {
OutputView {
global,
feedback,
@@ -320,7 +360,10 @@ mod tests {
select(
global,
&id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))]
[
view(global, &a, Some(id.clone())),
view(global, &b, Some(id.clone()))
]
)
.is_none()
);
@@ -331,7 +374,10 @@ mod tests {
select(
global,
&id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))]
[
view(global, &a, Some(id.clone())),
view(global, &b, Some(id.clone()))
]
)
.is_some()
);
@@ -347,7 +393,12 @@ mod tests {
let mut o = output(&[f], &[f]);
visible(&mut o, &id, f);
o.invalidate();
assert!(o.observed.is_none() && o.preferred.is_none() && o.visible.is_empty() && o.candidates.is_empty());
assert!(
o.observed.is_none()
&& o.preferred.is_none()
&& o.visible.is_empty()
&& o.candidates.is_empty()
);
o.observe_format(None);
assert!(o.preferred.is_none());
visible(&mut o, &id, f);
@@ -390,9 +441,12 @@ mod tests {
"clipped storage must remain hidden"
);
let faded = solid(bounds, 0.8);
assert!(!candidate_geometry(&faded, bounds, scale.into(), || panic!(
"faded elements must not probe storage"
)));
assert!(!candidate_geometry(
&faded,
bounds,
scale.into(),
|| panic!("faded elements must not probe storage")
));
let mut partial = bounds;
partial.size.w -= 1;
assert!(!candidate_geometry(
@@ -474,7 +528,9 @@ mod tests {
let f = format(Fourcc::Argb8888, Modifier::Linear);
let id = Id::new();
let mut o = output(&[f], &[f]);
let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || visible(&mut o, &id, f));
let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || {
visible(&mut o, &id, f)
});
assert!(failed.is_err());
assert!(o.visible.is_empty() && o.preferred.is_none());
super::super::capture::after_queue(Ok::<(), ()>(()), || visible(&mut o, &id, f)).unwrap();
+76 -24
View File
@@ -24,8 +24,14 @@ struct Transition {
// A single owner determines both the desired ramp and who loses control on failure.
enum Source {
Original,
Client { control: ZwlrGammaControlV1, ramp: Ramp },
NightLight { temperature: u16, ramp: Ramp },
Client {
control: ZwlrGammaControlV1,
ramp: Ramp,
},
NightLight {
temperature: u16,
ramp: Ramp,
},
}
#[derive(Default)]
@@ -136,7 +142,8 @@ impl Gamma {
fn configure(&mut self, settings: Settings, duration: Duration, now: Instant) {
if settings.enabled {
if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature) {
if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature)
{
return;
}
self.replace_source(Source::NightLight {
@@ -152,11 +159,14 @@ impl Gamma {
Source::Original => &self.original,
Source::NightLight { ramp, .. } | Source::Client { ramp, .. } => ramp,
};
self.transition = (!duration.is_zero() && !self.suspended && self.current != *target).then(|| Transition {
from: self.current.clone(),
started: now,
duration,
});
self.transition =
(!duration.is_zero() && !self.suspended && self.current != *target).then(|| {
Transition {
from: self.current.clone(),
started: now,
duration,
}
});
}
fn owns(&self, owner: &ZwlrGammaControlV1) -> bool {
@@ -181,8 +191,10 @@ impl Gamma {
};
let animated;
let ramp = if let Some(transition) = &self.transition {
let progress =
now.saturating_duration_since(transition.started).as_secs_f64() / transition.duration.as_secs_f64();
let progress = now
.saturating_duration_since(transition.started)
.as_secs_f64()
/ transition.duration.as_secs_f64();
if progress >= 1. {
self.transition = None;
ramp
@@ -222,15 +234,26 @@ impl Drop for Gamma {
}
}
pub(crate) fn acquire(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<usize> {
pub(crate) fn acquire(
state: &mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<usize> {
// Reconcile a just-reloaded configuration before assigning ownership.
sync_night_light(state);
if state.night_light.enabled {
return None;
}
let duration = state.animation_duration(TRANSITION_DURATION);
let backend = state.direct_backend.as_mut().filter(|backend| backend.active)?;
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
let backend = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)?;
for device in backend
.devices
.values_mut()
.filter(|device| device.drm.is_active())
{
for (crtc, output) in &mut device.outputs {
if &output.output != target || output.global.is_none() {
continue;
@@ -255,28 +278,41 @@ pub(crate) fn sync_night_light(state: &mut Ferese) {
if !state.gamma_dirty {
return;
}
if !state.direct_backend.as_ref().is_some_and(|backend| backend.active) {
if !state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.active)
{
return;
}
state.gamma_dirty = false;
if advance_night_light(state, true) && !state.gamma_timer_pending {
let result = state
.loop_handle
.insert_source(Timer::from_duration(TRANSITION_INTERVAL), |_, _, state| {
let result = state.loop_handle.insert_source(
Timer::from_duration(TRANSITION_INTERVAL),
|_, _, state| {
if advance_night_light(state, false) {
TimeoutAction::ToDuration(TRANSITION_INTERVAL)
} else {
state.gamma_timer_pending = false;
TimeoutAction::Drop
}
});
},
);
match result {
Ok(_) => state.gamma_timer_pending = true,
Err(error) => {
tracing::warn!(%error, "could not schedule night-light transition");
if let Some(backend) = &mut state.direct_backend {
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
for output in device.outputs.values_mut().filter(|output| !output.power.needs_wake()) {
for device in backend
.devices
.values_mut()
.filter(|device| device.drm.is_active())
{
for output in device
.outputs
.values_mut()
.filter(|output| !output.power.needs_wake())
{
if let Some(gamma) = output.gamma.controller_mut() {
gamma.transition = None;
gamma.resume(Instant::now());
@@ -292,11 +328,19 @@ pub(crate) fn sync_night_light(state: &mut Ferese) {
fn advance_night_light(state: &mut Ferese, force: bool) -> bool {
let duration = state.animation_duration(TRANSITION_DURATION);
let now = Instant::now();
let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.active) else {
let Some(backend) = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)
else {
return false;
};
let mut pending = false;
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
for device in backend
.devices
.values_mut()
.filter(|device| device.drm.is_active())
{
for (crtc, output) in &mut device.outputs {
let ready = !output.power.needs_wake();
let gamma = if ready {
@@ -346,7 +390,11 @@ pub(crate) fn night_light_status(state: &Ferese) -> Status {
status
}
fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<(&'a mut Gamma, bool)> {
fn owned<'a>(
state: &'a mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<(&'a mut Gamma, bool)> {
let duration = state.animation_duration(TRANSITION_DURATION);
let backend = state.direct_backend.as_mut()?;
for device in backend.devices.values_mut() {
@@ -362,7 +410,11 @@ fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1)
None
}
pub(crate) fn size(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<usize> {
pub(crate) fn size(
state: &mut Ferese,
target: &Output,
owner: &ZwlrGammaControlV1,
) -> Option<usize> {
owned(state, target, owner).map(|(gamma, _)| gamma.original.size())
}
+11 -6
View File
@@ -182,9 +182,13 @@ impl Reader {
"block",
),
)
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedFd>());
.and_then(|reply| {
reply.body().deserialize::<zbus::zvariant::OwnedFd>()
});
match inhibitor {
Ok(descriptor) => worker_lease.lock().unwrap().accept(generation, descriptor),
Ok(descriptor) => {
worker_lease.lock().unwrap().accept(generation, descriptor)
}
Err(error) => tracing::warn!(%error, "could not own logind lid policy"),
}
continue;
@@ -302,16 +306,17 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
backend.lid_reader.lease.lock().unwrap().suspend = false;
let probe = backend.lid.begin(reactivate, wake);
let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state
.loop_handle
.insert_source(Timer::from_duration(Duration::from_secs(3)), move |_, _, state| {
let timer = state.loop_handle.insert_source(
Timer::from_duration(Duration::from_secs(3)),
move |_, _, state| {
if let Some(backend) = state.direct_backend.as_mut() {
backend.lid_reader.deadline = None;
}
tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state");
complete_refresh(state, probe, None);
TimeoutAction::Drop
});
},
);
if sent && let Ok(token) = timer {
state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token);
+42 -19
View File
@@ -6,7 +6,8 @@ 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 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(
@@ -21,7 +22,10 @@ pub(super) fn elements(
source: &Output,
frame: &crate::state::FrameScene,
target: &mut DirectOutput,
) -> Result<Vec<crate::render::AnimatedWindowRenderElement>, smithay::backend::renderer::gles::GlesError> {
) -> 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(|| {
@@ -36,7 +40,13 @@ pub(super) fn elements(
},
)
});
compose(renderer, &mut target.mirror_texture, canvas, source, &elements)?;
compose(
renderer,
&mut target.mirror_texture,
canvas,
source,
&elements,
)?;
let texture = target
.mirror_texture
.as_ref()
@@ -44,7 +54,13 @@ pub(super) fn elements(
.clone();
target.mirror_commit.increment();
Ok(vec![
fitted_texture(texture, &target.output, target.mirror_id.clone(), target.mirror_commit).into(),
fitted_texture(
texture,
&target.output,
target.mirror_id.clone(),
target.mirror_commit,
)
.into(),
])
}
@@ -141,19 +157,20 @@ mod tests {
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 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());
@@ -175,9 +192,15 @@ mod tests {
CommitCounter::default(),
);
let mut target_texture = None;
let framebuffer = capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]).unwrap();
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)
.copy_framebuffer(
&framebuffer,
Rectangle::from_size((96, 64).into()),
Fourcc::Abgr8888,
)
.unwrap();
let pixels = renderer.map_texture(&mapping).unwrap();
let red = pixels
+47 -30
View File
@@ -8,7 +8,8 @@ pub(super) fn inject(mode: &str) -> bool {
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
static MODE: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok());
static MODE: LazyLock<Option<String>> =
LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok());
static USED: AtomicBool = AtomicBool::new(false);
MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel)
}
@@ -57,31 +58,38 @@ pub(super) fn arm_completion(
}
let identity = output.output.clone();
match handle.insert_source(Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| {
let Some(output) = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
.filter(|output| output.output == identity)
else {
return TimeoutAction::Drop;
};
match handle.insert_source(
Timer::from_duration(COMPLETION_TIMEOUT),
move |_, _, state| {
let Some(output) = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
.filter(|output| output.output == identity)
else {
return TimeoutAction::Drop;
};
if let Some(remaining) = output.completion.remaining(Instant::now()) {
if !remaining.is_zero() {
return TimeoutAction::ToDuration(remaining);
if let Some(remaining) = output.completion.remaining(Instant::now()) {
if !remaining.is_zero() {
return TimeoutAction::ToDuration(remaining);
}
output.completion_timer = None;
tracing::error!(
?node,
?crtc,
"DRM frame completion timed out; reconciling device"
);
reconcile_device(state, node);
} else {
output.completion_timer = None;
}
output.completion_timer = None;
tracing::error!(?node, ?crtc, "DRM frame completion timed out; reconciling device");
reconcile_device(state, node);
} else {
output.completion_timer = None;
}
TimeoutAction::Drop
}) {
TimeoutAction::Drop
},
) {
Ok(token) => {
output.completion_timer = Some(token);
true
@@ -106,10 +114,12 @@ pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) {
// sessions retain the invalidation for fresh enumeration on activation.
let active = backend.active;
if active {
match state.loop_handle.insert_source(Timer::immediate(), |_, _, state| {
reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
match state
.loop_handle
.insert_source(Timer::immediate(), |_, _, state| {
reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
Ok(_) => (),
Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"),
}
@@ -125,10 +135,14 @@ mod tests {
let now = Instant::now();
let mut completion = Completion::default();
completion.queued(now);
let (feedback, retired, error) = retirement::<(), _>(Err("injected KMS retirement failure"));
let (feedback, retired, error) =
retirement::<(), _>(Err("injected KMS retirement failure"));
assert_eq!(feedback, None);
assert!(!retired);
assert!(error.is_some(), "the production handler must reconcile the device");
assert!(
error.is_some(),
"the production handler must reconcile the device"
);
assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT));
assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None));
assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None));
@@ -148,7 +162,10 @@ mod tests {
Some(Duration::from_millis(1))
);
// A timeout observes uncertainty; it never frees the current buffer.
assert_eq!(completion.remaining(now + COMPLETION_TIMEOUT), Some(Duration::ZERO));
assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT),
Some(Duration::ZERO)
);
assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)),
Some(Duration::ZERO)
+69 -16
View File
@@ -13,7 +13,13 @@ impl AnimationFallback {
self.orphaned_since.is_some()
}
pub fn deadline(&mut self, now: Instant, interval: Duration, active: bool, chained: bool) -> Option<Instant> {
pub fn deadline(
&mut self,
now: Instant,
interval: Duration,
active: bool,
chained: bool,
) -> Option<Instant> {
if !active || chained {
self.orphaned_since = None;
return None;
@@ -55,12 +61,21 @@ impl SchedulingMetrics {
}
}
pub fn redraw(&mut self, source: &'static Location<'static>, global: bool, requested: usize, connected: usize) {
pub fn redraw(
&mut self,
source: &'static Location<'static>,
global: bool,
requested: usize,
connected: usize,
) {
if !self.enabled {
return;
}
let count = self.redraws.entry((source.file(), source.line(), global)).or_default();
let count = self
.redraws
.entry((source.file(), source.line(), global))
.or_default();
count.calls += 1;
count.requested_outputs += requested as u64;
count.connected_outputs += connected as u64;
@@ -119,8 +134,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
.map(|attributes| attributes.lock().unwrap().hotspot)
.unwrap_or_default()
});
smithay::desktop::utils::bbox_from_surface_tree(surface, (location - hotspot.to_f64()).to_i32_round())
.to_f64()
smithay::desktop::utils::bbox_from_surface_tree(
surface,
(location - hotspot.to_f64()).to_i32_round(),
)
.to_f64()
}
CursorImageStatus::Named(_) => {
@@ -135,7 +153,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
// One logical pixel covers physical rounding at fractional scales.
let bounds = Rectangle::<f64, Logical>::new(
(f64::from(bounds.loc.x) - 1.0, f64::from(bounds.loc.y) - 1.0).into(),
(f64::from(bounds.size.w) + 2.0, f64::from(bounds.size.h) + 2.0).into(),
(
f64::from(bounds.size.w) + 2.0,
f64::from(bounds.size.h) + 2.0,
)
.into(),
);
rect.overlaps(bounds)
}
@@ -152,7 +174,10 @@ mod tests {
let mut idle_fast = AnimationFallback::default();
let mut moving_slow = AnimationFallback::default();
assert_eq!(idle_fast.deadline(now, fast, false, false), None);
assert_eq!(moving_slow.deadline(now, slow, true, false), Some(now + slow * 2));
assert_eq!(
moving_slow.deadline(now, slow, true, false),
Some(now + slow * 2)
);
assert_eq!(
moving_slow.deadline(now + slow, slow, true, false),
Some(now + slow * 2)
@@ -166,7 +191,10 @@ mod tests {
let interval = Duration::from_millis(16);
let mut output = AnimationFallback::default();
output.deadline(now, interval, true, false);
assert_eq!(output.deadline(now + interval, interval, false, false), None);
assert_eq!(
output.deadline(now + interval, interval, false, false),
None
);
let reconnected = now + Duration::from_secs(1);
assert_eq!(
output.deadline(reconnected, interval, true, false),
@@ -192,7 +220,10 @@ mod tests {
last_tick_us = now_us;
}
assert_eq!(policy.deadline(start, Duration::from_micros(16_667), true, true), None);
assert_eq!(
policy.deadline(start, Duration::from_micros(16_667), true, true),
None
);
}
assert_eq!(old_wakes, 60);
@@ -210,9 +241,17 @@ mod tests {
assert_eq!(fallback.deadline(start, interval, true, true), None);
let plan = scheduler.begin().unwrap();
assert_eq!(plan, scheduled);
scheduler.rendered(plan, Duration::from_millis(2), Duration::from_millis(2), true);
scheduler.rendered(
plan,
Duration::from_millis(2),
Duration::from_millis(2),
true,
);
assert_eq!(scheduler.plan(interval), None);
assert_eq!(fallback.deadline(start + interval, interval, true, true), None);
assert_eq!(
fallback.deadline(start + interval, interval, true, true),
None
);
scheduler.presented(interval);
assert_eq!(
fallback.deadline(start + interval, interval, true, false),
@@ -220,7 +259,10 @@ mod tests {
);
scheduler.request(interval);
assert!(scheduler.plan(interval).is_some());
assert_eq!(fallback.deadline(start + interval, interval, true, true), None);
assert_eq!(
fallback.deadline(start + interval, interval, true, true),
None
);
}
#[test]
@@ -237,13 +279,17 @@ mod tests {
assert!(scheduler.has_pending_request());
let deadline = fallback.deadline(start, interval, true, false).unwrap();
let animation_active = false;
let needs_final_frame = animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request());
let needs_final_frame =
animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request());
assert_eq!(
fallback.deadline(deadline, interval, needs_final_frame, false),
Some(deadline)
);
// A successful retry installs the normal timer and cancels fallback.
assert_eq!(fallback.deadline(deadline, interval, needs_final_frame, true), None);
assert_eq!(
fallback.deadline(deadline, interval, needs_final_frame, true),
None
);
scheduler.begin().unwrap();
assert!(!scheduler.has_pending_request());
}
@@ -296,7 +342,11 @@ mod tests {
refresh,
}),
None,
Some(smithay::output::Scale::Fractional(if index == 0 { 2.0 } else { 1.0 })),
Some(smithay::output::Scale::Fractional(if index == 0 {
2.0
} else {
1.0
})),
Some(((index as i32) * 1920, 0).into()),
);
state.space.map_output(&output, ((index as i32) * 1920, 0));
@@ -342,7 +392,10 @@ mod tests {
},
);
assert!(cursor_on_output(&state, &outputs[0]));
assert!(cursor_on_output(&state, &outputs[1]), "cursor image straddles the seam");
assert!(
cursor_on_output(&state, &outputs[1]),
"cursor image straddles the seam"
);
if let Some(cursor) = state.named_cursor_frame() {
let x = 1920.0 - f64::from(cursor.size.w) / f64::from(cursor.buffer_scale) * 0.75
+ f64::from(cursor.hotspot.x) / f64::from(cursor.buffer_scale);
+9 -2
View File
@@ -138,8 +138,15 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
arm_animation_timer(state);
}
pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) {
output.scheduler.rendered(plan, monotonic_now(), cost, submitted);
pub(super) fn record_frame_schedule(
output: &mut DirectOutput,
plan: FramePlan,
cost: Duration,
submitted: bool,
) {
output
.scheduler
.rendered(plan, monotonic_now(), cost, submitted);
if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() {
let started = monotonic_now().saturating_sub(cost);
tracing::debug!(target: "ferese::render", output = %output.output.name(),
@@ -8,8 +8,14 @@ pub(super) struct SessionDevice<S: Session> {
impl<S: Session> SessionDevice<S> {
pub fn open(mut session: S, path: &Path) -> Result<Self, S::Error> {
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?;
Ok(Self { session, fd: Some(fd) })
let fd = session.open(
path,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
)?;
Ok(Self {
session,
fd: Some(fd),
})
}
pub fn device_fd(&self) -> io::Result<DeviceFd> {
@@ -20,7 +26,10 @@ impl<S: Session> SessionDevice<S> {
impl<S: Session> Drop for SessionDevice<S> {
fn drop(&mut self) {
if let Err(error) = self.session.close(self.fd.take().unwrap()) {
tracing::warn!(?error, "could not release DRM device through the session manager");
tracing::warn!(
?error,
"could not release DRM device through the session manager"
);
}
}
}
+150 -35
View File
@@ -31,11 +31,20 @@ impl<Node> Default for DeviceState<Node> {
pub(super) fn device_identity(path: &Path) -> Option<PathBuf> {
// The parent hardware identity survives cardN renumbering after unplug.
std::fs::canonicalize(Path::new("/sys/class/drm").join(path.file_name()?).join("device")).ok()
std::fs::canonicalize(
Path::new("/sys/class/drm")
.join(path.file_name()?)
.join("device"),
)
.ok()
}
impl<Node> DeviceState<Node> {
fn current_path<'a>(&self, original: &Path, observed: &'a [(PathBuf, Option<PathBuf>)]) -> Option<&'a Path> {
fn current_path<'a>(
&self,
original: &Path,
observed: &'a [(PathBuf, Option<PathBuf>)],
) -> Option<&'a Path> {
observed
.iter()
.find(|(path, identity)| match &self.identity {
@@ -48,7 +57,8 @@ impl<Node> DeviceState<Node> {
pub fn failed(&mut self, now: Instant) {
self.availability = Availability::Unavailable;
self.failures = self.failures.saturating_add(1);
self.retry_at = Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5)));
self.retry_at =
Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5)));
}
pub fn ready(&mut self, node: Node) {
@@ -128,7 +138,8 @@ impl<Node: Copy + Eq + std::hash::Hash> Topology<Node> {
) -> HashSet<Node> {
let mut retired = HashSet::new();
for (key, slot) in &mut self.devices {
if let Availability::Ready(previous) | Availability::Degraded(previous) = slot.availability
if let Availability::Ready(previous) | Availability::Degraded(previous) =
slot.availability
&& slot.current_path(key, observed).and_then(&node_for_path) != Some(previous)
{
retired.insert(previous);
@@ -145,7 +156,12 @@ impl<Node: Copy + Eq + std::hash::Hash> Topology<Node> {
pub fn retry_deadline(&self, active: bool) -> Option<Instant> {
active
.then(|| self.devices.values().filter_map(|device| device.retry_at).min())
.then(|| {
self.devices
.values()
.filter_map(|device| device.retry_at)
.min()
})
.flatten()
}
}
@@ -166,7 +182,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
if !backend.topology.request(backend.active && !backend.lid.pending()) {
if !backend
.topology
.request(backend.active && !backend.lid.pending())
{
return;
}
@@ -200,8 +219,11 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.output_profiles
.iter()
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
backend.low_power =
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent());
backend.low_power = automatic
&& super::super::power::low_power(
backend.low_power,
super::super::power::battery_percent(),
);
if let Ok(observed) = &observed {
backend.topology.observe_devices(observed);
@@ -230,7 +252,14 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.and_then(|devices| slot.current_path(&path, devices));
let existing = current_path
.and_then(|path| DrmNode::from_path(path).ok())
.filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node));
.filter(|node| {
state
.direct_backend
.as_ref()
.unwrap()
.devices
.contains_key(node)
});
let previous = match slot.availability {
Availability::Ready(node) | Availability::Degraded(node) => Some(node),
_ => existing,
@@ -269,7 +298,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let mut failed_outputs = Vec::new();
let activation = if reactivate && existing.is_some() {
let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap();
let device = state
.direct_backend
.as_mut()
.unwrap()
.devices
.get_mut(&node)
.unwrap();
device.drm.activate(true).map(|()| {
for (crtc, output) in &mut device.outputs {
cancel_output_timers(&state.loop_handle, output);
@@ -333,7 +368,12 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
ready.push((path, node));
}
let known_good = state.direct_backend.as_ref().unwrap().applied_outputs.clone();
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)
@@ -353,7 +393,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.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();
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();
@@ -380,7 +423,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
break;
}
}
if state.direct_backend.as_ref().unwrap().output_error.is_none() {
if state
.direct_backend
.as_ref()
.unwrap()
.output_error
.is_none()
{
remember_applied_configuration(state);
}
}
@@ -390,7 +439,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) {
desktop_changes.layout = true;
desktop_changes.shell = true;
desktop_changes.outputs.extend(state.space.outputs().cloned());
desktop_changes
.outputs
.extend(state.space.outputs().cloned());
}
let backend = state.direct_backend.as_mut().unwrap();
backend.topology.reconciling = false;
@@ -453,7 +504,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
});
let mode = OutputMode::from(mode);
let transform = output_transform(settings.transform);
let width = (transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32;
let width =
(transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32;
right_edge = right_edge.max(position[0].saturating_add(width));
crate::state::DesktopOutput {
output,
@@ -472,7 +524,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
for output in device.outputs.values_mut() {
if output.mirror_source.is_none() {
if output.global.is_none() {
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle));
output.global =
Some(output.output.create_global::<Ferese>(&state.display_handle));
}
} else {
// Mirrors are physical scanouts, not workspace-owning outputs.
@@ -483,7 +536,9 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
None,
);
}
output.surface.set_output_mode_source((&output.output).into());
output
.surface
.set_output_mode_source((&output.output).into());
}
}
changes
@@ -542,7 +597,11 @@ mod tests {
topology.devices.get_mut(&a).unwrap().ready(node);
// A policy failure keeps the device open, so it must retain ownership
// of its node until replacement retirement runs.
topology.devices.get_mut(&a).unwrap().degraded(node, Instant::now());
topology
.devices
.get_mut(&a)
.unwrap()
.degraded(node, Instant::now());
let observed = [(card.clone(), Some(b.clone()))];
topology.observe_devices(&observed);
assert_eq!(
@@ -551,9 +610,16 @@ mod tests {
);
assert_eq!(topology.devices[&a].availability, Availability::Missing);
assert_eq!(topology.devices[&a].current_path(&a, &observed), None);
assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card.as_path()));
assert_eq!(
topology.devices[&b].current_path(&b, &observed),
Some(card.as_path())
);
topology.devices.get_mut(&b).unwrap().ready(node);
assert!(topology.retire_changed_devices(&observed, |_| Some(node)).is_empty());
assert!(
topology
.retire_changed_devices(&observed, |_| Some(node))
.is_empty()
);
}
#[test]
@@ -567,15 +633,28 @@ mod tests {
let mut topology = Topology::<u64>::default();
topology.observe_devices(&[(card0.clone(), Some(a.clone()))]);
topology.devices.get_mut(&a).unwrap().ready(node0);
let observed = [(card0.clone(), Some(b.clone())), (card2.clone(), Some(a.clone()))];
let observed = [
(card0.clone(), Some(b.clone())),
(card2.clone(), Some(a.clone())),
];
for _ in 0..2 {
topology.observe_devices(&observed);
}
assert_eq!(topology.devices.len(), 2);
assert_eq!(topology.devices[&a].current_path(&a, &observed), Some(card2.as_path()));
assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card0.as_path()));
assert_eq!(
topology.retire_changed_devices(&observed, |path| Some(if path == card0 { node0 } else { node2 })),
topology.devices[&a].current_path(&a, &observed),
Some(card2.as_path())
);
assert_eq!(
topology.devices[&b].current_path(&b, &observed),
Some(card0.as_path())
);
assert_eq!(
topology.retire_changed_devices(&observed, |path| Some(if path == card0 {
node0
} else {
node2
})),
HashSet::from([node0])
);
}
@@ -590,7 +669,10 @@ mod tests {
topology.devices.get_mut(&card).unwrap().ready(node);
topology.observe_devices(&[(card.clone(), Some(identity.clone()))]);
assert_eq!(topology.devices.len(), 1);
assert_eq!(topology.devices[&identity].availability, Availability::Ready(node));
assert_eq!(
topology.devices[&identity].availability,
Availability::Ready(node)
);
}
#[test]
@@ -608,7 +690,10 @@ mod tests {
topology.retire_changed_devices(&unknown, |_| Some(1)),
HashSet::from([1])
);
assert_eq!(topology.devices[&identity].availability, Availability::Missing);
assert_eq!(
topology.devices[&identity].availability,
Availability::Missing
);
assert_eq!(
topology.devices[&card].current_path(&card, &unknown),
Some(card.as_path())
@@ -624,9 +709,17 @@ mod tests {
Some(card.as_path())
);
// The reopened node is still valid and can be reused for this identity.
assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty());
assert!(
topology
.retire_changed_devices(&known, |_| Some(1))
.is_empty()
);
topology.devices.get_mut(&identity).unwrap().ready(1);
assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty());
assert!(
topology
.retire_changed_devices(&known, |_| Some(1))
.is_empty()
);
}
#[test]
@@ -636,10 +729,16 @@ mod tests {
let card0 = PathBuf::from("/dev/dri/card0");
let card1 = PathBuf::from("/dev/dri/card1");
let mut topology = Topology::<u64>::default();
topology.observe_devices(&[(card0.clone(), Some(a.clone())), (card1.clone(), Some(b.clone()))]);
topology.observe_devices(&[
(card0.clone(), Some(a.clone())),
(card1.clone(), Some(b.clone())),
]);
topology.devices.get_mut(&a).unwrap().ready(0);
topology.devices.get_mut(&b).unwrap().ready(1);
let swapped = [(card0.clone(), Some(b.clone())), (card1.clone(), Some(a.clone()))];
let swapped = [
(card0.clone(), Some(b.clone())),
(card1.clone(), Some(a.clone())),
];
let node_for_path = |path: &Path| Some(u64::from(path == card1));
topology.observe_devices(&swapped);
assert_eq!(
@@ -650,7 +749,11 @@ mod tests {
assert_eq!(topology.devices[&b].availability, Availability::Missing);
topology.devices.get_mut(&a).unwrap().ready(1);
topology.devices.get_mut(&b).unwrap().ready(0);
assert!(topology.retire_changed_devices(&swapped, node_for_path).is_empty());
assert!(
topology
.retire_changed_devices(&swapped, node_for_path)
.is_empty()
);
}
#[test]
@@ -693,12 +796,17 @@ mod tests {
fn unavailable_devices_retry_independently_with_capped_backoff() {
let now = Instant::now();
let mut topology = Topology::<u64>::default();
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
topology
.devices
.insert("/dev/dri/card0".into(), DeviceState::default());
let healthy = 1;
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();
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 {
@@ -710,8 +818,15 @@ mod tests {
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_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());
}
}
+95 -26
View File
@@ -6,7 +6,11 @@ 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> {
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);
@@ -35,8 +39,9 @@ fn clear_secondary_planes(
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)
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)?;
@@ -59,12 +64,14 @@ impl Drop for Blobs {
}
}
pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Result<(), Box<dyn Error>> {
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)
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(());
@@ -82,7 +89,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
};
let mut buffers = Vec::new();
let mut framebuffers = Vec::new();
let mut allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
let mut allocator = GbmAllocator::new(
device.gbm.clone(),
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
);
let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node).into());
let mut used_planes = HashSet::new();
let desired_crtcs = scan
@@ -151,16 +161,33 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
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)
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)?;
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),
@@ -176,7 +203,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul
buffers.push(buffer);
framebuffers.push(fb);
}
drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?;
drm.atomic_commit(
AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET,
request,
)?;
Ok(())
}
@@ -348,13 +378,19 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
let previous = self.device.outputs.get_mut(&crtc).unwrap();
previous.surface = replacement.surface;
previous.dmabuf_feedback = replacement.dmabuf_feedback;
previous.surface.commit_frame().map_err(|error| error.to_string())?;
previous
.surface
.commit_frame()
.map_err(|error| error.to_string())?;
}
let Some(output) = self.device.outputs.get_mut(&crtc) else {
return Ok(());
};
output.dmabuf_feedback.invalidate();
output.surface.reset_state().map_err(|error| error.to_string())?;
output
.surface
.reset_state()
.map_err(|error| error.to_string())?;
let restored = output
.surface
.set_connectors(&[output.connector])
@@ -368,9 +404,16 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
)
.map_err(|error| error.to_string())
})
.and_then(|()| output.surface.commit_frame().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);
self.state.display_presentation.remove_output(&output.output);
self.state
.display_presentation
.remove_output(&output.output);
self.state
.desktop_transition
.as_mut()
@@ -378,7 +421,9 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
.redraw
.insert(output.output.clone());
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
output
.surface
.set_output_mode_source((&output.output).into());
cancel_output_timers(&self.state.loop_handle, output);
output.frame_pending = false;
restored
@@ -447,8 +492,12 @@ pub(super) fn apply_device(
}
for crtc in &changed {
let selection = &selections[crtc];
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap();
if output.connector != selection.connector.handle() || output.identity != selection.identity {
let mut output = staged
.remove(crtc)
.or_else(|| device.outputs.remove(crtc))
.unwrap();
if output.connector != selection.connector.handle() || output.identity != selection.identity
{
retire(state, &mut output, node, *crtc);
let (handle, _) = create_output(
state,
@@ -482,7 +531,9 @@ pub(super) fn apply_device(
state.display_presentation.remove_output(&output.output);
}
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
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;
@@ -513,7 +564,11 @@ 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) {
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;
@@ -568,7 +623,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none()))
.any(|device| {
device
.outputs
.values()
.any(|output| output.mirror_source.is_none())
})
{
return;
}
@@ -604,7 +664,11 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
let nodes = backend
.devices
.iter()
.filter(|(_, device)| candidate.key.starts_with(&format!("{}/", drm_device_key(&device.drm))))
.filter(|(_, device)| {
candidate
.key
.starts_with(&format!("{}/", drm_device_key(&device.drm)))
})
.map(|(node, _)| *node)
.collect::<Vec<_>>();
if validate_desired_outputs(state).is_ok() {
@@ -617,7 +681,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none()))
.any(|device| {
device
.outputs
.values()
.any(|output| output.mirror_source.is_none())
})
{
remember_applied_configuration(state);
state.direct_backend.as_mut().unwrap().output_error = original_error;
+37 -9
View File
@@ -26,12 +26,16 @@ pub struct LaunchConfig {
impl LaunchConfig {
pub fn from_environment() -> Result<Self, String> {
let graphical_session = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
let graphical_session =
std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
Self::parse(std::env::args_os().skip(1), graphical_session)
}
fn parse(args: impl IntoIterator<Item = OsString>, graphical_session: bool) -> Result<Self, String> {
fn parse(
args: impl IntoIterator<Item = OsString>,
graphical_session: bool,
) -> Result<Self, String> {
let mut args = args.into_iter().peekable();
let mut requested_backend = None;
let mut client = Vec::new();
@@ -113,7 +117,10 @@ fn parse_backend(value: &OsString) -> Result<Option<BackendKind>, String> {
Some("auto") => Ok(None),
Some("nested") => Ok(Some(BackendKind::Nested)),
Some("drm") => Ok(Some(BackendKind::Drm)),
_ => Err(format!("unknown backend {:?}; expected auto, nested, or drm", value)),
_ => Err(format!(
"unknown backend {:?}; expected auto, nested, or drm",
value
)),
}
}
@@ -148,7 +155,11 @@ mod tests {
.unwrap();
assert!(config.supervise_client);
assert_eq!(config.client, [OsString::from("ferese-session-shell")]);
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL));
assert!(
config
.client_capabilities
.contains(ClientCapabilities::SHELL_CONTROL)
);
}
#[test]
@@ -194,25 +205,42 @@ mod tests {
.unwrap();
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS));
assert!(
config
.client_capabilities
.contains(ClientCapabilities::EFFECTS)
);
}
#[test]
fn explicitly_grants_shell_control_to_the_launched_private_client() {
let config = LaunchConfig::parse(
[OsString::from("--grant-shell-control"), OsString::from("ferese-shell")],
[
OsString::from("--grant-shell-control"),
OsString::from("ferese-shell"),
],
true,
)
.unwrap();
assert_eq!(config.client, [OsString::from("ferese-shell")]);
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL));
assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS));
assert!(
config
.client_capabilities
.contains(ClientCapabilities::SHELL_CONTROL)
);
assert!(
!config
.client_capabilities
.contains(ClientCapabilities::EFFECTS)
);
}
#[test]
fn rejects_an_unknown_backend() {
let error = LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true).unwrap_err();
let error =
LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true)
.unwrap_err();
assert!(error.contains("unknown backend"));
}
+11 -6
View File
@@ -17,10 +17,10 @@ fn read_battery_percent(root: &Path) -> Option<u8> {
.ok()
.map(|text| text.trim().to_owned())
};
if entries
.iter()
.any(|path| read(path, "online").as_deref() == Some("1") && read(path, "type").as_deref() != Some("Battery"))
{
if entries.iter().any(|path| {
read(path, "online").as_deref() == Some("1")
&& read(path, "type").as_deref() != Some("Battery")
}) {
return None;
}
entries.iter().find_map(|path| {
@@ -57,7 +57,9 @@ pub(super) fn low_refresh_mode(size: (u16, u16), modes: &[(u16, u16, i32)]) -> O
modes
.iter()
.enumerate()
.filter(|(_, (width, height, refresh))| (*width, *height) == size && (59_000..=61_000).contains(refresh))
.filter(|(_, (width, height, refresh))| {
(*width, *height) == size && (59_000..=61_000).contains(refresh)
})
.min_by_key(|(_, (_, _, refresh))| refresh.abs_diff(60_000))
.map(|(index, _)| index)
}
@@ -96,7 +98,10 @@ mod tests {
assert_eq!(low_refresh_mode((2880, 1800), &modes), Some(3));
assert_eq!(low_refresh_mode((2880, 1800), &modes[..3]), Some(2));
assert_eq!(low_refresh_mode((2880, 1800), &modes[..2]), None);
assert_eq!(low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]), None);
assert_eq!(
low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]),
None
);
}
#[test]