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