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:
1 parent
a2be421974
commit
993bc0f88f
391 files changed
+130908
-76
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user