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:
Samuel Onoja committed 2026-10-24 19:58:54 +01:00
1 parent f49f62583d
commit 533cd74c94
40 files changed
+4704 -704

No files matched your search

+40 -5
View File
@@ -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,
+564 -39
View File
@@ -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"
);
}
}
+7 -3
View File
@@ -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(
+1 -1
View File
@@ -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