feat(render): add output-specific DMA-BUF feedback (#53)
Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
9251522a69
commit
c8dfb3074d
13 files changed
+913
-3
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user