perf(capture): add SceneLens GPU streams and retained scene reuse
Use persistent privacy-filtered capture targets and client-owned GPU buffers with fence completion, backpressure, negotiated frame rates and shared-memory fallback. Invalidate output views on scene changes while preserving inactive-window callbacks and capture revocation. Include OBS memory-mask negotiation and native pixel, transport and recording regressions.
This commit is contained in:
1 parent
f49f62583d
commit
533cd74c94
40 files changed
+4704
-704
No files matched your search
@@ -177,6 +177,24 @@ impl DirectBackendState {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn clear_stream_capture_targets(&mut self) {
|
||||
for device in self.devices.values_mut() {
|
||||
crate::render::clear_stream_capture_targets(&mut device.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn capture_renderer(&mut self, output: &Output) -> Option<&mut GlesRenderer> {
|
||||
self.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.map(|device| &mut device.renderer)
|
||||
}
|
||||
|
||||
pub(crate) fn capture_window_buffer(
|
||||
&mut self,
|
||||
window: &smithay::desktop::Window,
|
||||
@@ -295,13 +313,13 @@ struct DirectOutput {
|
||||
identity: String,
|
||||
mirror_source: Option<String>,
|
||||
mirror_texture: Option<GlesTexture>,
|
||||
mirror_capture_texture: Option<GlesTexture>,
|
||||
mirror_capture_texture: [Option<capture::CaptureTarget>; 2],
|
||||
mirror_canvas: Option<Output>,
|
||||
mirror_id: smithay::backend::renderer::element::Id,
|
||||
mirror_commit: CommitCounter,
|
||||
surface: OutputCompositor,
|
||||
primary_commit: Option<CommitCounter>,
|
||||
capture_texture: Option<GlesTexture>,
|
||||
capture_texture: [Option<capture::CaptureTarget>; 2],
|
||||
render_metrics: RenderMetrics,
|
||||
diagnostics: Diagnostics,
|
||||
dmabuf_feedback: feedback::OutputFeedback,
|
||||
@@ -452,7 +470,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
output.surface.reset_buffers();
|
||||
output.dmabuf_feedback.invalidate();
|
||||
output.primary_commit = None;
|
||||
output.capture_texture = None;
|
||||
output.capture_texture = Default::default();
|
||||
output.frame_pending = false;
|
||||
output.power.suspended();
|
||||
}
|
||||
@@ -1226,16 +1244,33 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
||||
|
||||
#[track_caller]
|
||||
pub fn render_all(state: &mut Ferese) {
|
||||
for output in state.space.outputs() {
|
||||
capture::changes::content_changed(output);
|
||||
}
|
||||
redraw(state, None, std::panic::Location::caller());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_on(state: &mut Ferese, outputs: &[Output]) {
|
||||
for output in outputs {
|
||||
capture::changes::content_changed(output);
|
||||
}
|
||||
redraw(state, Some(outputs), std::panic::Location::caller());
|
||||
}
|
||||
|
||||
/// Consumer demand schedules a frame without invalidating retained scene pixels.
|
||||
#[track_caller]
|
||||
pub(crate) fn schedule_capture(state: &mut Ferese, outputs: &[Output]) {
|
||||
redraw(state, Some(outputs), std::panic::Location::caller());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_cursor(state: &mut Ferese) {
|
||||
// Include nested outputs and both sides of an output crossing. This does not
|
||||
// invalidate the content revision used by cursor-hidden captures.
|
||||
for output in state.space.outputs() {
|
||||
capture::changes::cursor_changed(output);
|
||||
}
|
||||
let outputs = cursor_affected_outputs(state);
|
||||
redraw(state, Some(&outputs), std::panic::Location::caller());
|
||||
}
|
||||
@@ -2603,13 +2638,13 @@ fn create_direct_output(
|
||||
identity,
|
||||
mirror_source,
|
||||
mirror_texture: None,
|
||||
mirror_capture_texture: None,
|
||||
mirror_capture_texture: Default::default(),
|
||||
mirror_canvas: None,
|
||||
mirror_id: smithay::backend::renderer::element::Id::new(),
|
||||
mirror_commit: CommitCounter::default(),
|
||||
surface,
|
||||
primary_commit: None,
|
||||
capture_texture: None,
|
||||
capture_texture: Default::default(),
|
||||
render_metrics,
|
||||
diagnostics,
|
||||
dmabuf_feedback,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::damage::{Error as DamageError, OutputDamageTracker};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Offscreen, Texture};
|
||||
@@ -13,6 +14,42 @@ use crate::state::FrameScene;
|
||||
|
||||
const CACHE_IDLE: Duration = Duration::from_secs(2);
|
||||
|
||||
pub(crate) mod changes;
|
||||
|
||||
pub(crate) struct CaptureTarget {
|
||||
texture: GlesTexture,
|
||||
damage: OutputDamageTracker,
|
||||
output: Output,
|
||||
revision: u64,
|
||||
id: smithay::backend::renderer::element::Id,
|
||||
commit: smithay::backend::renderer::utils::CommitCounter,
|
||||
stamp: Option<changes::Stamp>,
|
||||
preparations: u64,
|
||||
reuses: u64,
|
||||
compositions: u64,
|
||||
trace: bool,
|
||||
reported: Instant,
|
||||
}
|
||||
|
||||
impl CaptureTarget {
|
||||
pub(crate) fn new(texture: GlesTexture, output: &Output) -> Self {
|
||||
Self {
|
||||
texture,
|
||||
damage: OutputDamageTracker::from_output(output),
|
||||
output: output.clone(),
|
||||
revision: 0,
|
||||
id: smithay::backend::renderer::element::Id::new(),
|
||||
commit: Default::default(),
|
||||
stamp: None,
|
||||
preparations: 0,
|
||||
reuses: 0,
|
||||
compositions: 0,
|
||||
trace: std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(),
|
||||
reported: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn touch_cache(state: &mut Ferese) {
|
||||
if state.capture_last_used.replace(Instant::now()).is_some() {
|
||||
return;
|
||||
@@ -28,16 +65,28 @@ pub(crate) fn touch_cache(state: &mut Ferese) {
|
||||
if idle < CACHE_IDLE {
|
||||
return TimeoutAction::ToDuration(CACHE_IDLE - idle);
|
||||
}
|
||||
if let Some(backend) = state.nested_backend.clone()
|
||||
&& let Ok(mut backend) = backend.try_borrow_mut()
|
||||
{
|
||||
crate::render::clear_stream_capture_targets(backend.renderer());
|
||||
}
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.clear_stream_capture_targets();
|
||||
}
|
||||
if !state.pending_screencopies.is_empty() || state.image_capture.has_sessions() {
|
||||
// A damage-waiting stream must retain the comparison image.
|
||||
return TimeoutAction::ToDuration(CACHE_IDLE);
|
||||
}
|
||||
// A stream can pause or fail without another repaint. Release its
|
||||
// offscreen targets and filtered-scene caches from the event loop.
|
||||
state.capture_last_used = None;
|
||||
state.capture_render = Default::default();
|
||||
state.nested_capture_texture = None;
|
||||
state.nested_capture_texture = Default::default();
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
for device in backend.devices.values_mut() {
|
||||
for output in device.outputs.values_mut() {
|
||||
output.capture_texture = None;
|
||||
output.mirror_capture_texture = None;
|
||||
output.capture_texture = Default::default();
|
||||
output.mirror_capture_texture = Default::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +101,7 @@ pub(crate) fn touch_cache(state: &mut Ferese) {
|
||||
pub(crate) fn capture_output(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &mut Option<GlesTexture>,
|
||||
textures: &mut [Option<CaptureTarget>; 2],
|
||||
output: &Output,
|
||||
scene: &FrameScene,
|
||||
) {
|
||||
@@ -61,15 +110,35 @@ pub(crate) fn capture_output(
|
||||
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 = capture_output_elements(state, renderer, output, include_cursor, scene);
|
||||
let Some(target) = capture_request(state, output, include_cursor, || {
|
||||
render_capture(renderer, texture, output, &elements)
|
||||
let texture = &mut textures[usize::from(include_cursor)];
|
||||
if !prepare_capture(
|
||||
state,
|
||||
renderer,
|
||||
texture,
|
||||
output,
|
||||
output,
|
||||
include_cursor,
|
||||
scene,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let texture = texture.as_mut().expect("capture target allocated");
|
||||
let Some((target, revision)) = capture_request(state, output, include_cursor, || {
|
||||
renderer
|
||||
.bind(&mut texture.texture)
|
||||
.map(|target| (target, texture.revision))
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
state.process_screencopies(renderer, &target, output, include_cursor);
|
||||
state.process_screencopies_with_revision(
|
||||
renderer,
|
||||
&target,
|
||||
output,
|
||||
include_cursor,
|
||||
Some(revision),
|
||||
);
|
||||
drop(target);
|
||||
state.process_output_streams(renderer, &texture.texture, output, include_cursor, revision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,47 +153,121 @@ pub(super) fn capture_mirror(
|
||||
if !state.has_pending_screencopy(&output.output, include_cursor) {
|
||||
continue;
|
||||
}
|
||||
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
|
||||
// Never sample mirror_texture: it contains the unfiltered display.
|
||||
let canvas = output
|
||||
.mirror_canvas
|
||||
.as_ref()
|
||||
.expect("mirror display composed");
|
||||
let Some(()) = capture_request(state, &output.output, include_cursor, || {
|
||||
super::mirror::compose(
|
||||
renderer,
|
||||
&mut output.mirror_capture_texture,
|
||||
canvas,
|
||||
source,
|
||||
&elements,
|
||||
)
|
||||
}) else {
|
||||
super::mirror::configure_canvas(canvas, source);
|
||||
let source_target = &mut output.mirror_capture_texture[usize::from(include_cursor)];
|
||||
if !prepare_capture(
|
||||
state,
|
||||
renderer,
|
||||
source_target,
|
||||
source,
|
||||
canvas,
|
||||
include_cursor,
|
||||
scene,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let source_target = source_target
|
||||
.as_ref()
|
||||
.expect("mirror capture target allocated");
|
||||
let element = super::mirror::fitted_texture(
|
||||
source_target.texture.clone(),
|
||||
&output.output,
|
||||
source_target.id.clone(),
|
||||
source_target.commit,
|
||||
);
|
||||
let Some((target, revision)) =
|
||||
capture_request(state, &output.output, include_cursor, || {
|
||||
render_capture_with_revision(
|
||||
renderer,
|
||||
&mut output.capture_texture[usize::from(include_cursor)],
|
||||
&output.output,
|
||||
&[element],
|
||||
)
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let element = super::mirror::fitted_texture(
|
||||
output
|
||||
.mirror_capture_texture
|
||||
state.process_screencopies_with_revision(
|
||||
renderer,
|
||||
&target,
|
||||
&output.output,
|
||||
include_cursor,
|
||||
Some(revision),
|
||||
);
|
||||
drop(target);
|
||||
state.process_output_streams(
|
||||
renderer,
|
||||
&output.capture_texture[usize::from(include_cursor)]
|
||||
.as_ref()
|
||||
.expect("successful mirror composition populated its capture texture")
|
||||
.expect("capture target allocated")
|
||||
.texture
|
||||
.clone(),
|
||||
&output.output,
|
||||
smithay::backend::renderer::element::Id::new(),
|
||||
Default::default(),
|
||||
include_cursor,
|
||||
revision,
|
||||
);
|
||||
let Some(target) = capture_request(state, &output.output, include_cursor, || {
|
||||
render_capture(
|
||||
renderer,
|
||||
&mut output.capture_texture,
|
||||
&output.output,
|
||||
&[element],
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
state.process_screencopies(renderer, &target, &output.output, include_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display damage cannot prove a filtered scene is unchanged: an opaque private
|
||||
/// window may hide updates that must remain visible to capture underneath it.
|
||||
/// Retain a separate source revision and only prepare the filtered scene on a
|
||||
/// real mutation. Delivery buffers still receive a complete image.
|
||||
fn prepare_capture(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &mut Option<CaptureTarget>,
|
||||
source: &Output,
|
||||
destination: &Output,
|
||||
include_cursor: bool,
|
||||
scene: &FrameScene,
|
||||
) -> bool {
|
||||
touch_cache(state);
|
||||
let stamp = changes::Stamp::new(state, source, include_cursor);
|
||||
let reusable = target.as_ref().is_some_and(|target| {
|
||||
target.stamp.as_ref() == Some(&stamp)
|
||||
&& target.output == *destination
|
||||
&& destination
|
||||
.current_mode()
|
||||
.is_some_and(|mode| target.texture.size() == (mode.size.w, mode.size.h).into())
|
||||
});
|
||||
if reusable {
|
||||
target.as_mut().expect("cached capture target").reuses += 1;
|
||||
} else {
|
||||
// Keep the capture image separate from the display swapchain and planes.
|
||||
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
|
||||
let Some(framebuffer) = capture_request(state, destination, include_cursor, || {
|
||||
render_capture_with_revision(renderer, target, destination, &elements)
|
||||
}) else {
|
||||
// A failed render can have changed the retained pixels already.
|
||||
if let Some(target) = target.as_mut() {
|
||||
target.stamp = None;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
drop(framebuffer);
|
||||
let target = target.as_mut().expect("capture target allocated");
|
||||
target.stamp = Some(stamp);
|
||||
target.preparations += 1;
|
||||
}
|
||||
let target = target.as_mut().expect("capture target allocated");
|
||||
if target.trace && target.reported.elapsed() >= Duration::from_secs(5) {
|
||||
tracing::info!(target: "ferese::render", output = %source.name(), include_cursor,
|
||||
preparations = target.preparations, scene_reuses = target.reuses,
|
||||
compositions = target.compositions, "capture scene outcomes");
|
||||
target.reported = Instant::now();
|
||||
target.preparations = 0;
|
||||
target.reuses = 0;
|
||||
target.compositions = 0;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn capture_request<T, E: Display>(
|
||||
state: &mut Ferese,
|
||||
output: &Output,
|
||||
@@ -147,7 +290,69 @@ pub(super) fn after_queue<T, E>(queued: Result<T, E>, capture: impl FnOnce()) ->
|
||||
Ok(queued)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &'a mut Option<CaptureTarget>,
|
||||
output: &Output,
|
||||
elements: &[E],
|
||||
) -> Result<GlesTarget<'a>, DamageError<GlesError>> {
|
||||
render_capture_with_revision(renderer, target, output, elements).map(|(target, _)| target)
|
||||
}
|
||||
|
||||
pub(super) fn render_capture_with_revision<'a, E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &'a mut Option<CaptureTarget>,
|
||||
output: &Output,
|
||||
elements: &[E],
|
||||
) -> Result<(GlesTarget<'a>, u64), DamageError<GlesError>> {
|
||||
let size = output.current_mode().expect("output has a mode").size;
|
||||
if target
|
||||
.as_ref()
|
||||
.is_none_or(|target| target.texture.size() != (size.w, size.h).into())
|
||||
{
|
||||
*target = Some(CaptureTarget::new(
|
||||
Offscreen::<GlesTexture>::create_buffer(
|
||||
renderer,
|
||||
Fourcc::Abgr8888,
|
||||
(size.w, size.h).into(),
|
||||
)
|
||||
.map_err(DamageError::Rendering)?,
|
||||
output,
|
||||
));
|
||||
}
|
||||
let target = target.as_mut().expect("capture target allocated");
|
||||
if target.output != *output {
|
||||
target.damage = OutputDamageTracker::from_output(output);
|
||||
target.output = output.clone();
|
||||
}
|
||||
// This texture retains the previous capture, independently of display
|
||||
// scanout and damage. Age one lets unchanged pixels survive; a new target
|
||||
// has no history and is repainted in full. Rebind after every readback.
|
||||
let mut framebuffer = renderer
|
||||
.bind(&mut target.texture)
|
||||
.map_err(DamageError::Rendering)?;
|
||||
let result = target.damage.render_output(
|
||||
renderer,
|
||||
&mut framebuffer,
|
||||
1,
|
||||
elements,
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
if result
|
||||
.damage
|
||||
.as_ref()
|
||||
.is_some_and(|damage| !damage.is_empty())
|
||||
{
|
||||
static NEXT_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
target.commit.increment();
|
||||
target.compositions += 1;
|
||||
target.revision = NEXT_REVISION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
Ok((framebuffer, target.revision))
|
||||
}
|
||||
|
||||
pub(super) fn render_texture<'a, E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &'a mut Option<GlesTexture>,
|
||||
output: &Output,
|
||||
@@ -184,6 +389,326 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn unchanged_capture_skips_preparation_without_losing_source_updates() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"backends::direct::capture::tests::unchanged_capture_skips_preparation_without_losing_source_updates",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||
config.wallpaper.path = None;
|
||||
config.animations_enabled = false;
|
||||
let mut state = Ferese::new(
|
||||
&mut events,
|
||||
smithay::reexports::wayland_server::Display::new().unwrap(),
|
||||
config,
|
||||
)
|
||||
.unwrap();
|
||||
let output = Output::new(
|
||||
"capture-cache".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: (320, 240).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "capture-cache".into());
|
||||
let (_window, _client) = crate::state::test_window(&mut state, &mut events, 0xffff0000);
|
||||
state.advance_animations(Instant::now());
|
||||
let device = EGLDevice::enumerate().unwrap().last().expect("EGL device");
|
||||
// SAFETY: the device and display outlive this exclusively owned context.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this test thread exclusively owns the fresh context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let mut target = None;
|
||||
let prepare = |state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &mut Option<CaptureTarget>| {
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
assert!(prepare_capture(
|
||||
state, renderer, target, &output, &output, false, &scene
|
||||
));
|
||||
};
|
||||
let pixels = |renderer: &mut GlesRenderer, target: &mut Option<CaptureTarget>| {
|
||||
let target = target.as_mut().unwrap();
|
||||
let framebuffer = renderer.bind(&mut target.texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&framebuffer,
|
||||
Rectangle::from_size((320, 240).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.unwrap();
|
||||
drop(framebuffer);
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
};
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
// Keep assertions independent of the opt-in periodic reporting clock.
|
||||
target.as_mut().unwrap().trace = false;
|
||||
let original = pixels(&mut renderer, &mut target);
|
||||
for _ in 0..100 {
|
||||
super::super::schedule_capture(&mut state, std::slice::from_ref(&output));
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
}
|
||||
assert_eq!(target.as_ref().unwrap().preparations, 1);
|
||||
assert_eq!(target.as_ref().unwrap().reuses, 100);
|
||||
assert_eq!(pixels(&mut renderer, &mut target), original);
|
||||
super::super::render_cursor(&mut state);
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
assert_eq!(target.as_ref().unwrap().preparations, 1);
|
||||
state.capture_epoch += 1;
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
assert_eq!(target.as_ref().unwrap().preparations, 2);
|
||||
let (_cover, _cover_client) =
|
||||
crate::state::test_window(&mut state, &mut events, 0xff00ff00);
|
||||
state.advance_animations(Instant::now());
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
assert_eq!(target.as_ref().unwrap().preparations, 3);
|
||||
assert_ne!(pixels(&mut renderer, &mut target), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn incremental_capture_matches_full_redraw_after_scene_and_output_changes() {
|
||||
let device = EGLDevice::enumerate().unwrap().last().expect("EGL device");
|
||||
// SAFETY: the enumerated device remains alive through its EGL display.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this thread exclusively owns the renderer and its fresh context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let output = |name: &str| {
|
||||
Output::new(
|
||||
name.into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
)
|
||||
};
|
||||
let outputs = [output("first"), output("second")];
|
||||
let mut capture = None;
|
||||
let mut reference = None;
|
||||
let background = SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
Rectangle::from_size((96, 96).into()),
|
||||
CommitCounter::default(),
|
||||
Color32F::new(0., 0., 1., 1.),
|
||||
Kind::Unspecified,
|
||||
);
|
||||
let item_id = Id::new();
|
||||
for output in outputs.iter().chain(std::iter::once(&outputs[0])) {
|
||||
for (width, height) in [(64, 48), (48, 64)] {
|
||||
for scale in [1., 1.25, 1.5] {
|
||||
for transform in [
|
||||
Transform::Normal,
|
||||
Transform::_90,
|
||||
Transform::_180,
|
||||
Transform::Flipped,
|
||||
] {
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: (width, height).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(transform),
|
||||
Some(smithay::output::Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
let mut commit = CommitCounter::default();
|
||||
// Include moves, color/alpha changes, removal, cursor
|
||||
// inclusion/exclusion and an entirely empty scene.
|
||||
for step in [6, 0, 1, 2, 3, 4, 5, 6] {
|
||||
if step == 2 {
|
||||
commit.increment();
|
||||
}
|
||||
let mut elements = Vec::new();
|
||||
if matches!(step, 0..=2 | 4) {
|
||||
elements.push(SolidColorRenderElement::new(
|
||||
item_id.clone(),
|
||||
Rectangle::new(
|
||||
(if step == 0 { 4 } else { 20 }, 4).into(),
|
||||
(12, 12).into(),
|
||||
),
|
||||
commit,
|
||||
if step < 2 {
|
||||
Color32F::new(0., 1., 0., 1.)
|
||||
} else {
|
||||
Color32F::new(0.5, 0., 0., 0.5)
|
||||
},
|
||||
Kind::Cursor,
|
||||
));
|
||||
}
|
||||
if step != 6 {
|
||||
elements.push(background.clone());
|
||||
}
|
||||
drop(
|
||||
render_capture(&mut renderer, &mut capture, output, &elements)
|
||||
.unwrap(),
|
||||
);
|
||||
// The repeated scene must preserve the same pixels when
|
||||
// damage tracking skips all drawing on the next capture.
|
||||
let actual =
|
||||
render_capture(&mut renderer, &mut capture, output, &elements)
|
||||
.unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&actual,
|
||||
Rectangle::from_size((width, height).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.unwrap();
|
||||
let actual_pixels = renderer.map_texture(&mapping).unwrap().to_vec();
|
||||
drop(actual);
|
||||
let expected =
|
||||
render_texture(&mut renderer, &mut reference, output, &elements)
|
||||
.unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&expected,
|
||||
Rectangle::from_size((width, height).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_pixels = renderer.map_texture(&mapping).unwrap();
|
||||
assert_eq!(
|
||||
actual_pixels,
|
||||
expected_pixels,
|
||||
"{} {width}x{height} scale={scale} {transform:?} step={step}",
|
||||
output.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual 4K capture benchmark; requires a hardware EGL device"]
|
||||
fn profile_output_capture() {
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.render_device_path().is_ok())
|
||||
.expect("a hardware EGL rendering device");
|
||||
eprintln!("capture_benchmark device={:?}", device.render_device_path());
|
||||
// SAFETY: the enumerated device remains alive through its EGL display.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this thread exclusively owns the renderer and its fresh context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let output = Output::new(
|
||||
"capture-benchmark".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: (3840, 2160).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut texture = None;
|
||||
let background_id = Id::new();
|
||||
let cursor_id = Id::new();
|
||||
let mut commit = CommitCounter::default();
|
||||
let panes: Vec<_> = (0..20)
|
||||
.map(|i| {
|
||||
SolidColorRenderElement::new(
|
||||
Id::new(),
|
||||
Rectangle::new((i * 150, i * 75).into(), (800, 600).into()),
|
||||
CommitCounter::default(),
|
||||
Color32F::new(0.1, 0.2, 0.3, 0.5),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for readback in [false, true] {
|
||||
let size = if readback { (3840, 2160) } else { (1, 1) };
|
||||
let mut pixels = vec![0u8; size.0 as usize * size.1 as usize * 4];
|
||||
for workload in ["static", "cursor", "full"] {
|
||||
for run in 0..=3 {
|
||||
let started = Instant::now();
|
||||
commit.increment();
|
||||
for tick in 0..60 {
|
||||
let color = if workload == "full" {
|
||||
commit.increment();
|
||||
tick as f32 / 60.
|
||||
} else {
|
||||
0.1
|
||||
};
|
||||
let mut elements = vec![SolidColorRenderElement::new(
|
||||
cursor_id.clone(),
|
||||
Rectangle::new(
|
||||
(if workload == "cursor" { tick * 30 } else { 0 }, 0).into(),
|
||||
(24, 24).into(),
|
||||
),
|
||||
CommitCounter::default(),
|
||||
Color32F::new(1., 1., 1., 1.),
|
||||
Kind::Cursor,
|
||||
)];
|
||||
elements.extend_from_slice(&panes);
|
||||
elements.push(SolidColorRenderElement::new(
|
||||
background_id.clone(),
|
||||
Rectangle::from_size((3840, 2160).into()),
|
||||
commit,
|
||||
Color32F::new(color, 0.1, 0.2, 1.),
|
||||
Kind::Unspecified,
|
||||
));
|
||||
let target =
|
||||
render_capture(&mut renderer, &mut texture, &output, &elements)
|
||||
.unwrap();
|
||||
// Mapping waits for GPU completion in both workloads. The full
|
||||
// readback also includes the copy into the client's SHM buffer.
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&target,
|
||||
Rectangle::from_size(size.into()),
|
||||
Fourcc::Argb8888,
|
||||
)
|
||||
.unwrap();
|
||||
let source = renderer.map_texture(&mapping).unwrap();
|
||||
let bytes = pixels.len();
|
||||
pixels.copy_from_slice(&source[..bytes]);
|
||||
std::hint::black_box(&pixels);
|
||||
}
|
||||
if run != 0 {
|
||||
eprintln!(
|
||||
"output_capture_benchmark readback={readback} workload={workload} run={run} frames=60 elapsed_us={}",
|
||||
started.elapsed().as_micros()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn capture_recomposes_cursor_without_touching_the_display_target() {
|
||||
@@ -234,7 +759,7 @@ mod tests {
|
||||
let mut displayed = None;
|
||||
let mut capture = None;
|
||||
{
|
||||
let _ = render_capture(
|
||||
let _ = render_texture(
|
||||
&mut renderer,
|
||||
&mut displayed,
|
||||
&output,
|
||||
@@ -289,7 +814,7 @@ mod tests {
|
||||
let _ =
|
||||
render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[])
|
||||
.unwrap();
|
||||
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
|
||||
assert_eq!(capture.as_ref().unwrap().texture.size(), (48, 16).into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Capture invalidation is independent of display damage and consumer requests.
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size, Transform};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Changes {
|
||||
content: AtomicU64,
|
||||
cursor: AtomicU64,
|
||||
}
|
||||
|
||||
pub(crate) fn content_changed(output: &Output) {
|
||||
if let Some(changes) = output.user_data().get::<Changes>() {
|
||||
changes.content.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_changed(output: &Output) {
|
||||
if let Some(changes) = output.user_data().get::<Changes>() {
|
||||
changes.cursor.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tracked(output: &Output) -> bool {
|
||||
output.user_data().get::<Changes>().is_some()
|
||||
}
|
||||
|
||||
/// Also compare output configuration: mode changes can precede a redraw request.
|
||||
/// Keeping the source identity prevents a remapped mirror from reusing its old view.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) struct Stamp {
|
||||
output: Output,
|
||||
content: u64,
|
||||
cursor: Option<u64>,
|
||||
epoch: u64,
|
||||
cursor_protected: bool,
|
||||
size: Option<Size<i32, Physical>>,
|
||||
scale: f64,
|
||||
transform: Transform,
|
||||
geometry: Option<Rectangle<i32, Logical>>,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
pub fn new(state: &crate::Ferese, output: &Output, include_cursor: bool) -> Self {
|
||||
let changes = output
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(Changes::default);
|
||||
Self {
|
||||
output: output.clone(),
|
||||
content: changes.content.load(Ordering::Relaxed),
|
||||
cursor: include_cursor.then(|| changes.cursor.load(Ordering::Relaxed)),
|
||||
epoch: state.capture_epoch,
|
||||
cursor_protected: include_cursor && state.capture_cursor_protected(),
|
||||
size: output.current_mode().map(|mode| mode.size),
|
||||
scale: output.current_scale().fractional_scale(),
|
||||
transform: output.current_transform(),
|
||||
geometry: state.space.output_geometry(output),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::output::{Mode, PhysicalProperties, Scale, Subpixel};
|
||||
|
||||
fn output() -> Output {
|
||||
let output = Output::new(
|
||||
"capture-test".into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
serial_number: String::new(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: (640, 480).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(Scale::Fractional(1.25)),
|
||||
Some((0, 0).into()),
|
||||
);
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_demand_does_not_invalidate_but_scene_redraws_do() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"backends::direct::capture::changes::tests::consumer_demand_does_not_invalidate_but_scene_redraws_do",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let first = output();
|
||||
let second = output();
|
||||
state.space.map_output(&first, (0, 0));
|
||||
state.space.map_output(&second, (640, 0));
|
||||
let before = Stamp::new(&state, &first, false);
|
||||
let other = Stamp::new(&state, &second, false);
|
||||
crate::backends::direct::schedule_capture(&mut state, std::slice::from_ref(&first));
|
||||
assert_eq!(before, Stamp::new(&state, &first, false));
|
||||
crate::backends::direct::render_on(&mut state, std::slice::from_ref(&first));
|
||||
assert_ne!(before, Stamp::new(&state, &first, false));
|
||||
assert_eq!(other, Stamp::new(&state, &second, false));
|
||||
let before = Stamp::new(&state, &first, false);
|
||||
state.defer_output_redraw(first.clone());
|
||||
assert_ne!(before, Stamp::new(&state, &first, false));
|
||||
crate::backends::direct::render_all(&mut state);
|
||||
assert_ne!(other, Stamp::new(&state, &second, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_privacy_and_output_configuration_change_the_appropriate_view() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"backends::direct::capture::changes::tests::cursor_privacy_and_output_configuration_change_the_appropriate_view",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let output = output();
|
||||
state.space.map_output(&output, (0, 0));
|
||||
let hidden = Stamp::new(&state, &output, false);
|
||||
let embedded = Stamp::new(&state, &output, true);
|
||||
crate::backends::direct::render_cursor(&mut state);
|
||||
assert_eq!(hidden, Stamp::new(&state, &output, false));
|
||||
assert_ne!(embedded, Stamp::new(&state, &output, true));
|
||||
state.capture_epoch += 1;
|
||||
assert_ne!(hidden, Stamp::new(&state, &output, false));
|
||||
let before = Stamp::new(&state, &output, false);
|
||||
output.change_current_state(None, Some(Transform::_180), None, None);
|
||||
assert_ne!(before, Stamp::new(&state, &output, false));
|
||||
let before = Stamp::new(&state, &output, false);
|
||||
output.change_current_state(None, None, Some(Scale::Fractional(1.5)), None);
|
||||
assert_ne!(before, Stamp::new(&state, &output, false));
|
||||
let before = Stamp::new(&state, &output, false);
|
||||
state.space.map_output(&output, (100, 0));
|
||||
assert_ne!(before, Stamp::new(&state, &output, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commits_and_the_final_animation_frame_invalidate_capture() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"backends::direct::capture::changes::tests::commits_and_the_final_animation_frame_invalidate_capture",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let output = output();
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "capture-animation".into());
|
||||
let (_window, mut client) = crate::state::test_window(&mut state, &mut events, 0xffff0000);
|
||||
let before = Stamp::new(&state, &output, false);
|
||||
// Even a callback-only commit goes through the mutation path. Damage
|
||||
// tracking can subsequently decide that the retained pixels still match.
|
||||
crate::state::test_client_request(&mut client, 2, 6, &[], None);
|
||||
crate::state::dispatch_test_client(&mut events, &mut state);
|
||||
assert_ne!(before, Stamp::new(&state, &output, false));
|
||||
let mut now = std::time::Instant::now();
|
||||
state.advance_animations(now);
|
||||
assert!(state.output_has_animations(&output));
|
||||
let mut settled = false;
|
||||
for _ in 0..500 {
|
||||
let before = Stamp::new(&state, &output, false);
|
||||
now += std::time::Duration::from_millis(16);
|
||||
state.advance_animations(now);
|
||||
assert_ne!(before, Stamp::new(&state, &output, false));
|
||||
if !state.output_has_animations(&output) {
|
||||
settled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
settled,
|
||||
"the opening/focus animation must reach its final frame"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,13 @@ pub(super) fn compose(
|
||||
source: &Output,
|
||||
elements: &[crate::render::AnimatedWindowRenderElement],
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
configure_canvas(canvas, source);
|
||||
let framebuffer = capture::render_texture(renderer, texture, canvas, elements)?;
|
||||
drop(framebuffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn configure_canvas(canvas: &Output, source: &Output) {
|
||||
let source_mode = source.current_mode().expect("source has a mode");
|
||||
canvas.change_current_state(
|
||||
Some(OutputMode {
|
||||
@@ -89,9 +96,6 @@ pub(super) fn compose(
|
||||
Some(source.current_scale()),
|
||||
None,
|
||||
);
|
||||
let framebuffer = capture::render_capture(renderer, texture, canvas, elements)?;
|
||||
drop(framebuffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn fitted_texture(
|
||||
|
||||
@@ -324,7 +324,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||
output.surface.reset_buffers();
|
||||
output.dmabuf_feedback.invalidate();
|
||||
output.primary_commit = None;
|
||||
output.capture_texture = None;
|
||||
output.capture_texture = Default::default();
|
||||
output.frame_pending = false;
|
||||
output.lock_frame_pending = false;
|
||||
output.power.suspended();
|
||||
|
||||
@@ -557,7 +557,7 @@ pub(super) fn apply_device(
|
||||
cancel_output_timers(&state.loop_handle, &mut output);
|
||||
output.frame_pending = false;
|
||||
output.primary_commit = None;
|
||||
output.capture_texture = None;
|
||||
output.capture_texture = Default::default();
|
||||
output.power.suspended();
|
||||
let clock = state
|
||||
.direct_backend
|
||||
|
||||
@@ -208,6 +208,12 @@ pub(crate) struct Animation {
|
||||
pub(crate) buffer_scale: i32,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub(crate) fn capture_revision(&self) -> (Option<CursorIcon>, Instant, usize) {
|
||||
(self.icon, self.epoch, self.frame)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Animation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -53,6 +53,12 @@ macro_rules! delegate_protocols {
|
||||
}
|
||||
|
||||
delegate_protocols!(
|
||||
protocols::ext::image_capture_source::v1::server::ext_image_capture_source_v1::ExtImageCaptureSourceV1,
|
||||
protocols::ext::image_capture_source::v1::server::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
|
||||
protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1,
|
||||
protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1,
|
||||
protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_cursor_session_v1::ExtImageCopyCaptureCursorSessionV1,
|
||||
protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::ExtImageCopyCaptureFrameV1,
|
||||
protocols::wp::idle_inhibit::zv1::server::zwp_idle_inhibit_manager_v1::ZwpIdleInhibitManagerV1,
|
||||
protocols::wp::idle_inhibit::zv1::server::zwp_idle_inhibitor_v1::ZwpIdleInhibitorV1,
|
||||
protocols::ext::data_control::v1::server::ext_data_control_device_v1::ExtDataControlDeviceV1,
|
||||
|
||||
@@ -0,0 +1,742 @@
|
||||
//! Persistent, damage-driven capture for the native portal. One frame may be
|
||||
//! outstanding per session; client buffers remain owned until GPU completion.
|
||||
use smithay::reexports::calloop::RegistrationToken;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
use smithay::backend::allocator::{Fourcc, Modifier};
|
||||
use smithay::backend::egl::EGLDevice;
|
||||
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::backend::renderer::utils::{CommitCounter, RendererSurfaceStateUserData, SurfaceView};
|
||||
use smithay::output::{Output, WeakOutput};
|
||||
use smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::FailureReason;
|
||||
use smithay::reexports::wayland_server::backend::ObjectId;
|
||||
use smithay::reexports::wayland_server::{Client, DisplayHandle, Resource};
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::utils::{Buffer, Logical, Point, Rectangle, Size, Transform};
|
||||
use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
|
||||
use smithay::wayland::image_capture_source::{ImageCaptureSource, ImageCaptureSourceHandler, OutputCaptureSourceHandler, OutputCaptureSourceState};
|
||||
use smithay::wayland::image_copy_capture::{BufferConstraints, DmabufConstraints, Frame, FrameRef, ImageCopyCaptureHandler, ImageCopyCaptureState, Session, SessionRef};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
const MAX_SESSIONS: usize = 16;
|
||||
|
||||
pub(super) enum Source {
|
||||
Output(WeakOutput),
|
||||
Window(WindowId),
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct SurfaceStamp {
|
||||
id: ObjectId,
|
||||
commit: CommitCounter,
|
||||
view: SurfaceView,
|
||||
scale: i32,
|
||||
transform: Transform,
|
||||
alpha: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct WindowStamp {
|
||||
surfaces: Vec<SurfaceStamp>,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
epoch: u64,
|
||||
cursor: Option<CursorStamp>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct CursorStamp {
|
||||
location: Point<f64, Logical>,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
image: CursorImageStamp,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CursorImageStamp {
|
||||
Named(Option<smithay::input::pointer::CursorIcon>, Instant, usize),
|
||||
Surface(Vec<SurfaceStamp>, Point<i32, Logical>),
|
||||
Hidden,
|
||||
}
|
||||
|
||||
fn surface_stamp(
|
||||
root: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
crop: Option<Rectangle<i32, Logical>>,
|
||||
) -> Vec<SurfaceStamp> {
|
||||
let mut stamp = Vec::new();
|
||||
with_surface_tree_downward(
|
||||
root,
|
||||
Point::<f64, Logical>::default(),
|
||||
|surface, states, location| {
|
||||
let Some(data) = states.data_map.get::<RendererSurfaceStateUserData>() else {
|
||||
return TraversalAction::SkipChildren;
|
||||
};
|
||||
use smithay::wayland::alpha_modifier::AlphaModifierSurfaceCachedState;
|
||||
let alpha = states
|
||||
.cached_state
|
||||
.get::<AlphaModifierSurfaceCachedState>()
|
||||
.current()
|
||||
.multiplier_f32();
|
||||
let data = data.lock().expect("surface metadata mutex poisoned");
|
||||
let Some(view) = data.view() else {
|
||||
return TraversalAction::SkipChildren;
|
||||
};
|
||||
let location = *location + view.offset.to_f64();
|
||||
if crop.is_none_or(|crop| {
|
||||
Rectangle::new(location, view.dst.to_f64()).overlaps(crop.to_f64())
|
||||
}) {
|
||||
stamp.push(SurfaceStamp {
|
||||
id: surface.id(),
|
||||
commit: data.current_commit(),
|
||||
view,
|
||||
scale: data.buffer_scale(),
|
||||
transform: data.buffer_transform(),
|
||||
alpha,
|
||||
});
|
||||
}
|
||||
// An off-crop parent can have a child offset back into the captured area.
|
||||
TraversalAction::DoChildren(location)
|
||||
},
|
||||
|_, _, _| {},
|
||||
|_, _, _| true,
|
||||
);
|
||||
stamp
|
||||
}
|
||||
|
||||
fn pending_frame_callbacks(
|
||||
root: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
) -> bool {
|
||||
let mut pending = false;
|
||||
with_surface_tree_downward(
|
||||
root,
|
||||
(),
|
||||
|_, states, _| {
|
||||
if states
|
||||
.data_map
|
||||
.get::<RendererSurfaceStateUserData>()
|
||||
.is_some_and(|data| {
|
||||
data.lock()
|
||||
.expect("surface metadata mutex poisoned")
|
||||
.view()
|
||||
.is_none()
|
||||
})
|
||||
{
|
||||
return TraversalAction::SkipChildren;
|
||||
}
|
||||
pending |= !states
|
||||
.cached_state
|
||||
.get::<smithay::wayland::compositor::SurfaceAttributes>()
|
||||
.current()
|
||||
.frame_callbacks
|
||||
.is_empty();
|
||||
TraversalAction::DoChildren(())
|
||||
},
|
||||
|_, _, _| {},
|
||||
|_, _, _| true,
|
||||
);
|
||||
pending
|
||||
}
|
||||
|
||||
struct Active {
|
||||
session: Session,
|
||||
pending: Option<Frame>,
|
||||
revision: Option<u64>,
|
||||
window_stamp: Option<WindowStamp>,
|
||||
size: Size<i32, Buffer>,
|
||||
dma_node: Option<smithay::backend::drm::DrmNode>,
|
||||
}
|
||||
|
||||
struct InFlight {
|
||||
frame: Frame,
|
||||
session: SessionRef,
|
||||
epoch: u64,
|
||||
fence: RegistrationToken,
|
||||
timeout: RegistrationToken,
|
||||
}
|
||||
|
||||
pub(crate) struct CaptureSessions {
|
||||
copy: ImageCopyCaptureState,
|
||||
outputs: OutputCaptureSourceState,
|
||||
active: Vec<Active>,
|
||||
in_flight: HashMap<u64, InFlight>,
|
||||
next_fence: u64,
|
||||
callback_timer: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
fn authorized(client: &Client) -> bool {
|
||||
super::screencopy::capture_allowed()
|
||||
&& client
|
||||
.get_data::<crate::state::ClientState>()
|
||||
.is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
}
|
||||
|
||||
impl CaptureSessions {
|
||||
pub(crate) fn new(display: &DisplayHandle) -> Self {
|
||||
Self {
|
||||
copy: ImageCopyCaptureState::new_with_filter::<Ferese, _>(display, authorized),
|
||||
outputs: OutputCaptureSourceState::new_with_filter::<Ferese, _>(display, authorized),
|
||||
active: Vec::new(),
|
||||
in_flight: HashMap::new(),
|
||||
next_fence: 0,
|
||||
callback_timer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_sessions(&self) -> bool {
|
||||
self.active.iter().any(|active| {
|
||||
active.pending.is_some()
|
||||
&& matches!(
|
||||
active.session.source().user_data().get::<Source>(),
|
||||
Some(Source::Output(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn pending_output(&self, output: &Output, cursor: bool) -> bool {
|
||||
self.active.iter().any(|active| {
|
||||
active.pending.is_some()
|
||||
&& active.session.draw_cursor() == cursor
|
||||
&& matches!(active.session.source().user_data().get::<Source>(),
|
||||
Some(Source::Output(weak)) if weak.upgrade().as_ref() == Some(output))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dma_constraints(renderer: &GlesRenderer) -> Option<Arc<DmabufConstraints>> {
|
||||
struct Cached(Option<Arc<DmabufConstraints>>);
|
||||
renderer
|
||||
.egl_context()
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(|| Cached(query_dma_constraints(renderer).map(Arc::new)))
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn query_dma_constraints(renderer: &GlesRenderer) -> Option<DmabufConstraints> {
|
||||
if !renderer.capabilities().contains(&Capability::ExportFence) {
|
||||
return None;
|
||||
}
|
||||
let display = renderer.egl_context().display();
|
||||
if !display
|
||||
.extensions()
|
||||
.iter()
|
||||
.any(|extension| extension == "EGL_ANDROID_native_fence_sync")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let node = EGLDevice::device_for_display(display)
|
||||
.ok()?
|
||||
.try_get_render_node()
|
||||
.ok()??;
|
||||
let format = smithay::backend::allocator::Format {
|
||||
code: Fourcc::Xrgb8888,
|
||||
modifier: Modifier::Linear,
|
||||
};
|
||||
display
|
||||
.dmabuf_render_formats()
|
||||
.contains(&format)
|
||||
.then_some(DmabufConstraints {
|
||||
node,
|
||||
formats: vec![(Fourcc::Xrgb8888, vec![Modifier::Linear])],
|
||||
})
|
||||
}
|
||||
|
||||
impl ImageCaptureSourceHandler for Ferese {}
|
||||
impl OutputCaptureSourceHandler for Ferese {
|
||||
fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState {
|
||||
&mut self.image_capture.outputs
|
||||
}
|
||||
fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) {
|
||||
source
|
||||
.user_data()
|
||||
.insert_if_missing(|| Source::Output(output.downgrade()));
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageCopyCaptureHandler for Ferese {
|
||||
fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState {
|
||||
&mut self.image_capture.copy
|
||||
}
|
||||
fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option<BufferConstraints> {
|
||||
let (size, output) = self.capture_source_geometry(source)?;
|
||||
Some(BufferConstraints {
|
||||
size,
|
||||
shm: vec![wl_shm::Format::Xrgb8888],
|
||||
dma: self.capture_dma_constraints(&output).as_deref().cloned(),
|
||||
})
|
||||
}
|
||||
fn new_session(&mut self, session: Session) {
|
||||
if self.image_capture.active.len() >= MAX_SESSIONS {
|
||||
return;
|
||||
}
|
||||
let Some(constraints) = session.current_constraints() else {
|
||||
return;
|
||||
};
|
||||
self.image_capture.active.push(Active {
|
||||
session,
|
||||
pending: None,
|
||||
revision: None,
|
||||
window_stamp: None,
|
||||
size: constraints.size,
|
||||
dma_node: constraints.dma.as_ref().map(|dma| dma.node),
|
||||
});
|
||||
}
|
||||
fn frame(&mut self, session: &SessionRef, frame: Frame) {
|
||||
let Some(active) = self
|
||||
.image_capture
|
||||
.active
|
||||
.iter_mut()
|
||||
.find(|active| active.session.as_ref() == *session)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if active.pending.is_some() {
|
||||
frame.fail(FailureReason::Unknown);
|
||||
return;
|
||||
}
|
||||
active.pending = Some(frame);
|
||||
if let Some(Source::Output(weak)) = session.source().user_data().get::<Source>()
|
||||
&& let Some(output) = weak.upgrade()
|
||||
{
|
||||
crate::backends::direct::schedule_capture(self, &[output]);
|
||||
}
|
||||
}
|
||||
|
||||
fn session_destroyed(&mut self, session: SessionRef) {
|
||||
self.image_capture
|
||||
.active
|
||||
.retain(|active| active.session.as_ref() != session);
|
||||
let fences: Vec<_> = self
|
||||
.image_capture
|
||||
.in_flight
|
||||
.iter()
|
||||
.filter_map(|(id, pending)| (pending.session == session).then_some(*id))
|
||||
.collect();
|
||||
for id in fences {
|
||||
self.cancel_capture_fence(id);
|
||||
}
|
||||
}
|
||||
fn frame_aborted(&mut self, frame: FrameRef) {
|
||||
let fences: Vec<_> = self
|
||||
.image_capture
|
||||
.in_flight
|
||||
.iter()
|
||||
.filter_map(|(id, pending)| (pending.frame == frame).then_some(*id))
|
||||
.collect();
|
||||
for id in fences {
|
||||
self.cancel_capture_fence(id);
|
||||
}
|
||||
for active in &mut self.image_capture.active {
|
||||
if active
|
||||
.pending
|
||||
.as_ref()
|
||||
.is_some_and(|pending| *pending == frame)
|
||||
{
|
||||
active.pending.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn capture_source_geometry(
|
||||
&self,
|
||||
source: &ImageCaptureSource,
|
||||
) -> Option<(Size<i32, Buffer>, Output)> {
|
||||
if self.session_lock.active() || !source.alive() || !super::screencopy::capture_allowed() {
|
||||
return None;
|
||||
}
|
||||
let (size, output) = match source.user_data().get::<Source>()? {
|
||||
Source::Window(id) => {
|
||||
let info = self.window_capture_info(*id)?;
|
||||
let size = info.geometry.size.to_physical_precise_round(info.scale);
|
||||
(Size::from((size.w, size.h)), info.output)
|
||||
}
|
||||
Source::Output(weak) => {
|
||||
let output = weak.upgrade()?;
|
||||
if !self.output_ids.contains_key(&output) {
|
||||
return None;
|
||||
}
|
||||
let size = output
|
||||
.current_transform()
|
||||
.transform_size(output.current_mode()?.size);
|
||||
(Size::from((size.w, size.h)), output)
|
||||
}
|
||||
};
|
||||
let bytes = u64::try_from(size.w)
|
||||
.ok()?
|
||||
.checked_mul(u64::try_from(size.h).ok()?)?
|
||||
.checked_mul(4)?;
|
||||
if bytes == 0 || bytes > 128 * 1024 * 1024 {
|
||||
return None;
|
||||
}
|
||||
Some((size, output))
|
||||
}
|
||||
|
||||
fn capture_dma_constraints(&mut self, output: &Output) -> Option<Arc<DmabufConstraints>> {
|
||||
if let Some(backend) = self.nested_backend.clone() {
|
||||
backend
|
||||
.try_borrow_mut()
|
||||
.ok()
|
||||
.and_then(|mut backend| dma_constraints(backend.renderer()))
|
||||
} else {
|
||||
self.direct_backend
|
||||
.as_mut()
|
||||
.and_then(|backend| backend.capture_renderer(output))
|
||||
.and_then(|renderer| dma_constraints(renderer))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn refresh_capture_source(&mut self, source: &ImageCaptureSource) {
|
||||
let mut outputs = Vec::new();
|
||||
for active in &mut self.image_capture.active {
|
||||
if active.session.source() != *source {
|
||||
continue;
|
||||
}
|
||||
active.revision = None;
|
||||
active.window_stamp = None;
|
||||
if active.pending.is_some()
|
||||
&& let Some(Source::Output(weak)) = source.user_data().get::<Source>()
|
||||
&& let Some(output) = weak.upgrade()
|
||||
&& !outputs.contains(&output)
|
||||
{
|
||||
outputs.push(output);
|
||||
}
|
||||
}
|
||||
crate::backends::direct::schedule_capture(self, &outputs);
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_capture_sessions(&mut self) {
|
||||
self.image_capture.copy.cleanup();
|
||||
let mut callback_delay: Option<Duration> = None;
|
||||
let mut sessions = std::mem::take(&mut self.image_capture.active);
|
||||
sessions.retain_mut(|active| {
|
||||
let Some((size, output)) = self.capture_source_geometry(&active.session.source())
|
||||
else {
|
||||
let session = active.session.as_ref();
|
||||
let fences: Vec<_> = self
|
||||
.image_capture
|
||||
.in_flight
|
||||
.iter()
|
||||
.filter_map(|(id, pending)| (pending.session == session).then_some(*id))
|
||||
.collect();
|
||||
for id in fences {
|
||||
self.cancel_capture_fence(id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
let dma = self.capture_dma_constraints(&output);
|
||||
let dma_node = dma.as_ref().map(|dma| dma.node);
|
||||
if size != active.size || dma_node != active.dma_node {
|
||||
active.size = size;
|
||||
active.dma_node = dma_node;
|
||||
active.revision = None;
|
||||
active.window_stamp = None;
|
||||
active.session.update_constraints(BufferConstraints {
|
||||
size,
|
||||
shm: vec![wl_shm::Format::Xrgb8888],
|
||||
dma: dma.as_deref().cloned(),
|
||||
});
|
||||
if let Some(frame) = active.pending.take() {
|
||||
frame.fail(FailureReason::BufferConstraints);
|
||||
}
|
||||
}
|
||||
if let Some(Source::Window(id)) = active.session.source().user_data().get::<Source>() {
|
||||
let id = *id;
|
||||
if let Some(frame) = active.pending.take() {
|
||||
let Some(info) = self.window_capture_info(id) else {
|
||||
return false;
|
||||
};
|
||||
let callback_interval = Duration::from_nanos(
|
||||
1_000_000_000_000
|
||||
/ info
|
||||
.output
|
||||
.current_mode()
|
||||
.map(|mode| mode.refresh.max(1000) as u64)
|
||||
.unwrap_or(60_000),
|
||||
);
|
||||
if let Some(toplevel) = info.window.toplevel()
|
||||
&& pending_frame_callbacks(toplevel.wl_surface())
|
||||
{
|
||||
smithay::desktop::utils::send_frames_surface_tree(
|
||||
toplevel.wl_surface(),
|
||||
&info.output,
|
||||
self.start_time.elapsed(),
|
||||
Some(callback_interval),
|
||||
|_, _| None,
|
||||
);
|
||||
}
|
||||
let mut stamp = WindowStamp {
|
||||
surfaces: Vec::new(),
|
||||
geometry: info.geometry,
|
||||
scale: info.scale,
|
||||
epoch: self.capture_epoch,
|
||||
cursor: None,
|
||||
};
|
||||
if let Some(toplevel) = info.window.toplevel() {
|
||||
stamp.surfaces = surface_stamp(toplevel.wl_surface(), Some(info.geometry));
|
||||
}
|
||||
let cursor = active
|
||||
.session
|
||||
.draw_cursor()
|
||||
.then(|| self.window_capture_cursor(id, &info.window, info.geometry))
|
||||
.flatten();
|
||||
if let Some(geometry) = cursor
|
||||
&& let Some(pointer) = self.seat.get_pointer()
|
||||
{
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
let image = match &self.cursor_status {
|
||||
CursorImageStatus::Named(_) => {
|
||||
let (icon, epoch, frame) = self.cursor_animation.capture_revision();
|
||||
CursorImageStamp::Named(icon, epoch, frame)
|
||||
}
|
||||
CursorImageStatus::Surface(surface) => {
|
||||
let hotspot =
|
||||
smithay::wayland::compositor::with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<smithay::input::pointer::CursorImageSurfaceData>(
|
||||
)
|
||||
.map(|data| {
|
||||
data.lock()
|
||||
.expect("cursor metadata mutex poisoned")
|
||||
.hotspot
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
CursorImageStamp::Surface(surface_stamp(surface, None), hotspot)
|
||||
}
|
||||
CursorImageStatus::Hidden => CursorImageStamp::Hidden,
|
||||
};
|
||||
stamp.cursor = Some(CursorStamp {
|
||||
location: pointer.current_location(),
|
||||
geometry,
|
||||
image,
|
||||
});
|
||||
}
|
||||
if active.window_stamp.as_ref() == Some(&stamp) {
|
||||
active.pending = Some(frame);
|
||||
if info
|
||||
.window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| pending_frame_callbacks(toplevel.wl_surface()))
|
||||
{
|
||||
callback_delay =
|
||||
Some(callback_delay.map_or(callback_interval, |delay| {
|
||||
delay.min(callback_interval)
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
crate::backends::direct::capture::touch_cache(self);
|
||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||
backend
|
||||
.try_borrow_mut()
|
||||
.map_err(|error| error.to_string())
|
||||
.and_then(|mut backend| {
|
||||
crate::render::capture_stream_window(
|
||||
backend.renderer(),
|
||||
self,
|
||||
&info,
|
||||
cursor,
|
||||
&frame.buffer(),
|
||||
)
|
||||
})
|
||||
} else if let Some(mut backend) = self.direct_backend.take() {
|
||||
let result = backend
|
||||
.capture_renderer(&info.output)
|
||||
.ok_or_else(|| "Capture output is unavailable".to_string())
|
||||
.and_then(|renderer| {
|
||||
crate::render::capture_stream_window(
|
||||
renderer,
|
||||
self,
|
||||
&info,
|
||||
cursor,
|
||||
&frame.buffer(),
|
||||
)
|
||||
});
|
||||
self.direct_backend = Some(backend);
|
||||
result
|
||||
} else {
|
||||
Err("Capture renderer is unavailable".into())
|
||||
};
|
||||
active.window_stamp = Some(stamp);
|
||||
self.finish_stream_frame(frame, active.session.as_ref(), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
self.image_capture.active = sessions;
|
||||
if let Some(delay) = callback_delay {
|
||||
if self.image_capture.callback_timer.is_none() {
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(delay), |_, _, state| {
|
||||
state.image_capture.callback_timer = None;
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
Ok(token) => self.image_capture.callback_timer = Some(token),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not schedule captured window callbacks")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(timer) = self.image_capture.callback_timer.take() {
|
||||
self.loop_handle.remove(timer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_output_streams(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &GlesTexture,
|
||||
output: &Output,
|
||||
cursor: bool,
|
||||
revision: u64,
|
||||
) {
|
||||
let mut sessions = std::mem::take(&mut self.image_capture.active);
|
||||
for active in &mut sessions {
|
||||
let matches = matches!(active.session.source().user_data().get::<Source>(), Some(Source::Output(weak)) if weak.upgrade().as_ref() == Some(output));
|
||||
if matches {
|
||||
let size = output.current_transform().transform_size(texture.size());
|
||||
if active.size != Size::from((size.w, size.h)) {
|
||||
if let Some(frame) = active.pending.take() {
|
||||
frame.fail(FailureReason::BufferConstraints);
|
||||
}
|
||||
active.revision = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if matches
|
||||
&& active.session.draw_cursor() == cursor
|
||||
&& active.revision != Some(revision)
|
||||
&& let Some(frame) = active.pending.take()
|
||||
{
|
||||
let result = crate::render::capture_stream_texture(
|
||||
renderer,
|
||||
texture,
|
||||
&frame.buffer(),
|
||||
output.current_transform(),
|
||||
false,
|
||||
);
|
||||
active.revision = Some(revision);
|
||||
self.finish_stream_frame(frame, active.session.as_ref(), result);
|
||||
}
|
||||
}
|
||||
self.image_capture.active = sessions;
|
||||
}
|
||||
|
||||
fn cancel_capture_fence(&mut self, id: u64) {
|
||||
if let Some(pending) = self.image_capture.in_flight.remove(&id) {
|
||||
self.loop_handle.remove(pending.fence);
|
||||
self.loop_handle.remove(pending.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_stream_frame(
|
||||
&mut self,
|
||||
frame: Frame,
|
||||
session: SessionRef,
|
||||
result: Result<smithay::backend::renderer::sync::SyncPoint, String>,
|
||||
) {
|
||||
match result {
|
||||
Ok(sync) if sync.is_reached() => frame.success(
|
||||
Transform::Normal,
|
||||
None,
|
||||
smithay::utils::Clock::<smithay::utils::Monotonic>::new().now(),
|
||||
),
|
||||
Ok(sync) => {
|
||||
let Some(fd) = sync.export() else {
|
||||
frame.fail(FailureReason::Unknown);
|
||||
return;
|
||||
};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::calloop::{Interest, Mode, PostAction, generic::Generic};
|
||||
self.image_capture.next_fence = self.image_capture.next_fence.wrapping_add(1);
|
||||
let id = self.image_capture.next_fence;
|
||||
let source = Generic::new(fd, Interest::READ, Mode::OneShot);
|
||||
let fence = match self.loop_handle.insert_source(source, move |_, _, state| {
|
||||
if let Some(pending) = state.image_capture.in_flight.remove(&id) {
|
||||
state.loop_handle.remove(pending.timeout);
|
||||
if pending.epoch == state.capture_epoch
|
||||
&& state
|
||||
.capture_source_geometry(&pending.session.source())
|
||||
.is_some()
|
||||
&& !pending.frame.has_failed()
|
||||
{
|
||||
pending.frame.success(
|
||||
Transform::Normal,
|
||||
None,
|
||||
smithay::utils::Clock::<smithay::utils::Monotonic>::new().now(),
|
||||
);
|
||||
} else {
|
||||
pending.frame.fail(
|
||||
if state
|
||||
.capture_source_geometry(&pending.session.source())
|
||||
.is_some()
|
||||
{
|
||||
FailureReason::BufferConstraints
|
||||
} else {
|
||||
FailureReason::Stopped
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(PostAction::Remove)
|
||||
}) {
|
||||
Ok(token) => token,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not register capture fence");
|
||||
frame.fail(FailureReason::Unknown);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let timeout = match self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(5)),
|
||||
move |_, _, state| {
|
||||
if let Some(pending) = state.image_capture.in_flight.remove(&id) {
|
||||
state.loop_handle.remove(pending.fence);
|
||||
pending.frame.fail(FailureReason::Stopped);
|
||||
state
|
||||
.image_capture
|
||||
.active
|
||||
.retain(|active| active.session.as_ref() != pending.session);
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
},
|
||||
) {
|
||||
Ok(token) => token,
|
||||
Err(error) => {
|
||||
self.loop_handle.remove(fence);
|
||||
tracing::warn!(%error, "could not register capture deadline");
|
||||
frame.fail(FailureReason::Unknown);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.image_capture.in_flight.insert(
|
||||
id,
|
||||
InFlight {
|
||||
frame,
|
||||
session,
|
||||
epoch: self.capture_epoch,
|
||||
fence,
|
||||
timeout,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "stream capture failed");
|
||||
frame.fail(FailureReason::Unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub(crate) mod activation;
|
||||
mod compositor;
|
||||
mod dispatch;
|
||||
pub(crate) mod gamma_control;
|
||||
pub(crate) mod image_capture;
|
||||
mod input_method;
|
||||
mod layer_shell;
|
||||
pub(crate) mod screencopy;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -22,6 +23,8 @@ use crate::Ferese;
|
||||
|
||||
const BYTES_PER_PIXEL: usize = 4;
|
||||
|
||||
type DamageHistory = Arc<Mutex<HashMap<(Output, bool), u64>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameData {
|
||||
pub(super) output: Option<Output>,
|
||||
@@ -29,6 +32,7 @@ pub(crate) struct FrameData {
|
||||
pub(super) overlay_cursor: bool,
|
||||
pub(super) used: Mutex<bool>,
|
||||
pub(super) snapshot: Mutex<Option<super::window_capture::Snapshot>>,
|
||||
pub(super) history: Option<DamageHistory>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -38,6 +42,7 @@ pub(crate) struct PendingScreencopy {
|
||||
region: Rectangle<i32, Buffer>,
|
||||
overlay_cursor: bool,
|
||||
with_damage: bool,
|
||||
history: Option<DamageHistory>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -140,6 +145,7 @@ impl PendingScreencopy {
|
||||
// The compositor-owned path deliberately omits the cursor.
|
||||
overlay_cursor: false,
|
||||
with_damage: false,
|
||||
history: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,17 +193,17 @@ impl GlobalDispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
_global_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
data_init.init(resource, DamageHistory::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
impl Dispatch<ZwlrScreencopyManagerV1, DamageHistory> for Ferese {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_manager: &ZwlrScreencopyManagerV1,
|
||||
request: zwlr_screencopy_manager_v1::Request,
|
||||
_data: &(),
|
||||
history: &DamageHistory,
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
@@ -207,7 +213,14 @@ impl Dispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
overlay_cursor,
|
||||
output,
|
||||
} => {
|
||||
create_frame(data_init, frame, output, None, overlay_cursor != 0);
|
||||
create_frame(
|
||||
data_init,
|
||||
frame,
|
||||
output,
|
||||
None,
|
||||
overlay_cursor != 0,
|
||||
history.clone(),
|
||||
);
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||
frame,
|
||||
@@ -224,6 +237,7 @@ impl Dispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
output,
|
||||
Some(Rectangle::new((x, y).into(), (width, height).into())),
|
||||
overlay_cursor != 0,
|
||||
history.clone(),
|
||||
);
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::Destroy => {}
|
||||
@@ -307,6 +321,12 @@ impl Ferese {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(history) = &data.history {
|
||||
history
|
||||
.lock()
|
||||
.expect("capture damage history mutex poisoned")
|
||||
.retain(|(output, _), _| self.output_ids.contains_key(output));
|
||||
}
|
||||
self.pending_screencopies.push(PendingScreencopy {
|
||||
sink: CaptureSink::Shm {
|
||||
frame: frame.clone(),
|
||||
@@ -316,8 +336,24 @@ impl Ferese {
|
||||
region: data.region,
|
||||
overlay_cursor: data.overlay_cursor,
|
||||
with_damage,
|
||||
history: data.history.clone(),
|
||||
});
|
||||
crate::backends::direct::schedule_capture(self, &[output]);
|
||||
}
|
||||
|
||||
pub(crate) fn prune_screencopies(&mut self) {
|
||||
self.pending_screencopies.retain(|capture| {
|
||||
if let CaptureSink::Shm { frame, buffer } = &capture.sink {
|
||||
if !frame.is_alive() {
|
||||
return false;
|
||||
}
|
||||
if !buffer.is_alive() {
|
||||
frame.failed();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
crate::backends::direct::render_on(self, &[output]);
|
||||
}
|
||||
|
||||
pub(crate) fn process_screencopies<R>(
|
||||
@@ -330,6 +366,17 @@ impl Ferese {
|
||||
where
|
||||
R: ExportMem,
|
||||
{
|
||||
self.process_screencopies_with_revision(renderer, framebuffer, output, overlay_cursor, None)
|
||||
}
|
||||
|
||||
pub(crate) fn process_screencopies_with_revision<R: ExportMem>(
|
||||
&mut self,
|
||||
renderer: &mut R,
|
||||
framebuffer: &R::Framebuffer<'_>,
|
||||
output: &Output,
|
||||
overlay_cursor: bool,
|
||||
revision: Option<u64>,
|
||||
) -> bool {
|
||||
let mut remaining = Vec::new();
|
||||
let mut framebuffer_binding_changed = false;
|
||||
let captures = std::mem::take(&mut self.pending_screencopies);
|
||||
@@ -347,10 +394,33 @@ impl Ferese {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = (output.clone(), overlay_cursor);
|
||||
if capture.history.is_some() && revision.is_none() {
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
if capture.with_damage {
|
||||
let previous = capture.history.as_ref().and_then(|history| {
|
||||
history
|
||||
.lock()
|
||||
.expect("capture damage history mutex poisoned")
|
||||
.get(&key)
|
||||
.copied()
|
||||
});
|
||||
if revision.is_none() || previous == revision {
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
framebuffer_binding_changed = true;
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err()
|
||||
{
|
||||
capture.fail();
|
||||
} else if let (Some(history), Some(revision)) = (&capture.history, revision) {
|
||||
history
|
||||
.lock()
|
||||
.expect("capture damage history mutex poisoned")
|
||||
.insert(key, revision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,10 +429,11 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool {
|
||||
self.pending_screencopies.iter().any(|capture| {
|
||||
capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
})
|
||||
self.image_capture.pending_output(output, overlay_cursor)
|
||||
|| self.pending_screencopies.iter().any(|capture| {
|
||||
capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) {
|
||||
@@ -391,6 +462,7 @@ fn create_frame(
|
||||
wl_output: smithay::reexports::wayland_server::protocol::wl_output::WlOutput,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
overlay_cursor: bool,
|
||||
history: DamageHistory,
|
||||
) {
|
||||
let Some(output) = Output::from_resource(&wl_output) else {
|
||||
let frame = data_init.init(
|
||||
@@ -401,6 +473,7 @@ fn create_frame(
|
||||
overlay_cursor,
|
||||
used: Mutex::new(true),
|
||||
snapshot: Mutex::new(None),
|
||||
history: Some(history.clone()),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
@@ -415,6 +488,7 @@ fn create_frame(
|
||||
overlay_cursor,
|
||||
used: Mutex::new(true),
|
||||
snapshot: Mutex::new(None),
|
||||
history: Some(history.clone()),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
@@ -428,6 +502,7 @@ fn create_frame(
|
||||
overlay_cursor,
|
||||
used: Mutex::new(false),
|
||||
snapshot: Mutex::new(None),
|
||||
history: Some(history.clone()),
|
||||
});
|
||||
let frame = data_init.init(frame, data);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex};
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1 as manager;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::FereseWindowCaptureManagerV1;
|
||||
use smithay::backend::renderer::utils::{RendererSurfaceStateUserData, SurfaceView};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1 as frame;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
@@ -11,7 +12,8 @@ use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::Rectangle;
|
||||
use smithay::utils::{Logical, Point, Rectangle};
|
||||
use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
use super::screencopy::FrameData;
|
||||
@@ -21,6 +23,15 @@ use crate::Ferese;
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
const MAX_FRAMES: usize = 8;
|
||||
|
||||
fn capture_scale(output_scale: f64, buffer_scale: i32, view: SurfaceView) -> f64 {
|
||||
if view.dst.w <= 0 || view.dst.h <= 0 {
|
||||
return output_scale;
|
||||
}
|
||||
let native_x = view.src.size.w * f64::from(buffer_scale) / f64::from(view.dst.w);
|
||||
let native_y = view.src.size.h * f64::from(buffer_scale) / f64::from(view.dst.h);
|
||||
output_scale.max(native_x).max(native_y)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Budget {
|
||||
bytes: usize,
|
||||
@@ -155,7 +166,7 @@ pub(super) fn init_global(
|
||||
}
|
||||
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(
|
||||
1,
|
||||
2,
|
||||
Global {
|
||||
budget: Arc::default(),
|
||||
worker,
|
||||
@@ -237,6 +248,44 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
_: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let manager::Request::RefreshSource { source } = request {
|
||||
if let Some(source) =
|
||||
smithay::wayland::image_capture_source::ImageCaptureSource::from_resource(&source)
|
||||
{
|
||||
state.refresh_capture_source(&source);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let manager::Request::CreateSource {
|
||||
window_hi,
|
||||
window_lo,
|
||||
source,
|
||||
} = request
|
||||
{
|
||||
use smithay::wayland::image_capture_source::{
|
||||
ImageCaptureSource, ImageCaptureSourceData,
|
||||
};
|
||||
let id = WindowId((u64::from(window_hi) << 32) | u64::from(window_lo));
|
||||
let capture = ImageCaptureSource::new();
|
||||
capture
|
||||
.user_data()
|
||||
.insert_if_missing(|| super::image_capture::Source::Window(id));
|
||||
let resource = data_init.init(
|
||||
source,
|
||||
ImageCaptureSourceData {
|
||||
source: capture.clone(),
|
||||
},
|
||||
);
|
||||
capture.add_instance(&resource);
|
||||
if let Some(info) = state.window_capture_info(id) {
|
||||
manager.source_geometry(
|
||||
&resource,
|
||||
info.geometry.size.w as u32,
|
||||
info.geometry.size.h as u32,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let manager::Request::CaptureWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
@@ -262,6 +311,7 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
overlay_cursor: false,
|
||||
used: Mutex::new(failed),
|
||||
snapshot: Mutex::new(None),
|
||||
history: None,
|
||||
}),
|
||||
);
|
||||
if failed {
|
||||
@@ -293,14 +343,16 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WindowInfo {
|
||||
pub window: smithay::desktop::Window,
|
||||
pub output: smithay::output::Output,
|
||||
pub geometry: Rectangle<i32, Logical>,
|
||||
pub scale: f64,
|
||||
pub bytes: usize,
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn window_snapshot(
|
||||
&mut self,
|
||||
id: WindowId,
|
||||
overlay_cursor: bool,
|
||||
budget: &Arc<Mutex<Budget>>,
|
||||
busy: &mut bool,
|
||||
) -> Option<Snapshot> {
|
||||
pub(super) fn window_capture_info(&self, id: WindowId) -> Option<WindowInfo> {
|
||||
if self.session_lock.active() || !super::screencopy::capture_allowed() {
|
||||
return None;
|
||||
}
|
||||
@@ -323,7 +375,34 @@ impl Ferese {
|
||||
.0
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let output_scale = output.current_scale().fractional_scale();
|
||||
// A low-density parent can contain higher-density content. Match the
|
||||
// rendered surface tree, excluding buffers outside the captured geometry.
|
||||
let mut scale = output_scale;
|
||||
if let Some(toplevel) = window.toplevel() {
|
||||
with_surface_tree_downward(
|
||||
toplevel.wl_surface(),
|
||||
Point::<f64, Logical>::default(),
|
||||
|_, states, location| {
|
||||
let Some(data) = states.data_map.get::<RendererSurfaceStateUserData>() else {
|
||||
return TraversalAction::SkipChildren;
|
||||
};
|
||||
let data = data
|
||||
.lock()
|
||||
.expect("Smithay surface metadata must remain consistent across callbacks");
|
||||
let Some(view) = data.view() else {
|
||||
return TraversalAction::SkipChildren;
|
||||
};
|
||||
let location = *location + view.offset.to_f64();
|
||||
if Rectangle::new(location, view.dst.to_f64()).overlaps(geometry.to_f64()) {
|
||||
scale = capture_scale(scale, data.buffer_scale(), view);
|
||||
}
|
||||
TraversalAction::DoChildren(location)
|
||||
},
|
||||
|_, _, _| {},
|
||||
|_, _, _| true,
|
||||
);
|
||||
}
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> =
|
||||
geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = usize::try_from(size.w)
|
||||
@@ -333,33 +412,65 @@ impl Ferese {
|
||||
if bytes == 0 || bytes > MAX_BYTES {
|
||||
return None;
|
||||
}
|
||||
Some(WindowInfo {
|
||||
window,
|
||||
output,
|
||||
geometry,
|
||||
scale,
|
||||
bytes,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn window_capture_cursor(
|
||||
&self,
|
||||
id: WindowId,
|
||||
window: &smithay::desktop::Window,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
) -> Option<Rectangle<i32, Logical>> {
|
||||
if !self.overview.is_presenting() && !self.capture_cursor_protected() {
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(window) {
|
||||
return None;
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
Some(Rectangle::new(
|
||||
(location - smithay::utils::Point::from((x, y))).to_i32_round(),
|
||||
geometry.size,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn window_snapshot(
|
||||
&mut self,
|
||||
id: WindowId,
|
||||
overlay_cursor: bool,
|
||||
budget: &Arc<Mutex<Budget>>,
|
||||
busy: &mut bool,
|
||||
) -> Option<Snapshot> {
|
||||
let WindowInfo {
|
||||
window,
|
||||
output,
|
||||
geometry,
|
||||
scale,
|
||||
bytes,
|
||||
} = self.window_capture_info(id)?;
|
||||
let Some(permit) = Permit::reserve(budget, bytes) else {
|
||||
*busy = true;
|
||||
return None;
|
||||
};
|
||||
let cursor =
|
||||
if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected()
|
||||
{
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(&window) {
|
||||
return None;
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
Some(Rectangle::new(
|
||||
(location - smithay::utils::Point::from((x, y))).to_i32_round(),
|
||||
geometry.size,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cursor = overlay_cursor
|
||||
.then(|| self.window_capture_cursor(id, &window, geometry))
|
||||
.flatten();
|
||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||
let mut backend = backend.try_borrow_mut().ok()?;
|
||||
crate::render::capture_window_frame(
|
||||
@@ -476,6 +587,40 @@ pub(super) fn copy_snapshot(
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sharing_preserves_backing_density_at_fractional_display_scales() {
|
||||
let integer = SurfaceView {
|
||||
src: Rectangle::from_size((640., 480.).into()),
|
||||
dst: (640, 480).into(),
|
||||
offset: (0, 0).into(),
|
||||
};
|
||||
let viewport = SurfaceView {
|
||||
src: Rectangle::from_size((1280., 960.).into()),
|
||||
..integer
|
||||
};
|
||||
for output_scale in [1., 1.25, 1.5, 1.75, 2.] {
|
||||
assert_eq!(capture_scale(output_scale, 2, integer), 2.);
|
||||
assert_eq!(capture_scale(output_scale, 1, viewport), 2.);
|
||||
}
|
||||
assert_eq!(capture_scale(2.5, 2, integer), 2.5);
|
||||
let fractional = SurfaceView {
|
||||
src: Rectangle::from_size((1120., 840.).into()),
|
||||
..integer
|
||||
};
|
||||
assert_eq!(capture_scale(1.25, 1, fractional), 1.75);
|
||||
assert_eq!(capture_scale(1.75, 1, fractional), 1.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharing_uses_the_viewport_crop_instead_of_the_whole_buffer() {
|
||||
let cropped = SurfaceView {
|
||||
src: Rectangle::new((320., 240.).into(), (640., 480.).into()),
|
||||
dst: (640, 480).into(),
|
||||
offset: (0, 0).into(),
|
||||
};
|
||||
assert_eq!(capture_scale(1.75, 1, cropped), 1.75);
|
||||
}
|
||||
|
||||
/// Exercise both asynchronous publication boundaries using real resources.
|
||||
pub(crate) fn check_deferred_snapshot(
|
||||
state: &Ferese,
|
||||
@@ -505,6 +650,7 @@ pub(crate) mod tests {
|
||||
overlay_cursor: false,
|
||||
used: Mutex::new(false),
|
||||
snapshot: Mutex::new(None),
|
||||
history: None,
|
||||
})
|
||||
};
|
||||
let (worker, _requests) = std::sync::mpsc::sync_channel(1);
|
||||
|
||||
@@ -209,6 +209,8 @@ fn after_dispatch_with_redraw(
|
||||
) {
|
||||
crate::backends::direct::gamma::sync_night_light(state);
|
||||
state.process_pending_dmabuf_imports();
|
||||
state.prune_screencopies();
|
||||
state.refresh_capture_sessions();
|
||||
state.update_named_cursor(std::time::Instant::now());
|
||||
state.poll_theme();
|
||||
if state.session_lock.active() || state.input_capture.active() {
|
||||
|
||||
@@ -179,14 +179,37 @@ pub(crate) fn capture_window_frame(
|
||||
|
||||
/// Convert premultiplied ABGR readback into the capture protocol's straight ARGB.
|
||||
pub(crate) fn convert_window_pixels(pixels: &mut [u8]) {
|
||||
// Compile the rounded, saturated conversion into 64 KiB of read-only data.
|
||||
// Capture threads share this table without allocating or dividing per pixel.
|
||||
static STRAIGHT_CHANNELS: [[u8; 256]; 256] = {
|
||||
let mut values = [[0; 256]; 256];
|
||||
let mut alpha = 1;
|
||||
while alpha < 256 {
|
||||
let mut channel = 0;
|
||||
while channel < 256 {
|
||||
let straight = (channel * 255 + alpha / 2) / alpha;
|
||||
values[alpha][channel] = if straight > 255 { 255 } else { straight as u8 };
|
||||
channel += 1;
|
||||
}
|
||||
alpha += 1;
|
||||
}
|
||||
values
|
||||
};
|
||||
for pixel in pixels.as_chunks_mut::<4>().0 {
|
||||
pixel.swap(0, 2);
|
||||
let alpha = pixel[3] as u16;
|
||||
let alpha = pixel[3];
|
||||
// Opaque content needs only the channel swap; avoid three divisions
|
||||
// per pixel for the usual window-sharing frame.
|
||||
if alpha == 255 {
|
||||
continue;
|
||||
}
|
||||
if alpha == 0 {
|
||||
pixel[..3].fill(0);
|
||||
continue;
|
||||
}
|
||||
let channels = &STRAIGHT_CHANNELS[usize::from(alpha)];
|
||||
for channel in &mut pixel[..3] {
|
||||
*channel = (*channel as u16 * 255 + alpha / 2)
|
||||
.checked_div(alpha)
|
||||
.unwrap_or(0)
|
||||
.min(255) as u8;
|
||||
*channel = channels[usize::from(*channel)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +218,93 @@ pub(crate) fn convert_window_pixels(pixels: &mut [u8]) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual release benchmark for window-capture pixel conversion"]
|
||||
fn profile_window_conversion() {
|
||||
use std::{
|
||||
hint::black_box,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
for (width, height) in [(1920usize, 1080usize), (3840, 2160)] {
|
||||
for workload in ["opaque", "edges", "translucent", "mixed"] {
|
||||
let mut source = vec![0u8; width * height * 4];
|
||||
for (i, pixel) in source.chunks_exact_mut(4).enumerate() {
|
||||
let alpha: u8 = if workload == "mixed" {
|
||||
i.wrapping_mul(37) as u8
|
||||
} else if workload == "translucent"
|
||||
|| (workload == "edges"
|
||||
&& (i % width < 16
|
||||
|| i % width >= width - 16
|
||||
|| i / width < 16
|
||||
|| i / width >= height - 16))
|
||||
{
|
||||
128
|
||||
} else {
|
||||
255
|
||||
};
|
||||
let color = (i as u32).wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let premultiplied =
|
||||
|shift: u32| (((color >> shift) & 255) * u32::from(alpha) / 255) as u8;
|
||||
pixel.copy_from_slice(&[
|
||||
premultiplied(0),
|
||||
premultiplied(8),
|
||||
premultiplied(16),
|
||||
alpha,
|
||||
]);
|
||||
}
|
||||
let mut pixels = source.clone();
|
||||
for run in 0..=3 {
|
||||
let mut elapsed = Duration::ZERO;
|
||||
for _ in 0..3 {
|
||||
pixels.copy_from_slice(&source);
|
||||
let started = Instant::now();
|
||||
convert_window_pixels(black_box(&mut pixels));
|
||||
elapsed += started.elapsed();
|
||||
black_box(&pixels);
|
||||
}
|
||||
if run != 0 {
|
||||
eprintln!(
|
||||
"window_conversion_benchmark width={width} workload={workload} run={run} frames=3 elapsed_us={}",
|
||||
elapsed.as_micros()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversion_matches_straight_argb_for_every_alpha_and_channel_value() {
|
||||
let mut pixels = Vec::new();
|
||||
let mut expected = Vec::new();
|
||||
for alpha in 0..=255u8 {
|
||||
for channel in 0..=255u8 {
|
||||
let source = [channel, 255 - channel, channel ^ alpha, alpha];
|
||||
pixels.extend_from_slice(&source);
|
||||
let straight = |value: u8| {
|
||||
if alpha == 0 {
|
||||
0
|
||||
} else {
|
||||
((u32::from(value) * 255 + u32::from(alpha) / 2) / u32::from(alpha))
|
||||
.min(255) as u8
|
||||
}
|
||||
};
|
||||
expected.extend_from_slice(&[
|
||||
straight(source[2]),
|
||||
straight(source[1]),
|
||||
straight(source[0]),
|
||||
alpha,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Incomplete trailing pixels are outside the conversion's input domain.
|
||||
pixels.extend_from_slice(&[17, 29, 43]);
|
||||
expected.extend_from_slice(&[17, 29, 43]);
|
||||
convert_window_pixels(&mut pixels);
|
||||
assert_eq!(pixels, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readback_conversion_preserves_color_and_handles_zero_alpha() {
|
||||
let mut pixels = vec![10, 20, 30, 255, 32, 64, 128, 128, 9, 8, 7, 0, 1, 1, 1, 1];
|
||||
@@ -207,3 +317,481 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StreamResources {
|
||||
textures: std::collections::VecDeque<GlesTexture>,
|
||||
bytes: usize,
|
||||
straight: Option<smithay::backend::renderer::gles::GlesTexProgram>,
|
||||
copy: Option<smithay::backend::renderer::gles::GlesTexProgram>,
|
||||
}
|
||||
|
||||
impl StreamResources {
|
||||
fn take_texture(&mut self, size: Size<i32, smithay::utils::Buffer>) -> Option<GlesTexture> {
|
||||
let index = self
|
||||
.textures
|
||||
.iter()
|
||||
.position(|texture| texture.size() == size)?;
|
||||
let texture = self.textures.remove(index)?;
|
||||
self.bytes -= texture.size().w as usize * texture.size().h as usize * 4;
|
||||
Some(texture)
|
||||
}
|
||||
|
||||
fn put_texture(&mut self, texture: GlesTexture) {
|
||||
let bytes = texture.size().w as usize * texture.size().h as usize * 4;
|
||||
// Scratch targets are shared by size across sources. Keep alternating
|
||||
// window sizes from reallocating every frame without retaining an
|
||||
// unbounded collection of old source sizes.
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
if bytes > MAX_BYTES {
|
||||
return;
|
||||
}
|
||||
while self.textures.len() >= 8 || self.bytes + bytes > MAX_BYTES {
|
||||
let Some(old) = self.textures.pop_front() else {
|
||||
break;
|
||||
};
|
||||
self.bytes -= old.size().w as usize * old.size().h as usize * 4;
|
||||
}
|
||||
self.bytes += bytes;
|
||||
self.textures.push_back(texture);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StreamCache(std::sync::Mutex<StreamResources>);
|
||||
|
||||
const STREAM_CAPTURE_SHADER: &str = r#"
|
||||
//_DEFINES
|
||||
#ifdef EXTERNAL
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
#endif
|
||||
precision highp float;
|
||||
#ifdef EXTERNAL
|
||||
uniform samplerExternalOES tex;
|
||||
#else
|
||||
uniform sampler2D tex;
|
||||
#endif
|
||||
varying vec2 v_coords;
|
||||
uniform float alpha;
|
||||
uniform float y_offset;
|
||||
void main() {
|
||||
// The renderer negates Y for inverted textures; translate that origin
|
||||
// back into [0, 1] so full-buffer captures sample both rows correctly.
|
||||
vec4 pixel = texture2D(tex, v_coords + vec2(0.0, y_offset));
|
||||
#ifdef NO_ALPHA
|
||||
pixel.a = 1.0;
|
||||
#endif
|
||||
#ifdef STRAIGHT
|
||||
float a = floor(pixel.a * 255.0 + 0.5);
|
||||
vec3 numerator = floor(pixel.rgb * 255.0 + 0.5) * 255.0 + floor(a * 0.5);
|
||||
vec3 quotient = floor(numerator / max(a, 1.0));
|
||||
vec3 remainder = numerator - quotient * max(a, 1.0);
|
||||
quotient += step(vec3(max(a, 1.0)), remainder);
|
||||
quotient -= vec3(lessThan(remainder, vec3(0.0)));
|
||||
gl_FragColor = vec4(a == 0.0 ? vec3(0.0) : clamp(quotient, 0.0, 255.0) / 255.0, 1.0);
|
||||
#else
|
||||
gl_FragColor = vec4(pixel.rgb, 1.0);
|
||||
#endif
|
||||
}
|
||||
"#;
|
||||
|
||||
pub(crate) fn capture_stream_window(
|
||||
renderer: &mut GlesRenderer,
|
||||
state: &Ferese,
|
||||
info: &crate::handlers::window_capture::WindowInfo,
|
||||
cursor: Option<Rectangle<i32, Logical>>,
|
||||
buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
|
||||
) -> Result<smithay::backend::renderer::sync::SyncPoint, String> {
|
||||
let cache = renderer
|
||||
.egl_context()
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(|| Arc::new(StreamCache::default()))
|
||||
.clone();
|
||||
let reusable = cache
|
||||
.0
|
||||
.lock()
|
||||
.expect("stream texture cache mutex poisoned")
|
||||
.take_texture({
|
||||
let size = info.geometry.size.to_physical_precise_round(info.scale);
|
||||
(size.w, size.h).into()
|
||||
});
|
||||
let mut snapshot = render_window_texture(
|
||||
renderer,
|
||||
&info.window,
|
||||
info.geometry,
|
||||
info.scale,
|
||||
128 * 1024 * 1024,
|
||||
reusable,
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Window has no capturable content")?;
|
||||
let size = snapshot.texture.size();
|
||||
if let Some(cursor) = cursor {
|
||||
let elements = cursor_elements(state, renderer, cursor, info.scale);
|
||||
let mut target = renderer
|
||||
.bind(&mut snapshot.texture)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut frame = renderer
|
||||
.render(&mut target, (size.w, size.h).into(), Transform::Normal)
|
||||
.map_err(|error| error.to_string())?;
|
||||
draw_render_elements(
|
||||
&mut frame,
|
||||
info.scale,
|
||||
&elements,
|
||||
&[Rectangle::from_size((size.w, size.h).into())],
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let _ = frame.finish().map_err(|error| error.to_string())?;
|
||||
}
|
||||
let result =
|
||||
capture_stream_texture(renderer, &snapshot.texture, buffer, Transform::Normal, true);
|
||||
cache
|
||||
.0
|
||||
.lock()
|
||||
.expect("stream texture cache mutex poisoned")
|
||||
.put_texture(snapshot.texture);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn capture_stream_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &GlesTexture,
|
||||
buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
|
||||
transform: Transform,
|
||||
straight: bool,
|
||||
) -> Result<smithay::backend::renderer::sync::SyncPoint, String> {
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
let cache = renderer
|
||||
.egl_context()
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(|| Arc::new(StreamCache::default()))
|
||||
.clone();
|
||||
let mut cache = cache.0.lock().expect("stream texture cache mutex poisoned");
|
||||
let program = if straight {
|
||||
&mut cache.straight
|
||||
} else {
|
||||
&mut cache.copy
|
||||
};
|
||||
if program.is_none() {
|
||||
*program =
|
||||
Some(compile_stream_program(renderer, straight).map_err(|error| error.to_string())?);
|
||||
}
|
||||
let program = program.as_ref().expect("stream shader compiled").clone();
|
||||
let source_size = texture.size();
|
||||
let size: Size<i32, Physical> = transform.transform_size((source_size.w, source_size.h).into());
|
||||
let draw = |renderer: &mut GlesRenderer,
|
||||
target: &mut smithay::backend::renderer::gles::GlesTarget<'_>| {
|
||||
draw_stream_texture(renderer, texture, target, transform, &program)
|
||||
};
|
||||
if let Ok(dmabuf) = get_dmabuf(buffer) {
|
||||
let mut dmabuf = dmabuf.clone();
|
||||
let mut target = renderer
|
||||
.bind(&mut dmabuf)
|
||||
.map_err(|error| error.to_string())?;
|
||||
return draw(renderer, &mut target);
|
||||
}
|
||||
let valid = with_buffer_contents_mut(buffer, |_, length, data| {
|
||||
data.width == size.w
|
||||
&& data.height == size.h
|
||||
&& data.format == smithay::reexports::wayland_server::protocol::wl_shm::Format::Xrgb8888
|
||||
&& data.stride >= size.w * 4
|
||||
&& data.offset >= 0
|
||||
&& usize::try_from(
|
||||
i64::from(data.offset) + i64::from(data.stride) * i64::from(data.height),
|
||||
)
|
||||
.is_ok_and(|required| required <= length)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
return Err("Invalid stream SHM buffer".into());
|
||||
}
|
||||
let mut texture = match cache.take_texture((size.w, size.h).into()) {
|
||||
Some(texture) => texture,
|
||||
None => Offscreen::<GlesTexture>::create_buffer(
|
||||
renderer,
|
||||
Fourcc::Abgr8888,
|
||||
(size.w, size.h).into(),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
};
|
||||
{
|
||||
let mut target = renderer
|
||||
.bind(&mut texture)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let _ = draw(renderer, &mut target)?;
|
||||
}
|
||||
let mapping = renderer
|
||||
.copy_texture(
|
||||
&texture,
|
||||
Rectangle::from_size((size.w, size.h).into()),
|
||||
Fourcc::Argb8888,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let source = renderer
|
||||
.map_texture(&mapping)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let row = size.w as usize * 4;
|
||||
let bytes = row * size.h as usize;
|
||||
if source.len() < bytes {
|
||||
return Err("Incomplete stream readback".into());
|
||||
}
|
||||
with_buffer_contents_mut(buffer, |destination, _, data| {
|
||||
for (y, pixels) in source[..bytes].chunks_exact(row).enumerate() {
|
||||
// SAFETY: the buffer bounds and stride were checked above. Source
|
||||
// memory belongs to the renderer and cannot overlap the client SHM.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
pixels.as_ptr(),
|
||||
destination.add(data.offset as usize + y * data.stride as usize),
|
||||
row,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("{error:?}"))?;
|
||||
cache.put_texture(texture);
|
||||
Ok(SyncPoint::signaled())
|
||||
}
|
||||
|
||||
fn compile_stream_program(
|
||||
renderer: &mut GlesRenderer,
|
||||
straight: bool,
|
||||
) -> Result<smithay::backend::renderer::gles::GlesTexProgram, GlesError> {
|
||||
use smithay::backend::renderer::gles::{UniformName, UniformType};
|
||||
let source = if straight {
|
||||
format!("#define STRAIGHT\n{STREAM_CAPTURE_SHADER}")
|
||||
} else {
|
||||
STREAM_CAPTURE_SHADER.to_string()
|
||||
};
|
||||
renderer
|
||||
.compile_custom_texture_shader(source, &[UniformName::new("y_offset", UniformType::_1f)])
|
||||
}
|
||||
|
||||
fn draw_stream_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
texture: &GlesTexture,
|
||||
target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
|
||||
transform: Transform,
|
||||
program: &smithay::backend::renderer::gles::GlesTexProgram,
|
||||
) -> Result<smithay::backend::renderer::sync::SyncPoint, String> {
|
||||
let source_size = texture.size();
|
||||
let size: Size<i32, Physical> = transform.transform_size((source_size.w, source_size.h).into());
|
||||
let mut frame = renderer
|
||||
.render(target, size, Transform::Normal)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let rect = Rectangle::from_size(size);
|
||||
frame
|
||||
.clear(Color32F::new(0., 0., 0., 1.), &[rect])
|
||||
.map_err(|error| error.to_string())?;
|
||||
frame
|
||||
.render_texture_from_to(
|
||||
texture,
|
||||
Rectangle::from_size(source_size).to_f64(),
|
||||
rect,
|
||||
&[rect],
|
||||
&[],
|
||||
transform,
|
||||
1.0,
|
||||
Some(program),
|
||||
&[smithay::backend::renderer::gles::Uniform::new(
|
||||
"y_offset",
|
||||
if texture.is_y_inverted() {
|
||||
1.0f32
|
||||
} else {
|
||||
0.0f32
|
||||
},
|
||||
)],
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
frame.finish().map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_stream_capture_targets(renderer: &mut GlesRenderer) {
|
||||
if let Some(cache) = renderer.egl_context().user_data().get::<Arc<StreamCache>>() {
|
||||
let mut cache = cache.0.lock().expect("stream texture cache mutex poisoned");
|
||||
cache.textures.clear();
|
||||
cache.bytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod stream_gpu_tests {
|
||||
use super::*;
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
use smithay::backend::renderer::ImportMem;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn straight_capture_shader_matches_every_alpha_and_channel_value() {
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.last()
|
||||
.expect("an EGL rendering device");
|
||||
// SAFETY: the display retains the enumerated device for this test.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this thread exclusively owns the fresh context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let mut pixels = Vec::with_capacity(256 * 256 * 4);
|
||||
for alpha in 0..=255u8 {
|
||||
for channel in 0..=255u8 {
|
||||
pixels.extend_from_slice(&[
|
||||
channel,
|
||||
channel.wrapping_add(85),
|
||||
channel.wrapping_add(170),
|
||||
alpha,
|
||||
]);
|
||||
}
|
||||
}
|
||||
let source = renderer
|
||||
.import_memory(&pixels, Fourcc::Abgr8888, (256, 256).into(), false)
|
||||
.unwrap();
|
||||
let program = compile_stream_program(&mut renderer, true).unwrap();
|
||||
let mut destination = Offscreen::<GlesTexture>::create_buffer(
|
||||
&mut renderer,
|
||||
Fourcc::Abgr8888,
|
||||
(256, 256).into(),
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let mut target = renderer.bind(&mut destination).unwrap();
|
||||
let _ = draw_stream_texture(
|
||||
&mut renderer,
|
||||
&source,
|
||||
&mut target,
|
||||
Transform::Normal,
|
||||
&program,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mapping = renderer
|
||||
.copy_texture(
|
||||
&destination,
|
||||
Rectangle::from_size((256, 256).into()),
|
||||
Fourcc::Argb8888,
|
||||
)
|
||||
.unwrap();
|
||||
let actual = renderer.map_texture(&mapping).unwrap();
|
||||
convert_window_pixels(&mut pixels);
|
||||
for (index, (actual, expected)) in actual
|
||||
.chunks_exact(4)
|
||||
.zip(pixels.chunks_exact(4))
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(
|
||||
&actual[..3],
|
||||
&expected[..3],
|
||||
"alpha={} channel={}",
|
||||
index / 256,
|
||||
index % 256
|
||||
);
|
||||
assert_eq!(actual[3], 255, "BGRx capture must be opaque");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn stream_transfer_matches_output_orientation_for_all_transforms() {
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.last()
|
||||
.expect("an EGL rendering device");
|
||||
// SAFETY: the renderer retains the enumerated device's display.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this test exclusively owns the renderer's context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
use Transform::*;
|
||||
let cases = [
|
||||
(Normal, "abcdef"),
|
||||
(_90, "daebfc"),
|
||||
(_180, "fedcba"),
|
||||
(_270, "cfbead"),
|
||||
(Flipped, "cbafed"),
|
||||
(Flipped90, "adbecf"),
|
||||
(Flipped180, "defabc"),
|
||||
(Flipped270, "fcebda"),
|
||||
];
|
||||
let program = compile_stream_program(&mut renderer, false).unwrap();
|
||||
let pixels: Vec<_> = b"abcdef"
|
||||
.iter()
|
||||
.flat_map(|value| [*value, 0, 0, 255])
|
||||
.collect();
|
||||
for flipped in [false, true] {
|
||||
let source = renderer
|
||||
.import_memory(&pixels, Fourcc::Abgr8888, (3, 2).into(), flipped)
|
||||
.unwrap();
|
||||
for (transform, expected) in cases {
|
||||
let size =
|
||||
transform.transform_size(Size::<i32, smithay::utils::Buffer>::from((3, 2)));
|
||||
let mut target =
|
||||
Offscreen::<GlesTexture>::create_buffer(&mut renderer, Fourcc::Abgr8888, size)
|
||||
.unwrap();
|
||||
{
|
||||
let mut target = renderer.bind(&mut target).unwrap();
|
||||
let _ = draw_stream_texture(
|
||||
&mut renderer,
|
||||
&source,
|
||||
&mut target,
|
||||
transform,
|
||||
&program,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mapping = renderer
|
||||
.copy_texture(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
let actual = renderer.map_texture(&mapping).unwrap();
|
||||
let actual: Vec<_> = actual.chunks_exact(4).map(|pixel| pixel[0]).collect();
|
||||
// A flipped texture describes the same image with reversed source
|
||||
// rows. Derive that reference before applying the output transform.
|
||||
let expected = if flipped {
|
||||
expected
|
||||
.bytes()
|
||||
.map(|value| if value < b'd' { value + 3 } else { value - 3 })
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
expected.as_bytes().to_vec()
|
||||
};
|
||||
assert_eq!(actual, expected, "{transform:?}, texture flipped={flipped}");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn alternating_capture_sizes_reuse_their_gpu_allocations() {
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.last()
|
||||
.expect("an EGL rendering device");
|
||||
// SAFETY: the renderer retains the enumerated device's display.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = EGLContext::new(&display).unwrap();
|
||||
// SAFETY: this test exclusively owns the renderer's context.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let mut cache = StreamResources::default();
|
||||
let sizes = [Size::from((64, 48)), Size::from((96, 64))];
|
||||
let mut allocations = [None, None];
|
||||
for _ in 0..100 {
|
||||
for (index, size) in sizes.into_iter().enumerate() {
|
||||
let texture = cache.take_texture(size).unwrap_or_else(|| {
|
||||
Offscreen::<GlesTexture>::create_buffer(&mut renderer, Fourcc::Abgr8888, size)
|
||||
.unwrap()
|
||||
});
|
||||
if let Some(previous) = allocations[index] {
|
||||
assert_eq!(
|
||||
texture.tex_id(),
|
||||
previous,
|
||||
"switching source sizes allocated another GPU target"
|
||||
);
|
||||
} else {
|
||||
allocations[index] = Some(texture.tex_id());
|
||||
}
|
||||
cache.put_texture(texture);
|
||||
}
|
||||
}
|
||||
assert_eq!(cache.textures.len(), 2);
|
||||
assert_eq!(cache.bytes, (64 * 48 + 96 * 64) * 4);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) use capture::{
|
||||
capture_resize_snapshot, capture_window_buffer, capture_window_frame, convert_window_pixels,
|
||||
capture_resize_snapshot, capture_stream_texture, capture_stream_window, capture_window_buffer,
|
||||
capture_window_frame, clear_stream_capture_targets, convert_window_pixels,
|
||||
};
|
||||
use decorations::{
|
||||
border_uniforms, color_with_alpha, corner_program, framebuffer_clip_rect,
|
||||
@@ -37,8 +38,7 @@ use overview::{overview_chrome_element, overview_strip_elements};
|
||||
pub(crate) use resources::RenderResources;
|
||||
use scene::cursor_elements;
|
||||
pub(crate) use scene::{
|
||||
animated_window_elements, capture_output_elements, frame_effect_metrics, layer_surfaces,
|
||||
sampled_output_elements,
|
||||
capture_output_elements, frame_effect_metrics, layer_surfaces, sampled_output_elements,
|
||||
};
|
||||
pub(crate) use shader::SharedPixelShaderElement;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
|
||||
@@ -7,26 +7,17 @@ use smithay::backend::renderer::utils::CommitCounter;
|
||||
|
||||
use super::{
|
||||
AnimatedWindowRenderElement, ConstrainAlign, ConstrainBehavior, ConstrainReference,
|
||||
CropRenderElement, CursorImageStatus, CursorImageSurfaceData, Duration, Ferese,
|
||||
FrameEffectMetrics, GlesRenderer, Layer, LayerSurface, Logical, MaterialSurface,
|
||||
MemoryRenderBufferRenderElement, Output, Physical, Point, PopupManager, Rectangle, Relocate,
|
||||
RelocateRenderElement, RenderElementKind, Renderer, RescaleRenderElement, Resource,
|
||||
RoundedRect, Size, SolidColorRenderElement, Transform, WaylandSurfaceRenderElement,
|
||||
append_material_surface, backdrop, constrain_space_element, corner_program,
|
||||
layer_map_for_output, material_element, overview_strip_elements,
|
||||
render_elements_from_surface_tree, resize_content_behavior, rounded_visual_rect,
|
||||
rounded_window_elements, window_border_element, window_corner_shape, window_shadow_element,
|
||||
window_tint_element, with_states,
|
||||
CropRenderElement, CursorImageStatus, CursorImageSurfaceData, Ferese, FrameEffectMetrics,
|
||||
GlesRenderer, Layer, LayerSurface, Logical, MaterialSurface, MemoryRenderBufferRenderElement,
|
||||
Output, Physical, Point, PopupManager, Rectangle, Relocate, RelocateRenderElement,
|
||||
RenderElementKind, Renderer, RescaleRenderElement, Resource, RoundedRect, Size,
|
||||
SolidColorRenderElement, Transform, WaylandSurfaceRenderElement, append_material_surface,
|
||||
backdrop, constrain_space_element, corner_program, layer_map_for_output, material_element,
|
||||
overview_strip_elements, render_elements_from_surface_tree, resize_content_behavior,
|
||||
rounded_visual_rect, rounded_window_elements, window_border_element, window_corner_shape,
|
||||
window_shadow_element, window_tint_element, with_states,
|
||||
};
|
||||
|
||||
pub(crate) fn animated_window_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
output_elements(state, renderer, output, true)
|
||||
}
|
||||
|
||||
pub(crate) fn frame_effect_metrics(
|
||||
_elements: &[AnimatedWindowRenderElement],
|
||||
_scale: f64,
|
||||
@@ -34,17 +25,6 @@ pub(crate) fn frame_effect_metrics(
|
||||
FrameEffectMetrics
|
||||
}
|
||||
|
||||
pub(crate) fn output_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
include_cursor: bool,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
let frame = state.sample_frame(output, Duration::ZERO);
|
||||
|
||||
sampled_output_elements(state, renderer, output, include_cursor, &frame)
|
||||
}
|
||||
|
||||
pub(crate) fn sampled_output_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
|
||||
@@ -894,7 +894,7 @@ fn ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects() {
|
||||
fixture.state.publish_ipc_events();
|
||||
let snapshot = fixture.state.ipc_event_snapshot();
|
||||
assert!(snapshot.workspaces.as_array().unwrap().is_empty());
|
||||
assert!(snapshot.windows.as_array().unwrap().is_empty());
|
||||
assert!(snapshot.windows.is_empty());
|
||||
assert!(snapshot.focus.is_null());
|
||||
assert_eq!(snapshot.lock.phase, "acquiring");
|
||||
assert!(receive.try_iter().any(
|
||||
|
||||
@@ -458,8 +458,9 @@ pub struct Ferese {
|
||||
pub(crate) render: crate::render::RenderResources,
|
||||
pub(crate) capture_render: crate::render::RenderResources,
|
||||
pub(crate) capture_last_used: Option<Instant>,
|
||||
pub(crate) nested_capture_texture: Option<smithay::backend::renderer::gles::GlesTexture>,
|
||||
pub(crate) nested_capture_texture: [Option<crate::backends::direct::capture::CaptureTarget>; 2],
|
||||
pub(crate) capture_epoch: u64,
|
||||
pub(crate) image_capture: crate::handlers::image_capture::CaptureSessions,
|
||||
capture_protected_windows: HashSet<WindowId>,
|
||||
capture_protected_cursor: Option<WlSurface>,
|
||||
pub(crate) daemons: Option<std::rc::Rc<std::cell::RefCell<crate::daemon::Runner>>>,
|
||||
@@ -733,6 +734,7 @@ impl Ferese {
|
||||
|
||||
let mut activities = crate::activities::Engine::default();
|
||||
activities.configure(&config.activity_settings, config.activity_matcher.clone());
|
||||
let image_capture = crate::handlers::image_capture::CaptureSessions::new(&display_handle);
|
||||
let mut state = Self {
|
||||
session_lock_state,
|
||||
session_lock: crate::session_lock::Lock::default(),
|
||||
@@ -778,8 +780,9 @@ impl Ferese {
|
||||
render: Default::default(),
|
||||
capture_render: Default::default(),
|
||||
capture_last_used: None,
|
||||
nested_capture_texture: None,
|
||||
nested_capture_texture: Default::default(),
|
||||
capture_epoch: 0,
|
||||
image_capture,
|
||||
capture_protected_windows: HashSet::new(),
|
||||
capture_protected_cursor: None,
|
||||
display_presentation: Default::default(),
|
||||
|
||||
@@ -191,6 +191,17 @@ impl Ferese {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark before advancing so the final settling frame invalidates capture
|
||||
// too, even if output_has_animations becomes false during this tick.
|
||||
for output in self.space.outputs() {
|
||||
if crate::backends::direct::capture::changes::tracked(output)
|
||||
&& (self.output_has_animations(output)
|
||||
|| self.output_has_pending_visual_changes(output))
|
||||
{
|
||||
crate::backends::direct::capture::changes::content_changed(output);
|
||||
}
|
||||
}
|
||||
|
||||
let delta = delta.mul_f64(self.animation_speed);
|
||||
if let Some(workspace) = self
|
||||
.focus_swipe()
|
||||
|
||||
@@ -296,7 +296,12 @@ fn pixels(
|
||||
output.clone(),
|
||||
Rectangle::from_size((320, 240).into()),
|
||||
));
|
||||
let mut target = Some(texture.clone());
|
||||
// Retain damage history as well as pixels: changing privacy policy
|
||||
// must erase content already present in the reused capture target.
|
||||
let mut target = std::mem::take(&mut state.nested_capture_texture);
|
||||
target[0].get_or_insert_with(|| {
|
||||
crate::backends::direct::capture::CaptureTarget::new(texture.clone(), output)
|
||||
});
|
||||
crate::backends::direct::capture::capture_output(
|
||||
state,
|
||||
renderer,
|
||||
@@ -304,6 +309,7 @@ fn pixels(
|
||||
output,
|
||||
&scene,
|
||||
);
|
||||
state.nested_capture_texture = target;
|
||||
let mut result = receiver.try_recv().unwrap().result.unwrap().pixels;
|
||||
for pixel in result.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
@@ -749,13 +755,15 @@ fn capture_privacy_pixels_and_policy_transitions() {
|
||||
Rectangle::new((32, 24).into(), (160, 120).into()),
|
||||
));
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
let mut target = std::mem::take(&mut state.nested_capture_texture);
|
||||
crate::backends::direct::capture::capture_output(
|
||||
&mut state,
|
||||
&mut renderer,
|
||||
&mut Some(capture_texture.clone()),
|
||||
&mut target,
|
||||
&output,
|
||||
&scene,
|
||||
);
|
||||
state.nested_capture_texture = target;
|
||||
let region = receiver.try_recv().unwrap().result.unwrap();
|
||||
assert_eq!((region.width, region.height), (160, 120));
|
||||
assert!(
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{DesktopOutput, Ferese, Logical, Output, OutputId, Point};
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn defer_output_redraw(&mut self, output: Output) {
|
||||
crate::backends::direct::capture::changes::content_changed(&output);
|
||||
if !self.output_redraw_pending.contains(&output) {
|
||||
self.output_redraw_pending.push(output);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use smithay::utils::{Physical, Rectangle, Size, Transform};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::metrics::{FrameEffectMetrics, RenderMetrics};
|
||||
use crate::render::{animated_window_elements, frame_effect_metrics, layer_surfaces};
|
||||
use crate::render::{frame_effect_metrics, layer_surfaces, sampled_output_elements};
|
||||
|
||||
pub(crate) type NestedBackend = Rc<RefCell<winit::WinitGraphicsBackend<GlesRenderer>>>;
|
||||
type DamageRenderResult = Result<
|
||||
@@ -144,7 +144,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let rendered = (|| -> DamageRenderResult {
|
||||
{
|
||||
let (renderer, mut framebuffer) = backend.bind()?;
|
||||
let elements = animated_window_elements(state, renderer, &output);
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
let elements = sampled_output_elements(state, renderer, &output, true, &scene);
|
||||
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
|
||||
let result = damage_tracker.render_output(
|
||||
renderer,
|
||||
@@ -164,8 +165,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
if state.has_pending_screencopy(&output, false)
|
||||
|| state.has_pending_screencopy(&output, true)
|
||||
{
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
let mut capture_texture = state.nested_capture_texture.take();
|
||||
let mut capture_texture = std::mem::take(&mut state.nested_capture_texture);
|
||||
crate::backends::direct::capture::capture_output(
|
||||
state,
|
||||
renderer,
|
||||
|
||||
Reference in new issue
Block a user