Migrate DRM outputs to Smithay DrmCompositor
This commit is contained in:
1 parent
cd19691f52
commit
0d7203f84b
8 files changed
+330
-59
No files matched your search
@@ -1,26 +1,31 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
mod capture;
|
||||||
|
mod planes;
|
||||||
mod scheduling;
|
mod scheduling;
|
||||||
use scheduling::*;
|
use scheduling::*;
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
|
use smithay::backend::drm::compositor::{DrmCompositor, 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::egl::{EGLContext, EGLDisplay};
|
||||||
use smithay::backend::input::InputEvent;
|
use smithay::backend::input::InputEvent;
|
||||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer};
|
use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer};
|
||||||
use smithay::backend::session::libseat::LibSeatSession;
|
use smithay::backend::session::libseat::LibSeatSession;
|
||||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||||
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
||||||
use smithay::desktop::layer_map_for_output;
|
use smithay::desktop::layer_map_for_output;
|
||||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||||
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel};
|
use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel};
|
||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
||||||
use smithay::reexports::drm::control::{
|
use smithay::reexports::drm::control::{
|
||||||
@@ -37,7 +42,7 @@ use smithay::wayland::presentation::Refresh;
|
|||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||||
use crate::metrics::RenderMetrics;
|
use crate::metrics::RenderMetrics;
|
||||||
use crate::render::{frame_effect_metrics, redraw_output, sampled_output_elements};
|
use crate::render::{frame_effect_metrics, sampled_output_elements};
|
||||||
|
|
||||||
pub struct DirectBackendState {
|
pub struct DirectBackendState {
|
||||||
pub session: LibSeatSession,
|
pub session: LibSeatSession,
|
||||||
@@ -147,10 +152,18 @@ struct PresentationClock {
|
|||||||
missed_deadlines: u64,
|
missed_deadlines: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutputCompositor = DrmCompositor<
|
||||||
|
GbmAllocator<DrmDeviceFd>,
|
||||||
|
GbmFramebufferExporter<DrmDeviceFd>,
|
||||||
|
OutputPresentationFeedback,
|
||||||
|
DrmDeviceFd,
|
||||||
|
>;
|
||||||
|
|
||||||
struct DirectDevice {
|
struct DirectDevice {
|
||||||
drm: DrmDevice,
|
drm: DrmDevice,
|
||||||
gbm: GbmDevice<DrmDeviceFd>,
|
gbm: GbmDevice<DrmDeviceFd>,
|
||||||
renderer: GlesRenderer,
|
renderer: GlesRenderer,
|
||||||
|
render_node: DrmNode,
|
||||||
outputs: HashMap<crtc::Handle, DirectOutput>,
|
outputs: HashMap<crtc::Handle, DirectOutput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +174,9 @@ struct DirectOutput {
|
|||||||
settings: OutputSettings,
|
settings: OutputSettings,
|
||||||
output: Output,
|
output: Output,
|
||||||
global: GlobalId,
|
global: GlobalId,
|
||||||
surface: GbmBufferedSurface<GbmAllocator<DrmDeviceFd>, OutputPresentationFeedback>,
|
surface: OutputCompositor,
|
||||||
damage_tracker: OutputDamageTracker,
|
primary_commit: Option<CommitCounter>,
|
||||||
|
capture_texture: Option<GlesTexture>,
|
||||||
render_metrics: RenderMetrics,
|
render_metrics: RenderMetrics,
|
||||||
frame_pending: bool,
|
frame_pending: bool,
|
||||||
scheduler: crate::frame_scheduler::FrameScheduler,
|
scheduler: crate::frame_scheduler::FrameScheduler,
|
||||||
@@ -257,7 +271,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
device.outputs.values_mut().for_each(|output| {
|
device.outputs.values_mut().for_each(|output| {
|
||||||
cancel_output_timers(&handle, output);
|
cancel_output_timers(&handle, output);
|
||||||
output.surface.reset_buffers();
|
output.surface.reset_buffers();
|
||||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
output.primary_commit = None;
|
||||||
|
output.capture_texture = None;
|
||||||
output.frame_pending = false;
|
output.frame_pending = false;
|
||||||
output.power_off = false;
|
output.power_off = false;
|
||||||
});
|
});
|
||||||
@@ -278,6 +293,15 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
for device in backend.devices.values_mut() {
|
for device in backend.devices.values_mut() {
|
||||||
if let Err(error) = device.drm.activate(true) {
|
if let Err(error) = device.drm.activate(true) {
|
||||||
tracing::error!(%error, "failed to reactivate DRM device");
|
tracing::error!(%error, "failed to reactivate DRM device");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for output in device.outputs.values_mut() {
|
||||||
|
// A suspended page flip may never arrive. Clear the old
|
||||||
|
// pending frame before accepting a fresh submission.
|
||||||
|
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
|
||||||
|
tracing::error!(%error, "failed to reset DRM output after activation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,7 +520,7 @@ fn open_primary_device(
|
|||||||
.and_then(|device| device.outputs.get_mut(&crtc))
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
.and_then(|output| match output.surface.frame_submitted() {
|
.and_then(|output| match output.surface.frame_submitted() {
|
||||||
Ok(feedback) => {
|
Ok(feedback) => {
|
||||||
retired = true;
|
retired = feedback.is_some();
|
||||||
feedback
|
feedback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +620,7 @@ fn open_primary_device(
|
|||||||
drm,
|
drm,
|
||||||
gbm,
|
gbm,
|
||||||
renderer,
|
renderer,
|
||||||
|
render_node,
|
||||||
outputs,
|
outputs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -620,7 +645,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match output.surface.surface().clear() {
|
match output.surface.clear() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
cancel_output_timers(&state.loop_handle, output);
|
cancel_output_timers(&state.loop_handle, output);
|
||||||
output.power_off = true;
|
output.power_off = true;
|
||||||
@@ -796,60 +821,77 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
|
|||||||
let frame = state.sample_frame(&output.output, horizon);
|
let frame = state.sample_frame(&output.output, horizon);
|
||||||
let rendered = {
|
let rendered = {
|
||||||
(|| -> Result<bool, Box<dyn Error>> {
|
(|| -> Result<bool, Box<dyn Error>> {
|
||||||
state.process_dmabuf_imports(&mut device.renderer);
|
state.process_dmabuf_imports(&mut device.renderer, Some(device.render_node));
|
||||||
let (mut buffer, age) = output.surface.next_buffer()?;
|
|
||||||
let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame);
|
let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame);
|
||||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||||
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
let flags = planes::frame_flags(
|
||||||
let result = output.damage_tracker.render_output(
|
state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()),
|
||||||
&mut device.renderer,
|
state.output_has_animations(&output.output),
|
||||||
&mut framebuffer,
|
state.session_lock.active,
|
||||||
usize::from(age),
|
);
|
||||||
&elements,
|
let result =
|
||||||
[0.035, 0.04, 0.055, 1.0],
|
output
|
||||||
)?;
|
.surface
|
||||||
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
|
.render_frame(&mut device.renderer, &elements, [0.035, 0.04, 0.055, 1.0], flags)?;
|
||||||
let captured_with_cursor =
|
|
||||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
|
||||||
|
|
||||||
if cursorless_capture {
|
if result.needs_sync()
|
||||||
let cursorless_elements =
|
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
|
||||||
sampled_output_elements(state, &mut device.renderer, &output.output, false, &frame);
|
{
|
||||||
redraw_output(
|
primary.sync.wait()?;
|
||||||
&mut device.renderer,
|
|
||||||
&mut framebuffer,
|
|
||||||
&output.output,
|
|
||||||
&cursorless_elements,
|
|
||||||
)?;
|
|
||||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
|
|
||||||
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
|
|
||||||
} else if captured_with_cursor {
|
|
||||||
let _ = device
|
|
||||||
.renderer
|
|
||||||
.render(
|
|
||||||
&mut framebuffer,
|
|
||||||
output.output.current_mode().expect("output has a mode").size,
|
|
||||||
output.output.current_transform(),
|
|
||||||
)?
|
|
||||||
.finish()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(damage) = result.damage.cloned() else {
|
capture::capture_output(
|
||||||
|
state,
|
||||||
|
&mut device.renderer,
|
||||||
|
&mut output.capture_texture,
|
||||||
|
&output.output,
|
||||||
|
&frame,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if result.is_empty {
|
||||||
output
|
output
|
||||||
.render_metrics
|
.render_metrics
|
||||||
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Count GPU composition damage; direct scanout and cursor-only
|
||||||
|
// updates do not redraw the primary swapchain.
|
||||||
|
let (damage, primary_commit) = match &result.primary_element {
|
||||||
|
PrimaryPlaneElement::Swapchain(primary) => {
|
||||||
|
let damage = primary.damage.damage_since(output.primary_commit);
|
||||||
|
let damage = damage.map_or_else(
|
||||||
|
|| {
|
||||||
|
vec![smithay::utils::Rectangle::from_size(
|
||||||
|
output.output.current_mode().unwrap().size,
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
|damage| {
|
||||||
|
damage
|
||||||
|
.iter()
|
||||||
|
.map(|rect| {
|
||||||
|
smithay::utils::Rectangle::new(
|
||||||
|
(rect.loc.x, rect.loc.y).into(),
|
||||||
|
(rect.size.w, rect.size.h).into(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(damage, Some(primary.damage.current_commit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryPlaneElement::Element(_) => (Vec::new(), None),
|
||||||
|
};
|
||||||
let presentation = crate::presentation::take_output_feedback(
|
let presentation = crate::presentation::take_output_feedback(
|
||||||
state,
|
state,
|
||||||
&output.output,
|
&output.output,
|
||||||
&result.states,
|
&result.states,
|
||||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||||
);
|
);
|
||||||
output
|
drop(result);
|
||||||
.surface
|
output.surface.queue_frame(presentation)?;
|
||||||
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
|
output.primary_commit = primary_commit;
|
||||||
output
|
output
|
||||||
.render_metrics
|
.render_metrics
|
||||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||||
@@ -992,7 +1034,9 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
output.surface.reset_buffer_ages();
|
||||||
|
output.primary_commit = None;
|
||||||
|
output.capture_texture = None;
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
let clock = backend.presentation.entry((node, crtc)).or_default();
|
let clock = backend.presentation.entry((node, crtc)).or_default();
|
||||||
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
||||||
@@ -1564,16 +1608,30 @@ fn create_direct_output(
|
|||||||
let identity = connector_identity(drm, &connector);
|
let identity = connector_identity(drm, &connector);
|
||||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||||
let surface = GbmBufferedSurface::new(
|
let node = DrmNode::from_file(gbm.as_fd())?;
|
||||||
|
let render_node = node
|
||||||
|
.node_with_type(NodeType::Render)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.unwrap_or(node);
|
||||||
|
let mut surface = DrmCompositor::new(
|
||||||
|
OutputModeSource::Static {
|
||||||
|
size: OutputMode::from(mode).size,
|
||||||
|
scale: settings.scale.into(),
|
||||||
|
transform: output_transform(settings.transform),
|
||||||
|
},
|
||||||
drm_surface,
|
drm_surface,
|
||||||
|
None,
|
||||||
allocator,
|
allocator,
|
||||||
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
GbmFramebufferExporter::new(gbm.clone(), Some(render_node)),
|
||||||
|
[Fourcc::Argb8888, Fourcc::Abgr8888],
|
||||||
renderer.dmabuf_formats(),
|
renderer.dmabuf_formats(),
|
||||||
|
drm.cursor_size(),
|
||||||
|
Some(gbm.clone()),
|
||||||
)?;
|
)?;
|
||||||
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
||||||
// leave a phantom output/workspace that could receive evacuated windows.
|
// leave a phantom output/workspace that could receive evacuated windows.
|
||||||
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
||||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
surface.set_output_mode_source((&output).into());
|
||||||
let render_metrics = RenderMetrics::from_environment(output.name());
|
let render_metrics = RenderMetrics::from_environment(output.name());
|
||||||
|
|
||||||
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
||||||
@@ -1585,7 +1643,8 @@ fn create_direct_output(
|
|||||||
output,
|
output,
|
||||||
global,
|
global,
|
||||||
surface,
|
surface,
|
||||||
damage_tracker,
|
primary_commit: None,
|
||||||
|
capture_texture: None,
|
||||||
render_metrics,
|
render_metrics,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
power_off: false,
|
power_off: false,
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture};
|
||||||
|
use smithay::backend::renderer::{Bind, Offscreen, Texture};
|
||||||
|
use smithay::output::Output;
|
||||||
|
|
||||||
|
use crate::Ferese;
|
||||||
|
use crate::render::{redraw_output, sampled_output_elements};
|
||||||
|
use crate::state::FrameScene;
|
||||||
|
|
||||||
|
pub(super) fn capture_output(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
texture: &mut Option<GlesTexture>,
|
||||||
|
output: &Output,
|
||||||
|
scene: &FrameScene,
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
for include_cursor in [true, false] {
|
||||||
|
if !state.has_pending_screencopy(output, include_cursor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample the same scene into an independent target: the displayed
|
||||||
|
// primary buffer may omit both a scanned-out client and hardware cursor.
|
||||||
|
let elements = sampled_output_elements(state, renderer, output, include_cursor, scene);
|
||||||
|
let target = render_capture(renderer, texture, output, &elements)?;
|
||||||
|
state.process_screencopies(renderer, &target, output, include_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
texture: &'a mut Option<GlesTexture>,
|
||||||
|
output: &Output,
|
||||||
|
elements: &[E],
|
||||||
|
) -> Result<GlesTarget<'a>, GlesError> {
|
||||||
|
let size = output.current_mode().expect("output has a mode").size;
|
||||||
|
|
||||||
|
if texture
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|texture| texture.size() != (size.w, size.h).into())
|
||||||
|
{
|
||||||
|
*texture = Some(Offscreen::<GlesTexture>::create_buffer(
|
||||||
|
renderer,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
(size.w, size.h).into(),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebinding for each capture restores GL state after readback.
|
||||||
|
let mut target = renderer.bind(texture.as_mut().expect("capture target allocated"))?;
|
||||||
|
redraw_output(renderer, &mut target, output, elements)?;
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||||
|
use smithay::backend::renderer::element::{Id, Kind};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::backend::renderer::{Color32F, ExportMem};
|
||||||
|
use smithay::output::{Mode, PhysicalProperties, Subpixel};
|
||||||
|
use smithay::utils::{Physical, Rectangle, Transform};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn capture_recomposes_cursor_without_touching_the_display_target() {
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let context = EGLContext::new(&display).unwrap();
|
||||||
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
|
let output = Output::new(
|
||||||
|
"capture".into(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (32, 24).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
Some(Transform::Normal),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let background = SolidColorRenderElement::new(
|
||||||
|
Id::new(),
|
||||||
|
Rectangle::<i32, Physical>::from_size((32, 24).into()),
|
||||||
|
CommitCounter::default(),
|
||||||
|
Color32F::new(1.0, 0.0, 0.0, 1.0),
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
let cursor = SolidColorRenderElement::new(
|
||||||
|
Id::new(),
|
||||||
|
Rectangle::new((0, 0).into(), (4, 4).into()),
|
||||||
|
CommitCounter::default(),
|
||||||
|
Color32F::new(0.0, 1.0, 0.0, 1.0),
|
||||||
|
Kind::Cursor,
|
||||||
|
);
|
||||||
|
let mut displayed = None;
|
||||||
|
let mut capture = None;
|
||||||
|
{
|
||||||
|
let _ = render_capture(
|
||||||
|
&mut renderer,
|
||||||
|
&mut displayed,
|
||||||
|
&output,
|
||||||
|
std::slice::from_ref(&background),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (elements, expected) in [
|
||||||
|
(vec![cursor, background.clone()], [0, 255, 0, 255]),
|
||||||
|
(vec![background], [255, 0, 0, 255]),
|
||||||
|
] {
|
||||||
|
let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap();
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||||
|
assert_eq!(&pixels[..4], &expected);
|
||||||
|
assert_eq!(&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], &[255, 0, 0, 255]);
|
||||||
|
drop(target);
|
||||||
|
|
||||||
|
let target = renderer.bind(displayed.as_mut().unwrap()).unwrap();
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(&renderer.map_texture(&mapping).unwrap()[..4], &[255, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (48, 16).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
Some(Transform::_90),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let _ = render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[]).unwrap();
|
||||||
|
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use smithay::backend::drm::compositor::FrameFlags;
|
||||||
|
|
||||||
|
pub(super) fn frame_flags(fullscreen: bool, animating: bool, locked: bool) -> FrameFlags {
|
||||||
|
let mut flags = FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
|
||||||
|
|
||||||
|
if fullscreen && !animating && !locked {
|
||||||
|
flags |= FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay planes stay disabled until their effects and capture behavior
|
||||||
|
// have been validated on hardware independently of fullscreen scanout.
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_settled_unlocked_fullscreen_frames_allow_primary_scanout() {
|
||||||
|
for fullscreen in [false, true] {
|
||||||
|
for animating in [false, true] {
|
||||||
|
for locked in [false, true] {
|
||||||
|
let flags = frame_flags(fullscreen, animating, locked);
|
||||||
|
assert_eq!(
|
||||||
|
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
|
||||||
|
fullscreen && !animating && !locked,
|
||||||
|
);
|
||||||
|
assert!(flags.contains(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT));
|
||||||
|
assert!(!flags.intersects(
|
||||||
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
|
||||||
|
| FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT
|
||||||
|
| FrameFlags::SKIP_CURSOR_ONLY_UPDATES
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -403,6 +403,11 @@ pub(super) fn rounded_window_elements(
|
|||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|inner| {
|
.map(|inner| {
|
||||||
|
if radius == 0.0 {
|
||||||
|
// Rectangular clipping is already enforced by the outer crop
|
||||||
|
// element. Keep the raw surface available for fullscreen scanout.
|
||||||
|
WindowContentRenderElement::from(inner)
|
||||||
|
} else {
|
||||||
RoundedSurfaceRenderElement {
|
RoundedSurfaceRenderElement {
|
||||||
inner,
|
inner,
|
||||||
programs: programs.clone(),
|
programs: programs.clone(),
|
||||||
@@ -411,6 +416,7 @@ pub(super) fn rounded_window_elements(
|
|||||||
clip_changed,
|
clip_changed,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -485,8 +485,9 @@ impl RenderElement<GlesRenderer> for RoundedSurfaceRenderElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||||
self.inner.underlying_storage(renderer)
|
// Storage is deliberately hidden: plane scanout would bypass clipping.
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -749,12 +749,16 @@ impl Ferese {
|
|||||||
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process_dmabuf_imports<R>(&mut self, renderer: &mut R)
|
pub(crate) fn process_dmabuf_imports<R>(&mut self, renderer: &mut R, node: Option<DrmNode>)
|
||||||
where
|
where
|
||||||
R: ImportDma,
|
R: ImportDma,
|
||||||
{
|
{
|
||||||
for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) {
|
for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) {
|
||||||
if renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
if renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
||||||
|
if let Some(node) = node {
|
||||||
|
dmabuf.set_node(node);
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(error) = notifier.successful::<Self>() {
|
if let Err(error) = notifier.successful::<Self>() {
|
||||||
tracing::debug!(?error, "dma-buf client disappeared before import completed");
|
tracing::debug!(?error, "dma-buf client disappeared before import completed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
let rendered = (|| -> DamageRenderResult {
|
let rendered = (|| -> DamageRenderResult {
|
||||||
{
|
{
|
||||||
let (renderer, mut framebuffer) = backend.bind()?;
|
let (renderer, mut framebuffer) = backend.bind()?;
|
||||||
state.process_dmabuf_imports(renderer);
|
state.process_dmabuf_imports(renderer, None);
|
||||||
let elements = animated_window_elements(state, renderer, &output);
|
let elements = animated_window_elements(state, renderer, &output);
|
||||||
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
|
||||||
let result = damage_tracker.render_output(
|
let result = damage_tracker.render_output(
|
||||||
|
|||||||
@@ -35,10 +35,19 @@ class WindowCapture(unittest.TestCase):
|
|||||||
config = root / "config/ferese/config.kdl"
|
config = root / "config/ferese/config.kdl"
|
||||||
config.parent.mkdir(parents=True)
|
config.parent.mkdir(parents=True)
|
||||||
config.write_text('window-rule app-id="ferese.test.window-capture" floating=#true\n')
|
config.write_text('window-rule app-id="ferese.test.window-capture" floating=#true\n')
|
||||||
|
# Remembered geometry deliberately places both clients together;
|
||||||
|
# ordinary placement now avoids overlap. Keep host state isolated.
|
||||||
|
state = root / "state/ferese"
|
||||||
|
state.mkdir(parents=True)
|
||||||
|
(state / "floating.json").write_text(json.dumps({
|
||||||
|
"ferese.test.window-capture": {
|
||||||
|
"output": "ferese-winit", "fractions": [.1, .1, .4, .4],
|
||||||
|
},
|
||||||
|
}))
|
||||||
display = Path(os.environ["WAYLAND_DISPLAY"])
|
display = Path(os.environ["WAYLAND_DISPLAY"])
|
||||||
if not display.is_absolute():
|
if not display.is_absolute():
|
||||||
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
||||||
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), WAYLAND_DISPLAY=str(display), FERESE_ENABLE_SCREENCOPY="1")
|
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), XDG_STATE_HOME=str(root / "state"), WAYLAND_DISPLAY=str(display), FERESE_ENABLE_SCREENCOPY="1")
|
||||||
env.pop("WAYLAND_SOCKET", None)
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
log = (root / "compositor.log").open("w")
|
log = (root / "compositor.log").open("w")
|
||||||
|
|||||||
Reference in new issue
Block a user