feat(render): add output-specific DMA-BUF feedback (#53)

Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
Samuel OnojaandSamuel Onoja authored and GitHub committed 2026-10-14 13:32:18 +01:00
1 parent 9251522a69
commit c8dfb3074d
13 files changed
+913 -3

No files matched your search

+36 -1
View File
@@ -8,6 +8,7 @@ use std::time::{Duration, Instant};
pub(crate) mod capture;
mod crtc_assignment;
mod diagnostics;
pub(crate) mod feedback;
pub(crate) mod gamma;
mod lid;
mod mirror;
@@ -22,14 +23,15 @@ use diagnostics::{Diagnostics, PrimaryOutcome};
use output_power::{OutputPower, Power};
use redraw::{AnimationFallback, SchedulingMetrics};
use scheduling::*;
use smithay::backend::allocator::Fourcc;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::{Buffer, Fourcc};
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
use smithay::backend::egl::{EGLContext, EGLDisplay};
use smithay::backend::input::InputEvent;
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer};
@@ -277,6 +279,7 @@ struct DirectOutput {
capture_texture: Option<GlesTexture>,
render_metrics: RenderMetrics,
diagnostics: Diagnostics,
dmabuf_feedback: feedback::OutputFeedback,
frame_pending: bool,
completion: recovery::Completion,
completion_timer: Option<RegistrationToken>,
@@ -413,6 +416,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
output.gamma.suspend();
cancel_output_timers(&handle, output);
output.surface.reset_buffers();
output.dmabuf_feedback.invalidate();
output.primary_commit = None;
output.capture_texture = None;
output.frame_pending = false;
@@ -971,6 +975,7 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
.dmabuf_state
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
state.dmabuf_imports.register(dmabuf_global);
state.dmabuf_feedback.register(dmabuf_global, feedback);
state
.direct_backend
.as_mut()
@@ -1334,6 +1339,22 @@ fn render_output(
)
.inspect_err(|_| output.diagnostics.render_failed())?;
let primary_format = match &result.primary_element {
PrimaryPlaneElement::Swapchain(primary) => Some(primary.buffer().format()),
PrimaryPlaneElement::Element(element) => match element.underlying_storage(&mut device.renderer) {
Some(UnderlyingStorage::Wayland(buffer)) => smithay::wayland::dmabuf::get_dmabuf(buffer)
.ok()
.map(|buffer| buffer.format()),
_ => None,
},
};
let feedback_candidates = feedback::candidates(
&mut device.renderer,
&elements,
&output.output,
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
);
if result.needs_sync()
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
{
@@ -1347,6 +1368,9 @@ fn render_output(
.into_iter()
.any(|cursor| state.has_pending_screencopy(&output.output, cursor))
});
output
.dmabuf_feedback
.observed_frame(primary_format, &result.states, feedback_candidates);
if output.mirror_source.is_some() {
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
} else {
@@ -1420,6 +1444,9 @@ fn render_output(
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
);
capture::after_queue(queued, || {
output
.dmabuf_feedback
.observed_frame(primary_format, &visibility, feedback_candidates);
if output.mirror_source.is_none() {
state.display_presentation.queued(&output.output, &visibility);
}
@@ -1663,6 +1690,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
state.loop_handle.remove(device.notifier);
state.dmabuf_imports.remove(device.dmabuf_global);
state.dmabuf_feedback.remove(device.dmabuf_global);
state
.dmabuf_state
.disable_global::<Ferese>(&state.display_handle, &device.dmabuf_global);
@@ -2252,6 +2280,12 @@ fn create_direct_output(
surface.set_output_mode_source((&output).into());
let render_metrics = RenderMetrics::from_environment(output.name());
let diagnostics = Diagnostics::from_environment(output.name());
let dmabuf_feedback = feedback::OutputFeedback::new(
render_node.dev_id(),
node.dev_id(),
renderer.dmabuf_formats(),
surface.surface().plane_info().formats.clone(),
);
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
Ok(DirectOutput {
@@ -2274,6 +2308,7 @@ fn create_direct_output(
capture_texture: None,
render_metrics,
diagnostics,
dmabuf_feedback,
frame_pending: false,
completion: Default::default(),
completion_timer: None,
+478
View File
@@ -0,0 +1,478 @@
//! Conservative output feedback for the existing primary-plane policy.
use std::collections::{HashMap, HashSet};
use smithay::backend::allocator::Format;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::renderer::element::{
Element, Id, RenderElement, RenderElementPresentationState, RenderElementStates, RenderingReason,
};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::with_renderer_surface_state;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use smithay::utils::{Physical, Rectangle, Scale};
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
use crate::Ferese;
use crate::render::AnimatedWindowRenderElement;
fn compatible(render: &FormatSet, primary: &FormatSet, observed: Format) -> bool {
// Do not equate implicit and explicit modifiers, alpha and opaque FourCCs,
// or formats from cursor/overlay planes. Exact intersection preserves import.
render.contains(&observed) && primary.contains(&observed)
}
pub(super) struct OutputFeedback {
render_device: libc::dev_t,
scanout_device: libc::dev_t,
render_formats: FormatSet,
primary_formats: FormatSet,
observed: Option<Format>,
preferred: Option<DmabufFeedback>,
visible: HashSet<Id>,
candidates: HashSet<Id>,
}
impl OutputFeedback {
pub fn new(
render_device: libc::dev_t,
scanout_device: libc::dev_t,
render_formats: FormatSet,
primary_formats: FormatSet,
) -> Self {
Self {
render_device,
scanout_device,
render_formats,
primary_formats,
observed: None,
preferred: None,
visible: HashSet::new(),
candidates: HashSet::new(),
}
}
pub fn invalidate(&mut self) {
self.observed = None;
self.preferred = None;
self.visible.clear();
self.candidates.clear();
}
fn observe_format(&mut self, format: Option<Format>) {
let Some(format) = format else {
return;
};
if self.observed == Some(format) {
return;
}
self.observed = Some(format);
self.preferred = None;
if compatible(&self.render_formats, &self.primary_formats, format) {
// 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, Some(TrancheFlags::Scanout), [format])
.build()
{
Ok(feedback) => self.preferred = Some(feedback),
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>) {
self.observe_format(format);
self.visible = states
.states
.iter()
.filter(|(_, state)| crate::display_presentation::is_visible(**state))
.map(|(id, _)| id.clone())
.collect();
self.candidates = candidates
.into_iter()
.filter(|id| {
states.states.get(id).is_some_and(|state| {
crate::display_presentation::is_visible(*state)
&& state.presentation_state
!= RenderElementPresentationState::Rendering {
reason: Some(RenderingReason::ScanoutFailed),
}
})
})
.collect();
}
}
pub(super) fn candidates(
renderer: &mut GlesRenderer,
elements: &[AnimatedWindowRenderElement],
output: &Output,
allowed: bool,
) -> HashSet<Id> {
if !allowed {
return HashSet::new();
}
let Some(mode) = output.current_mode() else {
return HashSet::new();
};
let bounds = Rectangle::from_size(output.current_transform().transform_size(mode.size));
let scale = output.current_scale().fractional_scale().into();
elements
.iter()
.filter(|&element| {
candidate_geometry(element, bounds, scale, || {
element.underlying_storage(renderer).is_some()
})
})
.map(|element| element.id().clone())
.collect()
}
fn candidate_geometry(
element: &impl Element,
bounds: Rectangle<i32, Physical>,
scale: Scale<f64>,
has_storage: impl FnOnce() -> bool,
) -> bool {
let geometry = element.geometry(scale);
if element.alpha() != 1.0 || !geometry.contains_rect(bounds) {
return false;
}
// A single opaque rectangle covering the output is deliberately more
// conservative than reconstructing an arbitrary union of client regions.
element
.opaque_regions(scale)
.iter()
.any(|region| Rectangle::new(region.loc + geometry.loc, region.size).contains_rect(bounds))
&& has_storage()
}
struct OutputView<'a> {
global: DmabufGlobal,
feedback: &'a OutputFeedback,
eligible: Option<Id>,
}
fn select<'a>(
global: DmabufGlobal,
surface: &Id,
outputs: impl IntoIterator<Item = OutputView<'a>>,
) -> Option<&'a DmabufFeedback> {
let mut visible = outputs
.into_iter()
.filter(|output| output.feedback.visible.contains(surface));
let output = visible.next()?;
// A surface visible on multiple outputs gets render fallback rather than
// whichever output happened to render last. Cross-GPU import is not assumed.
if visible.next().is_some()
|| output.global != global
|| output.eligible.as_ref() != Some(surface)
|| !output.feedback.candidates.contains(surface)
{
return None;
}
output.feedback.preferred.as_ref()
}
pub(crate) fn refresh(state: &mut Ferese) {
let mut preferred = HashMap::new();
if let Some(backend) = state.direct_backend.as_ref() {
for (surface, global) in state.dmabuf_feedback.requested() {
let id = Id::from(surface);
let outputs = backend
.devices
.values()
.flat_map(|device| device.outputs.values().map(move |output| (device, output)))
.filter(|(_, output)| state.output_id(&output.output).is_some());
let views = outputs.map(|(device, output)| {
let eligible = (backend.can_render()
&& device.drm.is_active()
&& output.power.can_render()
&& output.mirror_source.is_none()
&& !state.session_lock.active()
&& !state.output_has_animations(&output.output)
&& state.output_has_fullscreen(&output.output))
.then(|| {
state
.output_id(&output.output)
.and_then(|id| state.output_workspaces.active_workspace(id))
.and_then(|workspace| state.workspaces.workspace(workspace))
.and_then(|workspace| workspace.fullscreen)
.filter(|id| {
state
.current_window_presentation(*id)
.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)
})
.map(|toplevel| Id::from(toplevel.wl_surface()))
})
.flatten();
OutputView {
global: device.dmabuf_global,
feedback: &output.dmabuf_feedback,
eligible,
}
});
if let Some(feedback) = select(global, &id, views) {
preferred.insert(surface.clone(), feedback.clone());
}
}
}
state.dmabuf_feedback.refresh(&preferred);
}
#[cfg(test)]
mod tests {
use super::*;
use smithay::backend::allocator::{Fourcc, Modifier};
use smithay::backend::renderer::element::{RenderElementPresentationState, RenderElementState};
use smithay::reexports::wayland_server::Display;
use smithay::wayland::dmabuf::DmabufState;
fn format(code: Fourcc, modifier: Modifier) -> Format {
Format { code, modifier }
}
fn formats(values: &[Format]) -> FormatSet {
values.iter().copied().collect()
}
fn output(render: &[Format], primary: &[Format]) -> OutputFeedback {
OutputFeedback::new(10, 20, formats(render), formats(primary))
}
fn create_globals() -> (Display<Ferese>, DmabufGlobal, DmabufGlobal) {
let display = Display::new().unwrap();
let mut state = DmabufState::new();
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);
(display, global, other)
}
fn visible(output: &mut OutputFeedback, id: &Id, f: Format) {
let mut states = RenderElementStates::default();
states.states.insert(
id.clone(),
RenderElementState {
visible_area: 100,
presentation_state: RenderElementPresentationState::Rendering { reason: None },
},
);
output.observed_frame(Some(f), &states, HashSet::from([id.clone()]));
}
#[test]
fn preferences_require_exact_renderer_and_primary_intersection() {
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));
}
#[test]
fn observed_format_limits_preference_and_retains_renderer_fallback() {
let linear = format(Fourcc::Argb8888, Modifier::Linear);
let other = format(Fourcc::Abgr8888, Modifier::Linear);
let mut o = output(&[linear, other], &[linear, other]);
assert!(o.preferred.is_none());
o.observe_format(Some(linear));
let expected = DmabufFeedbackBuilder::new(10, [linear, other])
.add_preference_tranche(20, Some(TrancheFlags::Scanout), [linear])
.build()
.unwrap();
assert_eq!(o.preferred.as_ref(), Some(&expected));
o.observe_format(Some(other));
assert_ne!(o.preferred.as_ref(), Some(&expected));
o.observe_format(Some(format(Fourcc::Rgb565, Modifier::Linear)));
assert!(o.preferred.is_none());
}
#[test]
fn migration_multi_output_cross_gpu_and_policy_changes_fall_back() {
let (_display, global, other_global) = create_globals();
let id = Id::new();
let f = format(Fourcc::Argb8888, Modifier::Linear);
let mut a = output(&[f], &[f]);
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<'_> {
OutputView {
global,
feedback,
eligible,
}
}
assert!(select(global, &id, [view(global, &a, Some(id.clone()))]).is_some());
assert!(select(global, &id, [view(global, &a, None)]).is_none());
assert!(
select(
global,
&id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))]
)
.is_none()
);
assert!(select(other_global, &id, [view(global, &a, Some(id.clone()))]).is_none());
a.invalidate();
assert!(select(global, &id, [view(global, &a, Some(id.clone()))]).is_none());
assert!(
select(
global,
&id,
[view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))]
)
.is_some()
);
b.candidates.clear();
assert!(select(global, &id, [view(global, &b, Some(id.clone()))]).is_none());
}
#[test]
fn reconfiguration_invalidates_observations_until_a_new_frame() {
let (_display, global, _) = create_globals();
let id = Id::new();
let f = format(Fourcc::Argb8888, Modifier::Linear);
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());
o.observe_format(None);
assert!(o.preferred.is_none());
visible(&mut o, &id, f);
assert!(
select(
global,
&id,
[OutputView {
global,
feedback: &o,
eligible: Some(id.clone())
}]
)
.is_some()
);
}
#[test]
fn candidate_geometry_requires_covering_opaque_content_and_storage() {
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::utils::CommitCounter;
use smithay::utils::Transform;
for transform in [Transform::Normal, Transform::_90] {
let bounds = Rectangle::from_size(transform.transform_size((100, 60).into()));
for scale in [1.0, 1.25, 2.0] {
let solid = |geometry, alpha| {
SolidColorRenderElement::new(
Id::new(),
geometry,
CommitCounter::default(),
[1., 1., 1., alpha],
Kind::Unspecified,
)
};
let opaque = solid(bounds, 1.0);
assert!(candidate_geometry(&opaque, bounds, scale.into(), || true));
assert!(
!candidate_geometry(&opaque, bounds, scale.into(), || false),
"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"
)));
let mut partial = bounds;
partial.size.w -= 1;
assert!(!candidate_geometry(
&solid(partial, 1.0),
bounds,
scale.into(),
|| panic!("partial content must not probe storage")
));
}
}
}
#[test]
fn failed_scanout_and_occlusion_withdraw_preferences_but_format_hints_can_recover() {
let (_display, global, _) = create_globals();
let id = Id::new();
let f = format(Fourcc::Argb8888, Modifier::Linear);
let mut o = output(&[f], &[f]);
for (state, expected) in [
(
RenderElementState {
visible_area: 100,
presentation_state: RenderElementPresentationState::Rendering {
reason: Some(RenderingReason::FormatUnsupported),
},
},
true,
),
(
RenderElementState {
visible_area: 100,
presentation_state: RenderElementPresentationState::Rendering {
reason: Some(RenderingReason::ScanoutFailed),
},
},
false,
),
(
RenderElementState {
visible_area: 0,
presentation_state: RenderElementPresentationState::Skipped,
},
false,
),
(
RenderElementState {
visible_area: 100,
presentation_state: RenderElementPresentationState::ZeroCopy,
},
true,
),
] {
let states = RenderElementStates {
states: HashMap::from([(id.clone(), state)]),
};
o.observed_frame(Some(f), &states, HashSet::from([id.clone()]));
assert_eq!(
select(
global,
&id,
[OutputView {
global,
feedback: &o,
eligible: Some(id.clone())
}]
)
.is_some(),
expected
);
}
}
#[test]
fn queue_failure_never_publishes_prepared_feedback_observations() {
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));
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();
assert!(o.visible.contains(&id) && o.preferred.is_some());
}
}
@@ -274,6 +274,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
for (crtc, output) in &mut device.outputs {
cancel_output_timers(&state.loop_handle, output);
output.surface.reset_buffers();
output.dmabuf_feedback.invalidate();
output.primary_commit = None;
output.capture_texture = None;
output.frame_pending = false;
@@ -262,6 +262,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
.surface
.set_connectors(&[selection.connector.handle()])
.map_err(|error| error.to_string())?;
output.dmabuf_feedback.invalidate();
prepare(
&mut output.surface,
&mut self.device.renderer,
@@ -280,6 +281,7 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
let replacement = self.staged.remove(&crtc).unwrap();
let previous = self.device.outputs.get_mut(&crtc).unwrap();
previous.surface = replacement.surface;
previous.dmabuf_feedback = replacement.dmabuf_feedback;
self.replaced.insert(crtc);
}
self.staged
@@ -345,11 +347,13 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
.map_err(|error| error.to_string())?;
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())?;
}
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())?;
let restored = output
.surface
+78
View File
@@ -0,0 +1,78 @@
//! Allocation hints retain the import global's renderer as a composition fallback.
use std::collections::HashMap;
use smithay::reexports::wayland_server::Resource;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::with_states;
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufGlobal, SurfaceDmabufFeedbackState};
struct Surface {
global: DmabufGlobal,
fallback: DmabufFeedback,
}
#[derive(Default)]
pub(crate) struct FeedbackManager {
globals: HashMap<DmabufGlobal, DmabufFeedback>,
surfaces: HashMap<WlSurface, Surface>,
}
impl FeedbackManager {
pub fn register(&mut self, global: DmabufGlobal, render: DmabufFeedback) {
self.globals.insert(global, render);
}
pub fn new_surface(&mut self, surface: &WlSurface, global: DmabufGlobal) -> Option<DmabufFeedback> {
let fallback = self.globals.get(&global)?.clone();
self.surfaces.insert(
surface.clone(),
Surface {
global,
fallback: fallback.clone(),
},
);
Some(fallback)
}
pub fn requested(&self) -> impl Iterator<Item = (&WlSurface, DmabufGlobal)> {
self.surfaces.iter().map(|(surface, record)| (surface, record.global))
}
pub fn refresh(&mut self, preferred: &HashMap<WlSurface, DmabufFeedback>) {
self.surfaces.retain(|surface, _| surface.is_alive());
for (surface, record) in &self.surfaces {
let feedback = if self.globals.contains_key(&record.global) {
preferred.get(surface).unwrap_or(&record.fallback)
} else {
&record.fallback
};
// Smithay owns subscriptions, protocol messages and equality suppression.
with_states(surface, |states| {
if let Some(state) = SurfaceDmabufFeedbackState::from_states(states) {
state.set_feedback(feedback);
}
});
}
}
pub fn remove(&mut self, global: DmabufGlobal) {
self.globals.remove(&global);
for (surface, record) in self.surfaces.iter().filter(|(_, record)| record.global == global) {
// V4 requires a main-device tranche. Retain last-known render
// capabilities, withdraw scanout hints and retire the global. Bound
// resources cannot import through the retired global or another GPU.
with_states(surface, |states| {
if let Some(state) = SurfaceDmabufFeedbackState::from_states(states) {
state.set_feedback(&record.fallback);
}
});
}
}
pub fn destroyed(&mut self, surface: &WlSurface) {
self.surfaces.remove(surface);
}
}
#[cfg(test)]
mod tests;
+205
View File
@@ -0,0 +1,205 @@
//! Real v4 feedback subscriptions over a private Wayland socket; no DRM/EGL.
use std::io::Read;
use std::os::unix::net::UnixStream;
use smithay::backend::allocator::{Format, Fourcc, Modifier};
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use smithay::reexports::wayland_server::Display;
use smithay::wayland::dmabuf::DmabufFeedbackBuilder;
use super::*;
use crate::Ferese;
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window};
fn messages(wire: &mut UnixStream) -> Vec<(u32, u32, Vec<u32>)> {
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut result = Vec::new();
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
let size = (header >> 16) as usize;
result.push((
object,
header & 0xffff,
bytes[offset + 8..offset + size]
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap()))
.collect(),
));
offset += size;
}
result
}
fn flags(events: &[(u32, u32, Vec<u32>)], object: u32) -> Vec<u32> {
events
.iter()
.filter(|(id, opcode, _)| *id == object && *opcode == 6)
.map(|(_, _, args)| args[0])
.collect()
}
fn done(events: &[(u32, u32, Vec<u32>)], object: u32) -> usize {
events
.iter()
.filter(|(id, opcode, _)| *id == object && *opcode == 0)
.count()
}
#[test]
fn subscriptions_update_deduplicate_and_retire_device_preferences() {
if !crate::startup_tests::private_runtime(
"dmabuf_feedback::tests::subscriptions_update_deduplicate_and_retire_device_preferences",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut config = crate::config::Config::default().runtime_config().unwrap();
config.wallpaper.path = None;
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
let output = Output::new(
"feedback-test".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(Mode {
size: (100, 100).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "feedback-test".into());
let f = Format {
code: Fourcc::Argb8888,
modifier: Modifier::Linear,
};
let fallback = DmabufFeedbackBuilder::new(10, [f]).build().unwrap();
let preferred = DmabufFeedbackBuilder::new(10, [f])
.add_preference_tranche(20, Some(TrancheFlags::Scanout), [f])
.build()
.unwrap();
let global = state
.dmabuf_state
.create_global_with_default_feedback::<Ferese>(&state.display_handle, &fallback);
state.dmabuf_feedback.register(global, fallback.clone());
let (window, mut wire) = test_window(&mut state, &mut events, 0xffabcdef);
let surface = window.toplevel().unwrap().wl_surface().clone();
request(&mut wire, 1, 1, &[7], None); // wl_display.get_registry
dispatch(&mut events, &mut state);
let registry = messages(&mut wire);
let dmabuf_name = registry
.iter()
.find_map(|(object, opcode, args)| {
if *object != 7 || *opcode != 0 {
return None;
}
let bytes = args[2..].iter().flat_map(|word| word.to_ne_bytes()).collect::<Vec<_>>();
bytes.starts_with(b"zwp_linux_dmabuf_v1\0").then_some(args[0])
})
.expect("DMA-BUF v4 global");
let name = b"zwp_linux_dmabuf_v1\0";
let mut padded = name.to_vec();
padded.resize(name.len().next_multiple_of(4), 0);
let bind = [dmabuf_name, name.len() as u32]
.into_iter()
.chain(
padded
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
)
.chain([4, 8])
.collect::<Vec<_>>();
request(&mut wire, 7, 0, &bind, None);
request(&mut wire, 8, 3, &[9, 2], None); // get_surface_feedback
dispatch(&mut events, &mut state);
let initial = messages(&mut wire);
assert_eq!(flags(&initial, 9), [0]);
assert_eq!(done(&initial, 9), 1);
assert_eq!(state.dmabuf_feedback.requested().count(), 1);
let main = initial.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 2).unwrap();
assert_eq!(main.2[0] as usize, std::mem::size_of::<libc::dev_t>());
assert_eq!(main.2[1], 10, "main device remains the renderer");
let selected = HashMap::from([(surface.clone(), preferred.clone())]);
state.dmabuf_feedback.refresh(&selected);
state.display_handle.flush_clients().unwrap();
let changed = messages(&mut wire);
assert_eq!(flags(&changed, 9), [TrancheFlags::Scanout.bits(), 0]);
assert_eq!(done(&changed, 9), 1);
let target = changed.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 4).unwrap();
assert_eq!(target.2[1], 20, "scanout tranche targets the primary DRM device");
state.dmabuf_feedback.refresh(&selected);
state.display_handle.flush_clients().unwrap();
assert!(messages(&mut wire).is_empty(), "Smithay suppresses unchanged feedback");
request(&mut wire, 8, 3, &[10, 2], None);
dispatch(&mut events, &mut state);
let second = messages(&mut wire);
assert_eq!(flags(&second, 10), [TrancheFlags::Scanout.bits(), 0]);
state.dmabuf_feedback.refresh(&HashMap::new()); // migration/ineligibility fallback
state.display_handle.flush_clients().unwrap();
let fallback_events = messages(&mut wire);
assert_eq!(flags(&fallback_events, 9), [0]);
assert_eq!(flags(&fallback_events, 10), [0]);
assert_eq!(done(&fallback_events, 9), 1);
assert_eq!(done(&fallback_events, 10), 1);
request(&mut wire, 9, 0, &[], None);
request(&mut wire, 10, 0, &[], None);
dispatch(&mut events, &mut state);
messages(&mut wire);
request(&mut wire, 8, 3, &[11, 2], None);
request(&mut wire, 8, 2, &[12], None); // get_default_feedback
dispatch(&mut events, &mut state);
let renewed = messages(&mut wire);
assert_eq!(flags(&renewed, 11), [0], "renewal starts with safe render feedback");
assert_eq!(flags(&renewed, 12), [0]);
state.dmabuf_feedback.refresh(&selected);
state.display_handle.flush_clients().unwrap();
let promoted = messages(&mut wire);
assert_eq!(flags(&promoted, 11), [TrancheFlags::Scanout.bits(), 0]);
state.dmabuf_feedback.remove(global);
state
.dmabuf_state
.disable_global::<Ferese>(&state.display_handle, &global);
state.display_handle.flush_clients().unwrap();
let retired = messages(&mut wire);
assert_eq!(done(&retired, 11), 1);
assert_eq!(
flags(&retired, 11),
[0],
"retirement keeps a valid main-device render tranche"
);
assert_eq!(
done(&retired, 12),
0,
"default renderer capabilities remain unchanged on existing bindings"
);
assert!(
retired
.iter()
.any(|(id, opcode, args)| *id == 7 && *opcode == 1 && args[0] == dmabuf_name)
);
state.dmabuf_feedback.refresh(&selected); // A stale choice cannot resurrect a retired preference.
state.display_handle.flush_clients().unwrap();
assert!(messages(&mut wire).is_empty());
assert!(state.dmabuf_feedback.new_surface(&surface, global).is_none());
drop(wire);
dispatch(&mut events, &mut state);
state.dmabuf_feedback.refresh(&HashMap::new());
assert_eq!(state.dmabuf_feedback.requested().count(), 0);
}
+9
View File
@@ -84,6 +84,7 @@ impl CompositorHandler for Ferese {
}
fn destroyed(&mut self, surface: &WlSurface) {
self.dmabuf_feedback.destroyed(surface);
if self.session_lock.active() {
self.session_lock
.surfaces
@@ -112,6 +113,14 @@ impl DmabufHandler for Ferese {
&mut self.dmabuf_state
}
fn new_surface_feedback(
&mut self,
surface: &WlSurface,
global: &DmabufGlobal,
) -> Option<smithay::wayland::dmabuf::DmabufFeedback> {
self.dmabuf_feedback.new_surface(surface, *global)
}
fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
self.dmabuf_imports
.enqueue(*global, dmabuf, notifier, std::time::Instant::now());
+2
View File
@@ -5,6 +5,7 @@ mod config;
mod cursor;
mod daemon;
mod display_presentation;
mod dmabuf_feedback;
mod dmabuf_imports;
mod effects;
mod floating;
@@ -243,6 +244,7 @@ fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Fe
warn!(%error, "could not schedule wallpaper upload retry");
}
crate::backends::direct::feedback::refresh(state);
state.publish_ipc_events();
// Clients need registry/sync/configure replies even when no frame is drawn.
+2
View File
@@ -514,6 +514,7 @@ pub struct Ferese {
pub direct_backend: Option<DirectBackendState>,
_ipc_socket: Option<IpcSocketGuard>,
pub(crate) dmabuf_imports: crate::dmabuf_imports::ImportManager,
pub(crate) dmabuf_feedback: crate::dmabuf_feedback::FeedbackManager,
pub(crate) activation_inputs: crate::handlers::activation::InputHistory,
pub(crate) pending_screencopies: Vec<PendingScreencopy>,
// Screenshot requests outlive the readback that filled them: the
@@ -818,6 +819,7 @@ impl Ferese {
direct_backend: None,
_ipc_socket: None,
dmabuf_imports: crate::dmabuf_imports::ImportManager::default(),
dmabuf_feedback: Default::default(),
activation_inputs: Default::default(),
pending_screencopies: Vec::new(),
screenshot: Coordinator::new(),