Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
+426
-157
File diff suppressed because it is too large.
Load diff
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user