Reduce idle CPU use and prioritize compositor rendering (#62)
Avoid background title updates when no shell window list is visible, and skip output and capture invalidation for titles outside compositor decorations. Preserve focused titles, fresh window lists and capture privacy. Request high EGL context priority with fallback, cover rendering and native title behavior in CI, and keep audio recording flowing while video capture is idle.
This commit is contained in:
1 parent
b66b19e4d9
commit
979afbd213
21 files changed
+876
-312
No files matched your search
@@ -6,6 +6,7 @@ use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub(crate) mod capture;
|
||||
mod context;
|
||||
mod crtc_assignment;
|
||||
mod diagnostics;
|
||||
pub(crate) mod feedback;
|
||||
@@ -29,7 +30,7 @@ use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
||||
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
|
||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||
use smithay::backend::egl::EGLDisplay;
|
||||
use smithay::backend::input::InputEvent;
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage};
|
||||
@@ -1004,7 +1005,7 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
||||
let gbm = GbmDevice::new(fd)?;
|
||||
// SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display.
|
||||
let egl_display = unsafe { EGLDisplay::new(gbm.clone())? };
|
||||
let egl_context = EGLContext::new(&egl_display)?;
|
||||
let egl_context = context::try_new(&egl_display)?;
|
||||
// SAFETY: the new context is not current on another thread and remains renderer-owned.
|
||||
let renderer = unsafe { GlesRenderer::new(egl_context)? };
|
||||
state.shm_state.update_formats(renderer.shm_formats());
|
||||
|
||||
@@ -451,7 +451,7 @@ mod tests {
|
||||
);
|
||||
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);
|
||||
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.
|
||||
@@ -492,6 +492,27 @@ mod tests {
|
||||
assert_eq!(target.as_ref().unwrap().preparations, 1);
|
||||
assert_eq!(target.as_ref().unwrap().reuses, 100);
|
||||
assert_eq!(pixels(&mut renderer, &mut target), original);
|
||||
let toplevel = window.toplevel().unwrap();
|
||||
smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.title = Some("Metadata without new pixels".into());
|
||||
});
|
||||
<Ferese as smithay::wayland::shell::xdg::XdgShellHandler>::title_changed(
|
||||
&mut state,
|
||||
toplevel.clone(),
|
||||
);
|
||||
prepare(&mut state, &mut renderer, &mut target);
|
||||
assert_eq!(
|
||||
target.as_ref().unwrap().preparations,
|
||||
1,
|
||||
"a title change must reuse the captured pixels"
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use smithay::backend::egl::context::ContextPriority;
|
||||
use smithay::backend::egl::{EGLContext, EGLDisplay, Error};
|
||||
|
||||
pub(super) fn try_new(display: &EGLDisplay) -> Result<EGLContext, Error> {
|
||||
// Smithay omits the hint without EGL_IMG_context_priority and logs the
|
||||
// queried priority when available. A successful request need not grant High.
|
||||
try_with_high_priority(|priority| match priority {
|
||||
Some(priority) => EGLContext::new_with_priority(display, priority),
|
||||
None => EGLContext::new(display),
|
||||
})
|
||||
}
|
||||
|
||||
fn try_with_high_priority<T, E: Display>(
|
||||
mut create: impl FnMut(Option<ContextPriority>) -> Result<T, E>,
|
||||
) -> Result<T, E> {
|
||||
create(Some(ContextPriority::High)).or_else(|error| {
|
||||
tracing::warn!(
|
||||
%error,
|
||||
"EGL context creation with a high-priority hint failed; retrying without the hint"
|
||||
);
|
||||
create(None)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn requests_high_without_recreating_a_successful_context() {
|
||||
let mut requests = Vec::new();
|
||||
let context = try_with_high_priority(|priority| {
|
||||
requests.push(priority);
|
||||
Ok::<_, &str>("context")
|
||||
});
|
||||
|
||||
assert_eq!(context, Ok("context"));
|
||||
assert_eq!(requests, [Some(ContextPriority::High)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_without_a_hint_when_priority_creation_fails() {
|
||||
let mut requests = Vec::new();
|
||||
let context = try_with_high_priority(|priority| {
|
||||
requests.push(priority);
|
||||
match priority {
|
||||
Some(_) => Err("priority rejected"),
|
||||
None => Ok("fallback context"),
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(context, Ok("fallback context"));
|
||||
assert_eq!(requests, [Some(ContextPriority::High), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propagates_fallback_failure_without_retrying_again() {
|
||||
let mut requests = Vec::new();
|
||||
let context = try_with_high_priority(|priority| {
|
||||
requests.push(priority);
|
||||
match priority {
|
||||
Some(_) => Err::<(), _>("priority rejected"),
|
||||
None => Err("default creation failed"),
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(context, Err("default creation failed"));
|
||||
assert_eq!(requests, [Some(ContextPriority::High), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL device (software Mesa is supported)"]
|
||||
fn creates_a_usable_egl_context() {
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::egl::EGLDevice;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::utils::{Physical, Rectangle, Transform};
|
||||
|
||||
let software_only = std::env::var("LIBGL_ALWAYS_SOFTWARE").as_deref() == Ok("1");
|
||||
let mut tested = 0;
|
||||
for device in EGLDevice::enumerate().unwrap() {
|
||||
let software = device
|
||||
.extensions()
|
||||
.iter()
|
||||
.any(|name| name == "EGL_MESA_device_software");
|
||||
if software_only && !software {
|
||||
continue;
|
||||
}
|
||||
eprintln!(
|
||||
"EGL context test: software={software}, render_node={:?}",
|
||||
device.render_device_path().ok()
|
||||
);
|
||||
// SAFETY: the enumerated EGL device remains owned by the display.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let context = try_new(&display).unwrap();
|
||||
// SAFETY: this fresh context is not current on another thread.
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||
let mut texture: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (8, 8).into())
|
||||
.unwrap();
|
||||
let rect = Rectangle::<i32, Physical>::from_size((8, 8).into());
|
||||
{
|
||||
let mut target = renderer.bind(&mut texture).unwrap();
|
||||
{
|
||||
let mut frame = renderer
|
||||
.render(&mut target, (8, 8).into(), Transform::Normal)
|
||||
.unwrap();
|
||||
frame.clear(Color32F::new(1., 0., 0., 1.), &[rect]).unwrap();
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&target,
|
||||
Rectangle::from_size((8, 8).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.unwrap();
|
||||
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||
assert_eq!(pixels.len(), 8 * 8 * 4);
|
||||
assert!(
|
||||
pixels
|
||||
.chunks_exact(4)
|
||||
.all(|pixel| pixel == [255, 0, 0, 255])
|
||||
);
|
||||
}
|
||||
renderer.egl_context().unbind().unwrap();
|
||||
tested += 1;
|
||||
}
|
||||
assert!(tested > 0, "no matching EGL device was exercised");
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,23 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
self.refresh_idle_inhibition();
|
||||
self.send_shell_snapshots();
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
// Ordinary titles belong to the shell; only compositor-drawn labels
|
||||
// need output damage. Privacy changes already invalidate above.
|
||||
let tab_title = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
.is_some_and(|id| {
|
||||
self.workspaces
|
||||
.workspace_for_window(id)
|
||||
.is_some_and(|workspace| {
|
||||
self.visible_tab_bars(workspace)
|
||||
.any(|bar| bar.group.windows.contains(&id))
|
||||
})
|
||||
});
|
||||
if self.overview.is_presenting() || tab_title {
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,11 @@ fn outside_launcher_touches_are_consumed_until_up_or_cancel() {
|
||||
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<FereseShellV1, (), Ferese>(&state.display_handle, 10, ())
|
||||
.create_resource::<FereseShellV1, crate::shell_control::ShellData, Ferese>(
|
||||
&state.display_handle,
|
||||
10,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
state.shell_resources.push(shell.downgrade());
|
||||
state.process_input_event::<TouchInput>(InputEvent::TouchDown {
|
||||
|
||||
@@ -165,6 +165,11 @@ fn reconnect_reconciles_a_resume_missed_during_the_outage() {
|
||||
*address.lock().unwrap() = "unix:path=/nonexistent/ferese-test-bus".into();
|
||||
bus.stop();
|
||||
attempts.recv_timeout(Duration::from_secs(1)).unwrap();
|
||||
// The old listener has stopped, so no more subscription probes can arrive.
|
||||
// Slow delivery can queue several before confirm_subscription returns.
|
||||
for event in events.try_iter() {
|
||||
assert_eq!(event, Refresh::Resumed);
|
||||
}
|
||||
|
||||
let replacement = Bus::new();
|
||||
let manager = replacement.manager();
|
||||
|
||||
@@ -6,13 +6,26 @@ use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(10, ());
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(11, ());
|
||||
}
|
||||
|
||||
pub(crate) struct ShellData {
|
||||
background_title_updates: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for ShellData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background_title_updates: AtomicBool::new(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||
@@ -70,7 +83,7 @@ impl Dispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
) {
|
||||
match request {
|
||||
ferese_shell_manager_v1::Request::GetShell { id } => {
|
||||
let shell = data_init.init(id, ());
|
||||
let shell = data_init.init(id, ShellData::default());
|
||||
|
||||
shell.capabilities(1);
|
||||
if shell.version() >= 2
|
||||
@@ -88,20 +101,28 @@ impl Dispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
impl Dispatch<FereseShellV1, ShellData> for Ferese {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
shell: &FereseShellV1,
|
||||
request: ferese_shell_v1::Request,
|
||||
_data: &(),
|
||||
data: &ShellData,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if state.session_lock.active() {
|
||||
return;
|
||||
}
|
||||
match request {
|
||||
ferese_shell_v1::Request::SetBackgroundTitleUpdates { enabled } => {
|
||||
if data
|
||||
.background_title_updates
|
||||
.swap(enabled != 0, Ordering::Relaxed)
|
||||
!= (enabled != 0)
|
||||
&& !state.session_lock.active()
|
||||
{
|
||||
state.send_shell_snapshot(shell);
|
||||
}
|
||||
}
|
||||
_ if state.session_lock.active() => {}
|
||||
ferese_shell_v1::Request::Thumbnail {
|
||||
token,
|
||||
window_hi,
|
||||
@@ -222,7 +243,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
state: &mut Self,
|
||||
_client: smithay::reexports::wayland_server::backend::ClientId,
|
||||
shell: &FereseShellV1,
|
||||
_data: &(),
|
||||
_data: &ShellData,
|
||||
) {
|
||||
if state.logout_owner.as_ref() == Some(&shell.id()) {
|
||||
state.cancel_logout_confirmation();
|
||||
@@ -410,10 +431,17 @@ impl Ferese {
|
||||
self.last_shell_snapshot = None;
|
||||
return;
|
||||
}
|
||||
let background_title_updates = self.shell_resources.iter().any(|resource| {
|
||||
resource.upgrade().is_ok_and(|shell| {
|
||||
shell
|
||||
.data::<ShellData>()
|
||||
.is_some_and(|data| data.background_title_updates.load(Ordering::Relaxed))
|
||||
})
|
||||
});
|
||||
if self
|
||||
.last_shell_snapshot
|
||||
.as_ref()
|
||||
.is_some_and(|snapshot| self.shell_snapshot_matches(snapshot))
|
||||
.is_some_and(|snapshot| self.shell_snapshot_matches(snapshot, background_title_updates))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -428,7 +456,11 @@ impl Ferese {
|
||||
self.last_shell_snapshot = Some(snapshot);
|
||||
}
|
||||
|
||||
fn shell_snapshot_matches(&self, snapshot: &ShellSnapshot) -> bool {
|
||||
fn shell_snapshot_matches(
|
||||
&self,
|
||||
snapshot: &ShellSnapshot,
|
||||
background_title_updates: bool,
|
||||
) -> bool {
|
||||
if snapshot.workspace_preview_revision != self.workspace_preview_revision
|
||||
|| snapshot.binding_mode.as_deref() != self.binding_modes.active()
|
||||
{
|
||||
@@ -495,7 +527,9 @@ impl Ferese {
|
||||
if previous.id != id
|
||||
|| previous.workspace != workspace
|
||||
|| previous.app_id != app_id
|
||||
|| previous.title != title
|
||||
|| ((background_title_updates
|
||||
|| state.contains(ferese_shell_v1::WindowState::Focused))
|
||||
&& previous.title != title)
|
||||
|| previous.state != state
|
||||
|| previous.source_geometry != self.shell_window_geometry(id)
|
||||
{
|
||||
@@ -907,7 +941,7 @@ mod tests {
|
||||
for _ in 0..100 {
|
||||
// Building a snapshot for metadata changes must reuse layout geometry.
|
||||
assert_eq!(state.shell_snapshot(), original);
|
||||
assert!(state.shell_snapshot_matches(&original));
|
||||
assert!(state.shell_snapshot_matches(&original, true));
|
||||
}
|
||||
|
||||
assert_eq!(state.preview_cache_stats(), initial_cache);
|
||||
@@ -915,25 +949,25 @@ mod tests {
|
||||
.workspaces
|
||||
.insert_window(WindowId(99), ferese_layout::Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
assert!(!state.shell_snapshot_matches(&original));
|
||||
assert!(!state.shell_snapshot_matches(&original, true));
|
||||
let occupied = state.shell_snapshot();
|
||||
assert_eq!(state.preview_cache_stats().0, initial_cache.0 + 1);
|
||||
assert!(state.shell_snapshot_matches(&occupied));
|
||||
assert!(state.shell_snapshot_matches(&occupied, true));
|
||||
let output_id = state.output_ids[&output];
|
||||
let next = state.workspaces.create_workspace();
|
||||
state
|
||||
.output_workspaces
|
||||
.assign_workspace(output_id, next)
|
||||
.unwrap();
|
||||
assert!(!state.shell_snapshot_matches(&occupied));
|
||||
assert!(!state.shell_snapshot_matches(&occupied, true));
|
||||
let assigned = state.shell_snapshot();
|
||||
state
|
||||
.output_workspaces
|
||||
.switch_workspace(output_id, next)
|
||||
.unwrap();
|
||||
assert!(!state.shell_snapshot_matches(&assigned));
|
||||
assert!(!state.shell_snapshot_matches(&assigned, true));
|
||||
let selected = state.shell_snapshot();
|
||||
assert!(state.shell_snapshot_matches(&selected));
|
||||
assert!(state.shell_snapshot_matches(&selected, true));
|
||||
assert_eq!(
|
||||
state.output_workspaces.active_workspace(output_id),
|
||||
Some(next)
|
||||
@@ -983,7 +1017,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!state.shell_snapshot_matches(&selected),
|
||||
!state.shell_snapshot_matches(&selected, true),
|
||||
"hidden previews must invalidate even with no visible window geometry to change"
|
||||
);
|
||||
assert_ne!(
|
||||
@@ -991,7 +1025,7 @@ mod tests {
|
||||
hidden_before
|
||||
);
|
||||
let refreshed = state.shell_snapshot();
|
||||
assert!(state.shell_snapshot_matches(&refreshed));
|
||||
assert!(state.shell_snapshot_matches(&refreshed, true));
|
||||
state
|
||||
.reload_config_source(
|
||||
"layout { inner-gap 30; outer-gap 40; smart-gaps #false; }; input { repeat-rate 35; }".into(),
|
||||
@@ -1002,7 +1036,7 @@ mod tests {
|
||||
"unrelated settings do not invalidate layouts"
|
||||
);
|
||||
state.unregister_output(&output);
|
||||
assert!(!state.shell_snapshot_matches(&refreshed));
|
||||
assert!(!state.shell_snapshot_matches(&refreshed, true));
|
||||
state.workspace_preview_rects();
|
||||
assert_eq!(
|
||||
state.preview_cache_stats().1,
|
||||
|
||||
@@ -384,6 +384,103 @@ mod tests {
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_title_interest_suppresses_background_churn_and_refreshes_on_subscription() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"state::reconciliation::tests::shell_title_interest_suppresses_background_churn_and_refreshes_on_subscription",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
use crate::shell_control::ShellData;
|
||||
use crate::state::capture_privacy::tests::window;
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
||||
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut event_loop);
|
||||
state.begin_desktop_transition();
|
||||
let changes = state
|
||||
.publish_desktop(vec![output("titles", 0, 1200)])
|
||||
.unwrap();
|
||||
state.finish_desktop_transition(changes);
|
||||
let (background, _background_wire) = window(&mut state, &mut event_loop, 0xff112233);
|
||||
let (focused, _focused_wire) = window(&mut state, &mut event_loop, 0xff334455);
|
||||
let background = state.windows.ids()[&background];
|
||||
let focused = state.windows.ids()[&focused];
|
||||
assert!(state.activate_managed_window(focused));
|
||||
let (server, mut wire) = UnixStream::pair().unwrap();
|
||||
let client = state
|
||||
.display_handle
|
||||
.insert_client(server, Arc::new(ClientState::default()))
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<FereseShellV1, ShellData, Ferese>(
|
||||
&state.display_handle,
|
||||
11,
|
||||
ShellData::default(),
|
||||
)
|
||||
.unwrap();
|
||||
state.shell_resources.push(shell.downgrade());
|
||||
state.send_shell_snapshots();
|
||||
events(&mut state, &mut wire);
|
||||
let initial = state.shell_snapshot_serial;
|
||||
state.windows.record_mut(background).unwrap().title = "Default subscription".into();
|
||||
state.send_shell_snapshots();
|
||||
assert!(
|
||||
state.shell_snapshot_serial > initial,
|
||||
"new controllers initially receive all titles"
|
||||
);
|
||||
events(&mut state, &mut wire);
|
||||
|
||||
let opcode = FereseShellV1::interface()
|
||||
.requests
|
||||
.iter()
|
||||
.position(|request| request.name == "set_background_title_updates")
|
||||
.unwrap() as u32;
|
||||
request(&mut wire, shell.id().protocol_id(), opcode, &[0]);
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(10), &mut state)
|
||||
.unwrap();
|
||||
events(&mut state, &mut wire);
|
||||
let idle = state.shell_snapshot_serial;
|
||||
for number in 0..100 {
|
||||
state.windows.record_mut(background).unwrap().title = format!("spinner {number}");
|
||||
state.send_shell_snapshots();
|
||||
}
|
||||
assert_eq!(state.shell_snapshot_serial, idle);
|
||||
assert!(events(&mut state, &mut wire).is_empty());
|
||||
|
||||
request(&mut wire, shell.id().protocol_id(), opcode, &[1]);
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(10), &mut state)
|
||||
.unwrap();
|
||||
assert!(state.shell_snapshot_serial > idle);
|
||||
let snapshot = events(&mut state, &mut wire);
|
||||
let bytes: Vec<_> = snapshot
|
||||
.iter()
|
||||
.flat_map(|(_, _, words)| words.iter().flat_map(|word| word.to_ne_bytes()))
|
||||
.collect();
|
||||
assert!(
|
||||
bytes
|
||||
.windows(b"spinner 99".len())
|
||||
.any(|bytes| bytes == b"spinner 99"),
|
||||
"opening a list must receive the latest suppressed title"
|
||||
);
|
||||
|
||||
request(&mut wire, shell.id().protocol_id(), opcode, &[0]);
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(10), &mut state)
|
||||
.unwrap();
|
||||
events(&mut state, &mut wire);
|
||||
let before = state.shell_snapshot_serial;
|
||||
state.windows.record_mut(focused).unwrap().title = "Focused title".into();
|
||||
state.send_shell_snapshots();
|
||||
assert!(state.shell_snapshot_serial > before);
|
||||
let before = state.shell_snapshot_serial;
|
||||
assert!(state.activate_managed_window(background));
|
||||
state.send_shell_snapshots();
|
||||
assert!(state.shell_snapshot_serial > before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planned_geometry_matches_smithay_at_fractional_scale_and_rotation() {
|
||||
for transform in [Transform::Normal, Transform::_90, Transform::Flipped270] {
|
||||
@@ -453,7 +550,11 @@ mod tests {
|
||||
.insert_client(server, Arc::new(ClientState::default()))
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<FereseShellV1, (), Ferese>(&state.display_handle, 6, ())
|
||||
.create_resource::<FereseShellV1, crate::shell_control::ShellData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
state.shell_resources.push(shell.downgrade());
|
||||
state.send_shell_snapshots();
|
||||
|
||||
@@ -36,6 +36,105 @@ pub(super) fn fixture() -> (
|
||||
(events, state, output)
|
||||
}
|
||||
|
||||
fn set_title(
|
||||
events: &mut smithay::reexports::calloop::EventLoop<'static, Ferese>,
|
||||
state: &mut Ferese,
|
||||
wire: &mut std::os::unix::net::UnixStream,
|
||||
title: &str,
|
||||
) {
|
||||
let mut text = title.as_bytes().to_vec();
|
||||
text.push(0);
|
||||
let length = text.len() as u32;
|
||||
text.resize(text.len().next_multiple_of(4), 0);
|
||||
let mut args = vec![length];
|
||||
args.extend(
|
||||
text.chunks_exact(4)
|
||||
.map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap())),
|
||||
);
|
||||
// The window fixture creates its xdg_toplevel as object 4; set_title is opcode 2.
|
||||
request(wire, 4, 2, &args, None);
|
||||
dispatch(events, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_only_changes_do_not_schedule_plain_window_redraws() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"state::window_lifecycle_tests::title_only_changes_do_not_schedule_plain_window_redraws",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, _output) = fixture();
|
||||
let (app, mut wire) = window(&mut state, &mut events, 0xff112233);
|
||||
let id = state.windows.ids()[&app];
|
||||
state.output_redraw_pending.clear();
|
||||
for number in 0..20 {
|
||||
let title = format!("spinner {number}");
|
||||
set_title(&mut events, &mut state, &mut wire, &title);
|
||||
assert_eq!(state.windows.record(id).unwrap().title, title);
|
||||
assert!(
|
||||
state.output_redraw_pending.is_empty(),
|
||||
"metadata alone must not invalidate the output or its capture scene"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_changes_redraw_tab_headers_and_overview_labels() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"state::window_lifecycle_tests::title_changes_redraw_tab_headers_and_overview_labels",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, output) = fixture();
|
||||
state.toggle_layout_mode();
|
||||
let (first, mut first_wire) = window(&mut state, &mut events, 0xff112233);
|
||||
let (_second, _second_wire) = window(&mut state, &mut events, 0xff445566);
|
||||
state.consume_focused_window();
|
||||
let workspace = state.workspaces.active_id();
|
||||
assert_eq!(state.visible_tab_bars(workspace).count(), 1);
|
||||
assert!(state.space.element_location(&first).is_none());
|
||||
state.output_redraw_pending.clear();
|
||||
set_title(
|
||||
&mut events,
|
||||
&mut state,
|
||||
&mut first_wire,
|
||||
"Inactive tab title",
|
||||
);
|
||||
assert!(state.output_redraw_pending.contains(&output));
|
||||
|
||||
state.leave_tab();
|
||||
state.set_overview_active(true);
|
||||
assert!(state.overview.is_presenting());
|
||||
state.output_redraw_pending.clear();
|
||||
set_title(&mut events, &mut state, &mut first_wire, "Overview label");
|
||||
assert!(state.output_redraw_pending.contains(&output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_privacy_transitions_still_invalidate_capture_and_output() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"state::window_lifecycle_tests::title_privacy_transitions_still_invalidate_capture_and_output",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, output) = fixture();
|
||||
let (app, mut wire) = window(&mut state, &mut events, 0xff112233);
|
||||
let id = state.windows.ids()[&app];
|
||||
state
|
||||
.reload_config_source(
|
||||
"window-rule title=\"Private\" block-out-from-screencasts=#true".into(),
|
||||
)
|
||||
.unwrap();
|
||||
for (title, protected) in [("Private", true), ("Public", false)] {
|
||||
let epoch = state.capture_epoch;
|
||||
state.output_redraw_pending.clear();
|
||||
set_title(&mut events, &mut state, &mut wire, title);
|
||||
assert_eq!(state.capture_protected_windows.contains(&id), protected);
|
||||
assert!(state.capture_epoch > epoch);
|
||||
assert!(state.output_redraw_pending.contains(&output));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
|
||||
Reference in new issue
Block a user