Integrate native Rime and stabilize input, output, and rendering

- Add optional modifier-only Super binding whose tap is cancelled by
  other input, focus changes, grabs, locks, and session teardown.
- Ship the native ferese-rime user service, Rime defaults, installer
  inventory, package dependencies, session environment, and docs.
- Vendor Smithay 8a27023 with the SHM texture-cache format fix and add
  GPU regressions for format changes and resize snapshots.
- Release reused CRTC owners before staging output modes, then restore
  changed outputs before former owners when a transaction fails.
- Wait for resize-capture GPU completion, expand filtered surface
  damage, and preserve IME popup origin under fractional scaling.
- Mark weekends like holidays in both calendar layouts.
This commit is contained in:
aria committed 2026-10-30 03:13:43 +09:00
1 parent a2be421974
commit 993bc0f88f
391 files changed
+130908 -76

No files matched your search

+3 -1
View File
@@ -31,7 +31,7 @@ use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneE
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
use smithay::backend::egl::EGLDisplay;
use smithay::backend::input::InputEvent;
use smithay::backend::input::{Device as _, InputEvent};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
@@ -425,6 +425,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
}
InputEvent::DeviceRemoved { device } => {
state.super_tap.device_removed(&device.id());
state.cancel_window_interaction();
if let Some(backend) = state.direct_backend.as_mut() {
backend
@@ -439,6 +440,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
.handle()
.insert_source(notifier, move |event, _, state| match event {
SessionEvent::PauseSession => {
state.super_tap.reset();
state.cancel_window_interaction();
state.set_binding_mode("default");
let handle = state.loop_handle.clone();
+14 -3
View File
@@ -450,8 +450,8 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
}
}
/// All allocations/tests precede commits. Logical publication waits for the
/// final usable inventory; devices are independently applied and restored.
/// A complete device test precedes application. Release former connector owners
/// before per-surface tests; publish only after the final inventory is usable.
pub(super) fn apply_device(
state: &mut Ferese,
node: DrmNode,
@@ -483,6 +483,17 @@ pub(super) fn apply_device(
.filter(|crtc| !selections.contains_key(crtc))
.copied()
.collect::<Vec<_>>();
// A connector replacement can reuse the same CRTC, so it is not a removal
// from the keyed inventory. Its old routing/resources must still be released
// before staging any other output's new mode (for example HDMI -> DP).
let mut releases = removals.clone();
releases.extend(selections.iter().filter_map(|(crtc, selection)| {
device
.outputs
.get(crtc)
.and_then(|output| (output.connector != selection.connector.handle()).then_some(*crtc))
}));
releases.sort_by_key(|crtc| u32::from(*crtc));
let hardware_changes = changed
.iter()
.filter(|crtc| {
@@ -500,7 +511,7 @@ pub(super) fn apply_device(
staged: HashMap::new(),
replaced: HashSet::new(),
};
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &removals);
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &releases);
let mut staged = std::mem::take(&mut driver.staged);
if let Err(failure) = result {
for crtc in failure.unavailable {
+26 -2
View File
@@ -123,6 +123,7 @@ pub(super) fn parse_binding(
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
BindingMatch::Pointer => BindingTrigger::Pointer(PointerTrigger::from_key(identity.key)),
BindingMatch::SuperTap => BindingTrigger::SuperTap,
BindingMatch::Swipe => BindingTrigger::Swipe {
fingers: identity.key / 4,
direction: [
@@ -154,6 +155,21 @@ pub(super) fn binding_identity(
configured: &BindingConfig,
keymap: &xkb::Keymap,
) -> Result<BindingIdentity, ConfigError> {
if matches!(
configured.keys.trim().to_ascii_lowercase().as_str(),
"super" | "logo" | "mod4"
) {
if configured.match_mode != BindingMatch::Keysym {
return Err(ConfigError::Binding(
"Super tap cannot use physical key matching".into(),
));
}
return Ok(BindingIdentity {
modifiers: BindingModifiers::default(),
match_mode: BindingMatch::SuperTap,
key: 0,
});
}
let (modifiers, key) = parse_chord(&configured.keys)?;
if let Some(trigger) = PointerTrigger::parse(&key)? {
if configured.match_mode != BindingMatch::Keysym {
@@ -192,7 +208,7 @@ pub(super) fn binding_identity(
configured.keys
))
})?,
BindingMatch::Swipe | BindingMatch::Pointer => {
BindingMatch::Swipe | BindingMatch::Pointer | BindingMatch::SuperTap => {
return Err(ConfigError::Binding(
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
));
@@ -211,6 +227,7 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
BindingTrigger::Pointer(trigger) => (BindingMatch::Pointer, trigger.key()),
BindingTrigger::SuperTap => (BindingMatch::SuperTap, 0),
BindingTrigger::Swipe { fingers, direction } => {
(BindingMatch::Swipe, fingers * 4 + direction as u32)
}
@@ -574,6 +591,7 @@ pub(super) enum BindingTrigger {
Keysym(u32),
Physical(Keycode),
Pointer(PointerTrigger),
SuperTap,
Swipe {
fingers: u32,
direction: crate::gestures::SwipeDirection,
@@ -679,6 +697,7 @@ impl Binding {
})
}
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
(BindingTrigger::SuperTap, BindingTrigger::SuperTap) => true,
_ => false,
}
}
@@ -702,7 +721,9 @@ impl Binding {
&& match self.trigger {
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
BindingTrigger::Physical(expected) => expected == keycode,
BindingTrigger::Swipe { .. } | BindingTrigger::Pointer(_) => false,
BindingTrigger::Swipe { .. }
| BindingTrigger::Pointer(_)
| BindingTrigger::SuperTap => false,
}
}
@@ -797,6 +818,7 @@ impl Binding {
}
}
parts.push(match self.trigger {
BindingTrigger::SuperTap => "Super (tap)".into(),
BindingTrigger::Pointer(trigger) => trigger.label(),
BindingTrigger::Keysym(symbol) => {
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
@@ -852,6 +874,8 @@ pub(super) enum BindingMatch {
Swipe,
#[serde(skip)]
Pointer,
#[serde(skip)]
SuperTap,
}
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
+14 -1
View File
@@ -27,7 +27,10 @@ impl From<Vec<Binding>> for BindingSet {
if matches!(
identity.match_mode,
BindingMatch::Keysym | BindingMatch::Physical | BindingMatch::Pointer
BindingMatch::Keysym
| BindingMatch::Physical
| BindingMatch::Pointer
| BindingMatch::SuperTap
) {
index.entry(identity).or_insert(position);
}
@@ -46,6 +49,16 @@ impl std::ops::Deref for BindingSet {
}
impl BindingSet {
pub(crate) fn super_tap(&self) -> Option<&Binding> {
self.index
.get(&BindingIdentity {
modifiers: BindingModifiers::default(),
match_mode: BindingMatch::SuperTap,
key: 0,
})
.map(|index| &self.ordered[*index])
}
pub(crate) fn binding_for_pointer(
&self,
trigger: super::PointerTrigger,
+40 -4
View File
@@ -1,14 +1,17 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::reexports::wayland_server::Client;
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Client, Resource};
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role,
is_sync_subsurface,
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
add_blocker, add_pre_commit_hook, get_parent, get_role, is_sync_subsurface, with_states,
};
use smithay::wayland::dmabuf::{
DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,
};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::shm::{ShmHandler, ShmState};
use super::{layer_shell, xdg_shell};
@@ -29,6 +32,39 @@ impl CompositorHandler for Ferese {
fn new_surface(&mut self, surface: &WlSurface) {
self.update_surface_preferences(surface);
// A commit is not proof that the GPU has finished writing its dma-buf.
// Keep the previous surface state renderable until the acquire fence
// signals, including during resize snapshots and subsurface updates.
add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {
let dmabuf = with_states(surface, |states| {
states
.cached_state
.get::<SurfaceAttributes>()
.pending()
.buffer
.as_ref()
.and_then(|assignment| match assignment {
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok().cloned(),
_ => None,
})
});
let Some((dmabuf, client)) = dmabuf.zip(surface.client()) else {
return;
};
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
let inserted = state.loop_handle.insert_source(source, move |_, _, state| {
let dh = state.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &dh);
Ok(())
});
match inserted {
Ok(_) => add_blocker(surface, blocker),
Err(error) => tracing::warn!(%error, "could not monitor surface GPU readiness"),
}
}
});
}
fn new_subsurface(&mut self, surface: &WlSurface, _parent: &WlSurface) {
+2
View File
@@ -81,6 +81,7 @@ impl SeatHandler for Ferese {
}
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
self.super_tap.cancel();
self.pending_layer_focus = None;
// Smithay has already sent enter. last_enter uses a separate mutex;
// querying current_focus here would reenter the held keyboard lock.
@@ -157,6 +158,7 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
.and_then(|keyboard| keyboard.current_focus());
if focused.as_ref() == Some(inhibitor.wl_surface()) {
self.super_tap.cancel();
inhibitor.activate();
self.active_shortcuts_inhibitor = Some(inhibitor);
}
+5 -1
View File
@@ -19,6 +19,7 @@ impl VirtualKeyboardHandler for Ferese {
_ => None,
};
if let Some(device) = device {
self.super_tap.cancel();
keyboard.release_source(self, device.source());
}
return;
@@ -78,7 +79,10 @@ impl VirtualKeyboardHandler for Ferese {
context.set_modifier_mask(mods_depressed, mods_latched, mods_locked, group)
});
}
InputEvent::DeviceRemoved { device } => keyboard.release_source(self, device.source()),
InputEvent::DeviceRemoved { device } => {
self.super_tap.cancel();
keyboard.release_source(self, device.source());
}
_ => (),
}
}
+32
View File
@@ -1,6 +1,7 @@
mod window;
pub(crate) use window::WindowInteraction;
mod pointer;
pub(crate) mod super_tap;
use ferese_layout::Direction;
pub(crate) use pointer::ScrollBindings;
use smithay::backend::input::{
@@ -150,6 +151,16 @@ impl Ferese {
event: InputEvent<I>,
source: KeyboardSource,
) {
match &event {
InputEvent::Keyboard { event } => self.super_tap.observe_key(source, event.key_code()),
InputEvent::PointerButton { event } => self.super_tap.pointer_button(
event.device().id(),
event.button_code(),
event.state(),
),
InputEvent::DeviceRemoved { device } => self.super_tap.device_removed(&device.id()),
_ => self.super_tap.cancel(),
}
if self.input_capture.active() {
self.set_binding_mode("default");
}
@@ -320,6 +331,10 @@ impl Ferese {
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
let keycode = event.key_code();
let state = event.state();
let keys_were_clear = keyboard.pressed_keys().is_empty();
let tap_focus = keyboard.current_focus().map(|surface| surface.id());
let tap_allowed = self.super_tap_allowed(&keyboard);
let mut tap_action = None;
let was_captured = self.input_capture.active();
if self.input_capture.captures(1) {
@@ -349,6 +364,18 @@ impl Ferese {
serial,
event.time(),
|data, modifiers, keysym| {
if state == KeyState::Released {
if data.super_tap.release(source, keycode, tap_allowed, tap_focus.clone()) {
tap_action = data.binding_modes.bindings(&data.bindings)
.super_tap().map(|binding| binding.action.clone());
}
} else if keys_were_clear && tap_allowed
&& !modifiers.ctrl && !modifiers.alt && !modifiers.shift
&& keysym.raw_syms().iter().any(|symbol| matches!(symbol.raw(), keysyms::KEY_Super_L | keysyms::KEY_Super_R))
&& data.binding_modes.bindings(&data.bindings).super_tap().is_some()
{
data.super_tap.arm(source, keycode, tap_focus.clone());
}
if data.focus_cycle.is_some() {
if data.session_lock.active() || data.input_capture.captures(1) {
data.cancel_focus_cycle();
@@ -530,6 +557,11 @@ impl Ferese {
FilterResult::Intercept(())
},
);
// Finish forwarding the original release to its owner before
// changing focus or opening a layer surface for the action.
if let Some(action) = tap_action {
self.execute_binding(action);
}
if state == KeyState::Pressed && intercepted.is_none() && client_grab {
self.record_activation_input(serial, interaction_focus);
}
+88
View File
@@ -0,0 +1,88 @@
//! A modifier tap is armed only by an otherwise isolated Super press. It never
//! owns the keyboard event: normal clients, IMEs and Super chords keep theirs.
use std::collections::HashSet;
use smithay::backend::input::ButtonState;
use smithay::input::keyboard::{KeyboardHandle, KeyboardSource, Keycode};
use smithay::reexports::wayland_server::backend::ObjectId;
use smithay::wayland::input_method::InputMethodKeyboardGrab;
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
#[derive(Default)]
pub(crate) struct SuperTap {
candidate: Option<(KeyboardSource, Keycode, Option<ObjectId>)>,
pointer_buttons: HashSet<(String, u32)>,
}
impl SuperTap {
pub(crate) fn cancel(&mut self) {
self.candidate = None;
}
pub(crate) fn reset(&mut self) {
self.cancel();
self.pointer_buttons.clear();
}
pub(crate) fn device_removed(&mut self, device: &str) {
self.cancel();
self.pointer_buttons.retain(|(owner, _)| owner != device);
}
pub(super) fn pointer_button(&mut self, device: String, button: u32, state: ButtonState) {
self.cancel();
if state == ButtonState::Pressed {
self.pointer_buttons.insert((device, button));
} else {
self.pointer_buttons.remove(&(device, button));
}
}
// Observe before Smithay deduplicates a second input source's identical
// keycode. That second source must invalidate an isolated tap as well.
pub(super) fn observe_key(&mut self, source: KeyboardSource, key: Keycode) {
if self
.candidate
.as_ref()
.is_some_and(|(owner, held, _)| *owner != source || *held != key)
{
self.cancel();
}
}
pub(super) fn arm(&mut self, source: KeyboardSource, key: Keycode, focus: Option<ObjectId>) {
self.candidate = Some((source, key, focus));
}
pub(super) fn release(
&mut self,
source: KeyboardSource,
key: Keycode,
allowed: bool,
focus: Option<ObjectId>,
) -> bool {
self.candidate.take().is_some_and(|(owner, held, initial)| {
allowed && owner == source && held == key && focus == initial
})
}
}
impl crate::Ferese {
pub(super) fn super_tap_allowed(&self, keyboard: &KeyboardHandle<Self>) -> bool {
!self.session_lock.active()
&& !self.input_capture.active()
&& !self.seat.keyboard_shortcuts_inhibited()
&& self.active_shortcuts_inhibitor.is_none()
&& self.pointer_operation.is_none()
&& self.super_tap.pointer_buttons.is_empty()
&& self.seat.get_pointer().is_none_or(|pointer| !pointer.is_grabbed())
// A native Rime grab is part of normal text input. Popup/client
// grabs still retain their exclusive keyboard interaction.
&& keyboard.with_grab(|_, grab| grab.is::<InputMethodKeyboardGrab>()).unwrap_or(true)
}
}
#[cfg(test)]
mod security_tests;
#[cfg(test)]
mod tests;
@@ -0,0 +1,181 @@
use crate::Ferese;
use crate::input::binding_mode_tests::key;
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::{
ext_session_lock_manager_v1::ExtSessionLockManagerV1, ext_session_lock_v1::ExtSessionLockV1,
};
use smithay::reexports::wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::server::zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1;
use smithay::reexports::wayland_server::{Client, Resource};
use smithay::utils::SERIAL_COUNTER;
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
use std::io::Read;
use std::os::unix::net::UnixStream;
const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\n";
fn fixture() -> (Ferese, EventLoop<'static, Ferese>) {
let (mut state, events, _) = crate::input::layer_focus_tests::fixture();
state.reload_config_source(SOURCE.into()).unwrap();
(state, events)
}
fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) {
request(wire, 1, 1, &[7], None);
dispatch(events, state);
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut seat_global = None;
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 7 && header as u16 == 0 {
let length =
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
seat_global = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
));
}
}
offset += (header >> 16) as usize;
}
let mut args = vec![seat_global.unwrap(), 8];
args.extend(
b"wl_seat\0"
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
);
args.extend([7, 8]);
request(wire, 7, 0, &args, None);
dispatch(events, state);
}
fn create_manager<R: Resource + 'static>(state: &mut Ferese, client: &Client) -> R
where
Ferese: smithay::reexports::wayland_server::Dispatch<R, smithay::wayland::GlobalData>,
{
client
.create_resource::<R, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap()
}
#[test]
fn inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release() {
if !crate::startup_tests::private_runtime(
"input::super_tap::security_tests::inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release",
) {
return;
}
let (mut state, mut events) = fixture();
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
let surface = window.toplevel().unwrap().wl_surface().clone();
let client = surface.client().unwrap();
let keyboard = state.seat.get_keyboard().unwrap();
keyboard.set_focus(
&mut state,
Some(surface.clone()),
SERIAL_COUNTER.next_serial(),
);
bind_seat(&mut state, &mut events, &mut wire);
let manager = create_manager::<ZwpKeyboardShortcutsInhibitManagerV1>(&mut state, &client);
key(&mut state, 133, true);
assert!(state.super_tap.candidate.is_some());
let opcode = ZwpKeyboardShortcutsInhibitManagerV1::interface()
.requests
.iter()
.position(|request| request.name == "inhibit_shortcuts")
.unwrap() as u32;
request(
&mut wire,
manager.id().protocol_id(),
opcode,
&[9, surface.id().protocol_id(), 8],
None,
);
dispatch(&mut events, &mut state);
assert!(state.seat.keyboard_shortcuts_inhibited());
assert!(
state.super_tap.candidate.is_none(),
"activation must cancel immediately"
);
request(&mut wire, 9, 0, &[], None);
dispatch(&mut events, &mut state);
assert!(!state.seat.keyboard_shortcuts_inhibited());
key(&mut state, 133, false);
assert_eq!(state.binding_modes.active(), None);
key(&mut state, 133, true);
key(&mut state, 133, false);
assert_eq!(state.binding_modes.active(), Some("tapped"));
}
#[test]
fn session_lock_protocol_cancels_pending_tap_before_further_keyboard_input() {
if !crate::startup_tests::private_runtime(
"input::super_tap::security_tests::session_lock_protocol_cancels_pending_tap_before_further_keyboard_input",
) {
return;
}
let (mut state, mut events) = fixture();
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
let surface = window.toplevel().unwrap().wl_surface().clone();
let client = surface.client().unwrap();
state.seat.get_keyboard().unwrap().set_focus(
&mut state,
Some(surface),
SERIAL_COUNTER.next_serial(),
);
let manager = create_manager::<ExtSessionLockManagerV1>(&mut state, &client);
key(&mut state, 133, true);
assert!(state.super_tap.candidate.is_some());
let opcode = ExtSessionLockManagerV1::interface()
.requests
.iter()
.position(|request| request.name == "lock")
.unwrap() as u32;
request(&mut wire, manager.id().protocol_id(), opcode, &[7], None);
dispatch(&mut events, &mut state);
assert!(state.session_lock.active());
assert!(
client
.object_from_protocol_id::<ExtSessionLockV1>(&state.display_handle, 7)
.is_ok()
);
assert!(state.super_tap.candidate.is_none());
key(&mut state, 133, false);
key(&mut state, 133, true);
key(&mut state, 133, false);
assert_eq!(state.binding_modes.active(), None);
}
#[test]
fn focus_departure_and_return_do_not_restore_a_held_tap() {
if !crate::startup_tests::private_runtime(
"input::super_tap::security_tests::focus_departure_and_return_do_not_restore_a_held_tap",
) {
return;
}
let (mut state, mut events) = fixture();
let (window, _wire) = test_window(&mut state, &mut events, 0xff102030);
let surface = window.toplevel().unwrap().wl_surface().clone();
let keyboard = state.seat.get_keyboard().unwrap();
keyboard.set_focus(
&mut state,
Some(surface.clone()),
SERIAL_COUNTER.next_serial(),
);
key(&mut state, 133, true);
assert!(state.super_tap.candidate.is_some());
keyboard.set_focus(&mut state, None, SERIAL_COUNTER.next_serial());
keyboard.set_focus(&mut state, Some(surface), SERIAL_COUNTER.next_serial());
assert!(state.super_tap.candidate.is_none());
key(&mut state, 133, false);
assert_eq!(state.binding_modes.active(), None);
}
+508
View File
@@ -0,0 +1,508 @@
use super::*;
use crate::Ferese;
use crate::config::{BindingAction, Config};
use crate::input::binding_mode_tests::key;
use crate::input::{Event, InputBackend, InputEvent, PointerButtonEvent, PointerMotionEvent};
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
use smithay::backend::input::{
Axis, AxisRelativeDirection, AxisSource, InputTime, KeyState, PointerAxisEvent, UnusedEvent,
};
use smithay::backend::winit::WinitVirtualDevice;
use smithay::input::keyboard::{GrabStartData, KeyboardGrab, KeyboardInnerHandle, ModifiersState};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
use smithay::utils::{SERIAL_COUNTER, Serial};
use std::io::Read;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
macro_rules! private {
($test:ident) => {
if !crate::startup_tests::private_runtime(concat!(
"input::super_tap::tests::",
stringify!($test)
)) {
return;
}
};
}
const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\nbinding Super+Space binding-mode chord\nbinding-mode chord { binding a none; }\n";
fn fixture(source: &str) -> (Ferese, EventLoop<'static, Ferese>) {
let (mut state, events, _) = crate::input::layer_focus_tests::fixture();
state.reload_config_source(source.into()).unwrap();
(state, events)
}
fn tap(state: &mut Ferese, code: u32) {
key(state, code, true);
key(state, code, false);
}
fn tapped(state: &Ferese) -> bool {
state.binding_modes.active() == Some("tapped")
}
#[test]
fn parsing_indexes_taps_separately_and_preserves_space_chord() {
let config = Config::parse_source("binding Super toggle-launcher\n").unwrap();
let bindings: crate::config::BindingSet = config
.bindings(&config.input_settings().unwrap())
.unwrap()
.into();
assert!(matches!(
bindings.super_tap().unwrap().action,
BindingAction::ToggleLauncher
));
assert!(
bindings
.binding_for_key(
Keycode::new(133),
&[smithay::input::keyboard::keysyms::KEY_Super_L],
true,
false,
false,
false
)
.is_none()
);
assert!(matches!(
bindings
.binding_for_key(
Keycode::new(65),
&[smithay::input::keyboard::keysyms::KEY_space],
true,
false,
false,
false
)
.unwrap()
.action,
BindingAction::ToggleLauncher
));
let guide = bindings.super_tap().unwrap().guide_entry(None).unwrap();
assert_eq!(guide["keys"], "Super (tap)");
for source in [
"binding Super toggle-launcher\nbinding Logo none\n",
"binding Super match=physical action=toggle-launcher\n",
"binding Ctrl toggle-launcher\n",
] {
let config = Config::parse_source(source).unwrap();
assert!(
config.bindings(&config.input_settings().unwrap()).is_err(),
"{source}"
);
}
let disabled = Config::parse_source("binding Super disabled=#true\n").unwrap();
let bindings: crate::config::BindingSet = disabled
.bindings(&disabled.input_settings().unwrap())
.unwrap()
.into();
assert!(bindings.super_tap().is_none());
}
#[test]
fn left_right_taps_execute_on_release_and_repeat_does_not_rearm() {
private!(left_right_taps_execute_on_release_and_repeat_does_not_rearm);
let (mut state, _events) = fixture(SOURCE);
for code in [133, 134] {
key(&mut state, code, true);
assert!(!tapped(&state));
key(&mut state, code, true); // backend repeat, not another isolated tap
assert!(!tapped(&state));
key(&mut state, code, false);
assert!(tapped(&state));
state.set_binding_mode("default");
key(&mut state, code, false); // duplicate release
assert!(!tapped(&state));
assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty());
assert!(
state.intercepted_keys.is_empty(),
"tap must not own either half of Super"
);
}
}
#[test]
fn existing_chords_held_keys_and_both_super_keys_never_become_taps() {
private!(existing_chords_held_keys_and_both_super_keys_never_become_taps);
let (mut state, _events) = fixture(SOURCE);
for sequence in [
vec![(133, true), (38, true), (38, false), (133, false)],
vec![(38, true), (133, true), (38, false), (133, false)],
vec![(133, true), (134, true), (133, false), (134, false)],
vec![(134, true), (133, true), (134, false), (133, false)],
vec![(37, true), (133, true), (37, false), (133, false)],
vec![(133, true), (50, true), (50, false), (133, false)],
] {
for (code, down) in sequence {
key(&mut state, code, down);
}
assert!(!tapped(&state));
assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty());
}
key(&mut state, 133, true);
tap(&mut state, 65); // existing Super+Space, action is deliberately distinct
assert_eq!(state.binding_modes.active(), Some("chord"));
key(&mut state, 133, false);
assert_eq!(state.binding_modes.active(), Some("chord"));
}
#[derive(Debug)]
struct PointerInput;
impl InputBackend for PointerInput {
type Device = WinitVirtualDevice;
type KeyboardKeyEvent = UnusedEvent;
type PointerButtonEvent = Button;
type PointerMotionEvent = Motion;
type PointerAxisEvent = Scroll;
type PointerMotionAbsoluteEvent = UnusedEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
#[derive(Debug)]
struct Button(ButtonState);
#[derive(Debug)]
struct Motion;
#[derive(Debug)]
struct Scroll;
macro_rules! pointer_event {
($event:ty) => {
impl Event<PointerInput> for $event {
fn time(&self) -> InputTime {
InputTime::from_millis(1)
}
fn device(&self) -> WinitVirtualDevice {
WinitVirtualDevice
}
}
};
}
pointer_event!(Button);
pointer_event!(Motion);
pointer_event!(Scroll);
impl PointerButtonEvent<PointerInput> for Button {
fn button_code(&self) -> u32 {
0x111
}
fn state(&self) -> ButtonState {
self.0
}
}
impl PointerMotionEvent<PointerInput> for Motion {
fn delta_x(&self) -> f64 {
1.
}
fn delta_y(&self) -> f64 {
0.
}
fn delta_x_unaccel(&self) -> f64 {
1.
}
fn delta_y_unaccel(&self) -> f64 {
0.
}
}
impl PointerAxisEvent<PointerInput> for Scroll {
fn amount(&self, axis: Axis) -> Option<f64> {
(axis == Axis::Vertical).then_some(15.)
}
fn amount_v120(&self, axis: Axis) -> Option<f64> {
(axis == Axis::Vertical).then_some(120.)
}
fn source(&self) -> AxisSource {
AxisSource::Wheel
}
fn relative_direction(&self, _: Axis) -> AxisRelativeDirection {
AxisRelativeDirection::Identical
}
}
fn button(state: &mut Ferese, down: bool) {
state.process_input_event::<PointerInput>(InputEvent::PointerButton {
event: Button(if down {
ButtonState::Pressed
} else {
ButtonState::Released
}),
});
}
#[test]
fn mouse_motion_buttons_scroll_and_already_held_button_cancel_taps() {
private!(mouse_motion_buttons_scroll_and_already_held_button_cancel_taps);
let (mut state, _events) = fixture(SOURCE);
key(&mut state, 133, true);
button(&mut state, true);
button(&mut state, false);
key(&mut state, 133, false);
assert!(!tapped(&state));
button(&mut state, true);
tap(&mut state, 133);
assert!(!tapped(&state));
button(&mut state, false);
key(&mut state, 133, true);
state.process_input_event::<PointerInput>(InputEvent::PointerMotion { event: Motion });
key(&mut state, 133, false);
assert!(!tapped(&state));
key(&mut state, 133, true);
state.process_input_event::<PointerInput>(InputEvent::PointerAxis { event: Scroll });
key(&mut state, 133, false);
assert!(!tapped(&state));
tap(&mut state, 133);
assert!(
tapped(&state),
"fresh isolated tap must recover after mouse use"
);
}
#[test]
fn reload_focus_loss_and_second_source_do_not_resurrect_pending_taps() {
private!(reload_focus_loss_and_second_source_do_not_resurrect_pending_taps);
let (mut state, _events) = fixture(SOURCE);
key(&mut state, 133, true);
state.reload_config_source(SOURCE.into()).unwrap();
// An unchanged config need not replace bindings; an actual input/binding
// edit must invalidate the press even if its action stays the same.
state
.reload_config_source(format!("{SOURCE}\nbinding b none\n"))
.unwrap();
key(&mut state, 133, false);
assert!(!tapped(&state));
key(&mut state, 133, true);
state.release_nested_keyboard();
key(&mut state, 133, false);
assert!(!tapped(&state));
let auxiliary = KeyboardSource::new_auxiliary();
state
.super_tap
.arm(KeyboardSource::MAIN, Keycode::new(133), None);
state.super_tap.observe_key(auxiliary, Keycode::new(133));
assert!(
!state
.super_tap
.release(KeyboardSource::MAIN, Keycode::new(133), true, None)
);
tap(&mut state, 134);
assert!(tapped(&state));
}
struct ClientGrab(GrabStartData<Ferese>);
impl KeyboardGrab<Ferese> for ClientGrab {
fn input(
&mut self,
data: &mut Ferese,
handle: &mut KeyboardInnerHandle<'_, Ferese>,
key: Keycode,
state: KeyState,
mods: Option<ModifiersState>,
serial: Serial,
time: InputTime,
) {
handle.input(data, key, state, mods, serial, time);
}
fn set_focus(
&mut self,
data: &mut Ferese,
handle: &mut KeyboardInnerHandle<'_, Ferese>,
focus: Option<WlSurface>,
serial: Serial,
) {
handle.set_focus(data, focus, serial);
}
fn start_data(&self) -> &GrabStartData<Ferese> {
&self.0
}
fn unset(&mut self, _: &mut Ferese) {}
}
#[test]
fn native_ime_grab_allows_tap_but_client_grab_keeps_keyboard() {
private!(native_ime_grab_allows_tap_but_client_grab_keeps_keyboard);
let (mut state, mut _events) = fixture(SOURCE);
let keyboard = state.seat.get_keyboard().unwrap();
// Use a real input-method grab and its own virtual keyboard, as Rime
// does, rather than an empty grab whose protocol resources do not exist.
use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::{
zwp_input_method_manager_v2::ZwpInputMethodManagerV2,
zwp_input_method_v2::ZwpInputMethodV2,
};
use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
use smithay::wayland::input_method::InputMethodSeat;
use std::os::fd::AsRawFd;
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let request = crate::state::test_client_request;
request(&mut wire, 1, 1, &[2], None); // display.get_registry
crate::state::dispatch_test_client(&mut _events, &mut state);
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut seat_global = None;
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 2 && header as u16 == 0 {
let length =
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
seat_global = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
));
}
}
offset += (header >> 16) as usize;
}
let mut bind = vec![seat_global.unwrap(), 8];
bind.extend(
b"wl_seat\0"
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
);
bind.extend([7, 3]);
request(&mut wire, 2, 0, &bind, None);
crate::state::dispatch_test_client(&mut _events, &mut state);
let manager = client
.create_resource::<ZwpInputMethodManagerV2, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
let vk_manager = client
.create_resource::<ZwpVirtualKeyboardManagerV1, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
request(&mut wire, manager.id().protocol_id(), 0, &[3, 4], None);
crate::state::dispatch_test_client(&mut _events, &mut state);
let grab_opcode = ZwpInputMethodV2::interface()
.requests
.iter()
.position(|request| request.name == "grab_keyboard")
.unwrap() as u32;
request(&mut wire, 4, grab_opcode, &[5], None);
request(&mut wire, vk_manager.id().protocol_id(), 0, &[3, 6], None);
let fd = tempfile::tempfile().unwrap();
request(&mut wire, 6, 0, &[0, 0], Some(fd.as_raw_fd())); // no_keymap, retain seat mapping
crate::state::dispatch_test_client(&mut _events, &mut state);
assert_eq!(
state.seat.input_method().keyboard_grab_client(),
Some(client)
);
key(&mut state, 133, true);
assert!(state.super_tap.candidate.is_some());
request(&mut wire, 6, 1, &[1, 125, 1], None); // trusted Rime echo of physical Super press
crate::state::dispatch_test_client(&mut _events, &mut state);
assert!(
state.super_tap.candidate.is_some(),
"trusted IME echo must bypass auxiliary tap cancellation"
);
assert!(!tapped(&state));
key(&mut state, 133, false);
assert!(tapped(&state));
request(&mut wire, 6, 1, &[2, 125, 0], None); // trusted release echo after the physical tap
crate::state::dispatch_test_client(&mut _events, &mut state);
assert!(tapped(&state));
assert!(keyboard.pressed_keys().is_empty());
state.set_binding_mode("default");
keyboard.unset_grab(&mut state);
keyboard.set_grab(
&mut state,
ClientGrab(GrabStartData { focus: None }),
SERIAL_COUNTER.next_serial(),
);
tap(&mut state, 133);
assert!(!tapped(&state));
keyboard.unset_grab(&mut state);
tap(&mut state, 133);
assert!(tapped(&state));
}
fn launcher_events(state: &mut Ferese, wire: &mut UnixStream, shell: &FereseShellV1) -> usize {
state.display_handle.flush_clients().unwrap();
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
let opcode = FereseShellV1::interface()
.events
.iter()
.position(|e| e.name == "toggle_launcher")
.unwrap() as u16;
let (mut offset, mut count) = (0, 0);
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == shell.id().protocol_id() && header as u16 == opcode {
count += 1;
}
offset += (header >> 16) as usize;
}
count
}
#[test]
fn actual_launcher_protocol_is_sent_once_after_release_and_space_still_works() {
private!(actual_launcher_protocol_is_sent_once_after_release_and_space_still_works);
let (mut state, _events) = fixture("binding Super toggle-launcher\n");
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let shell = client
.create_resource::<FereseShellV1, crate::shell_control::ShellData, Ferese>(
&state.display_handle,
10,
Default::default(),
)
.unwrap();
state.shell_resources.push(shell.downgrade());
for code in [133, 134] {
key(&mut state, code, true);
assert_eq!(launcher_events(&mut state, &mut wire, &shell), 0);
key(&mut state, code, false);
assert_eq!(launcher_events(&mut state, &mut wire, &shell), 1);
}
key(&mut state, 133, true);
tap(&mut state, 65);
key(&mut state, 133, false);
assert_eq!(
launcher_events(&mut state, &mut wire, &shell),
1,
"Space chord must not also produce a tap"
);
}
#[test]
fn lost_pointer_devices_and_session_reset_do_not_leave_taps_disabled() {
let mut tap = SuperTap::default();
tap.pointer_button("mouse-a".into(), 0x110, ButtonState::Pressed);
tap.pointer_button("mouse-b".into(), 0x110, ButtonState::Pressed);
tap.device_removed("mouse-a");
assert_eq!(tap.pointer_buttons.len(), 1);
tap.device_removed("mouse-b");
assert!(tap.pointer_buttons.is_empty());
tap.pointer_button("mouse-a".into(), 0x111, ButtonState::Pressed);
tap.arm(KeyboardSource::MAIN, Keycode::new(133), None);
tap.reset();
assert!(tap.pointer_buttons.is_empty());
assert!(tap.candidate.is_none());
}
+160 -8
View File
@@ -17,30 +17,39 @@ pub(crate) struct Failure<K> {
/// Caller must validate the *complete* device plan first. No logical publication
/// is permitted until success. Unavailable rollback results must be retired.
/// `releases` includes removed outputs and old connector owners whose CRTC is
/// reused by a change. Release those resources before per-surface staging tests.
pub(crate) fn apply<T: HardwareTransaction>(
driver: &mut T,
changes: &[T::Key],
removals: &[T::Key],
releases: &[T::Key],
) -> Result<(), Failure<T::Key>> {
let mut touched = Vec::new();
let mut touched_changes = Vec::new();
let mut touched_releases = Vec::new();
let result = (|| {
for key in releases {
touched_releases.push(*key);
driver.disable(*key)?;
}
for key in changes {
touched.push(*key);
touched_changes.push(*key);
driver.stage(*key)?;
}
for key in changes {
driver.commit(*key)?;
}
for key in removals {
touched.push(*key);
driver.disable(*key)?;
}
Ok::<_, String>(())
})();
if let Err(error) = result {
driver.abort_staged();
let unavailable = touched
// Drop new surfaces first, then restore surviving changed outputs before
// re-enabling former owners. Restoring a released CRTC too early can
// exceed the very resource budget that required its release.
let unavailable = touched_changes
.into_iter()
.rev()
.filter(|key| !touched_releases.contains(key))
.chain(touched_releases.iter().rev().copied())
.filter(|key| driver.restore(*key).is_err())
.collect();
return Err(Failure { error, unavailable });
@@ -48,6 +57,149 @@ pub(crate) fn apply<T: HardwareTransaction>(
Ok(())
}
#[cfg(test)]
mod resource_tests {
use super::*;
use std::collections::BTreeMap;
/// Per-CRTC staging probes see the current hardware plus that one proposed
/// mode. The final plan fits, but an old HDMI route consumes the budget that
/// a surviving DP output's higher refresh needs. CRTC 589 is then reused by
/// another connector, so it appears in both changes and pre-releases.
struct LimitedHardware {
active: BTreeMap<u16, u8>,
previous: BTreeMap<u16, u8>,
desired: BTreeMap<u16, u8>,
events: Vec<(&'static str, u16)>,
fail: Option<(&'static str, u16)>,
restore_fails: Option<u16>,
}
impl LimitedHardware {
fn new() -> Self {
let previous = BTreeMap::from([(205, 0), (397, 1), (589, 2)]);
Self {
active: previous.clone(),
previous,
desired: BTreeMap::from([(205, 0), (397, 2), (589, 1)]),
events: vec![],
fail: None,
restore_fails: None,
}
}
fn fits(&self, key: u16, cost: u8) -> Result<(), String> {
let mut proposed = self.active.clone();
proposed.insert(key, cost);
if proposed.values().copied().sum::<u8>() > 3 {
Err("intermediate topology exceeds device resources".into())
} else {
Ok(())
}
}
fn event(&mut self, operation: &'static str, key: u16) -> Result<(), String> {
self.events.push((operation, key));
if self.fail == Some((operation, key)) {
Err(format!("injected {operation} failure on {key}"))
} else {
Ok(())
}
}
}
impl HardwareTransaction for LimitedHardware {
type Key = u16;
fn stage(&mut self, key: u16) -> Result<(), String> {
self.fits(key, self.desired[&key])?;
self.event("stage", key)
}
fn commit(&mut self, key: u16) -> Result<(), String> {
self.fits(key, self.desired[&key])?;
self.active.insert(key, self.desired[&key]);
// A failed operation can have already changed hardware.
self.event("commit", key)
}
fn disable(&mut self, key: u16) -> Result<(), String> {
self.active.remove(&key);
self.event("disable", key)
}
fn abort_staged(&mut self) {
self.events.push(("abort", 0));
self.active.retain(|key, _| self.previous.contains_key(key));
}
fn restore(&mut self, key: u16) -> Result<(), String> {
self.events.push(("restore", key));
if self.restore_fails == Some(key) {
self.active.remove(&key);
return Err("previous output unavailable".into());
}
if let Some(cost) = self.previous.get(&key).copied() {
self.fits(key, cost)?;
self.active.insert(key, cost);
} else {
self.active.remove(&key);
}
Ok(())
}
}
#[test]
fn reused_crtc_releases_its_old_connector_before_other_modes_are_tested() {
let mut hardware = LimitedHardware::new();
assert_eq!(hardware.desired.values().copied().sum::<u8>(), 3);
apply(&mut hardware, &[397, 589], &[589]).unwrap();
assert_eq!(hardware.active, hardware.desired);
assert_eq!(hardware.events[0], ("disable", 589));
assert!(hardware.events.iter().all(|(_, key)| *key != 205));
}
#[test]
fn rollback_reverts_new_modes_before_restoring_released_connector_owners() {
for failure in [("stage", 589), ("commit", 397), ("commit", 589)] {
let mut hardware = LimitedHardware::new();
hardware.fail = Some(failure);
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
assert!(result.error.starts_with("injected"), "{}", result.error);
assert!(result.unavailable.is_empty(), "{result:?}");
assert_eq!(hardware.active, hardware.previous);
assert_eq!(
&hardware.events[hardware.events.len() - 3..],
&[("abort", 0), ("restore", 397), ("restore", 589)]
);
}
}
#[test]
fn partially_failed_release_restores_the_old_output_without_staging_changes() {
let mut hardware = LimitedHardware::new();
hardware.fail = Some(("disable", 589));
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
assert_eq!(result.error, "injected disable failure on 589");
assert!(result.unavailable.is_empty());
assert_eq!(hardware.active, hardware.previous);
assert_eq!(
hardware.events,
[("disable", 589), ("abort", 0), ("restore", 589)]
);
}
#[test]
fn failed_rollback_reports_lost_output_and_still_restores_the_released_owner() {
let mut hardware = LimitedHardware::new();
hardware.fail = Some(("commit", 589));
hardware.restore_fails = Some(397);
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
assert_eq!(result.unavailable, [397]);
assert_eq!(hardware.active, BTreeMap::from([(205, 0), (589, 2)]));
}
}
#[cfg(test)]
mod tests {
use super::*;
+182
View File
@@ -0,0 +1,182 @@
//! Compare damaged swapchain frames with an independently repainted scene.
use super::*;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::solid::SolidColorBuffer;
use smithay::utils::user_data::UserDataMap;
fn blur(
renderer: &mut GlesRenderer,
program: &BlurProgram,
scale: f64,
size: Size<i32, Physical>,
transform: Transform,
) -> BlurRenderElement {
let sample = Rectangle::<i32, Logical>::new((0, 10).into(), (145, 80).into());
let visible = Rectangle::<i32, Logical>::new((10, 20).into(), (120, 55).into());
let sample_physical = sample.to_physical_precise_round(scale);
let framebuffer = framebuffer_clip_rect(sample_physical, size, transform.invert());
let capture = framebuffer_capture_rect(framebuffer);
let texture = renderer
.create_buffer(Fourcc::Abgr8888, (capture[2], capture[3]).into())
.unwrap();
BlurRenderElement::new(
texture,
program.0.clone(),
&MaterialParameters {
panel: None,
corner_radii: [0.; 4],
presentation_alpha: 1.,
background_opacity: 0.,
blur: (8. * scale) as f32,
sample_geometry: sample,
sample_physical,
sample_framebuffer: framebuffer,
shadow_rect: [0.; 4],
shadow_values: [0.; 2],
shadow_bounds: visible,
geometry: visible,
tint: [0.; 4],
generation: 0,
opaque: false,
visible_framebuffer: framebuffer_clip_rect(
visible.to_physical_precise_round(scale),
size,
transform.invert(),
),
},
Arc::new(AtomicBool::new(true)),
)
}
fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
let size = texture.size();
let target = renderer.bind(texture).unwrap();
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
renderer.map_texture(&mapping).unwrap().to_vec()
}
#[test]
#[ignore = "requires an EGL rendering device"]
fn blur_behind_moving_opaque_window_matches_full_frames_at_buffer_age_three() {
let mut renderer = {
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
let device = EGLDevice::enumerate()
.unwrap()
.find(|device| device.is_software() == software)
.unwrap();
// SAFETY: The enumerated device remains owned by the EGL display/context.
let display = unsafe { EGLDisplay::new(device).unwrap() };
// SAFETY: This test owns the renderer's fresh context exclusively.
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }
};
let mut resources = RenderResources::default();
let program = blur_program(&mut resources, &mut renderer).unwrap();
for scale in [1., 1.5, 2.] {
for transform in [Transform::Normal, Transform::Flipped180] {
let size: Size<i32, Physical> = ((160. * scale) as i32, (100. * scale) as i32).into();
let mut tracker = OutputDamageTracker::new(size, scale, transform);
let mut targets = (0..3)
.map(|_| {
renderer
.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into())
.unwrap()
})
.collect::<Vec<GlesTexture>>();
let mut reference: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into())
.unwrap();
let effect = blur(&mut renderer, &program, scale, size, transform);
let mut background = SolidColorBuffer::new((160, 100), [0.1, 0.7, 0.2, 1.]);
let foreground = SolidColorBuffer::new((60, 90), [0.2, 0.1, 0.8, 1.]);
for frame_index in 0..9 {
if frame_index % 3 == 0 {
background.set_color([0.1, 0.3 + frame_index as f32 * 0.05, 0.2, 1.]);
}
let solid = || {
SolidColorRenderElement::from_buffer(
&background,
(0, 0),
scale,
1.,
RenderElementKind::Unspecified,
)
};
let front = SolidColorRenderElement::from_buffer(
&foreground,
(((100 - frame_index * 3) as f64 * scale) as i32, 0),
scale,
1.,
RenderElementKind::Unspecified,
);
let elements: Vec<AnimatedWindowRenderElement> =
vec![front.clone().into(), effect.clone().into(), solid().into()];
backdrop::update(&elements, scale);
let slot = frame_index as usize % 3;
{
let mut target = renderer.bind(&mut targets[slot]).unwrap();
tracker
.render_output(
&mut renderer,
&mut target,
if frame_index < 3 { 0 } else { 3 },
&elements,
Color32F::TRANSPARENT,
)
.unwrap()
.sync
.wait()
.unwrap();
}
// Paint every layer without opaque culling or damage reuse.
// A separate capture texture prevents the oracle from repairing
// the incremental renderer's cached copy as a side effect.
let reference_blur = blur(&mut renderer, &program, scale, size, transform);
let full_elements: Vec<AnimatedWindowRenderElement> =
vec![front.into(), reference_blur.into(), solid().into()];
{
let mut target = renderer.bind(&mut reference).unwrap();
let mut frame = renderer
.render(&mut target, size, transform.invert())
.unwrap();
let full = Rectangle::from_size(size);
frame.clear(Color32F::TRANSPARENT, &[full]).unwrap();
for element in full_elements.iter().rev() {
let cache = UserDataMap::default();
let rect = element.geometry(scale.into());
if element.is_framebuffer_effect() {
element
.capture_framebuffer(&mut frame, element.src(), rect, &cache)
.unwrap();
}
element
.draw(
&mut frame,
element.src(),
rect,
&[Rectangle::from_size(rect.size)],
&[],
element.is_framebuffer_effect().then_some(&cache),
)
.unwrap();
}
frame.finish().unwrap().wait().unwrap();
}
let actual = pixels(&mut renderer, &mut targets[slot]);
let expected = pixels(&mut renderer, &mut reference);
let differing = actual
.iter()
.zip(&expected)
.filter(|(a, b)| a.abs_diff(**b) > 1)
.count();
assert_eq!(
differing, 0,
"scale={scale} transform={transform:?} frame={frame_index}"
);
}
}
}
}
+11 -2
View File
@@ -18,7 +18,7 @@ pub(crate) fn capture_resize_snapshot(
) -> Result<Option<ResizeSnapshot>, GlesError> {
// Called before on_commit_buffer_handler: renderer surface state still
// owns the previous buffer during an actual resize handoff.
render_window_texture(renderer, window, geometry, scale, remaining, None)
render_window_texture(renderer, window, geometry, scale, remaining, None, true)
}
fn render_window_texture(
@@ -28,6 +28,7 @@ fn render_window_texture(
scale: f64,
remaining: usize,
reusable: Option<GlesTexture>,
before_source_release: bool,
) -> Result<Option<ResizeSnapshot>, GlesError> {
let Some(toplevel) = window.toplevel() else {
return Ok(None);
@@ -68,7 +69,13 @@ fn render_window_texture(
let damage = Rectangle::from_size(size);
frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?;
draw_render_elements(&mut frame, scale, &content, &[damage])?;
let _ = frame.finish()?;
let copied = frame.finish()?;
if before_source_release {
// This function runs before the old wl_buffer is replaced. The
// client may immediately reuse it after release, so the detached
// animation image must be complete before that release is possible.
copied.wait().map_err(|_| GlesError::SyncInterrupted)?;
}
}
Ok(Some(ResizeSnapshot {
texture,
@@ -118,6 +125,7 @@ pub(crate) fn capture_window_frame(
scale,
128 * 1024 * 1024,
reusable,
false,
)
.map_err(|error| error.to_string())?
.ok_or("Window has no capturable content")?;
@@ -422,6 +430,7 @@ pub(crate) fn capture_stream_window(
info.scale,
128 * 1024 * 1024,
reusable,
false,
)
.map_err(|error| error.to_string())?
.ok_or("Window has no capturable content")?;
+138
View File
@@ -0,0 +1,138 @@
//! Real input-method protocol resources and SHM bounds; no host GUI is used.
use super::layer_popup_geometry;
use crate::Ferese;
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window};
use smithay::desktop::PopupKind;
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::zwp_input_method_manager_v2::ZwpInputMethodManagerV2;
use smithay::reexports::wayland_server::protocol::{
wl_compositor::WlCompositor, wl_shm::WlShm, wl_surface::WlSurface,
};
use smithay::reexports::wayland_server::Resource;
use smithay::utils::{Physical, Point, Rectangle};
use smithay::wayland::input_method::PopupParent;
use smithay::wayland::text_input::TextInputSeat;
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) {
request(wire, 1, 1, &[7], None);
dispatch(events, state);
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut seat = None;
while offset < bytes.len() {
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
if object == 7 && header as u16 == 0 {
let length =
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
seat = Some(u32::from_ne_bytes(
bytes[offset + 8..offset + 12].try_into().unwrap(),
));
}
}
offset += (header >> 16) as usize;
}
let mut args = vec![seat.unwrap(), 8];
args.extend(
b"wl_seat\0"
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
);
args.extend([7, 8]);
request(wire, 7, 0, &args, None);
dispatch(events, state);
}
#[test]
fn input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds() {
if !crate::startup_tests::private_runtime(
"render::scene::layer_popup_tests::input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
let parent = window.toplevel().unwrap().wl_surface().clone();
let client = parent.client().unwrap();
state.seat.text_input().set_focus(Some(parent.clone()));
bind_seat(&mut state, &mut events, &mut wire);
let manager = client
.create_resource::<ZwpInputMethodManagerV2, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
let compositor = client
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
6,
smithay::wayland::GlobalData,
)
.unwrap();
let shm = client
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
request(&mut wire, manager.id().protocol_id(), 0, &[8, 9], None);
request(&mut wire, compositor.id().protocol_id(), 0, &[10], None);
request(&mut wire, 9, 4, &[11, 10], None); // get_input_popup_surface
let mut file = tempfile::tempfile().unwrap();
file.write_all(&0xff204060u32.to_ne_bytes().repeat(364 * 80))
.unwrap();
request(
&mut wire,
shm.id().protocol_id(),
0,
&[12, 364 * 80 * 4],
Some(file.as_raw_fd()),
);
request(&mut wire, 12, 0, &[13, 0, 364, 80, 364 * 4, 0], None);
request(&mut wire, 10, 1, &[13, 0, 0], None);
request(&mut wire, 10, 6, &[], None);
dispatch(&mut events, &mut state);
let surface = client
.object_from_protocol_id::<WlSurface>(&state.display_handle, 10)
.unwrap();
let Some(PopupKind::InputMethod(mut popup)) = state.popups.find_popup(&surface) else {
panic!("input-method popup must be tracked");
};
// Parent geometry is global, but the layer renderer receives output-local
// coordinates. Cover a primary output and a secondary output left/above it.
for output_origin in [(0, 0), (-1920, -300)] {
let layer_origin = Point::from((360, 241));
let global_parent =
Rectangle::new(layer_origin + Point::from(output_origin), (560, 70).into());
popup.set_parent(Some(PopupParent {
surface: parent.clone(),
location: global_parent,
}));
popup.set_location((148, 55).into());
let kind = PopupKind::InputMethod(popup.clone());
assert_eq!(kind.geometry(), global_parent);
let (bounds, content_origin) = layer_popup_geometry(layer_origin, popup.location(), &kind);
assert_eq!(content_origin, Point::from((508, 296)));
assert_eq!(
bounds,
Rectangle::new((508, 296).into(), (364, 80).into()),
"candidate bounds must come from its own buffer, not its parent"
);
for scale in [1.0, 1.5, 2.0] {
let pixels: Rectangle<i32, Physical> = bounds.to_physical_precise_round(scale);
assert_eq!(pixels.loc, content_origin.to_physical_precise_round(scale));
assert_eq!(pixels.size.h, (80.0 * scale) as i32);
}
}
}
+128 -24
View File
@@ -1,4 +1,6 @@
mod backdrop;
#[cfg(test)]
mod backdrop_gpu_tests;
mod capture;
mod closing;
mod window_blur;
@@ -272,9 +274,25 @@ impl Element for BlurRenderElement {
fn kind(&self) -> RenderElementKind {
RenderElementKind::Unspecified
}
fn is_framebuffer_effect(&self) -> bool {
// The complete sampling region must be repainted before capture,
// including pixels hidden by opaque elements above this blur.
true
}
}
impl RenderElement<GlesRenderer> for BlurRenderElement {
fn capture_framebuffer(
&self,
frame: &mut GlesFrame<'_, '_>,
_src: Rectangle<f64, Buffer>,
_dst: Rectangle<i32, Physical>,
_cache: &smithay::utils::user_data::UserDataMap,
) -> Result<(), GlesError> {
self.capture(frame)
}
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
@@ -282,14 +300,37 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
_cache: Option<&smithay::utils::user_data::UserDataMap>,
cache: Option<&smithay::utils::user_data::UserDataMap>,
) -> Result<(), GlesError> {
// Simple full-frame renderers do not implement the effect callback.
// Damage-tracked outputs must use its callback, never the dirty flag:
// the tracker knows when occlusion or buffer restoration needs a copy.
if cache.is_none() && self.capture_dirty.load(Ordering::Relaxed) {
self.capture(frame)?;
}
frame.render_texture_from_to(
&self.texture,
src,
dst,
damage,
opaque_regions,
Transform::Normal,
1.0,
Some(&self.program),
&self.uniforms,
)
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
None
}
}
impl BlurRenderElement {
fn capture(&self, frame: &mut GlesFrame<'_, '_>) -> Result<(), GlesError> {
let texture = self.texture.tex_id();
let [x, y, width, height] = self.capture_rect;
// Buffer-age restoration and unrelated partial damage must reuse the
// clean captured scene. Recapturing a partly repainted framebuffer
// would feed previous blurred pixels back into the blur.
if self.capture_dirty.load(Ordering::Relaxed) {
{
// SAFETY: with_context makes this frame context current. GL writes into live local
// scalars, and the capture texture belongs to this renderer with the allocated
// framebuffer bounds.
@@ -341,22 +382,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
}
self.capture_dirty.store(false, Ordering::Relaxed);
}
frame.render_texture_from_to(
&self.texture,
src,
dst,
damage,
opaque_regions,
Transform::Normal,
1.0,
Some(&self.program),
&self.uniforms,
)
}
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
None
Ok(())
}
}
@@ -538,6 +564,47 @@ fn clipped_surface_damage(
}
}
fn sampled_surface_damage(
size: Size<i32, Physical>,
source: Size<f64, Buffer>,
transform: Transform,
clip_changed: bool,
client_damage: impl FnOnce() -> DamageSet<i32, Physical>,
) -> DamageSet<i32, Physical> {
clipped_surface_damage(size, clip_changed, || {
// Linear filtering also changes destination samples just outside the
// changed texels. Smithay maps their rectangle to physical coordinates,
// but does not include this half-texel sampling footprint. Expanding in
// physical pixels prevents stale fringes on fractional-scale redraws.
let source = transform.transform_size(source);
let footprint = |pixels: i32, texels: f64| {
if texels > 0.0 {
(f64::from(pixels) / texels * 0.5).ceil() as i32
} else {
0
}
};
let x = footprint(size.w, source.w);
let y = footprint(size.h, source.h);
let bounds = Rectangle::from_size(size);
client_damage()
.into_iter()
.filter(|rect| !rect.is_empty())
.filter_map(|rect| {
Rectangle::new(
(rect.loc.x.saturating_sub(x), rect.loc.y.saturating_sub(y)).into(),
(
rect.size.w.saturating_add(x.saturating_mul(2)),
rect.size.h.saturating_add(y.saturating_mul(2)),
)
.into(),
)
.intersection(bounds)
})
.collect()
})
}
impl Element for RoundedSurfaceRenderElement {
fn id(&self) -> &Id {
self.inner.id()
@@ -564,9 +631,13 @@ impl Element for RoundedSurfaceRenderElement {
scale: RenderScale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || {
self.inner.damage_since(scale, commit)
})
sampled_surface_damage(
self.inner.geometry(scale).size,
self.inner.src().size,
self.inner.transform(),
self.clip_changed,
|| self.inner.damage_since(scale, commit),
)
}
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
@@ -688,6 +759,39 @@ mod tests {
shadow_bounds,
};
#[test]
fn filtered_surface_damage_stays_local_and_preserves_full_clip_changes() {
use smithay::backend::renderer::utils::DamageSet;
let size = (120, 80).into();
let source = (80.0, 40.0).into();
for (transform, x_padding) in [(Transform::Normal, 1), (Transform::_90, 2)] {
let damage = super::sampled_surface_damage(size, source, transform, false, || {
DamageSet::from_slice(&[
Rectangle::new((0, 0).into(), (4, 3).into()),
Rectangle::new((118, 78).into(), (2, 2).into()),
])
});
assert_eq!(
damage.iter().copied().collect::<Vec<_>>(),
vec![
Rectangle::new((0, 0).into(), (4 + x_padding, 4).into()),
Rectangle::new((118 - x_padding, 77).into(), (2 + x_padding, 3).into(),),
],
);
assert!(
super::sampled_surface_damage(size, source, transform, false, DamageSet::default)
.is_empty()
);
let full = super::sampled_surface_damage(size, source, transform, true, || {
panic!("a changed clip must keep full damage without consulting the client")
});
assert_eq!(
full.iter().copied().collect::<Vec<_>>(),
vec![Rectangle::from_size(size)]
);
}
}
#[test]
#[ignore = "requires an EGL rendering device"]
fn solid_corner_mask_preserves_premultiplied_color() {
+26 -3
View File
@@ -556,9 +556,7 @@ pub(super) fn layer_elements(
let mut elements = Vec::new();
for (geometry, layer) in layers {
for (popup, offset) in PopupManager::popups_for_surface(layer.wl_surface()) {
let mut bounds = popup.geometry();
let content_origin = geometry.loc + offset - bounds.loc;
bounds.loc = geometry.loc + offset;
let (bounds, content_origin) = layer_popup_geometry(geometry.loc, offset, &popup);
append_material_surface(
state,
renderer,
@@ -587,6 +585,27 @@ pub(super) fn layer_elements(
elements
}
fn layer_popup_geometry(
layer_origin: Point<i32, Logical>,
offset: Point<i32, Logical>,
popup: &smithay::desktop::PopupKind,
) -> (Rectangle<i32, Logical>, Point<i32, Logical>) {
if matches!(popup, smithay::desktop::PopupKind::InputMethod(_)) {
// IME geometry describes its parent in global coordinates, unlike
// xdg window geometry. Its location is already parent-relative, and
// its candidate surface can extend beyond the parent's bounds.
let origin = layer_origin + offset;
return (
smithay::desktop::utils::bbox_from_surface_tree(popup.wl_surface(), origin),
origin,
);
}
let mut bounds = popup.geometry();
let origin = layer_origin + offset - bounds.loc;
bounds.loc = layer_origin + offset;
(bounds, origin)
}
fn layer_output_crop(size: Size<i32, Physical>, transform: Transform) -> Rectangle<i32, Physical> {
Rectangle::from_size(transform.transform_size(size))
}
@@ -675,6 +694,10 @@ pub(super) fn cursor_elements(
}
}
#[cfg(test)]
#[path = "layer_popup_tests.rs"]
mod layer_popup_tests;
#[cfg(test)]
mod layer_crop_tests {
use super::*;
+1
View File
@@ -452,6 +452,7 @@ impl SessionLockHandler for Ferese {
}
fn lock(&mut self, confirmation: SessionLocker) {
self.super_tap.cancel();
self.dismiss_launcher();
if !self.session_lock.lifecycle.can_acquire() {
return;
+3
View File
@@ -505,6 +505,7 @@ pub struct Ferese {
pub(crate) binding_modes: crate::config::BindingModes,
pub(crate) binding_mode_pressed: HashSet<Keycode>,
pub(crate) binding_mode_repeats: HashMap<Keycode, crate::config::Binding>,
pub(crate) super_tap: crate::input::super_tap::SuperTap,
workspace_auto_back_and_forth: bool,
pub(crate) window_rules: Vec<WindowRule>,
pub(crate) theme_settings: ThemeSettings,
@@ -847,6 +848,7 @@ impl Ferese {
binding_modes: config.binding_modes,
binding_mode_pressed: HashSet::new(),
binding_mode_repeats: HashMap::new(),
super_tap: Default::default(),
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
window_rules: config.window_rules,
theme_settings: config.theme_settings,
@@ -1073,6 +1075,7 @@ impl Ferese {
self.gap_config = config.gap_config;
self.input_settings = config.input_settings;
if bindings_changed || input_changed {
self.super_tap.cancel();
self.binding_mode_repeats.clear();
self.bindings = config.bindings.into();
self.binding_modes = config.binding_modes;
@@ -1,4 +1,9 @@
//! Private protocol clients and offscreen EGL; no host desktop or DRM required.
#[path = "resize_scene_gpu_tests.rs"]
mod resize_scene_gpu_tests;
#[path = "partial_shm_gpu_tests.rs"]
mod partial_shm_gpu_tests;
use std::io::{Read, Write};
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::net::UnixStream;
@@ -0,0 +1,461 @@
//! Streaming SHM updates compared with independent full imports and full repaint.
use super::*;
use std::borrow::Cow;
use std::fs::File;
use std::os::unix::fs::FileExt;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::{Element, RenderElement};
use smithay::backend::renderer::{Color32F, Frame, Renderer};
use smithay::utils::{Buffer, Physical, Size};
const WIDTH: u32 = 193;
const HEIGHT: u32 = 137;
const STRIDE: u32 = (WIDTH + 7) * 4;
const OFFSET: u32 = 64;
struct ReusableShm {
file: File,
buffer: u32,
version: usize,
format: u32,
}
fn buffer_pixels(pixels: &[u8], format: u32, switching_formats: bool) -> Cow<'_, [u8]> {
if !switching_formats {
return Cow::Borrowed(pixels);
}
let mut encoded = pixels.to_vec();
for pixel in encoded.chunks_exact_mut(4) {
if format == 1 {
// XRGB's unused high byte may legally be zero.
pixel[3] = 0;
} else {
// A valid premultiplied translucent ARGB surface makes a stale
// opaque shader variant visibly differ in RGB as well as alpha.
for channel in &mut pixel[..3] {
*channel = (u16::from(*channel) * 128 / 255) as u8;
}
pixel[3] = 128;
}
}
Cow::Owned(encoded)
}
fn write_region(file: &File, pixels: &[u8], rect: Rectangle<i32, Buffer>) {
for y in rect.loc.y..rect.loc.y + rect.size.h {
let source = (y as u32 * WIDTH + rect.loc.x as u32) as usize * 4;
let target = u64::from(OFFSET + y as u32 * STRIDE + rect.loc.x as u32 * 4);
file.write_all_at(&pixels[source..source + rect.size.w as usize * 4], target)
.unwrap();
}
}
fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
let size = smithay::backend::renderer::Texture::size(texture);
let target = renderer.bind(texture).unwrap();
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
renderer.map_texture(&mapping).unwrap().to_vec()
}
#[test]
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
fn reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale() {
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale";
if !crate::startup_tests::private_runtime(TEST) {
return;
}
run_shm_scenarios(&[(1, None), (3, None)]);
}
#[test]
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
fn same_sized_argb_xrgb_switches_match_fresh_full_uploads() {
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::same_sized_argb_xrgb_switches_match_fresh_full_uploads";
if !crate::startup_tests::private_runtime(TEST) {
return;
}
run_shm_scenarios(&[(2, Some(0)), (2, Some(1))]);
}
fn run_shm_scenarios(scenarios: &[(usize, Option<u32>)]) {
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
let device = EGLDevice::enumerate()
.unwrap()
.find(|device| device.is_software() == software)
.expect("requested EGL device");
eprintln!(
"partial SHM EGL software={software}, render node={:?}",
device.render_device_path()
);
// SAFETY: The display owns the enumerated device; each renderer below owns
// a fresh, unshared context used only by this test thread.
let display = unsafe { EGLDisplay::new(device).unwrap() };
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
let scale = 1.5;
let physical: Size<i32, Physical> = (480, 360).into();
let mut mismatching_frames = 0;
for &(buffer_count, first_format) in scenarios {
let switching_formats = first_format.is_some();
let mut events = EventLoop::try_new().unwrap();
let config = crate::config::Config::parse_source(
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
)
.unwrap()
.runtime_config()
.unwrap();
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
state.animation_test_time = Some(Duration::ZERO);
state.focus_effect.active_opacity = 1.0;
state.focus_effect.inactive_opacity = 1.0;
state.focus_effect.inactive_dim = 0.0;
state.theme_settings.window_radius = 12.0;
let output = Output::new(
"partial-shm".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: physical,
refresh: 60_000,
}),
Some(Transform::Normal),
Some(Scale::Fractional(scale)),
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "partial-shm".into());
let (window, mut wire) = window(&mut state, &mut events, 0xff203c5c);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.set_floating_window_geometry(
&window,
(56, 64).into(),
(WIDTH as i32, HEIGHT as i32).into(),
);
state.display_handle.flush_clients().unwrap();
ack_configure(&mut wire, 3);
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
let shm = client
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
let compositor = client
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
6,
smithay::wayland::GlobalData,
)
.unwrap();
let mut raster = 0xff203c5cu32
.to_ne_bytes()
.repeat((WIDTH * HEIGHT) as usize);
let full: Rectangle<i32, Buffer> =
Rectangle::from_size((WIDTH as i32, HEIGHT as i32).into());
let mut buffers = Vec::new();
for slot in 0..buffer_count {
let format = first_format.map_or(0, |first| first ^ (slot as u32 % 2));
let file = tempfile::tempfile().unwrap();
let len = OFFSET + STRIDE * HEIGHT;
file.set_len(u64::from(len)).unwrap();
// Nontrivial offset/stride/padding exercise pixel-store state too.
file.write_all_at(&vec![0xa5; len as usize], 0).unwrap();
write_region(
&file,
&buffer_pixels(&raster, format, switching_formats),
full,
);
let pool = 7 + slot as u32 * 2;
let buffer = pool + 1;
request(
&mut wire,
shm.id().protocol_id(),
0,
&[pool, len],
Some(file.as_raw_fd()),
);
request(
&mut wire,
pool,
0,
&[buffer, OFFSET, WIDTH, HEIGHT, STRIDE, format],
None,
);
buffers.push(ReusableShm {
file,
buffer,
version: 0,
format,
});
}
let region = 7 + buffer_count as u32 * 2;
request(&mut wire, compositor.id().protocol_id(), 1, &[region], None);
if switching_formats {
// ARGB is genuinely translucent in this scenario; XRGB receives
// automatic opacity from its declared SHM format.
request(&mut wire, 2, 4, &[0], None);
} else {
request(&mut wire, region, 1, &[0, 0, WIDTH, HEIGHT], None);
request(&mut wire, 2, 4, &[region], None);
}
request(&mut wire, region, 0, &[], None);
request(&mut wire, 3, 3, &[0, 0, WIDTH, HEIGHT], None);
request(&mut wire, 2, 1, &[buffers[0].buffer, 0, 0], None);
request(&mut wire, 2, 9, &[0, 0, WIDTH, HEIGHT], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
state.windows.update(id, |record| record.opening = None);
state.animations_enabled = true;
state.advance_animations_at(Duration::from_secs(1), Duration::from_secs(1));
let fixed_geometry = state.windows.geometry(&id).unwrap().visual.current;
assert_eq!(window.geometry().size, (WIDTH as i32, HEIGHT as i32).into());
let mut tracker = OutputDamageTracker::from_output(&output);
let mut targets: Vec<GlesTexture> = (0..3)
.map(|_| {
renderer
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
.unwrap()
})
.collect();
let mut history = Vec::new();
let mut total_changed_bytes = 0;
let mut partial_frames = 0;
for frame_index in 0..24 {
let mut changed = None;
if frame_index > 0 {
let rect = if switching_formats {
full
} else {
Rectangle::<i32, Buffer>::new(
(
(frame_index * 37 % (WIDTH as usize - 29)) as i32,
(frame_index * 23 % (HEIGHT as usize - 13)) as i32,
)
.into(),
(29, 13).into(),
)
};
let color = 0xff000000u32
| (((frame_index * 41) % 220 + 20) as u32) << 16
| (((frame_index * 67) % 220 + 20) as u32) << 8
| (((frame_index * 97) % 220 + 20) as u32);
for y in rect.loc.y..rect.loc.y + rect.size.h {
for x in rect.loc.x..rect.loc.x + rect.size.w {
let at = (y as u32 * WIDTH + x as u32) as usize * 4;
raster[at..at + 4].copy_from_slice(&color.to_ne_bytes());
}
}
history.push(rect);
let buffer = &mut buffers[frame_index % buffer_count];
let encoded = buffer_pixels(&raster, buffer.format, switching_formats);
// A recycled client buffer catches up only the rectangles changed
// since its last use. Surface damage describes this frame's delta
// from the preceding surface commit, independent of buffer age.
for pending in &history[buffer.version..] {
write_region(&buffer.file, &encoded, *pending);
total_changed_bytes += pending.size.w * pending.size.h * 4;
}
buffer.version = history.len();
request(&mut wire, 2, 1, &[buffer.buffer, 0, 0], None);
request(
&mut wire,
2,
9,
&[
rect.loc.x as u32,
rect.loc.y as u32,
rect.size.w as u32,
rect.size.h as u32,
],
None,
);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
changed = Some((rect, color));
partial_frames += 1;
}
state.advance_animations_at(
Duration::from_millis(16),
Duration::from_millis(1016 + frame_index as u64 * 16),
);
let scene = state.sample_frame(&output, Duration::ZERO);
assert_eq!(
scene.windows[&id].presentation.bounds.current, fixed_geometry,
"streaming moved the window"
);
assert_eq!(scene.windows[&id].presentation.alpha(), 1.0);
let elements = crate::render::sampled_output_elements(
&mut state,
&mut renderer,
&output,
false,
&scene,
);
assert!(
elements
.iter()
.all(|element| !element.is_framebuffer_effect())
);
let slot = frame_index % 3;
{
let mut target = renderer.bind(&mut targets[slot]).unwrap();
tracker
.render_output(
&mut renderer,
&mut target,
if frame_index < 3 { 0 } else { 3 },
&elements,
Color32F::new(0.035, 0.04, 0.055, 1.0),
)
.unwrap()
.sync
.wait()
.unwrap();
}
let actual = pixels(&mut renderer, &mut targets[slot]);
let cached_format = smithay::backend::renderer::utils::with_renderer_surface_state(
window.toplevel().unwrap().wl_surface(),
|surface| {
surface
.texture::<GlesTexture>(renderer.context_id())
.and_then(smithay::backend::renderer::Texture::format)
},
)
.flatten();
// A fresh, unshared context cannot reuse the display path's imported
// SHM texture. Smithay creates a new texture with a complete TexImage2D
// upload regardless of partial damage, then this oracle repaints every
// layer without damage history or opaque-region culling.
let mut oracle =
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
let oracle_context = oracle.context_id().erased();
let mut reference: GlesTexture = oracle
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
.unwrap();
let full_elements = crate::render::sampled_output_elements(
&mut state,
&mut oracle,
&output,
false,
&scene,
);
let fresh_format = smithay::backend::renderer::utils::with_renderer_surface_state(
window.toplevel().unwrap().wl_surface(),
|surface| {
surface
.texture::<GlesTexture>(oracle.context_id())
.and_then(smithay::backend::renderer::Texture::format)
},
)
.flatten();
{
let mut target = oracle.bind(&mut reference).unwrap();
let mut frame = oracle
.render(&mut target, physical, Transform::Normal)
.unwrap();
frame
.clear(
Color32F::new(0.035, 0.04, 0.055, 1.0),
&[Rectangle::from_size(physical)],
)
.unwrap();
for element in full_elements.iter().rev() {
let rect = element.geometry(scale.into());
element
.draw(
&mut frame,
element.src(),
rect,
&[Rectangle::from_size(rect.size)],
&[],
None,
)
.unwrap();
}
frame.finish().unwrap().wait().unwrap();
}
let expected = pixels(&mut oracle, &mut reference);
let differing = actual
.iter()
.zip(&expected)
.filter(|(a, b)| a.abs_diff(**b) > 1)
.count();
if differing > 0 {
mismatching_frames += 1;
let differences = actual
.chunks_exact(4)
.zip(expected.chunks_exact(4))
.enumerate()
.filter(|(_, (a, b))| a.iter().zip(*b).any(|(x, y)| x.abs_diff(*y) > 1))
.map(|(at, (a, b))| {
(
(at % physical.w as usize, at / physical.w as usize),
a.to_vec(),
b.to_vec(),
)
})
.collect::<Vec<_>>();
let bounds = differences.iter().fold(
(usize::MAX, usize::MAX, 0, 0),
|(l, t, r, b), ((x, y), _, _)| (l.min(*x), t.min(*y), r.max(*x), b.max(*y)),
);
eprintln!(
"reused client buffers={buffer_count}, partial frame={frame_index}, different channels={differing}, pixel bounds={bounds:?}, samples={:?}",
&differences[..differences.len().min(8)]
);
if switching_formats {
let rgb_differing = actual
.chunks_exact(4)
.zip(expected.chunks_exact(4))
.filter(|(a, b)| {
a[..3].iter().zip(&b[..3]).any(|(x, y)| x.abs_diff(*y) > 1)
})
.count();
eprintln!(
"same-size SHM first_format={first_format:?} attached_format={} cached_texture_format={cached_format:?} fresh_texture_format={fresh_format:?} RGB-mismatching-pixels={rgb_differing}",
buffers[frame_index % buffer_count].format
);
}
}
if !switching_formats && let Some((rect, color)) = changed {
let x = ((fixed_geometry.x + f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.)
* scale) as usize;
let y = ((fixed_geometry.y + f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.)
* scale) as usize;
let at = (y * physical.w as usize + x) * 4;
let cpu_rgba = [(color >> 16) as u8, (color >> 8) as u8, color as u8, 255];
assert_eq!(
&actual[at..at + 4],
&cpu_rgba,
"partial upload missed CPU raster at frame={frame_index}"
);
}
state.render.forget_context(&oracle_context);
}
assert_eq!(partial_frames, 23);
if !switching_formats {
assert!(total_changed_bytes < (WIDTH * HEIGHT * 4 * partial_frames as u32) as i32);
}
eprintln!(
"stable scale={scale}, reused wl_buffers={buffer_count}: 24 age-3 frames, {partial_frames} partial commits, {total_changed_bytes} locally written SHM bytes, 24 fresh-context full-upload oracle frames"
);
}
assert_eq!(
mismatching_frames, 0,
"partial SHM frames differ from complete fresh uploads"
);
}
@@ -0,0 +1,341 @@
//! Exercise the display scene's damage history, independently of capture repainting.
use super::*;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::{Element, RenderElement};
use smithay::backend::renderer::{Color32F, Frame, Renderer, Texture};
use smithay::utils::{Physical, Size};
fn commit_raster(
state: &mut Ferese,
events: &mut EventLoop<'static, Ferese>,
window: &Window,
wire: &mut UnixStream,
object: &mut u32,
size: (u32, u32),
phase: u32,
) {
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
let shm = client
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
1,
smithay::wayland::GlobalData,
)
.unwrap();
let compositor = client
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
&state.display_handle,
6,
smithay::wayland::GlobalData,
)
.unwrap();
let (width, height) = size;
// A high-contrast grid catches stale/cropped pixels, unlike a solid window.
let mut raster = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
let color = if (x / 13 + y / 11 + phase) % 2 == 0 {
0xff26b976_u32
} else {
0xffd44381_u32
};
raster.extend_from_slice(&color.to_ne_bytes());
}
}
let mut file = tempfile::tempfile().unwrap();
file.write_all(&raster).unwrap();
request(
wire,
shm.id().protocol_id(),
0,
&[*object, width * height * 4],
Some(file.as_raw_fd()),
);
request(
wire,
*object,
0,
&[*object + 1, 0, width, height, width * 4, 0],
None,
);
request(wire, 3, 3, &[0, 0, width, height], None);
request(wire, 2, 1, &[*object + 1, 0, 0], None);
// Real opaque desktop clients declare an opaque region. Without it the
// damage tracker cannot exercise the rounded element's occlusion path.
request(wire, compositor.id().protocol_id(), 1, &[*object + 2], None);
request(wire, *object + 2, 1, &[0, 0, width, height], None);
request(wire, 2, 4, &[*object + 2], None);
request(wire, *object + 2, 0, &[], None);
request(wire, 2, 2, &[0, 0, width, height], None);
request(wire, 2, 6, &[], None);
*object += 3;
dispatch(events, state);
}
fn read_pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
let size = texture.size();
let target = renderer.bind(texture).unwrap();
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
renderer.map_texture(&mapping).unwrap().to_vec()
}
#[test]
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
fn floating_maximize_handoff_matches_full_repaint_at_buffer_age_three() {
const TEST: &str = "state::capture_privacy::tests::resize_scene_gpu_tests::floating_maximize_handoff_matches_full_repaint_at_buffer_age_three";
if !crate::startup_tests::private_runtime(TEST) {
return;
}
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
let device = EGLDevice::enumerate()
.unwrap()
.find(|device| device.is_software() == software)
.expect("requested EGL rendering device");
eprintln!(
"resize scene EGL software={software}, render node={:?}",
device.render_device_path()
);
// SAFETY: This test owns the enumerated device and the fresh EGL context.
let display = unsafe { EGLDisplay::new(device).unwrap() };
// SAFETY: GL work remains on this renderer's owning test thread.
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
for scale in [1.0, 1.5] {
let mut events = EventLoop::try_new().unwrap();
let config = crate::config::Config::parse_source(
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
)
.unwrap()
.runtime_config()
.unwrap();
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
state.animation_test_time = Some(Duration::ZERO);
state.focus_effect.active_opacity = 1.0;
state.focus_effect.inactive_opacity = 1.0;
state.focus_effect.inactive_dim = 0.0;
state.theme_settings.window_radius = 12.0;
let output = Output::new(
"resize-age-three".into(),
PhysicalProperties {
serial_number: String::new(),
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let physical: Size<i32, Physical> = ((320.0 * scale) as i32, (240.0 * scale) as i32).into();
output.change_current_state(
Some(Mode {
size: physical,
refresh: 60_000,
}),
Some(Transform::Normal),
Some(Scale::Fractional(scale)),
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "resize-age-three".into());
let (window, mut wire) = window(&mut state, &mut events, 0xff26b976);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.set_floating_window_geometry(&window, (106, 92).into(), (128, 80).into());
state.display_handle.flush_clients().unwrap();
ack_configure(&mut wire, 3);
let mut object = 7;
commit_raster(
&mut state,
&mut events,
&window,
&mut wire,
&mut object,
(128, 80),
0,
);
state.windows.update(id, |record| record.opening = None);
state.animations_enabled = true;
let mut tracker = OutputDamageTracker::from_output(&output);
let mut targets: Vec<GlesTexture> = (0..3)
.map(|_| {
renderer
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
.unwrap()
})
.collect();
let mut reference: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
.unwrap();
let mut frame_index = 0;
let mut now = Duration::ZERO;
let mut snapshot_frames = 0;
let mut fill_frames = 0;
let mut moving_frames = 0;
let mut repaint_frames = 0;
let mut native_draw_frames = 0;
let mut opaque_frames = 0;
// Delayed commits exercise both old-frame cropping on shrink and neutral
// fill on grow. Repeating in both directions also reuses every target.
for (cycle, delay) in [3, 1, 5, 0].into_iter().enumerate() {
state.set_window_maximized(id, cycle % 2 == 0);
state.display_handle.flush_clients().unwrap();
let (_, configured) = ack_configure_with_size(&mut wire, 3, Some(4));
let requested = configured.expect("maximize/restore configure");
for tick in 0..28 {
if tick == delay {
// The offscreen fixture has no DirectBackend. Invoke the
// same capture-before-release operation used by that backend.
let snapshot = crate::render::capture_resize_snapshot(
&mut renderer,
&window,
window.geometry(),
scale,
32 * 1024 * 1024,
)
.unwrap()
.expect("old client pixels");
state.render.set_snapshot(id, snapshot);
commit_raster(
&mut state,
&mut events,
&window,
&mut wire,
&mut object,
requested,
cycle as u32 + 1,
);
repaint_frames += 1;
} else if tick > delay && tick % 2 == 0 {
// IDE repainting alternates with unchanged client buffers,
// so both client damage and compositor-only motion matter.
commit_raster(
&mut state,
&mut events,
&window,
&mut wire,
&mut object,
requested,
cycle as u32 + tick as u32,
);
repaint_frames += 1;
}
now += Duration::from_millis(16);
state.advance_animations_at(Duration::from_millis(16), now);
let scene = state.sample_frame(&output, Duration::from_millis(5));
let sample = &scene.windows[&id];
assert_eq!(sample.presentation.alpha(), 1.0, "ordinary opaque window");
if sample.geometry.visual.current != sample.geometry.visual.target {
moving_frames += 1;
}
snapshot_frames += usize::from(state.render.snapshot(&id).is_some());
let source = window.geometry().size;
let visual = sample.presentation.bounds.current;
fill_frames += usize::from(
f64::from(source.w) < visual.width || f64::from(source.h) < visual.height,
);
let elements = crate::render::sampled_output_elements(
&mut state,
&mut renderer,
&output,
false,
&scene,
);
native_draw_frames +=
usize::from(state.render.snapshot(&id).is_some_and(|snapshot| {
elements.iter().any(|element| element.id() == &snapshot.id)
}));
opaque_frames += usize::from(
elements
.iter()
.any(|element| !element.opaque_regions(scale.into()).is_empty()),
);
assert!(
elements
.iter()
.all(|element| !element.is_framebuffer_effect()),
"this regression must cover opaque resize without backdrop blur"
);
let slot = frame_index % 3;
{
let mut target = renderer.bind(&mut targets[slot]).unwrap();
tracker
.render_output(
&mut renderer,
&mut target,
if frame_index < 3 { 0 } else { 3 },
&elements,
Color32F::new(0.035, 0.04, 0.055, 1.0),
)
.unwrap()
.sync
.wait()
.unwrap();
}
// Independent oracle: no damage history, no opaque-region
// culling, and every layer painted back-to-front into a clean FBO.
{
let mut target = renderer.bind(&mut reference).unwrap();
let mut frame = renderer
.render(&mut target, physical, Transform::Normal)
.unwrap();
frame
.clear(
Color32F::new(0.035, 0.04, 0.055, 1.0),
&[Rectangle::from_size(physical)],
)
.unwrap();
for element in elements.iter().rev() {
let rect = element.geometry(scale.into());
element
.draw(
&mut frame,
element.src(),
rect,
&[Rectangle::from_size(rect.size)],
&[],
None,
)
.unwrap();
}
frame.finish().unwrap().wait().unwrap();
}
let actual = read_pixels(&mut renderer, &mut targets[slot]);
let expected = read_pixels(&mut renderer, &mut reference);
let differing = actual
.iter()
.zip(&expected)
.filter(|(a, b)| a.abs_diff(**b) > 1)
.count();
assert_eq!(
differing, 0,
"scale={scale} cycle={cycle} tick={tick} frame={frame_index} source={source:?} visual={visual:?}"
);
frame_index += 1;
}
}
assert!(snapshot_frames > 20, "resize handoff was not exercised");
assert!(
fill_frames > 0,
"growing native content never needed resize fill"
);
eprintln!(
"opaque resize scale={scale}: {frame_index} age-3 frames, {snapshot_frames} snapshot frames, {native_draw_frames} snapshot draw frames, {fill_frames} fill frames, {moving_frames} moving frames, {repaint_frames} client repaint frames, {opaque_frames} opaque frames"
);
assert!(moving_frames > 20, "maximize/restore animation did not run");
assert!(
repaint_frames > 40,
"client repainting did not overlap motion"
);
assert!(
native_draw_frames > 20,
"retained snapshots were not in the display scene"
);
assert!(
opaque_frames > 20,
"opaque-region culling was not exercised"
);
}
}
+1
View File
@@ -271,6 +271,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
impl Ferese {
pub(crate) fn release_nested_keyboard(&mut self) {
self.super_tap.reset();
// Host shortcuts can consume releases while this window is unfocused.
// Release only the physical/nested source, including modifiers, without
// executing compositor shortcuts. Auxiliary keyboards retain ownership.
+1 -1
View File
@@ -1778,7 +1778,7 @@ mod manager_loop {
state
.reload_config_source(
"xwayland { enabled #true; startup \"on-demand\"; path \"".to_owned()
+ &wrapper(&directory, &[]).display().to_string()
+ wrapper(&directory, &[]).display().to_string().as_str()
+ "\"; }",
)
.expect("the reload must be accepted");