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(),
+1
View File
@@ -46,6 +46,7 @@ and [guides at that tag](https://github.com/ferese-wm/ferese/tree/v0.1.0-beta/do
- [Animation model](animation-model.md): geometry, spring motion, gestures and prediction.
- [Desktop reconciliation](desktop-reconciliation.md): topology publication and presentation ownership.
- [Rendering diagnostics](rendering-performance.md): outcome counters, capture cost and repeatable hardware baselines.
- [DMA-BUF feedback](dmabuf-feedback.md): conservative allocation hints, fallback and hardware validation.
- [Output management tests](output-management-testing.md): profiles, hotplug and DRM failures.
- [Nested resize checks](nested-resize-testing.md).
- [Client soak testing](native-soak-testing.md).
+1
View File
@@ -30,6 +30,7 @@ host. Hardware checks remain separate.
- [Live Activities checks](#live-activities-checks): core, CLI, shell hooks, installer setup and native rendering.
- [Desktop reconciliation](desktop-reconciliation.md): topology publication, hardware failure and presentation ownership.
- [Rendering diagnostics](rendering-performance.md): per-output outcomes and repeatable hardware baselines.
- [DMA-BUF feedback](dmabuf-feedback.md): conservative allocation hints, fallback and hardware validation.
- [Output management tests](output-management-testing.md): profiles, hotplug, mirroring, lid handling and failed DRM commits.
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
+94
View File
@@ -0,0 +1,94 @@
# DMA-BUF allocation feedback
DRM sessions publish renderer formats through a Linux DMA-BUF v4 global for each
render device. Its default feedback identifies that renderer's render node (or
its primary node when no render node is available). Per-surface feedback starts
with this same render fallback; requesting feedback does not require a mapped
window or a scanout-capable buffer.
After a successful display queue, or a successful render returning no display
changes, Ferese can add an output-specific `scanout` preference. It uses the
primary plane selected by that output's DRM surface, not a union of all planes.
Only an exact FourCC/modifier pair shared by that plane and the renderer is
preferred. The pair must have been observed on the primary result. This narrow
choice follows the current policy's full-format comparison with the swapchain;
it does not advertise broader primary formats, cursor formats or overlay formats.
Implicit modifiers are not equated with explicit linear modifiers, and alpha
formats are not equated with their opaque counterparts.
The preference's target device is the output's primary DRM node; its main device
and renderer fallback remain the originating global's render device. Every
preferred pair is also renderer-importable. Failed preference construction retains
render feedback rather than failing an already queued display frame.
## Surface selection and invalidation
Only the current mapped fullscreen toplevel can receive the preference. Its last
successful frame must expose underlying buffer storage, opaque content covering
the output and full opacity. Rounded shader clipping continues to hide storage.
Current opacity or dimming also withdraws the preference immediately.
The surface must be visible on exactly one registered output, and that output
must belong to the originating import global. Animation, overview, locking,
paused/inactive outputs and mirror targets receive render fallback. A failed
scanout reported by Smithay withdraws the preference; a format-unsupported result
can still receive a compatible allocation hint.
Feedback selection refreshes after event dispatch, before flushing protocol
messages, even when no frame is drawn. Migration, fullscreen exit, unmapping and
changes in eligibility restore render-only feedback. Multiple visible outputs do
not race to replace one another's feedback. Subsurfaces, popups, layer surfaces
and cursors currently retain render-only feedback; eligibility expansion is a
separate hardware-gated phase.
Mode changes, surface replacement, rollback and session pause/reactivation
invalidate output observations. A fresh successful frame is needed before a
preference returns. Retiring a GPU withdraws scanout preferences and retires its
global. Version 4
requires a main-device tranche, so existing bindings retain last-known renderer
capabilities rather than receiving empty feedback or being retargeted to another
GPU. These retained capabilities do not promise device availability: imports
through that retired global are rejected. A reconnected device publishes a new
global. Destroyed surface requests are pruned.
Smithay owns the subscriptions, format-table transport, tranche encoding and
suppression of unchanged updates. Its feedback state is shared by all subscriptions
for one surface; the initial live request determines the originating global.
Destroy all feedback objects before requesting the surface through a different
global to change that association.
Cross-GPU migration keeps the originating renderer's fallback instead of
advertising unverified formats or retargeting imports to another GPU. This does
not establish cross-GPU composition compatibility: imports are still checked
through the originating global's renderer, and the existing destination-renderer
behavior needs physical validation. Version 3 clients have no per-surface feedback
updates; their existing import checks remain authoritative.
Allocation feedback is an optimization hint, not proof that a particular scene
or client buffer will scan out. The compositor still owns plane assignment,
clipping, blur, capture privacy, lock protection, gamma and composition fallback.
No plane eligibility flags change here.
## Checks and hardware gate
```sh
cargo test --locked -p ferese backends::direct::feedback -- --nocapture
cargo test --locked -p ferese dmabuf_feedback -- --nocapture
```
Deterministic tests cover exact intersections, device identities, observed format
changes, candidate geometry, queue failure, migration, multiple outputs, policy
withdrawal, scanout failure and reconfiguration. A private Wayland v4 client checks
initial render feedback, subscriptions, preference/fallback changes, unchanged
update suppression, feedback renewal and retired-device preference withdrawal. It does not
create a real DMA-BUF, use DRM or establish hardware acceptance.
Before expanding eligibility, record the build, GPU/driver/kernel, output modes,
scale/transforms, application and its actual allocated FourCC/modifier. Decode
v4 feedback device IDs and format tables and verify preferences against the live
renderer and selected primary plane. Test fullscreen entry/exit, animations,
opacity/clipping, popups, sharing and lock/unlock; compare captured pixels and
allocation outcomes. Exercise mixed outputs, mirrors, migration, hotplug,
reconfiguration/rollback, pause/resume, gamma and failed scanout. Include v3 clients
and applications that ignore or follow feedback, and explicitly validate
cross-GPU imports. Use the [output-management hardware checks](output-management-testing.md).
Physical scanout rate, GPU work, power and presentation latency remain unmeasured
until those hardware baselines are collected.
+2 -2
View File
@@ -86,5 +86,5 @@ actual framebuffer recapture counts/cost, asynchronous capture encoding and
window-capture readback separately. Current effect metrics are placeholders;
`capture_us` covers monitor capture batches only. Exact cursor-only KMS updates
and detailed scanout rejection causes require additional upstream observability.
Output/per-surface DMA-BUF feedback and eligibility expansion remain later phases,
with broader primary and overlay policies validated separately on hardware.
See [DMA-BUF feedback](dmabuf-feedback.md) for the current allocation hints.
Broader primary-plane and overlay policies still need hardware validation.