Merge upstream/master (COSMIC 1.6) into local/yoda-main

32 commits amont, aucun conflit de code : seul le lockfile divergeait,
il est regenere. Les 9 commits yoda sont preserves, dont le patch des
onglets de piles suivant WindowControlsPosition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-08-19 21:56:05 +02:00
commit f60c573f4d
39 changed files with 1945 additions and 657 deletions

425
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -102,6 +102,7 @@ features = [
"backend_drm", "backend_drm",
"backend_gbm", "backend_gbm",
"backend_egl", "backend_egl",
"backend_libei",
"backend_libinput", "backend_libinput",
"backend_session_libseat", "backend_session_libseat",
"backend_udev", "backend_udev",
@ -177,4 +178,4 @@ dnd = { path = "../window_clipboard/dnd" }
mime = { path = "../window_clipboard/mime" } mime = { path = "../window_clipboard/mime" }
[patch.crates-io] [patch.crates-io]
smithay = { git = "https://github.com/smithay/smithay.git", rev = "1ed69cb" } smithay = { git = "https://github.com/smithay/smithay.git", rev = "5fb12b8" }

View file

@ -0,0 +1,28 @@
a11y-zoom-move-continuously = Visningen bevæger sig kontinuerligt med markøren
a11y-zoom-move-onedge = Vis bevægelser når markør rammer kant
a11y-zoom-move-centered = Vis bevægelser, for at holde markør centreret
a11y-zoom-settings = Forstørrelsesglas-indstillinger...
grow-window = Gro
shrink-window = Skrump
swap-windows = Byt Vinduer
stack-windows = Stabel Vinduer
window-menu-tiled = Gør vindue flydende
window-menu-screenshot = Tag skærmbillede
window-menu-move = Flyt
window-menu-resize = Skift størrelse
window-menu-move-prev-workspace = Flyt til forrige arbejdsområde
window-menu-move-next-workspace = Flyt til næste arbejdsområde
window-menu-stack = Opret vinduestak
window-menu-unstack-all = Udtag vinduer af stak
window-menu-unstack = Udtag vindue af stak
window-menu-sticky = Fastgjort vindue
window-menu-close = Luk
window-menu-close-all = Luk alle vinduer
window-menu-resize-edge-top = Top
window-menu-resize-edge-left = Venstre
window-menu-resize-edge-right = Højre
window-menu-resize-edge-bottom = Bund
window-menu-minimize = Minimér
window-menu-maximize = Maksimér
window-menu-fullscreen = Fuld skærm
unknown-keybinding = <uangiven>

View file

@ -0,0 +1,28 @@
a11y-zoom-move-continuously = View moves continuously with pointer
a11y-zoom-move-onedge = View moves when pointer reaches edge
a11y-zoom-move-centered = View moves to keep pointer centred
a11y-zoom-settings = Magnifier settings...
grow-window = Grow
shrink-window = Shrink
swap-windows = Swap Windows
stack-windows = Stack Windows
unknown-keybinding = <unset>
window-menu-minimize = Minimise
window-menu-maximize = Maximise
window-menu-fullscreen = Fullscreen
window-menu-tiled = Float window
window-menu-screenshot = Take screenshot
window-menu-move = Move
window-menu-resize = Resize
window-menu-move-prev-workspace = Move to previous workspace
window-menu-move-next-workspace = Move to next workspace
window-menu-stack = Create window stack
window-menu-unstack-all = Unstack windows
window-menu-unstack = Unstack window
window-menu-sticky = Sticky window
window-menu-close = Close
window-menu-close-all = Close all windows
window-menu-resize-edge-top = Top
window-menu-resize-edge-left = Left
window-menu-resize-edge-right = Right
window-menu-resize-edge-bottom = Bottom

View file

@ -0,0 +1,28 @@
a11y-zoom-move-continuously = A vista móvese continuamente co punteiro
a11y-zoom-move-onedge = A vista móvese cando o punteiro chega ao bordo
a11y-zoom-move-centered = Mover a vista para manter o cursor centrado
a11y-zoom-settings = Configuración da lupa...
grow-window = Aumentar
shrink-window = Encoller
swap-windows = Cambiar xanelas
stack-windows = Apilar xanelas
unknown-keybinding = <unset>
window-menu-minimize = Minimizar
window-menu-maximize = Maximizar
window-menu-fullscreen = Pantalla Completa
window-menu-tiled = Xanela flotante
window-menu-screenshot = Capturar pantalla
window-menu-move = Mover
window-menu-resize = Redimensionar
window-menu-move-prev-workspace = Mover ao espazo de traballo anterior
window-menu-move-next-workspace = Mover ao seguinte espazo de traballo
window-menu-stack = Crear pila de xanelas
window-menu-unstack-all = Desapilar xanelas
window-menu-unstack = Desapilar xanela
window-menu-sticky = Xanela persistente
window-menu-close = Pechar
window-menu-close-all = Pechar todas as xanelas
window-menu-resize-edge-top = Arriba
window-menu-resize-edge-left = Esquerda
window-menu-resize-edge-right = Dereita
window-menu-resize-edge-bottom = Fondo

View file

@ -1,4 +1,4 @@
grow-window = Ingrandisci grow-window = Crescere
shrink-window = Riduci shrink-window = Riduci
swap-windows = Scambia le finestre swap-windows = Scambia le finestre
stack-windows = Sovrapponi le finestre stack-windows = Sovrapponi le finestre
@ -6,7 +6,7 @@ unknown-keybinding = <unset>
window-menu-minimize = Minimizza window-menu-minimize = Minimizza
window-menu-maximize = Massimizza window-menu-maximize = Massimizza
window-menu-tiled = Finestra flottante window-menu-tiled = Finestra flottante
window-menu-screenshot = Cattura una schermata window-menu-screenshot = Scatta uno screenshot
window-menu-move = Sposta window-menu-move = Sposta
window-menu-resize = Ridimensiona window-menu-resize = Ridimensiona
window-menu-move-prev-workspace = Sposta all'area di lavoro precedente window-menu-move-prev-workspace = Sposta all'area di lavoro precedente
@ -22,7 +22,7 @@ window-menu-resize-edge-left = Sinistra
window-menu-resize-edge-right = Destra window-menu-resize-edge-right = Destra
window-menu-resize-edge-bottom = In basso window-menu-resize-edge-bottom = In basso
window-menu-fullscreen = Schermo intero window-menu-fullscreen = Schermo intero
a11y-zoom-settings = Opzioni lente d'ingrandimento... a11y-zoom-settings = Impostazioni lente d'ingrandimento...
a11y-zoom-move-continuously = Segue sempre il cursorse a11y-zoom-move-continuously = Segue sempre il cursorse
a11y-zoom-move-centered = Mantiene il cursore centrato a11y-zoom-move-centered = Mantiene il cursore centrato
a11y-zoom-move-onedge = Segue il cursore ai bordi a11y-zoom-move-onedge = Segue il cursore ai bordi

View file

View file

@ -59,7 +59,7 @@ use std::{
time::Duration, time::Duration,
}; };
use super::{drm_helpers, socket::Socket, surface::Surface}; use super::{drm_helpers, surface::Surface};
#[derive(Debug)] #[derive(Debug)]
pub struct EGLInternals { pub struct EGLInternals {
@ -98,14 +98,12 @@ pub struct Device {
pub texture_formats: FormatSet, pub texture_formats: FormatSet,
event_token: Option<RegistrationToken>, event_token: Option<RegistrationToken>,
pub socket: Option<Socket>,
} }
#[derive(Debug)] #[derive(Debug)]
struct ReusableDevice { struct ReusableDevice {
leasing_global: Option<DrmLeaseState>, leasing_global: Option<DrmLeaseState>,
active_clients: HashSet<ClientId>, active_clients: HashSet<ClientId>,
socket: Option<Socket>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -593,7 +591,7 @@ impl State {
} }
} }
pub fn device_removed(&mut self, dev: dev_t, dh: &DisplayHandle) -> Result<()> { pub fn device_removed(&mut self, dev: dev_t, _dh: &DisplayHandle) -> Result<()> {
let backend = self.backend.kms(); let backend = self.backend.kms();
// we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs // we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs
let drm_node = backend let drm_node = backend
@ -616,13 +614,6 @@ impl State {
if let Some(token) = device.event_token.take() { if let Some(token) = device.event_token.take() {
self.common.event_loop_handle.remove(token); self.common.event_loop_handle.remove(token);
} }
if let Some(socket) = device.socket.take() {
self.common.event_loop_handle.remove(socket.token);
self.common
.dmabuf_state
.destroy_global::<State>(dh, socket.dmabuf_global);
dh.remove_global::<State>(socket.drm_global);
}
backend.api.as_mut().remove_node(&device.inner.render_node); backend.api.as_mut().remove_node(&device.inner.render_node);
backend backend
.primary_node .primary_node
@ -755,23 +746,7 @@ impl Device {
let ReusableDevice { let ReusableDevice {
leasing_global, leasing_global,
active_clients, active_clients,
socket,
} = reuse.unwrap_or_else(|| { } = reuse.unwrap_or_else(|| {
let socket = match (!is_software)
.then(|| common.create_socket(dh, render_node, texture_formats.clone()))
.transpose()
{
Ok(socket) => socket,
Err(err) => {
warn!(
?err,
"Failed to initialize hardware-acceleration for clients on {}.",
render_node,
);
None
}
};
let leasing_global = match (!is_software) let leasing_global = match (!is_software)
.then(|| DrmLeaseState::new::<State>(dh, &dev_node)) .then(|| DrmLeaseState::new::<State>(dh, &dev_node))
.transpose() .transpose()
@ -790,7 +765,6 @@ impl Device {
ReusableDevice { ReusableDevice {
leasing_global, leasing_global,
active_clients: HashSet::new(), active_clients: HashSet::new(),
socket,
} }
}); });
@ -831,7 +805,6 @@ impl Device {
texture_formats, texture_formats,
event_token: Some(token), event_token: Some(token),
socket,
}) })
} }
@ -892,7 +865,6 @@ impl Device {
let device = ReusableDevice { let device = ReusableDevice {
leasing_global: self.inner.leasing_global, leasing_global: self.inner.leasing_global,
active_clients: self.inner.active_clients, active_clients: self.inner.active_clients,
socket: self.socket,
}; };
let state = OldDeviceState { let state = OldDeviceState {

View file

@ -75,6 +75,7 @@ pub fn display_configuration(
// And then cleanup // And then cleanup
if device.is_atomic() { if device.is_atomic() {
let mut req = AtomicModeReq::new(); let mut req = AtomicModeReq::new();
let mut has_changes = false;
let plane_handles = device.plane_handles()?; let plane_handles = device.plane_handles()?;
// We cannot just shortcut and use the legacy api for all cleanups because of this. // We cannot just shortcut and use the legacy api for all cleanups because of this.
@ -95,10 +96,15 @@ pub fn display_configuration(
let fb_id = get_prop(device, plane, "FB_ID")?; let fb_id = get_prop(device, plane, "FB_ID")?;
req.add_property(plane, crtc_id, property::Value::CRTC(None)); req.add_property(plane, crtc_id, property::Value::CRTC(None));
req.add_property(plane, fb_id, property::Value::Framebuffer(None)); req.add_property(plane, fb_id, property::Value::Framebuffer(None));
has_changes = true;
} }
} }
} }
device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?; // Skip an empty commit: a no-op modeset that also fails with EPERM
// without DRM master (e.g. a render-only secondary GPU).
if has_changes {
device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?;
}
} else { } else {
for crtc in res_handles.crtcs() { for crtc in res_handles.crtcs() {
#[allow(deprecated)] #[allow(deprecated)]

View file

@ -4,12 +4,15 @@ use crate::{
config::{CompOutputConfig, ScreenFilter}, config::{CompOutputConfig, ScreenFilter},
shell::Shell, shell::Shell,
state::BackendData, state::BackendData,
utils::{env::dev_var, prelude::*}, utils::{env::dev_var, global::remove_global_with_timer, prelude::*},
wayland::protocols::output_power::OutputPowerState, wayland::protocols::{drm::WlDrmState, output_power::OutputPowerState},
}; };
use anyhow::{Context, Result}; use anyhow::{self, Context, Result};
use calloop::LoopSignal; use calloop::{
LoopSignal,
timer::{TimeoutAction, Timer},
};
use cosmic_comp_config::output::comp::{AdaptiveSync, OutputState}; use cosmic_comp_config::output::comp::{AdaptiveSync, OutputState};
use indexmap::IndexMap; use indexmap::IndexMap;
use render::gles::GbmGlowBackend; use render::gles::GbmGlowBackend;
@ -32,11 +35,12 @@ use smithay::{
control::{Device as _, connector::Interface, crtc}, control::{Device as _, connector::Interface, crtc},
}, },
input::{self, Libinput}, input::{self, Libinput},
wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags,
wayland_server::{Client, DisplayHandle}, wayland_server::{Client, DisplayHandle},
}, },
utils::{Clock, DevPath, Monotonic, Size}, utils::{Clock, DevPath, Monotonic, Size},
wayland::{ wayland::{
dmabuf::DmabufGlobal, dmabuf::{DmabufFeedbackBuilder, DmabufGlobal},
drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd}, drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd},
relative_pointer::RelativePointerManagerState, relative_pointer::RelativePointerManagerState,
}, },
@ -48,12 +52,12 @@ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::Path, path::Path,
sync::{Arc, RwLock, atomic::AtomicBool}, sync::{Arc, RwLock, atomic::AtomicBool},
time::Duration,
}; };
mod device; mod device;
mod drm_helpers; mod drm_helpers;
pub mod render; pub mod render;
mod socket;
mod surface; mod surface;
use device::*; use device::*;
pub(crate) use surface::Surface; pub(crate) use surface::Surface;
@ -74,6 +78,7 @@ pub struct KmsState {
libinput: Libinput, libinput: Libinput,
pub syncobj_state: Option<DrmSyncobjState>, pub syncobj_state: Option<DrmSyncobjState>,
pub dmabuf_global: Option<DmabufGlobal>,
} }
pub struct KmsGuard<'a> { pub struct KmsGuard<'a> {
@ -131,6 +136,7 @@ pub fn init_backend(
libinput: libinput_context, libinput: libinput_context,
syncobj_state: None, syncobj_state: None,
dmabuf_global: None,
}); });
// manually add already present gpus // manually add already present gpus
@ -142,9 +148,10 @@ pub fn init_backend(
} }
} }
if let Err(err) = state.backend.kms().select_primary_gpu(dh) { if let Err(err) = state.select_primary_gpu(dh) {
warn!("Failed to determine primary gpu: {}", err); warn!("Failed to determine primary gpu: {}", err);
} }
state.update_default_feedback();
if let Err(err) = state.refresh_output_config() { if let Err(err) = state.refresh_output_config() {
info!( info!(
@ -204,7 +211,7 @@ fn init_libinput(
state.backend.kms().input_devices.remove(&*device.name()); state.backend.kms().input_devices.remove(&*device.name());
} }
state.process_input_event(event); state.process_input_event(event, crate::input::InputBackendId::Normal);
for output in state.common.shell.read().outputs() { for output in state.common.shell.read().outputs() {
state.backend.kms().schedule_render(output); state.backend.kms().schedule_render(output);
@ -317,11 +324,13 @@ fn init_udev(
{ {
let backend = state.backend.kms(); let backend = state.backend.kms();
if matches!(event, UdevEvent::Added { .. } | UdevEvent::Removed { .. }) if matches!(event, UdevEvent::Added { .. } | UdevEvent::Removed { .. }) {
&& backend.primary_node.read().unwrap().is_none() if backend.primary_node.read().unwrap().is_none()
&& let Err(err) = state.backend.kms().select_primary_gpu(&dh) && let Err(err) = state.select_primary_gpu(&dh)
{ {
warn!("Failed to determine a new primary gpu: {}", err); warn!("Failed to determine a new primary gpu: {}", err);
}
state.update_default_feedback();
} }
} }
@ -465,50 +474,118 @@ impl State {
device.drm.pause(); device.drm.pause();
} }
} }
}
impl KmsState {
fn select_primary_gpu(&mut self, dh: &DisplayHandle) -> Result<()> { fn select_primary_gpu(&mut self, dh: &DisplayHandle) -> Result<()> {
// We don't have to check the allow/blocklist here, // We don't have to check the allow/blocklist here,
// as any disallowed devices won't be in `self.drm_devices`. // as any disallowed devices won't be in `self.drm_devices`.
let mut primary_node = self.primary_node.write().unwrap(); let kms = self.backend.kms();
let mut primary_node = kms.primary_node.write().unwrap();
let _ = primary_node.take(); // if we error don't leave an old node in place let _ = primary_node.take(); // if we error don't leave an old node in place
*primary_node = determine_primary_gpu(&self.drm_devices, self.session.seat())?; *primary_node = determine_primary_gpu(&kms.drm_devices, kms.session.seat())?;
if let Some(node) = *primary_node { if let Some(node) = *primary_node {
info!("Using {} as primary gpu for rendering.", node); info!("Using {} as primary gpu for rendering.", node);
self.software_renderer.take(); kms.software_renderer.take();
} else if self.software_renderer.is_none() {
// setup minimal feedback. We will update it in `update_default_feedback`
let primary_formats = kms
.drm_devices
.values()
.find(|dev| dev.inner.render_node == node)
.unwrap()
.texture_formats
.clone();
let feedback = DmabufFeedbackBuilder::new(node.dev_id(), primary_formats.clone())
.build()
.unwrap();
if let Some(global) = kms.dmabuf_global.as_ref() {
self.common
.dmabuf_state
.set_default_feedback(global, &feedback);
} else {
let dmabuf_global = self
.common
.dmabuf_state
.create_global_with_default_feedback::<State>(dh, &feedback);
kms.dmabuf_global = Some(dmabuf_global);
};
let device_path = node
.dev_path_with_type(NodeType::Render)
.or_else(|| node.dev_path())
.ok_or(anyhow::anyhow!(
"Could not determine path for gpu node: {}",
node
))?;
if let Some(drm) = self.common.wl_drm_state.as_mut() {
drm.update_device(device_path, primary_formats);
} else {
self.common.wl_drm_state = Some(WlDrmState::new::<State>(
dh,
device_path,
primary_formats,
kms.dmabuf_global.as_ref().unwrap(),
));
}
} else if kms.software_renderer.is_none() {
info!("Failed to find a suitable gpu, using software renderingr"); info!("Failed to find a suitable gpu, using software renderingr");
self.software_renderer = match software_renderer() { kms.software_renderer = match software_renderer() {
Ok(renderer) => Some(renderer), Ok(renderer) => Some(renderer),
Err(err) => { Err(err) => {
error!(?err, "Failed to initialize software EGL renderer."); error!(?err, "Failed to initialize software EGL renderer.");
None None
} }
}; };
if let Some(drm) = self.common.wl_drm_state.take() {
remove_global_with_timer(dh, &self.common.event_loop_handle, drm.global().clone());
}
if let Some(global) = kms.dmabuf_global.take() {
self.common
.dmabuf_state
.disable_global::<State>(dh, &global);
let source = Timer::from_duration(Duration::from_secs(5));
let res =
self.common
.event_loop_handle
.insert_source(source, move |_, _, state| {
state
.common
.dmabuf_state
.destroy_global::<State>(&state.common.display_handle, global);
TimeoutAction::Drop
});
if let Err(err) = res {
tracing::error!(
"failed to insert timer source to destroy output global: {}",
err
);
}
}
} }
if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) { if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) {
if let Some(primary_node) = primary_node if let Some(primary_node) = primary_node
.as_ref() .as_ref()
.and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok())) .and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok()))
&& let Some(device) = self.drm_devices.get(&primary_node) && let Some(device) = kms.drm_devices.get(&primary_node)
{ {
let import_device = device.drm.device().device_fd().clone(); let import_device = device.drm.device().device_fd().clone();
if supports_syncobj_eventfd(&import_device) { if supports_syncobj_eventfd(&import_device) {
if let Some(state) = self.syncobj_state.as_mut() { if let Some(state) = kms.syncobj_state.as_mut() {
state.update_device(import_device); state.update_device(import_device);
} else { } else {
let syncobj_state = DrmSyncobjState::new::<State>(dh, import_device); let syncobj_state = DrmSyncobjState::new::<State>(dh, import_device);
self.syncobj_state = Some(syncobj_state); kms.syncobj_state = Some(syncobj_state);
} }
return Ok(()); return Ok(());
} }
} }
if let Some(old_state) = self.syncobj_state.take() { if let Some(old_state) = kms.syncobj_state.take() {
dh.remove_global::<State>(old_state.into_global()); dh.remove_global::<State>(old_state.into_global());
} }
} }
@ -516,6 +593,45 @@ impl KmsState {
Ok(()) Ok(())
} }
fn update_default_feedback(&mut self) {
let kms = self.backend.kms();
let primary_node = kms.primary_node.read().unwrap();
if let Some(primary_node) = *primary_node {
let primary_formats = kms
.drm_devices
.values()
.find(|dev| dev.inner.render_node == primary_node)
.unwrap()
.texture_formats
.clone();
let mut feedback =
DmabufFeedbackBuilder::new(primary_node.dev_id(), primary_formats.clone());
for dev in kms
.drm_devices
.values()
.filter(|dev| dev.inner.render_node != primary_node)
{
feedback = feedback.add_preference_tranche(
dev.inner.render_node.dev_id(),
TrancheFlags::Sampling,
dev.texture_formats.iter().cloned(),
6..=6,
);
}
let default_feedback = feedback.build().unwrap();
self.common.dmabuf_state.set_default_feedback(
kms.dmabuf_global
.as_ref()
.expect("Primary node but no dmabuf global?"),
&default_feedback,
);
}
}
}
impl KmsState {
pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> { pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> {
self.session.change_vt(num).map_err(Into::into) self.session.change_vt(num).map_err(Into::into)
} }
@ -523,29 +639,25 @@ impl KmsState {
pub fn dmabuf_imported( pub fn dmabuf_imported(
&mut self, &mut self,
client: Option<Client>, client: Option<Client>,
global: &DmabufGlobal, _global: &DmabufGlobal,
dmabuf: Dmabuf, dmabuf: Dmabuf,
) -> Result<DrmNode> { ) -> Result<DrmNode> {
let device_node = dmabuf
.node()
.unwrap_or_else(|| self.primary_node.read().unwrap().unwrap());
let mut device = self let mut device = self
.drm_devices .drm_devices
.values_mut() .values_mut()
.find(|device| { .find(|dev| dev.inner.render_node == device_node)
device .ok_or(anyhow::anyhow!(
.socket "Unable to find device for node: {}",
.as_ref() device_node
.map(|s| &s.dmabuf_global == global) ))?;
.unwrap_or(false)
})
.context("Couldn't find gpu for dmabuf global")?;
// If device advertised to client doesn't support format/modifier, select // If device advertised to client doesn't support format/modifier, select
// first device that does. This is needed for image-copy from // first device that does. This is needed for image-copy from
// output/toplevel on a different node. // output/toplevel on a different node.
// if dmabuf.node().is_none() && !device.texture_formats.contains(&dmabuf.format()) {
// TODO: After
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/268,
// only try the device specified explicitly by the client, if set.
if !device.texture_formats.contains(&dmabuf.format()) {
device = self device = self
.drm_devices .drm_devices
.values_mut() .values_mut()

View file

@ -1006,12 +1006,16 @@ impl SurfaceThreadState {
&self.shell.read(), &self.shell.read(),
); );
// Acquiring a renderer can fail transiently when the underlying DRM
// device is lost (e.g. after a GPU reset).
let mut renderer = if render_node != self.target_node { let mut renderer = if render_node != self.target_node {
self.api self.api
.renderer(&render_node, &self.target_node, compositor.format()) .renderer(&render_node, &self.target_node, compositor.format())
.unwrap() .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))?
} else { } else {
self.api.single_renderer(&self.target_node).unwrap() self.api
.single_renderer(&self.target_node)
.map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))?
}; };
self.timings.start_render(&self.clock); self.timings.start_render(&self.clock);
@ -1267,7 +1271,10 @@ impl SurfaceThreadState {
}) })
.context("Failed to draw to offscreen render target")?; .context("Failed to draw to offscreen render target")?;
renderer = self.api.single_renderer(&self.target_node).unwrap(); renderer = self
.api
.single_renderer(&self.target_node)
.map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))?;
elements = postprocess_elements( elements = postprocess_elements(
&mut renderer, &mut renderer,
@ -1522,7 +1529,7 @@ fn get_surface_dmabuf_feedback(
render_node: DrmNode, render_node: DrmNode,
target_node: DrmNode, target_node: DrmNode,
render_formats: FormatSet, render_formats: FormatSet,
_target_formats: FormatSet, target_formats: FormatSet,
primary_plane_formats: FormatSet, primary_plane_formats: FormatSet,
overlay_plane_formats: Option<FormatSet>, overlay_plane_formats: Option<FormatSet>,
) -> SurfaceDmabufFeedback { ) -> SurfaceDmabufFeedback {
@ -1540,60 +1547,40 @@ fn get_surface_dmabuf_feedback(
.cloned() .cloned()
.collect::<FormatSet>() .collect::<FormatSet>()
}); });
let builder = DmabufFeedbackBuilder::new(render_node.dev_id(), render_formats);
/* let mut builder = DmabufFeedbackBuilder::new(render_node.dev_id(), render_formats.clone());
// Sadly no implementation would pick this up as a preferred render tranche,
// where the combined formats would increase our chances of doing a dmabuf copy.
// .. So we should probably not advertise this on the off-chance it actually triggers bugs.
//
let combined_formats = render_formats.intersection(&target_formats).cloned().collect::<FormatSet>(); if target_node != render_node {
if target_node != render_node.dev_id() && !combined_formats.is_empty() {
builder = builder.add_preference_tranche(
render_node.dev_id(),
None,
combined_formats,
);
};
// We also can't advertise scan out tranches for the actual display device,
// as e.g. the nvidia driver might then send us dmabufs, that makes e.g. the iris hangs on import...
if target_node != render_node.dev_id() && !combined_formats.is_empty() {
builder = builder.add_preference_tranche( builder = builder.add_preference_tranche(
target_node.dev_id(), target_node.dev_id(),
Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), zwp_linux_dmabuf_feedback_v1::TrancheFlags::Sampling,
combined_formats, target_formats,
6..=6,
); );
}; };
// So no fun combinations, we gotta wait for dmabuf-v6
*/
let render_feedback = builder.clone().build().unwrap(); let render_feedback = builder.clone().build().unwrap();
let primary_scanout_feedback = (target_node == render_node).then(|| {
let primary_scanout_feedback = builder
.clone()
.add_preference_tranche(
target_node.dev_id(),
zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout,
primary_plane_formats,
4..=6,
)
.build()
.unwrap();
let overlay_scanout_feedback = overlay_plane_formats.map(|formats| {
builder builder
.clone()
.add_preference_tranche( .add_preference_tranche(
render_node.dev_id(), target_node.dev_id(),
Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout,
primary_plane_formats, formats,
4..=6,
) )
.build() .build()
.unwrap() .unwrap()
}); });
let overlay_scanout_feedback = overlay_plane_formats
.filter(|_| target_node == render_node)
.map(|formats| {
builder
.add_preference_tranche(
render_node.dev_id(),
Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout),
formats,
)
.build()
.unwrap()
});
SurfaceDmabufFeedback { SurfaceDmabufFeedback {
render_feedback, render_feedback,

View file

@ -6,6 +6,7 @@ use crate::{
shell::{Devices, SeatExt}, shell::{Devices, SeatExt},
state::{BackendData, Common}, state::{BackendData, Common},
utils::prelude::*, utils::prelude::*,
wayland::protocols::drm::WlDrmState,
}; };
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use cosmic_comp_config::output::comp::{OutputConfig, TransformDef}; use cosmic_comp_config::output::comp::{OutputConfig, TransformDef};
@ -273,7 +274,7 @@ fn init_egl_client_side(
.create_global_with_default_feedback::<State>(dh, &feedback); .create_global_with_default_feedback::<State>(dh, &feedback);
let render_node = render_node.unwrap().unwrap(); let render_node = render_node.unwrap().unwrap();
let _drm_global_id = state.common.wl_drm_state.create_global::<State>( state.common.wl_drm_state = Some(WlDrmState::new::<State>(
dh, dh,
render_node render_node
.dev_path_with_type(NodeType::Render) .dev_path_with_type(NodeType::Render)
@ -284,7 +285,7 @@ fn init_egl_client_side(
))?, ))?,
dmabuf_formats, dmabuf_formats,
&dmabuf_global, &dmabuf_global,
); ));
info!("EGL hardware-acceleration enabled."); info!("EGL hardware-acceleration enabled.");
} }
@ -309,7 +310,9 @@ impl State {
WinitEvent::Focus(true) => { WinitEvent::Focus(true) => {
for seat in self.common.shell.read().seats.iter() { for seat in self.common.shell.read().seats.iter() {
let devices = seat.user_data().get::<Devices>().unwrap(); let devices = seat.user_data().get::<Devices>().unwrap();
if devices.has_device(&WinitVirtualDevice) { if devices
.has_device(&WinitVirtualDevice, &crate::input::InputBackendId::Normal)
{
seat.set_active_output(&self.backend.winit().output); seat.set_active_output(&self.backend.winit().output);
break; break;
} }
@ -339,7 +342,9 @@ impl State {
render_ping.ping(); render_ping.ping();
} }
WinitEvent::Redraw => render_ping.ping(), WinitEvent::Redraw => render_ping.ping(),
WinitEvent::Input(event) => self.process_input_event(event), WinitEvent::Input(event) => {
self.process_input_event(event, crate::input::InputBackendId::Normal)
}
WinitEvent::CloseRequested => { WinitEvent::CloseRequested => {
self.common.should_stop = true; self.common.should_stop = true;
} }

View file

@ -6,6 +6,7 @@ use crate::{
shell::{Devices, SeatExt}, shell::{Devices, SeatExt},
state::{BackendData, Common}, state::{BackendData, Common},
utils::prelude::*, utils::prelude::*,
wayland::protocols::drm::WlDrmState,
}; };
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use cosmic_comp_config::output::comp::OutputConfig; use cosmic_comp_config::output::comp::OutputConfig;
@ -497,7 +498,7 @@ where
.common .common
.dmabuf_state .dmabuf_state
.create_global_with_default_feedback::<State>(dh, &default_feedback); .create_global_with_default_feedback::<State>(dh, &default_feedback);
let _drm_global_id = state.common.wl_drm_state.create_global::<State>( state.common.wl_drm_state = Some(WlDrmState::new::<State>(
dh, dh,
render_node render_node
.dev_path_with_type(NodeType::Render) .dev_path_with_type(NodeType::Render)
@ -508,7 +509,7 @@ where
))?, ))?,
renderer.dmabuf_formats(), renderer.dmabuf_formats(),
&dmabuf_global, &dmabuf_global,
); ));
info!("EGL hardware-acceleration enabled."); info!("EGL hardware-acceleration enabled.");
@ -533,14 +534,14 @@ impl State {
let device = event.device(); let device = event.device();
for seat in self.common.shell.read().seats.iter() { for seat in self.common.shell.read().seats.iter() {
let devices = seat.user_data().get::<Devices>().unwrap(); let devices = seat.user_data().get::<Devices>().unwrap();
if devices.has_device(&device) { if devices.has_device(&device, &crate::input::InputBackendId::Normal) {
seat.set_active_output(&output); seat.set_active_output(&output);
break; break;
} }
} }
}; };
self.process_input_event(event); self.process_input_event(event, crate::input::InputBackendId::Normal);
// TODO actually figure out the output // TODO actually figure out the output
for output in self.common.shell.read().outputs() { for output in self.common.shell.read().outputs() {
self.backend.x11().schedule_render(output); self.backend.x11().schedule_render(output);

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use crate::{ use crate::{
input::InputBackendId,
shell::Shell, shell::Shell,
state::{BackendData, State}, state::{BackendData, State},
utils::prelude::OutputExt, utils::prelude::OutputExt,
@ -820,6 +821,27 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
} }
} }
} }
let ei_connections = state
.common
.ei_keyboard_source
.keys()
.cloned()
.collect::<Vec<_>>();
for conn in &ei_connections {
state.release_ei_keyboard(conn);
state.clear_input_source_state(&InputBackendId::Ei(conn.clone()));
}
for ei_seat in state.common.ei_seats.values() {
if let Err(err) =
ei_seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&value))
{
warn!(?err, "Failed to update libei keyboard keymap");
}
}
if !state.common.ei_seats.is_empty() {
let seat = state.common.shell.read().seats.last_active().clone();
state.broadcast_ei_keyboard_modifiers(&seat);
}
state.common.config.cosmic_conf.xkb_config = value; state.common.config.cosmic_conf.xkb_config = value;
} }
"keyboard_config" => { "keyboard_config" => {

View file

@ -150,9 +150,10 @@ impl A11yKeyboardMonitorState {
} }
pub fn key_event(&self, modifiers: &ModifiersState, keysym: &KeysymHandle, state: KeyState) { pub fn key_event(&self, modifiers: &ModifiersState, keysym: &KeysymHandle, state: KeyState) {
let has_key_grab = self.has_key_grab(modifiers, keysym.modified_sym());
let clients = self.clients.lock().unwrap(); let clients = self.clients.lock().unwrap();
for (unique_name, client) in clients.0.iter() { for (unique_name, client) in clients.0.iter() {
if !client.watched && !self.has_key_grab(modifiers, keysym.modified_sym()) { if !client.watched && !has_key_grab {
continue; continue;
} }

80
src/dbus/ei.rs Normal file
View file

@ -0,0 +1,80 @@
use std::{
os::unix::net::UnixStream,
sync::{Arc, Mutex},
};
use smithay::reexports::calloop;
use zbus::names::{UniqueName, WellKnownName};
use super::name_owners::NameOwners;
static ALLOWED_NAMES: &[WellKnownName] = &[
WellKnownName::from_static_str_unchecked("org.freedesktop.impl.portal.desktop.cosmic"),
WellKnownName::from_static_str_unchecked("com.system76.CosmicOSK"),
];
/// Channel for handing the EI socketpair (and requested device types)
/// It's `None` until the EI sender side has been set up
type EiSender = Arc<Mutex<Option<calloop::channel::Sender<crate::libei::EiRequest>>>>;
struct Ei {
ei_sender: EiSender,
name_owners: NameOwners,
}
impl Ei {
async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result<()> {
if self.name_owners.check_owner(sender, ALLOWED_NAMES).await {
Ok(())
} else {
Err(zbus::fdo::Error::AccessDenied("Access denied".to_string()))
}
}
}
#[zbus::interface(name = "com.system76.CosmicComp.Ei")]
impl Ei {
/// Create a new EI sender context
async fn get_sender_socket(
&self,
device_types: u32,
#[zbus(header)] header: zbus::message::Header<'_>,
) -> zbus::fdo::Result<zbus::zvariant::OwnedFd> {
if let Some(sender) = header.sender() {
self.check_sender_allowed(sender).await?;
}
let (comp_stream, client_stream) = UnixStream::pair().map_err(|err| {
zbus::fdo::Error::Failed(format!("Failed to create socket pair: {err}"))
})?;
{
let guard = self.ei_sender.lock().unwrap();
let sender = guard
.as_ref()
.ok_or_else(|| zbus::fdo::Error::Failed("EI sender not available".to_string()))?;
sender.send((comp_stream, device_types)).map_err(|err| {
zbus::fdo::Error::Failed(format!("Failed to hand off EI socket: {err}"))
})?;
}
Ok(std::os::fd::OwnedFd::from(client_stream).into())
}
}
/// Register the `com.system76.CosmicComp.Ei` interface on the shared session connection.
pub async fn init(
conn: &zbus::Connection,
name_owners: &NameOwners,
ei_sender: EiSender,
) -> zbus::Result<()> {
let ei = Ei {
ei_sender,
name_owners: name_owners.clone(),
};
conn.object_server()
.at("/com/system76/CosmicComp/Ei", ei)
.await?;
conn.request_name("com.system76.CosmicComp").await?;
Ok(())
}

View file

@ -9,11 +9,13 @@ use std::{
cell::{RefCell, RefMut}, cell::{RefCell, RefMut},
collections::HashMap, collections::HashMap,
rc::Rc, rc::Rc,
sync::{Arc, Mutex},
}; };
use tracing::{error, warn}; use tracing::{error, warn};
pub mod a11y_keyboard_monitor; pub mod a11y_keyboard_monitor;
use a11y_keyboard_monitor::A11yKeyboardMonitorState; use a11y_keyboard_monitor::A11yKeyboardMonitorState;
pub mod ei;
#[cfg(feature = "logind")] #[cfg(feature = "logind")]
pub mod logind; pub mod logind;
mod name_owners; mod name_owners;
@ -29,6 +31,7 @@ struct DBusStateInner {
session_conn: zbus::Result<zbus::Connection>, session_conn: zbus::Result<zbus::Connection>,
system_conn: zbus::Result<zbus::Connection>, system_conn: zbus::Result<zbus::Connection>,
a11y_keyboard_monitor: RefCell<Option<a11y_keyboard_monitor::A11yKeyboardMonitorState>>, a11y_keyboard_monitor: RefCell<Option<a11y_keyboard_monitor::A11yKeyboardMonitorState>>,
ei_sender: Arc<Mutex<Option<calloop::channel::Sender<crate::libei::EiRequest>>>>,
} }
impl DBusState { impl DBusState {
@ -42,6 +45,7 @@ impl DBusState {
session_conn, session_conn,
system_conn, system_conn,
a11y_keyboard_monitor: RefCell::new(None), a11y_keyboard_monitor: RefCell::new(None),
ei_sender: Arc::new(Mutex::new(None)),
})); }));
evlh.insert_source(source, |_, _, _| {}).unwrap(); evlh.insert_source(source, |_, _, _| {}).unwrap();
let state_clone = state.clone(); let state_clone = state.clone();
@ -65,6 +69,10 @@ impl DBusState {
RefMut::filter_map(self.0.a11y_keyboard_monitor.borrow_mut(), |x| x.as_mut()).ok() RefMut::filter_map(self.0.a11y_keyboard_monitor.borrow_mut(), |x| x.as_mut()).ok()
} }
pub fn set_ei_sender(&self, sender: calloop::channel::Sender<crate::libei::EiRequest>) {
*self.0.ei_sender.lock().unwrap() = Some(sender);
}
// TODO Lazy async init when we don't have anything blocking main thread // TODO Lazy async init when we don't have anything blocking main thread
async fn session_conn(&self) -> zbus::Result<&zbus::Connection> { async fn session_conn(&self) -> zbus::Result<&zbus::Connection> {
self.0.session_conn.as_ref().map_err(|err| err.clone()) self.0.session_conn.as_ref().map_err(|err| err.clone())
@ -85,6 +93,7 @@ async fn init_session(state: &DBusState) -> zbus::Result<()> {
let a11y_keyboard_monitor_state = let a11y_keyboard_monitor_state =
A11yKeyboardMonitorState::new(conn, &name_owners, &state.0.executor).await?; A11yKeyboardMonitorState::new(conn, &name_owners, &state.0.executor).await?;
*state.0.a11y_keyboard_monitor.borrow_mut() = Some(a11y_keyboard_monitor_state); *state.0.a11y_keyboard_monitor.borrow_mut() = Some(a11y_keyboard_monitor_state);
ei::init(conn, &name_owners, state.0.ei_sender.clone()).await?;
Ok(()) Ok(())
} }

View file

@ -2,6 +2,7 @@
use crate::{ use crate::{
config::{Action, PrivateAction}, config::{Action, PrivateAction},
input::InputBackendId,
shell::{ shell::{
FocusResult, InvalidWorkspaceIndex, MoveResult, SeatExt, Trigger, WorkspaceDelta, FocusResult, InvalidWorkspaceIndex, MoveResult, SeatExt, Trigger, WorkspaceDelta,
focus::{FocusTarget, target::KeyboardFocusTarget}, focus::{FocusTarget, target::KeyboardFocusTarget},
@ -39,6 +40,7 @@ impl State {
pub fn handle_action( pub fn handle_action(
&mut self, &mut self,
action: Action, action: Action,
backend_id: &InputBackendId,
seat: &Seat<State>, seat: &Seat<State>,
serial: Serial, serial: Serial,
time: u32, time: u32,
@ -69,7 +71,7 @@ impl State {
Action::Shortcut(action) => { Action::Shortcut(action) => {
let propagate = propagate_by_default(&action); let propagate = propagate_by_default(&action);
self.handle_shortcut_action( self.handle_shortcut_action(
action, seat, serial, time, pattern, direction, propagate, action, backend_id, seat, serial, time, pattern, direction, propagate,
) )
} }
Action::Private(PrivateAction::Escape) => { Action::Private(PrivateAction::Escape) => {
@ -143,6 +145,7 @@ impl State {
pub fn handle_shortcut_action( pub fn handle_shortcut_action(
&mut self, &mut self,
action: shortcuts::Action, action: shortcuts::Action,
backend_id: &InputBackendId,
seat: &Seat<State>, seat: &Seat<State>,
serial: Serial, serial: Serial,
time: u32, time: u32,
@ -232,6 +235,7 @@ impl State {
{ {
self.handle_shortcut_action( self.handle_shortcut_action(
Action::SwitchOutput(inferred), Action::SwitchOutput(inferred),
backend_id,
seat, seat,
serial, serial,
time, time,
@ -271,6 +275,7 @@ impl State {
{ {
self.handle_shortcut_action( self.handle_shortcut_action(
Action::SwitchOutput(inferred), Action::SwitchOutput(inferred),
backend_id,
seat, seat,
serial, serial,
time, time,
@ -392,6 +397,7 @@ impl State {
} else { } else {
Action::SendToOutput(inferred) Action::SendToOutput(inferred)
}, },
backend_id,
seat, seat,
serial, serial,
time, time,
@ -417,6 +423,7 @@ impl State {
} else { } else {
Action::SendToWorkspace(1) Action::SendToWorkspace(1)
}, },
backend_id,
seat, seat,
serial, serial,
time, time,
@ -483,6 +490,7 @@ impl State {
} else { } else {
Action::SendToOutput(inferred) Action::SendToOutput(inferred)
}, },
backend_id,
seat, seat,
serial, serial,
time, time,
@ -508,6 +516,7 @@ impl State {
} else { } else {
Action::SendToLastWorkspace Action::SendToLastWorkspace
}, },
backend_id,
seat, seat,
serial, serial,
time, time,
@ -532,7 +541,9 @@ impl State {
if propagate if propagate
&& let Some((serial, prev_output, prev_idx)) = && let Some((serial, prev_output, prev_idx)) =
shell.previous_workspace_idx.take() shell.previous_workspace_idx.take()
&& seat.last_modifier_change().is_some_and(|s| s == serial) && seat
.last_modifier_change_for(backend_id)
.is_some_and(|s| s == serial)
&& prev_output == current_output && prev_output == current_output
{ {
let _ = shell.activate( let _ = shell.activate(
@ -705,6 +716,7 @@ impl State {
if res.is_ok() { if res.is_ok() {
self.handle_shortcut_action( self.handle_shortcut_action(
Action::SwitchOutput(direction), Action::SwitchOutput(direction),
backend_id,
seat, seat,
serial, serial,
time, time,
@ -741,7 +753,8 @@ impl State {
}; };
if let Some(direction) = dir { if let Some(direction) = dir {
if let Some(last_mod_serial) = seat.last_modifier_change() { if let Some(last_mod_serial) = seat.last_modifier_change_for(backend_id)
{
let mut shell = self.common.shell.write(); let mut shell = self.common.shell.write();
if !shell if !shell
.previous_workspace_idx .previous_workspace_idx
@ -776,6 +789,7 @@ impl State {
self.handle_shortcut_action( self.handle_shortcut_action(
action, action,
backend_id,
seat, seat,
serial, serial,
time, time,
@ -800,7 +814,7 @@ impl State {
.move_current_element(direction, seat); .move_current_element(direction, seat);
match res { match res {
MoveResult::MoveFurther(_move_further) => { MoveResult::MoveFurther(_move_further) => {
if let Some(last_mod_serial) = seat.last_modifier_change() { if let Some(last_mod_serial) = seat.last_modifier_change_for(backend_id) {
let mut shell = self.common.shell.write(); let mut shell = self.common.shell.write();
if !shell if !shell
.previous_workspace_idx .previous_workspace_idx
@ -834,6 +848,7 @@ impl State {
self.handle_shortcut_action( self.handle_shortcut_action(
action, action,
backend_id,
seat, seat,
serial, serial,
time, time,

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ use smithay::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use state::{LastRefresh, State}; use state::{BackendData, LastRefresh, State};
use std::{ use std::{
env, env,
ffi::OsString, ffi::OsString,
@ -43,6 +43,7 @@ pub mod dbus;
pub mod debug; pub mod debug;
pub mod hooks; pub mod hooks;
pub mod input; pub mod input;
pub mod libei;
mod logger; mod logger;
pub mod session; pub mod session;
pub mod shell; pub mod shell;
@ -176,6 +177,10 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box<dyn Error>> {
with_xwayland, with_xwayland,
kiosk_command, kiosk_command,
); );
// Set up the libei sender side before the backend spawns Xwayland.
let ei_sender = libei::setup_ei(&event_loop.handle());
state.common.dbus_state.set_ei_sender(ei_sender);
// init backend // init backend
backend::init_backend_auto(&display, &mut event_loop, &mut state)?; backend::init_backend_auto(&display, &mut event_loop, &mut state)?;
@ -222,19 +227,17 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box<dyn Error>> {
// Kiosk child exited with status // Kiosk child exited with status
Ok(Some(exit_status)) => { Ok(Some(exit_status)) => {
info!("Command exited with status {:?}", exit_status); info!("Command exited with status {:?}", exit_status);
match exit_status.code() { // Stop cleanly so surface threads are joined before exit() (signal -> 1).
// Exiting with the same status as the kiosk child state.common.kiosk_exit_code = Some(exit_status.code().unwrap_or(1));
Some(code) => process::exit(code), state.common.should_stop = true;
// The kiosk child exited with signal, exiting with error
None => process::exit(1),
}
} }
// Command still running // Command still running
Ok(None) => {} Ok(None) => {}
// Kiosk child disappeared, exiting with error // Kiosk child disappeared, exiting with error
Err(err) => { Err(err) => {
warn!(?err, "Failed to wait for command"); warn!(?err, "Failed to wait for command");
process::exit(1); state.common.kiosk_exit_code = Some(1);
state.common.should_stop = true;
} }
} }
} }
@ -245,10 +248,32 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box<dyn Error>> {
let _ = child.kill(); let _ = child.kill();
} }
let kiosk_exit_code = state.common.kiosk_exit_code;
// Join surface threads before exit() so no thread is mid-eglCreateSync when
// Mesa's atexit handlers run and corrupt the heap (issue #2375). Safe here
// because the event loop has stopped; an unconditional join in Surface::Drop
// would instead deadlock against apply_config_for_outputs.
if let BackendData::Kms(kms) = &mut state.backend {
// Release master first so the surface drop path skips its blocking commit.
for device in kms.drm_devices.values_mut() {
device.drm.pause();
}
for device in kms.drm_devices.values_mut() {
for (_, surface) in device.inner.surfaces.drain() {
surface.drop_and_join();
}
}
}
// drop eventloop & state before logger // drop eventloop & state before logger
std::mem::drop(event_loop); std::mem::drop(event_loop);
std::mem::drop(state); std::mem::drop(state);
if let Some(code) = kiosk_exit_code {
process::exit(code);
}
Ok(()) Ok(())
} }

228
src/libei.rs Normal file
View file

@ -0,0 +1,228 @@
use std::os::unix::net::UnixStream;
use reis::eis;
use smithay::reexports::reis;
use smithay::backend::libei::{EiInput, EiInputEvent, EiRegion};
use smithay::input::keyboard::Keysym;
use smithay::reexports::calloop;
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::text_input::TextInputSeat;
use crate::config::xkb_config_to_wl;
use crate::input::InputBackendId;
use crate::state::{BackendData, State};
use crate::utils::{geometry::RectGlobalExt, prelude::OutputExt};
// Requested device types for an EI connection, mirroring the XDG RemoteDesktop portal `DeviceType` bitmask
const DEVICE_TYPE_KEYBOARD: u32 = 1;
const DEVICE_TYPE_POINTER: u32 = 2;
const DEVICE_TYPE_TOUCHSCREEN: u32 = 4;
// Name of the EI absolute-pointer device. Shared so the connect path and the
// re-advertise-on-output-change path recreate the same device.
const ABSOLUTE_POINTER_NAME: &str = "virtual absolute pointer";
pub type EiRequest = (UnixStream, u32);
/// Build the regions advertised on the EI absolute devices (absolute pointer and touch) for the
/// output layout: one region per output, each at its **global logical** offset with
/// its logical size and scale.
pub fn absolute_regions(state: &State) -> Vec<EiRegion> {
let shell = state.common.shell.read();
shell
.outputs()
.map(|output| {
let scale = output.current_scale().fractional_scale();
EiRegion {
// Keep the signed logical rect
// the u32 clamp happens at the `ei_device.region` in smithay.
rect: output.geometry().as_logical(),
scale: scale as f32,
// Tie the region to its output so a client can correlate it with the
// matching screencast stream (the portal advertises the same name).
mapping_id: Some(output.name()),
}
})
.collect()
}
/// Re-advertise the coordinate regions on every active EI seat's absolute devices.
pub fn refresh_absolute_pointer_regions(state: &State) {
if state.common.ei_seats.is_empty() {
return;
}
let regions = absolute_regions(state);
for seat in state.common.ei_seats.values() {
seat.update_regions(&regions);
}
}
pub fn setup_ei(
handle: &calloop::LoopHandle<'static, State>,
) -> calloop::channel::Sender<EiRequest> {
let (sender, channel) = calloop::channel::channel::<EiRequest>();
let handle_clone = handle.clone();
handle
.insert_source(channel, move |event, _, _| {
let calloop::channel::Event::Msg((stream, device_types)) = event else {
return;
};
let context = match eis::Context::new(stream) {
Ok(context) => context,
Err(err) => {
tracing::error!("Failed to create EI context: {}", err);
return;
}
};
let source = EiInput::new(context);
if let Err(err) =
handle_clone.insert_source(source, move |event, connection, data| match event {
EiInputEvent::Connected => {
let conn = connection.eis_connection().clone();
let seat = connection.add_seat("default");
let wants_keyboard = device_types & DEVICE_TYPE_KEYBOARD != 0;
if wants_keyboard {
let conf = data.common.config.xkb_config();
// The ei_keyboard device is given the compositor keymap; its key
// events feed the shared seat like any other keyboard.
let _ = seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&conf));
// The text device lets clients inject keysyms/utf8 directly, delivered
seat.add_text("virtual text");
}
if device_types & DEVICE_TYPE_POINTER != 0 {
seat.add_pointer("virtual pointer");
let regions = absolute_regions(data);
seat.add_pointer_absolute(ABSOLUTE_POINTER_NAME, &regions);
}
if device_types & DEVICE_TYPE_TOUCHSCREEN != 0 {
let regions = absolute_regions(data);
seat.add_touch("virtual touch", &regions);
}
// Kb-capable connections get a shared-seat source so their `ei_keyboard`
// key events feed the seat keyboard tracking.
if wants_keyboard {
data.common.ei_keyboard_source.insert(
conn.clone(),
smithay::input::keyboard::KeyboardSource::new_auxiliary(),
);
}
// Track the seat for every connection
data.common.ei_seats.insert(conn, seat);
data.update_ei_input_method();
}
EiInputEvent::Disconnected => {
let conn = connection.eis_connection().clone();
let backend_id = InputBackendId::Ei(conn.clone());
// Release any keys/modifiers and pointer buttons this remote still holds
data.release_ei_keyboard(&conn);
data.release_ei_pointer(&conn);
data.clear_input_source_state(&backend_id);
data.common.ei_seats.remove(&conn);
data.common.ei_keyboard_source.remove(&conn);
data.common.ei_pointer_buttons.remove(&conn);
data.update_ei_input_method();
// Notify the remaining libei clients of the now-cleared modifier state
let seat = data.common.shell.read().seats.last_active().clone();
data.broadcast_ei_keyboard_modifiers(&seat);
}
EiInputEvent::Event(event) => {
use smithay::backend::input::{InputEvent, KeyboardKeyEvent};
match event {
InputEvent::Keyboard { event } => {
data.inject_ei_key(
connection.eis_connection(),
event.key_code(),
event.state(),
);
}
other => {
let backend_id =
InputBackendId::Ei(connection.eis_connection().clone());
data.process_input_event(other, backend_id);
if matches!(data.backend, BackendData::Kms(_)) {
for output in data.common.shell.read().outputs() {
data.backend.kms().schedule_render(output);
}
}
}
}
}
EiInputEvent::TextKeysym { keysym, state } => {
data.inject_ei_text_keysym(connection.eis_connection(), keysym, state);
}
EiInputEvent::TextUtf8 { text } => {
data.inject_ei_text(&text);
}
})
{
tracing::error!("Failed to insert EI input source: {}", err);
}
})
.expect("Failed to insert EI channel source into the event loop");
sender
}
impl State {
/// Act as the input method for text injection while any text-capable EI connection is
/// active, so `ei_text` UTF-8 can be committed into the focused app even without a real
/// IME, but only when none is bound (a real IME always wins)
pub(crate) fn update_ei_input_method(&mut self) {
let active = !self.common.ei_keyboard_source.is_empty();
let seats = self
.common
.shell
.read()
.seats
.iter()
.cloned()
.collect::<Vec<_>>();
for seat in seats {
let has_ime = seat.input_method().has_instance();
let text_input = seat.text_input();
if active {
if !has_ime {
text_input.set_compositor_input_method(true);
}
} else {
text_input.set_compositor_input_method(false);
}
}
}
/// Inject UTF-8 text (from an EI `ei_text` device) into the focused client.
pub fn inject_ei_text(&mut self, text: &str) {
let seat = self.common.shell.read().seats.last_active().clone();
// Only commit through text-input when we're the active input method (no real IME)
if !seat.input_method().has_instance() {
let text_input = seat.text_input();
let mut injected = false;
text_input.with_active_text_input(|ti, _surface| {
ti.commit_string(Some(text.to_owned()));
injected = true;
});
if injected {
text_input.done(false);
return;
}
}
// Bind the whole chunk to spare keycodes in one temporary keymap per batch (delivered
// to just the focused client), so we change the keymap ~once per chunk instead of once
// per character. Leaves the seat's own keyboard state untouched.
let keysyms: Vec<Keysym> = text
.chars()
.map(Keysym::from_char)
.filter(|keysym| keysym.raw() != 0)
.collect();
let Some(keyboard) = seat.get_keyboard() else {
return;
};
// At most ~247 keysyms fit one spare keymap (keycodes 9..=255); leave margin.
const BATCH: usize = 240;
for batch in keysyms.chunks(BATCH) {
keyboard.inject_text_keysyms(self, batch);
}
}
}

View file

@ -838,10 +838,7 @@ impl CosmicSurface {
self.0 self.0
.send_dmabuf_feedback(output, primary_scan_out_output, |_, data| { .send_dmabuf_feedback(output, primary_scan_out_output, |_, data| {
if is_fullscreen { if is_fullscreen {
feedback &feedback.primary_scanout_feedback
.primary_scanout_feedback
.as_ref()
.unwrap_or(&feedback.render_feedback)
} else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate {
feedback feedback
.overlay_scanout_feedback .overlay_scanout_feedback

View file

@ -212,6 +212,29 @@ impl Shell {
state.common.shell.write().update_active(); state.common.shell.write().update_active();
} }
// We suppress Element(X) to Fullscreen(X) transition to avoid
// loss of focus by the X window when having a transition to fullscreen
// but in the case of X11 unmap/map the leave/enter needs to happen for the X11
// internal state to be focused on the window
pub fn set_focus_on_x11_map(
state: &mut State,
target: &KeyboardFocusTarget,
seat: &Seat<State>,
update_cursor: bool,
) {
let need_reset = seat
.get_keyboard()
.and_then(|keyboard| keyboard.current_focus())
.and_then(|current| current.x11_surface())
.is_some_and(|current| Some(current) == target.x11_surface());
if need_reset {
update_focus_state(seat, None, state, None, false);
}
Shell::set_focus(state, Some(target), seat, None, update_cursor);
}
pub fn append_focus_stack(&mut self, target: impl Into<FocusTarget>, seat: &Seat<State>) { pub fn append_focus_stack(&mut self, target: impl Into<FocusTarget>, seat: &Seat<State>) {
let target = target.into(); let target = target.into();
if target.is_minimized() { if target.is_minimized() {

View file

@ -326,7 +326,7 @@ impl KeyboardFocusTarget {
} }
} }
fn x11_surface(&self) -> Option<X11Surface> { pub fn x11_surface(&self) -> Option<X11Surface> {
match self { match self {
KeyboardFocusTarget::Element(mapped) => mapped.active_window().x11_surface().cloned(), KeyboardFocusTarget::Element(mapped) => mapped.active_window().x11_surface().cloned(),
KeyboardFocusTarget::Fullscreen(surface) => surface.x11_surface().cloned(), KeyboardFocusTarget::Fullscreen(surface) => surface.x11_surface().cloned(),

View file

@ -13,6 +13,7 @@ use smithay::{
use crate::{ use crate::{
config::key_bindings::cosmic_modifiers_from_smithay, config::key_bindings::cosmic_modifiers_from_smithay,
input::InputBackendId,
shell::{Trigger, layout::tiling::NodeDesc}, shell::{Trigger, layout::tiling::NodeDesc},
state::State, state::State,
}; };
@ -73,6 +74,7 @@ impl KeyboardGrab<State> for SwapWindowGrab {
data.handle_shortcut_action( data.handle_shortcut_action(
shortcuts::Action::Focus(direction), shortcuts::Action::Focus(direction),
&InputBackendId::Normal,
&self.seat, &self.seat,
serial, serial,
time, time,

View file

@ -5024,6 +5024,20 @@ impl Shell {
let mut output_presentation_feedback = OutputPresentationFeedback::new(output); let mut output_presentation_feedback = OutputPresentationFeedback::new(output);
if let Some(active) = self.active_space(output) { if let Some(active) = self.active_space(output) {
for fs in active.get_fullscreen_surfaces() {
fs.surface.take_presentation_feedback(
&mut output_presentation_feedback,
surface_primary_scanout_output,
|surface, _| {
surface_presentation_feedback_flags_from_states(
surface,
None,
render_element_states,
)
},
);
}
active.mapped().for_each(|mapped| { active.mapped().for_each(|mapped| {
mapped.active_window().take_presentation_feedback( mapped.active_window().take_presentation_feedback(
&mut output_presentation_feedback, &mut output_presentation_feedback,

View file

@ -5,7 +5,7 @@ use std::{any::Any, cell::RefCell, collections::HashMap, sync::Mutex};
use crate::{ use crate::{
backend::render::cursor::CursorState, backend::render::cursor::CursorState,
config::{Config, xkb_config_to_wl}, config::{Config, xkb_config_to_wl},
input::{ModifiersShortcutQueue, SupressedButtons, SupressedKeys}, input::{InputBackendId, ModifiersShortcutQueue, SupressedButtons, SupressedKeys},
state::State, state::State,
}; };
use smithay::{ use smithay::{
@ -82,12 +82,25 @@ impl Seats {
self.last_active = Some(seat.clone()); self.last_active = Some(seat.clone());
} }
pub fn for_device<D: Device>(&self, device: &D) -> Option<&Seat<State>> { pub fn for_device<D: Device>(
self.iter().find(|seat| { &self,
let userdata = seat.user_data(); device: &D,
let devices = userdata.get::<Devices>().unwrap(); backend_id: &InputBackendId,
devices.has_device(device) ) -> Option<&Seat<State>> {
}) self.iter()
.find(|seat| {
let userdata = seat.user_data();
let devices = userdata.get::<Devices>().unwrap();
devices.has_device(device, backend_id)
})
.or_else(|| {
// EI devices can be transiently unregistered while the compositor recreates
// the absolute-pointer device (e.g. on a scale/geometry change), which would
// otherwise drop all pointer/touch input until the client re-binds it. EI is
// single-seat, so fall back to the active seat here, matching the EI keyboard
// path, which always targets the active seat.
matches!(backend_id, InputBackendId::Ei(_)).then(|| self.last_active())
})
} }
} }
@ -96,6 +109,7 @@ impl Devices {
&self, &self,
device: &D, device: &D,
led_state: LedState, led_state: LedState,
backend_id: &InputBackendId,
) -> Vec<DeviceCapability> { ) -> Vec<DeviceCapability> {
let id = device.id(); let id = device.id();
let mut map = self.capabilities.borrow_mut(); let mut map = self.capabilities.borrow_mut();
@ -113,7 +127,7 @@ impl Devices {
.cloned() .cloned()
.filter(|c| map.values().flatten().all(|has| *c != *has)) .filter(|c| map.values().flatten().all(|has| *c != *has))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
map.insert(id, caps); map.insert((backend_id.clone(), id), caps);
if device.has_capability(DeviceCapability::Keyboard) if device.has_capability(DeviceCapability::Keyboard)
&& let Some(device) = <dyn Any>::downcast_ref::<InputDevice>(device) && let Some(device) = <dyn Any>::downcast_ref::<InputDevice>(device)
@ -126,11 +140,18 @@ impl Devices {
new_caps new_caps
} }
pub fn has_device<D: Device>(&self, device: &D) -> bool { /// Whether the given backend's device with this id is registered on the seat.
self.capabilities.borrow().contains_key(&device.id()) pub fn has_device<D: Device>(&self, device: &D, backend_id: &InputBackendId) -> bool {
self.capabilities
.borrow()
.contains_key(&(backend_id.clone(), device.id()))
} }
pub fn remove_device<D: Device>(&self, device: &D) -> Vec<DeviceCapability> { pub fn remove_device<D: Device>(
&self,
device: &D,
backend_id: &InputBackendId,
) -> Vec<DeviceCapability> {
let id = device.id(); let id = device.id();
let mut keyboards = self.keyboards.borrow_mut(); let mut keyboards = self.keyboards.borrow_mut();
@ -139,7 +160,7 @@ impl Devices {
} }
let mut map = self.capabilities.borrow_mut(); let mut map = self.capabilities.borrow_mut();
map.remove(&id) map.remove(&(backend_id.clone(), id))
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.filter(|c| map.values().flatten().all(|has| *c != *has)) .filter(|c| map.values().flatten().all(|has| *c != *has))
@ -155,7 +176,8 @@ impl Devices {
#[derive(Default)] #[derive(Default)]
pub struct Devices { pub struct Devices {
capabilities: RefCell<HashMap<String, Vec<DeviceCapability>>>, // Keyed by `(backend, device_id)`
capabilities: RefCell<HashMap<(InputBackendId, String), Vec<DeviceCapability>>>,
// Used for updating keyboard leds on kms backend // Used for updating keyboard leds on kms backend
keyboards: RefCell<Vec<InputDevice>>, keyboards: RefCell<Vec<InputDevice>>,
} }
@ -185,7 +207,7 @@ struct FocusedOutput(pub Mutex<Option<Output>>);
pub struct PointerConstraintHint(pub Mutex<Option<(WlSurface, Point<f64, Logical>)>>); pub struct PointerConstraintHint(pub Mutex<Option<(WlSurface, Point<f64, Logical>)>>);
#[derive(Default)] #[derive(Default)]
pub struct LastModifierChange(pub Mutex<Option<Serial>>); pub struct LastModifierChange(pub Mutex<(HashMap<InputBackendId, Serial>, Option<Serial>)>);
pub fn create_seat( pub fn create_seat(
dh: &DisplayHandle, dh: &DisplayHandle,
@ -265,6 +287,9 @@ pub trait SeatExt {
fn supressed_buttons(&self) -> &SupressedButtons; fn supressed_buttons(&self) -> &SupressedButtons;
fn modifiers_shortcut_queue(&self) -> &ModifiersShortcutQueue; fn modifiers_shortcut_queue(&self) -> &ModifiersShortcutQueue;
fn last_modifier_change(&self) -> Option<Serial>; fn last_modifier_change(&self) -> Option<Serial>;
fn last_modifier_change_for(&self, backend_id: &InputBackendId) -> Option<Serial>;
fn set_last_modifier_change(&self, backend_id: &InputBackendId, serial: Serial);
fn clear_last_modifier_change(&self, backend_id: &InputBackendId);
fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point<f64, Logical>)>; fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point<f64, Logical>)>;
fn set_pointer_constraint_hint(&self, hint: Option<(WlSurface, Point<f64, Logical>)>); fn set_pointer_constraint_hint(&self, hint: Option<(WlSurface, Point<f64, Logical>)>);
@ -343,13 +368,48 @@ impl SeatExt for Seat<State> {
} }
fn last_modifier_change(&self) -> Option<Serial> { fn last_modifier_change(&self) -> Option<Serial> {
*self self.user_data()
.user_data()
.get::<LastModifierChange>() .get::<LastModifierChange>()
.unwrap() .unwrap()
.0 .0
.lock() .lock()
.unwrap() .unwrap()
.1
}
fn last_modifier_change_for(&self, backend_id: &InputBackendId) -> Option<Serial> {
self.user_data()
.get::<LastModifierChange>()
.unwrap()
.0
.lock()
.unwrap()
.0
.get(backend_id)
.copied()
}
fn set_last_modifier_change(&self, backend_id: &InputBackendId, serial: Serial) {
let mut guard = self
.user_data()
.get::<LastModifierChange>()
.unwrap()
.0
.lock()
.unwrap();
guard.0.insert(backend_id.clone(), serial);
guard.1 = Some(serial);
}
fn clear_last_modifier_change(&self, backend_id: &InputBackendId) {
self.user_data()
.get::<LastModifierChange>()
.unwrap()
.0
.lock()
.unwrap()
.0
.remove(backend_id);
} }
fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point<f64, Logical>)> { fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point<f64, Logical>)> {

View file

@ -244,9 +244,32 @@ pub struct Common {
pub clock: Clock<Monotonic>, pub clock: Clock<Monotonic>,
pub startup_done: Arc<AtomicBool>, pub startup_done: Arc<AtomicBool>,
pub should_stop: bool, pub should_stop: bool,
pub kiosk_exit_code: Option<i32>,
pub gesture_state: Option<GestureState>, pub gesture_state: Option<GestureState>,
/// Active libei sender seats, keyed by their `eis` connection. Tracked so their virtual
/// keyboards can be re-created when the keyboard configuration changes at runtime.
pub ei_seats: std::collections::HashMap<
smithay::reexports::reis::eis::Connection,
smithay::backend::libei::EiInputSeat,
>,
/// The shared-seat [`KeyboardSource`] assigned to each libei connection, so its
/// `ei_keyboard` key events feed the seat keyboard with independent per-source hold
/// tracking (and can be released together on disconnect). Keyed by connection.
pub ei_keyboard_source: std::collections::HashMap<
smithay::reexports::reis::eis::Connection,
smithay::input::keyboard::KeyboardSource,
>,
/// Pointer buttons currently held by each libei connection, so they can be released when the
/// connection drops
pub ei_pointer_buttons: std::collections::HashMap<
smithay::reexports::reis::eis::Connection,
std::collections::HashSet<u32>,
>,
pub kiosk_child: Option<Child>, pub kiosk_child: Option<Child>,
pub theme: cosmic::Theme, pub theme: cosmic::Theme,
@ -275,7 +298,7 @@ pub struct Common {
pub idle_inhibiting_surfaces: HashSet<WlSurface>, pub idle_inhibiting_surfaces: HashSet<WlSurface>,
pub shm_state: ShmState, pub shm_state: ShmState,
pub cursor_shape_manager_state: CursorShapeManagerState, pub cursor_shape_manager_state: CursorShapeManagerState,
pub wl_drm_state: WlDrmState<Option<DrmNode>>, pub wl_drm_state: Option<WlDrmState<Option<DrmNode>>>,
pub viewporter_state: ViewporterState, pub viewporter_state: ViewporterState,
pub kde_decoration_state: KdeDecorationState, pub kde_decoration_state: KdeDecorationState,
pub xdg_decoration_state: XdgDecorationState, pub xdg_decoration_state: XdgDecorationState,
@ -325,7 +348,7 @@ pub enum LockedBackend<'a> {
pub struct SurfaceDmabufFeedback { pub struct SurfaceDmabufFeedback {
pub render_feedback: DmabufFeedback, pub render_feedback: DmabufFeedback,
pub overlay_scanout_feedback: Option<DmabufFeedback>, pub overlay_scanout_feedback: Option<DmabufFeedback>,
pub primary_scanout_feedback: Option<DmabufFeedback>, pub primary_scanout_feedback: DmabufFeedback,
} }
#[derive(Debug)] #[derive(Debug)]
@ -670,7 +693,7 @@ impl State {
let cursor_shape_manager_state = CursorShapeManagerState::new::<State>(dh); let cursor_shape_manager_state = CursorShapeManagerState::new::<State>(dh);
let seat_state = SeatState::<Self>::new(); let seat_state = SeatState::<Self>::new();
let viewporter_state = ViewporterState::new::<Self>(dh); let viewporter_state = ViewporterState::new::<Self>(dh);
let wl_drm_state = WlDrmState::<Option<DrmNode>>::default(); let wl_drm_state = None;
let kde_decoration_state = KdeDecorationState::new::<Self>(dh, Mode::Client); let kde_decoration_state = KdeDecorationState::new::<Self>(dh, Mode::Client);
let xdg_decoration_state = XdgDecorationState::new::<Self>(dh); let xdg_decoration_state = XdgDecorationState::new::<Self>(dh);
let session_lock_manager_state = let session_lock_manager_state =
@ -754,7 +777,11 @@ impl State {
clock, clock,
startup_done: Arc::new(AtomicBool::new(false)), startup_done: Arc::new(AtomicBool::new(false)),
should_stop: false, should_stop: false,
kiosk_exit_code: None,
gesture_state: None, gesture_state: None,
ei_seats: std::collections::HashMap::new(),
ei_keyboard_source: std::collections::HashMap::new(),
ei_pointer_buttons: std::collections::HashMap::new(),
kiosk_child: None, kiosk_child: None,
theme: cosmic::theme::system_preference(), theme: cosmic::theme::system_preference(),
@ -954,8 +981,11 @@ impl Common {
); );
if let Some(output) = primary_scanout_output { if let Some(output) = primary_scanout_output {
with_fractional_scale(states, |fraction_scale| { with_fractional_scale(states, |fraction_scale| {
fraction_scale // The 1.0 clamp is a workaround for Chromium
.set_preferred_scale(output.current_scale().fractional_scale()); // TODO: remove if Chromium ever gets fixed
fraction_scale.set_preferred_scale(
output.current_scale().fractional_scale().max(1.0),
);
}); });
} }
} }
@ -1061,10 +1091,7 @@ impl Common {
surface, surface,
render_element_states, render_element_states,
&feedback.render_feedback, &feedback.render_feedback,
feedback &feedback.primary_scanout_feedback,
.primary_scanout_feedback
.as_ref()
.unwrap_or(&feedback.render_feedback),
) )
}, },
) )

28
src/utils/global.rs Normal file
View file

@ -0,0 +1,28 @@
use std::time::Duration;
use calloop::{
LoopHandle,
timer::{TimeoutAction, Timer},
};
use smithay::reexports::wayland_server::DisplayHandle;
use wayland_backend::server::GlobalId;
pub fn remove_global_with_timer<D: 'static>(
dh: &DisplayHandle,
event_loop_handle: &LoopHandle<D>,
id: GlobalId,
) {
dh.disable_global::<D>(id.clone());
let source = Timer::from_duration(Duration::from_secs(5));
let dh = dh.clone();
let res = event_loop_handle.insert_source(source, move |_, _, _state| {
dh.remove_global::<D>(id.clone());
TimeoutAction::Drop
});
if let Err(err) = res {
tracing::error!(
"failed to insert timer source to destroy output global: {}",
err
);
}
}

View file

@ -4,6 +4,7 @@ pub mod env;
mod ids; mod ids;
pub(crate) use self::ids::id_gen; pub(crate) use self::ids::id_gen;
pub mod geometry; pub mod geometry;
pub mod global;
pub mod iced; pub mod iced;
pub mod prelude; pub mod prelude;
pub mod quirks; pub mod quirks;

View file

@ -40,7 +40,7 @@ impl DmabufHandler for State {
fn new_surface_feedback( fn new_surface_feedback(
&mut self, &mut self,
surface: &WlSurface, surface: &WlSurface,
global: &DmabufGlobal, _global: &DmabufGlobal,
) -> Option<DmabufFeedback> { ) -> Option<DmabufFeedback> {
let BackendData::Kms(kms) = &self.backend else { let BackendData::Kms(kms) = &self.backend else {
return None; return None;
@ -57,18 +57,7 @@ impl DmabufHandler for State {
f.ended_at.is_none() && f.surface.has_surface(surface, WindowSurfaceType::all()) f.ended_at.is_none() && f.surface.has_surface(surface, WindowSurfaceType::all())
}); });
let node = kms let node = kms.primary_node.read().unwrap().unwrap();
.drm_devices
.values()
.find(|device| {
device
.socket
.as_ref()
.map(|s| &s.dmabuf_global == global)
.unwrap_or(false)
})?
.inner
.render_node;
let kms_surface = kms let kms_surface = kms
.drm_devices .drm_devices
.values() .values()
@ -77,9 +66,7 @@ impl DmabufHandler for State {
Some(with_states(surface, |data| { Some(with_states(surface, |data| {
if is_fullscreen { if is_fullscreen {
feedback feedback.primary_scanout_feedback
.primary_scanout_feedback
.unwrap_or(feedback.render_feedback)
} else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate {
feedback feedback
.overlay_scanout_feedback .overlay_scanout_feedback

View file

@ -49,7 +49,10 @@ impl FractionalScaleHandler for State {
with_states(&surface, |states| { with_states(&surface, |states| {
with_fractional_scale(states, |fractional_scale| { with_fractional_scale(states, |fractional_scale| {
fractional_scale.set_preferred_scale(output.current_scale().fractional_scale()); // The 1.0 clamp is a workaround for Chromium
// TODO: remove if Chromium ever gets fixed
fractional_scale
.set_preferred_scale(output.current_scale().fractional_scale().max(1.0));
}); });
}); });
} }

View file

@ -230,6 +230,16 @@ impl State {
state.common.output_configuration_state.update(); state.common.output_configuration_state.update();
}); });
// Output scale or geometry may have changed. EI absolute-pointer regions
// are immutable per device, so any connected EI client (e.g. an RDP server)
// keeps mapping with the old scale until it reconnects. Recreate the
// device with the updated region so the mapping tracks the change live.
// (drop the backend lock first: refresh borrows `self` immutably.)
drop(backend);
if !test_only {
crate::libei::refresh_absolute_pointer_regions(self);
}
true true
} }
} }

View file

@ -3,10 +3,12 @@
use crate::state::State; use crate::state::State;
use smithay::{ use smithay::{
backend::input::TabletToolDescriptor, input::pointer::CursorImageStatus, backend::input::TabletToolDescriptor, input::pointer::CursorImageStatus,
wayland::tablet_manager::TabletSeatHandler, input::tablet::TabletSeatHandler, reexports::wayland_server::protocol::wl_surface,
}; };
impl TabletSeatHandler for State { impl TabletSeatHandler for State {
type ToolFocus = wl_surface::WlSurface;
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, _image: CursorImageStatus) { fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, _image: CursorImageStatus) {
// TODO display cursor for each tablet tool // TODO display cursor for each tablet tool
} }

View file

@ -333,17 +333,15 @@ impl XdgShellHandler for State {
if let Some(ref grab) = grab { if let Some(ref grab) = grab {
if grab.has_ended() { if grab.has_ended() {
should_ungrab = true; should_ungrab = true;
} else if let Some(target) = grab.current_grab() { } else if let Some(target) = grab.current_grab()
if let Some(wl_surface) = target.wl_surface() { && let Some(wl_surface) = target.wl_surface()
if wl_surface.as_ref() == surface.wl_surface() && (wl_surface.as_ref() == surface.wl_surface()
|| smithay::desktop::PopupManager::popups_for_surface( || smithay::desktop::PopupManager::popups_for_surface(
surface.wl_surface(), surface.wl_surface(),
) )
.any(|(p, _)| p.wl_surface() == wl_surface.as_ref()) .any(|(p, _)| p.wl_surface() == wl_surface.as_ref()))
{ {
should_ungrab = true; should_ungrab = true;
}
}
} }
} }
if should_ungrab { if should_ungrab {

View file

@ -190,7 +190,7 @@ where
let radius_exists = with_states(surface.wl_surface(), |surface_data| { let radius_exists = with_states(surface.wl_surface(), |surface_data| {
let hook_id = surface_data let hook_id = surface_data
.data_map .data_map
.get_or_insert_threadsafe(|| ToplevelHookId::new(None)); .get_or_insert_threadsafe(|| LayerHookId::new(None));
let guard = hook_id.lock().unwrap(); let guard = hook_id.lock().unwrap();
guard.as_ref().map(|(_, t)| t.upgrade().is_ok()) guard.as_ref().map(|(_, t)| t.upgrade().is_ok())
}); });

View file

@ -38,7 +38,11 @@ use smithay::{
}; };
use tracing::trace; use tracing::trace;
use std::{convert::TryFrom, path::PathBuf, sync::Arc}; use std::{
convert::TryFrom,
path::PathBuf,
sync::{Arc, Mutex},
};
pub enum ImportError { pub enum ImportError {
Failed, Failed,
@ -52,19 +56,28 @@ pub trait DrmHandler<R: 'static> {
} }
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct WlDrmState<R>(std::marker::PhantomData<R>); pub struct WlDrmState<R> {
device: Arc<Mutex<DrmGlobalDeviceData>>,
global: GlobalId,
_marker: std::marker::PhantomData<R>,
}
#[derive(Debug)]
pub struct DrmGlobalDeviceData {
formats: Vec<Fourcc>,
path: PathBuf,
}
/// Data associated with a drm global. /// Data associated with a drm global.
pub struct DrmGlobalData { pub struct DrmGlobalData {
filter: Box<dyn for<'a> Fn(&'a Client) -> bool + Send + Sync>, filter: Box<dyn for<'a> Fn(&'a Client) -> bool + Send + Sync>,
formats: Arc<Vec<Fourcc>>,
device_path: PathBuf,
dmabuf_global: DmabufGlobal, dmabuf_global: DmabufGlobal,
device: Arc<Mutex<DrmGlobalDeviceData>>,
} }
pub struct DrmInstanceData { pub struct DrmInstanceData {
formats: Arc<Vec<Fourcc>>, device: Arc<Mutex<DrmGlobalDeviceData>>,
dmabuf_global: DmabufGlobal, dmabuf_global: DmabufGlobal,
} }
@ -86,16 +99,24 @@ where
data_init: &mut DataInit<'_, D>, data_init: &mut DataInit<'_, D>,
) { ) {
let data = DrmInstanceData { let data = DrmInstanceData {
formats: global_data.formats.clone(), device: global_data.device.clone(),
dmabuf_global: global_data.dmabuf_global, dmabuf_global: global_data.dmabuf_global,
}; };
let drm_instance = data_init.init(resource, data); let drm_instance = data_init.init(resource, data);
drm_instance.device(global_data.device_path.to_string_lossy().into_owned()); drm_instance.device(
global_data
.device
.lock()
.unwrap()
.path
.to_string_lossy()
.into_owned(),
);
if drm_instance.version() >= 2 { if drm_instance.version() >= 2 {
drm_instance.capabilities(wl_drm::Capability::Prime as u32); drm_instance.capabilities(wl_drm::Capability::Prime as u32);
} }
for format in global_data.formats.iter() { for format in global_data.device.lock().unwrap().formats.iter() {
if let Ok(converted) = wl_drm::Format::try_from(*format as u32) { if let Ok(converted) = wl_drm::Format::try_from(*format as u32) {
drm_instance.format(converted as u32); drm_instance.format(converted as u32);
} }
@ -148,7 +169,7 @@ where
} => { } => {
let format = match Fourcc::try_from(format) { let format = match Fourcc::try_from(format) {
Ok(format) => { Ok(format) => {
if !data.formats.contains(&format) { if !data.device.lock().unwrap().formats.contains(&format) {
drm.post_error( drm.post_error(
wl_drm::Error::InvalidFormat, wl_drm::Error::InvalidFormat,
String::from("Format not advertised by wl_drm"), String::from("Format not advertised by wl_drm"),
@ -220,13 +241,12 @@ where
} }
impl<R: 'static> WlDrmState<R> { impl<R: 'static> WlDrmState<R> {
pub fn create_global<D>( pub fn new<D>(
&mut self,
display: &DisplayHandle, display: &DisplayHandle,
device_path: PathBuf, device_path: PathBuf,
formats: FormatSet, formats: FormatSet,
dmabuf_global: &DmabufGlobal, dmabuf_global: &DmabufGlobal,
) -> GlobalId ) -> Self
where where
D: GlobalDispatch<wl_drm::WlDrm, DrmGlobalData> D: GlobalDispatch<wl_drm::WlDrm, DrmGlobalData>
+ Dispatch<wl_drm::WlDrm, DrmInstanceData> + Dispatch<wl_drm::WlDrm, DrmInstanceData>
@ -234,19 +254,16 @@ impl<R: 'static> WlDrmState<R> {
+ DmabufHandler + DmabufHandler
+ 'static, + 'static,
{ {
self.create_global_with_filter::<D, _>(display, device_path, formats, dmabuf_global, |_| { Self::new_with_filter::<D, _>(display, device_path, formats, dmabuf_global, |_| true)
true
})
} }
pub fn create_global_with_filter<D, F>( pub fn new_with_filter<D, F>(
&mut self,
display: &DisplayHandle, display: &DisplayHandle,
device_path: PathBuf, device_path: PathBuf,
formats: FormatSet, formats: FormatSet,
dmabuf_global: &DmabufGlobal, dmabuf_global: &DmabufGlobal,
client_filter: F, client_filter: F,
) -> GlobalId ) -> Self
where where
D: GlobalDispatch<wl_drm::WlDrm, DrmGlobalData> D: GlobalDispatch<wl_drm::WlDrm, DrmGlobalData>
+ Dispatch<wl_drm::WlDrm, DrmInstanceData> + Dispatch<wl_drm::WlDrm, DrmInstanceData>
@ -255,21 +272,45 @@ impl<R: 'static> WlDrmState<R> {
+ 'static, + 'static,
F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static,
{ {
let formats = Arc::new( let device = Arc::new(Mutex::new(DrmGlobalDeviceData {
formats formats: formats
.into_iter() .into_iter()
.filter(|f| f.modifier == Modifier::Invalid) .filter(|f| f.modifier == Modifier::Invalid)
.map(|f| f.code) .map(|f| f.code)
.collect(), .collect(),
); path: device_path,
}));
let data = DrmGlobalData { let data = DrmGlobalData {
filter: Box::new(client_filter), filter: Box::new(client_filter),
formats, device: device.clone(),
device_path,
dmabuf_global: *dmabuf_global, dmabuf_global: *dmabuf_global,
}; };
display.create_global::<D, wl_drm::WlDrm, _>(2, data) let global = display.create_global::<D, wl_drm::WlDrm, _>(2, data);
WlDrmState {
device,
global,
_marker: Default::default(),
}
}
pub fn update_device(&mut self, device_path: PathBuf, formats: FormatSet) {
let new_device = DrmGlobalDeviceData {
formats: formats
.into_iter()
.filter(|f| f.modifier == Modifier::Invalid)
.map(|f| f.code)
.collect(),
path: device_path,
};
*self.device.lock().unwrap() = new_device;
}
pub fn global(&self) -> &GlobalId {
&self.global
} }
} }

View file

@ -1,9 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use calloop::{ use calloop::LoopHandle;
LoopHandle,
timer::{TimeoutAction, Timer},
};
use cosmic_comp_config::output::comp::AdaptiveSync; use cosmic_comp_config::output::comp::AdaptiveSync;
use cosmic_protocols::output_management::v1::server::{ use cosmic_protocols::output_management::v1::server::{
zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1, zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1,
@ -30,7 +27,7 @@ use smithay::{
utils::{Logical, Physical, Point, Size, Transform}, utils::{Logical, Physical, Point, Size, Transform},
wayland::output::WlOutputData, wayland::output::WlOutputData,
}; };
use std::{convert::TryFrom, sync::Mutex, time::Duration}; use std::{convert::TryFrom, sync::Mutex};
mod handlers; mod handlers;
@ -519,26 +516,6 @@ where
} }
} }
fn remove_global_with_timer<D: 'static>(
dh: &DisplayHandle,
event_loop_handle: &LoopHandle<D>,
id: GlobalId,
) {
dh.disable_global::<D>(id.clone());
let source = Timer::from_duration(Duration::from_secs(5));
let dh = dh.clone();
let res = event_loop_handle.insert_source(source, move |_, _, _state| {
dh.remove_global::<D>(id.clone());
TimeoutAction::Drop
});
if let Err(err) = res {
tracing::error!(
"failed to insert timer source to destroy output global: {}",
err
);
}
}
macro_rules! delegate_output_configuration { macro_rules! delegate_output_configuration {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
@ -578,4 +555,4 @@ macro_rules! delegate_output_configuration {
} }
pub(crate) use delegate_output_configuration; pub(crate) use delegate_output_configuration;
use crate::utils::prelude::OutputExt; use crate::utils::{global::remove_global_with_timer, prelude::OutputExt};

View file

@ -115,7 +115,7 @@ impl State {
&self.common.display_handle, &self.common.display_handle,
None, None,
std::iter::empty::<(OsString, OsString)>(), std::iter::empty::<(OsString, OsString)>(),
std::iter::empty::<OsString>(), ["-enable-ei-portal"],
true, true,
Stdio::null(), Stdio::null(),
Stdio::null(), Stdio::null(),
@ -394,6 +394,7 @@ impl Common {
sym: Keysym, sym: Keysym,
code: Keycode, code: Keycode,
state: KeyState, state: KeyState,
modifiers: ModifiersState,
serial: Serial, serial: Serial,
time: u32, time: u32,
) { ) {
@ -418,7 +419,6 @@ impl Common {
.last_active() .last_active()
.get_keyboard() .get_keyboard()
.unwrap(); .unwrap();
let modifiers = keyboard.modifier_state();
let is_modifier = sym.is_modifier_key(); let is_modifier = sym.is_modifier_key();
let xstate = self.xwayland_state.as_mut().unwrap(); let xstate = self.xwayland_state.as_mut().unwrap();
@ -858,7 +858,7 @@ impl XwmHandler for State {
if let Some(target) = res { if let Some(target) = res {
let seat = shell.seats.last_active().clone(); let seat = shell.seats.last_active().clone();
std::mem::drop(shell); std::mem::drop(shell);
Shell::set_focus(self, Some(&target), &seat, None, false); Shell::set_focus_on_x11_map(self, &target, &seat, false);
} }
} }
} }