Skip to main content

blit_compositor/
imp.rs

1//! Headless Wayland compositor using `wayland-server` directly.
2//!
3//! Handles
4//! wl_compositor, wl_subcompositor, xdg_shell, wl_shm, wl_seat,
5//! wl_output, and zwp_linux_dmabuf_v1.  Pixel data is read on every
6//! commit and sent to the server via `CompositorEvent::SurfaceCommit`.
7
8use crate::input_region::{self, RegionOp};
9use crate::pointer_focus::{
10    ButtonRouting, button_routing, focus_transition, keyboard_focus_after_popup_close,
11};
12use crate::positioner::PositionerGeometry;
13use std::collections::HashMap;
14use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::mpsc;
18
19use calloop::generic::Generic;
20use calloop::{EventLoop, Interest, LoopSignal, PostAction};
21use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_device_v1::{
22    self, WpCursorShapeDeviceV1,
23};
24use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_manager_v1::{
25    self, WpCursorShapeManagerV1,
26};
27use wayland_protocols::wp::fractional_scale::v1::server::wp_fractional_scale_manager_v1::{
28    self, WpFractionalScaleManagerV1,
29};
30use wayland_protocols::wp::fractional_scale::v1::server::wp_fractional_scale_v1::WpFractionalScaleV1;
31use wayland_protocols::wp::presentation_time::server::wp_presentation::{
32    self, WpPresentation,
33};
34use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::{
35    Kind as WpPresentationFeedbackKind, WpPresentationFeedback,
36};
37use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_buffer_params_v1::{
38    self, ZwpLinuxBufferParamsV1,
39};
40use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1;
41use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::{
42    self, ZwpLinuxDmabufV1,
43};
44use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_confined_pointer_v1::ZwpConfinedPointerV1;
45use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_locked_pointer_v1::ZwpLockedPointerV1;
46use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_pointer_constraints_v1::{
47    self, ZwpPointerConstraintsV1,
48};
49use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::{
50    self, ZwpPrimarySelectionDeviceManagerV1,
51};
52use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::{
53    self, ZwpPrimarySelectionDeviceV1,
54};
55use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::{
56    self, ZwpPrimarySelectionOfferV1,
57};
58use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::{
59    self, ZwpPrimarySelectionSourceV1,
60};
61use wayland_protocols::wp::relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::{
62    self, ZwpRelativePointerManagerV1,
63};
64use wayland_protocols::wp::relative_pointer::zv1::server::zwp_relative_pointer_v1::ZwpRelativePointerV1;
65use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_manager_v3::{
66    self, ZwpTextInputManagerV3,
67};
68use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_v3::{
69    self, ZwpTextInputV3,
70};
71use wayland_protocols::wp::viewporter::server::wp_viewport::WpViewport;
72use wayland_protocols::wp::viewporter::server::wp_viewporter::{self, WpViewporter};
73use wayland_protocols::xdg::activation::v1::server::xdg_activation_token_v1::{
74    self, XdgActivationTokenV1,
75};
76use wayland_protocols::xdg::activation::v1::server::xdg_activation_v1::{
77    self, XdgActivationV1,
78};
79use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1::{
80    self, ZxdgDecorationManagerV1,
81};
82use wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::{
83    self, ZxdgToplevelDecorationV1,
84};
85use wayland_protocols::xdg::shell::server::xdg_popup::{self, XdgPopup};
86use wayland_protocols::xdg::shell::server::xdg_positioner::XdgPositioner;
87use wayland_protocols::xdg::shell::server::xdg_surface::{self, XdgSurface};
88use wayland_protocols::xdg::shell::server::xdg_toplevel::{self, XdgToplevel};
89use wayland_protocols::xdg::shell::server::xdg_wm_base::{self, XdgWmBase};
90use wayland_server::protocol::wl_buffer::WlBuffer;
91use wayland_server::protocol::wl_callback::WlCallback;
92use wayland_server::protocol::wl_compositor::WlCompositor;
93use wayland_server::protocol::wl_data_device::{self, WlDataDevice};
94use wayland_server::protocol::wl_data_device_manager::{self, WlDataDeviceManager};
95use wayland_server::protocol::wl_data_offer::{self, WlDataOffer};
96use wayland_server::protocol::wl_data_source::{self, WlDataSource};
97use wayland_server::protocol::wl_keyboard::{self, WlKeyboard};
98use wayland_server::protocol::wl_output::{self, WlOutput};
99use wayland_server::protocol::wl_pointer::{self, WlPointer};
100use wayland_server::protocol::wl_region::WlRegion;
101use wayland_server::protocol::wl_seat::{self, WlSeat};
102use wayland_server::protocol::wl_shm::{self, WlShm};
103use wayland_server::protocol::wl_shm_pool::WlShmPool;
104use wayland_server::protocol::wl_subcompositor::WlSubcompositor;
105use wayland_server::protocol::wl_subsurface::WlSubsurface;
106use wayland_server::protocol::wl_surface::WlSurface;
107use wayland_server::backend::ObjectId;
108use wayland_server::{
109    Client, DataInit, Dispatch, Display, DisplayHandle, GlobalDispatch, New, Resource,
110};
111
112// ---------------------------------------------------------------------------
113// Public types (re-exported from lib.rs)
114// ---------------------------------------------------------------------------
115
116/// Pixel data in its native format, avoiding unnecessary colorspace conversions.
117#[derive(Clone)]
118pub enum PixelData {
119    Bgra(Arc<Vec<u8>>),
120    Rgba(Arc<Vec<u8>>),
121    Nv12 {
122        data: Arc<Vec<u8>>,
123        y_stride: usize,
124        uv_stride: usize,
125    },
126    DmaBuf {
127        fd: Arc<OwnedFd>,
128        fourcc: u32,
129        modifier: u64,
130        stride: u32,
131        offset: u32,
132        /// When true the image origin is bottom-left (OpenGL convention).
133        /// The Vulkan renderer flips the V texture coordinate to display
134        /// the image right-side-up.
135        y_invert: bool,
136    },
137    /// NV12 in a single DMA-BUF (Y at offset 0, UV at uv_offset) —
138    /// zero-copy from Vulkan compute shader to VA-API encoder.
139    Nv12DmaBuf {
140        fd: Arc<OwnedFd>,
141        stride: u32,
142        uv_offset: u32,
143        width: u32,
144        height: u32,
145        /// Optional sync_fd exported from the Vulkan fence that guards the
146        /// BGRA→NV12 compute dispatch.  The consumer (encoder) must poll()
147        /// this fd before reading the NV12 data.  `None` when implicit
148        /// DMA-BUF fencing handles synchronisation (linear buffers).
149        sync_fd: Option<Arc<OwnedFd>>,
150    },
151    /// VA-API surface ready for VPP/encode — zero-copy path.
152    VaSurface {
153        surface_id: u32,
154        va_display: usize,
155        _fd: Arc<OwnedFd>,
156    },
157}
158
159/// A bitstream a compositor-resident encoder produced for exactly one
160/// client.
161///
162/// Unlike `PixelData`, this is never shared between subscribers: Vulkan
163/// Video owns one encoder per `(surface_id, client_id)`, so each viewer
164/// gets its own GOP and its own quantizer.
165pub struct EncodedFrame {
166    pub surface_id: u16,
167    pub client_id: u64,
168    pub width: u32,
169    pub height: u32,
170    pub data: Arc<Vec<u8>>,
171    pub is_keyframe: bool,
172    /// Codec flag matching `SURFACE_FRAME_CODEC_*` constants.
173    pub codec_flag: u8,
174}
175
176/// A DMA-BUF fd exported from a VA-API surface for use as a GPU
177/// renderer output target.  The compositor renders into the EGL FBO
178/// backed by this fd; the encoder references the VA-API surface by ID.
179/// Per-plane offset + pitch for multi-plane DMA-BUF import (e.g. AMD DCC).
180#[derive(Clone, Copy, Default)]
181pub struct ExternalOutputPlane {
182    pub offset: u32,
183    pub pitch: u32,
184}
185
186pub struct ExternalOutputBuffer {
187    pub fd: Arc<OwnedFd>,
188    pub fourcc: u32,
189    pub modifier: u64,
190    pub stride: u32,
191    pub offset: u32,
192    pub width: u32,
193    pub height: u32,
194    pub va_surface_id: u32,
195    pub va_display: usize,
196    /// All planes for this buffer (main surface + optional metadata planes).
197    pub planes: Vec<ExternalOutputPlane>,
198    /// NV12 output for the compute shader.  When present, the compositor
199    /// imports it into Vulkan (as buffer if linear, as image if tiled),
200    /// writes NV12 via compute, and returns Nv12DmaBuf.
201    pub nv12_fd: Option<Arc<OwnedFd>>,
202    pub nv12_stride: u32,
203    pub nv12_uv_offset: u32,
204    /// DRM format modifier for the NV12 surface (0 = linear).
205    pub nv12_modifier: u64,
206    /// NV12 surface dimensions (may be larger than width×height due to
207    /// encoder alignment, e.g. AV1 64-pixel superblock alignment).
208    pub nv12_width: u32,
209    pub nv12_height: u32,
210}
211
212pub mod drm_fourcc {
213    pub const ARGB8888: u32 = u32::from_le_bytes(*b"AR24");
214    pub const XRGB8888: u32 = u32::from_le_bytes(*b"XR24");
215    pub const ABGR8888: u32 = u32::from_le_bytes(*b"AB24");
216    pub const XBGR8888: u32 = u32::from_le_bytes(*b"XB24");
217    pub const NV12: u32 = u32::from_le_bytes(*b"NV12");
218}
219
220impl PixelData {
221    pub fn to_rgba(&self, width: u32, height: u32) -> Vec<u8> {
222        let w = width as usize;
223        let h = height as usize;
224        match self {
225            PixelData::Rgba(data) => data.as_ref().clone(),
226            PixelData::Bgra(data) => {
227                let mut rgba = Vec::with_capacity(w * h * 4);
228                for px in data.chunks_exact(4) {
229                    rgba.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
230                }
231                rgba
232            }
233            PixelData::Nv12 {
234                data,
235                y_stride,
236                uv_stride,
237            } => {
238                let y_plane_size = *y_stride * h;
239                let uv_h = h.div_ceil(2);
240                let uv_plane_size = *uv_stride * uv_h;
241                if data.len() < y_plane_size + uv_plane_size {
242                    return Vec::new();
243                }
244                let y_plane = &data[..y_plane_size];
245                let uv_plane = &data[y_plane_size..];
246                let mut rgba = Vec::with_capacity(w * h * 4);
247                for row in 0..h {
248                    for col in 0..w {
249                        let y = y_plane[row * y_stride + col];
250                        let uv_idx = (row / 2) * uv_stride + (col / 2) * 2;
251                        if uv_idx + 1 >= uv_plane.len() {
252                            rgba.extend_from_slice(&[0, 0, 0, 255]);
253                            continue;
254                        }
255                        let u = uv_plane[uv_idx];
256                        let v = uv_plane[uv_idx + 1];
257                        let [r, g, b] = yuv420_to_rgb(y, u, v);
258                        rgba.extend_from_slice(&[r, g, b, 255]);
259                    }
260                }
261                rgba
262            }
263            PixelData::DmaBuf {
264                fd,
265                fourcc,
266                stride,
267                offset,
268                ..
269            } => {
270                let raw = fd.as_raw_fd();
271                let stride_usize = *stride as usize;
272                let plane_offset = *offset as usize;
273                let map_size = plane_offset + stride_usize * h;
274                if map_size == 0 {
275                    return Vec::new();
276                }
277                // Best-effort DMA-BUF sync: try a non-blocking poll to see
278                // if the implicit GPU fence is signaled.  If it is, bracket
279                // the read with SYNC_START/SYNC_END for cache coherency.
280                // If poll fails (fd doesn't support it, e.g. Vulkan WSI) or
281                // the fence isn't ready yet, skip the sync and read anyway —
282                // a slightly stale frame is far better than a black surface.
283                const DMA_BUF_SYNC_READ: u64 = 1;
284                const DMA_BUF_SYNC_START: u64 = 0;
285                const DMA_BUF_SYNC_END: u64 = 4;
286                const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
287                let did_sync = {
288                    let mut pfd = libc::pollfd {
289                        fd: raw,
290                        events: libc::POLLIN,
291                        revents: 0,
292                    };
293                    let ready = unsafe { libc::poll(&mut pfd, 1, 0) };
294                    if ready > 0 {
295                        let s: u64 = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ;
296                        unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
297                        true
298                    } else {
299                        false
300                    }
301                };
302                let ptr = unsafe {
303                    libc::mmap(
304                        std::ptr::null_mut(),
305                        map_size,
306                        libc::PROT_READ,
307                        libc::MAP_SHARED,
308                        raw,
309                        0,
310                    )
311                };
312                if ptr == libc::MAP_FAILED {
313                    if did_sync {
314                        let s: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
315                        unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
316                    }
317                    return Vec::new();
318                }
319                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
320                let row_bytes = w * 4;
321                let mut pixels = Vec::with_capacity(w * h * 4);
322                for row in 0..h {
323                    let start = plane_offset + row * stride_usize;
324                    if start + row_bytes <= slice.len() {
325                        pixels.extend_from_slice(&slice[start..start + row_bytes]);
326                    }
327                }
328                let is_bgr_mem = matches!(*fourcc, drm_fourcc::ARGB8888 | drm_fourcc::XRGB8888);
329                let force_alpha = matches!(*fourcc, drm_fourcc::XRGB8888 | drm_fourcc::XBGR8888);
330                for px in pixels.chunks_exact_mut(4) {
331                    if is_bgr_mem {
332                        px.swap(0, 2);
333                    }
334                    if force_alpha {
335                        px[3] = 255;
336                    }
337                }
338                unsafe { libc::munmap(ptr, map_size) };
339                if did_sync {
340                    let s: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
341                    unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
342                }
343                pixels
344            }
345            PixelData::Nv12DmaBuf {
346                fd,
347                stride,
348                uv_offset,
349                width: nv12_w,
350                height: nv12_h,
351                sync_fd,
352            } => {
353                // The compositor writes BGRA → NV12 from a Vulkan compute
354                // shader into this DMA-BUF.  Wait on the fence (if any) so
355                // we don't CPU-read a half-written buffer.  Without this,
356                // thumbnails (scaled subscriptions, which need CPU RGBA for
357                // the software downscale) get garbage or stale pixels.
358                if let Some(sync) = sync_fd {
359                    let mut pfd = libc::pollfd {
360                        fd: sync.as_raw_fd(),
361                        events: libc::POLLIN,
362                        revents: 0,
363                    };
364                    // Up to 10 ms: at 60 fps we have ~16 ms of budget; we
365                    // must not block the server delivery tick for longer
366                    // than one frame's worth of time.
367                    unsafe {
368                        libc::poll(&mut pfd, 1, 10);
369                    }
370                }
371                let nw = *nv12_w as usize;
372                let nh = *nv12_h as usize;
373                let stride_usize = *stride as usize;
374                let uv_off = *uv_offset as usize;
375                let y_plane_size = stride_usize * nh;
376                let uv_h = nh.div_ceil(2);
377                let uv_plane_size = stride_usize * uv_h;
378                let map_size = uv_off + uv_plane_size;
379                if map_size == 0 || nw == 0 || nh == 0 {
380                    return Vec::new();
381                }
382                let raw = fd.as_raw_fd();
383                const DMA_BUF_SYNC_READ: u64 = 1;
384                const DMA_BUF_SYNC_START: u64 = 0;
385                const DMA_BUF_SYNC_END: u64 = 4;
386                const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
387                let s_start: u64 = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ;
388                let did_sync = unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s_start) == 0 };
389                let ptr = unsafe {
390                    libc::mmap(
391                        std::ptr::null_mut(),
392                        map_size,
393                        libc::PROT_READ,
394                        libc::MAP_SHARED,
395                        raw,
396                        0,
397                    )
398                };
399                if ptr == libc::MAP_FAILED {
400                    if did_sync {
401                        let s_end: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
402                        unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s_end) };
403                    }
404                    return Vec::new();
405                }
406                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
407                let y_plane = &slice[..y_plane_size.min(slice.len())];
408                let uv_plane = &slice[uv_off.min(slice.len())..];
409                // The caller asks for (w, h) — typically matches (nw, nh)
410                // but we guard anyway.
411                let out_w = w.min(nw);
412                let out_h = h.min(nh);
413                let mut rgba = Vec::with_capacity(w * h * 4);
414                for row in 0..out_h {
415                    for col in 0..out_w {
416                        let y_idx = row * stride_usize + col;
417                        let uv_idx = (row / 2) * stride_usize + (col / 2) * 2;
418                        if y_idx >= y_plane.len() || uv_idx + 1 >= uv_plane.len() {
419                            rgba.extend_from_slice(&[0, 0, 0, 255]);
420                            continue;
421                        }
422                        let y = y_plane[y_idx];
423                        let u = uv_plane[uv_idx];
424                        let v = uv_plane[uv_idx + 1];
425                        let [r, g, b] = yuv420_to_rgb(y, u, v);
426                        rgba.extend_from_slice(&[r, g, b, 255]);
427                    }
428                    // Pad row if caller asked for more width than we have.
429                    for _ in out_w..w {
430                        rgba.extend_from_slice(&[0, 0, 0, 255]);
431                    }
432                }
433                for _ in out_h..h {
434                    for _ in 0..w {
435                        rgba.extend_from_slice(&[0, 0, 0, 255]);
436                    }
437                }
438                unsafe { libc::munmap(ptr, map_size) };
439                if did_sync {
440                    let s_end: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
441                    unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s_end) };
442                }
443                rgba
444            }
445            PixelData::VaSurface { .. } => Vec::new(),
446        }
447    }
448
449    pub fn is_empty(&self) -> bool {
450        match self {
451            PixelData::Bgra(v) | PixelData::Rgba(v) => v.is_empty(),
452            PixelData::Nv12 { data, .. } => data.is_empty(),
453            PixelData::DmaBuf { .. }
454            | PixelData::VaSurface { .. }
455            | PixelData::Nv12DmaBuf { .. } => false,
456        }
457    }
458
459    pub fn is_dmabuf(&self) -> bool {
460        matches!(self, PixelData::DmaBuf { .. })
461    }
462
463    pub fn is_va_surface(&self) -> bool {
464        matches!(self, PixelData::VaSurface { .. })
465    }
466}
467
468#[derive(Clone)]
469pub enum CursorImage {
470    Named(String),
471    Custom {
472        hotspot_x: u16,
473        hotspot_y: u16,
474        width: u16,
475        height: u16,
476        rgba: Vec<u8>,
477    },
478    Hidden,
479}
480
481pub enum CompositorEvent {
482    SurfaceCreated {
483        surface_id: u16,
484        title: String,
485        app_id: String,
486        parent_id: u16,
487        width: u16,
488        height: u16,
489    },
490    SurfaceDestroyed {
491        surface_id: u16,
492    },
493    SurfaceCommit {
494        surface_id: u16,
495        width: u32,
496        height: u32,
497        pixels: PixelData,
498        /// CLOCK_MONOTONIC milliseconds at commit time so the server can
499        /// stamp surface frames with the source's presentation timing
500        /// rather than the (jittery) encode-delivery wall clock.
501        timestamp_ms: u32,
502    },
503    /// A compositor-resident encoder produced a bitstream for one client.
504    /// Carries its own timestamp for the same reason `SurfaceCommit` does.
505    SurfaceEncoded {
506        frame: EncodedFrame,
507        timestamp_ms: u32,
508    },
509    /// No Vulkan Video encoder could be created for this `(surface,
510    /// client)` pair, so the server must encode for that client itself.
511    VulkanEncoderUnavailable {
512        surface_id: u16,
513        client_id: u64,
514    },
515    SurfaceTitle {
516        surface_id: u16,
517        title: String,
518    },
519    SurfaceAppId {
520        surface_id: u16,
521        app_id: String,
522    },
523    SurfaceResized {
524        surface_id: u16,
525        width: u16,
526        height: u16,
527    },
528    ClipboardContent {
529        mime_type: String,
530        data: Vec<u8>,
531    },
532    SurfaceCursor {
533        surface_id: u16,
534        cursor: CursorImage,
535    },
536}
537
538pub enum CompositorCommand {
539    KeyInput {
540        surface_id: u16,
541        keycode: u32,
542        pressed: bool,
543    },
544    PointerMotion {
545        surface_id: u16,
546        x: f64,
547        y: f64,
548    },
549    PointerButton {
550        surface_id: u16,
551        button: u32,
552        pressed: bool,
553    },
554    PointerAxis {
555        surface_id: u16,
556        axis: u8,
557        value: f64,
558    },
559    SurfaceResize {
560        surface_id: u16,
561        width: u16,
562        height: u16,
563        scale_120: u16,
564    },
565    SurfaceFocus {
566        surface_id: u16,
567    },
568    SurfaceClose {
569        surface_id: u16,
570    },
571    ClipboardOffer {
572        mime_type: String,
573        data: Vec<u8>,
574    },
575    Capture {
576        surface_id: u16,
577        scale_120: u16,
578        reply: mpsc::SyncSender<Option<(u32, u32, Vec<u8>)>>,
579    },
580    RequestFrame {
581        surface_id: u16,
582    },
583    ReleaseKeys {
584        keycodes: Vec<u32>,
585    },
586    /// List available clipboard MIME types.
587    ClipboardListMimes {
588        reply: mpsc::SyncSender<Vec<String>>,
589    },
590    /// Read clipboard content for a specific MIME type.
591    ClipboardGet {
592        mime_type: String,
593        reply: mpsc::SyncSender<Option<Vec<u8>>>,
594    },
595    /// Set externally-allocated DMA-BUF fds as GPU renderer output
596    /// targets for a (surface, encoder target size) pair.  Each
597    /// per-client encoder owns its own pool of target-sized buffers;
598    /// the compositor composites at native size, then GPU-blits
599    /// (LINEAR) into each registered target so every viewer gets a
600    /// zero-copy stream at its own physical viewport.  Pass an empty
601    /// `buffers` to clear a target.
602    SetExternalOutputBuffers {
603        surface_id: u32,
604        target_w: u32,
605        target_h: u32,
606        buffers: Vec<ExternalOutputBuffer>,
607    },
608    /// Allocate a server-side BGRA "downscale target" for a per-client
609    /// encoder that doesn't import GBM buffers (NVENC, software h264,
610    /// software AV1).  After registration the renderer GPU-blits
611    /// (LINEAR) the native composite into a target-sized BGRA image
612    /// then copies it into a CPU-mapped staging buffer; the resulting
613    /// frame is delivered as `PixelData::Bgra` sized at
614    /// `(target_w, target_h)` so the per-client encoder consumes
615    /// already-downscaled pixels.  Sending the same `(surface_id,
616    /// target_w, target_h)` again is a no-op.
617    RegisterDownscaleTarget {
618        surface_id: u32,
619        target_w: u32,
620        target_h: u32,
621    },
622    /// Tear down the BGRA downscale target previously registered for
623    /// `(surface_id, target_w, target_h)`.  No-op when none exists.
624    ClearDownscaleTarget {
625        surface_id: u32,
626        target_w: u32,
627        target_h: u32,
628    },
629    /// Synthesize text input as key press/release sequences.
630    TextInput {
631        text: String,
632    },
633    /// Update the advertised output refresh rate (millihertz).
634    SetRefreshRate {
635        mhz: u32,
636    },
637    /// Set up a Vulkan Video encoder for one `(surface, client)` pair.
638    SetVulkanEncoder {
639        surface_id: u32,
640        client_id: u64,
641        codec: u8,
642        qp: u8,
643        width: u32,
644        height: u32,
645    },
646    /// Retarget one client's encoder quantizer without rebuilding it.
647    SetVulkanEncoderQp {
648        surface_id: u32,
649        client_id: u64,
650        qp: u8,
651    },
652    /// Request a keyframe from one client's Vulkan Video encoder.
653    RequestVulkanKeyframe {
654        surface_id: u32,
655        client_id: u64,
656    },
657    /// Destroy Vulkan Video encoders for a surface: one client's when
658    /// `client_id` is `Some`, every client's when it is `None`.
659    DestroyVulkanEncoder {
660        surface_id: u32,
661        client_id: Option<u64>,
662    },
663    Shutdown,
664}
665
666// ---------------------------------------------------------------------------
667// Internal state
668// ---------------------------------------------------------------------------
669
670/// Per-wl_surface state.  `pub(crate)` so render.rs can access fields.
671pub(crate) struct Surface {
672    pub surface_id: u16,
673    pub wl_surface: WlSurface,
674
675    // pending state
676    pending_buffer: Option<WlBuffer>,
677    pending_buffer_scale: i32,
678    pending_damage: bool,
679    pending_frame_callbacks: Vec<WlCallback>,
680    pending_presentation_feedbacks: Vec<WpPresentationFeedback>,
681    pending_opaque: bool,
682    /// Outer `Some` means `set_input_region` was called and is awaiting a
683    /// commit; the inner `None` is the protocol's nil region.
684    pending_input_region: Option<Option<Vec<RegionOp>>>,
685
686    // committed state
687    pub buffer_scale: i32,
688    pub is_opaque: bool,
689    /// Where this surface accepts pointer input, in surface-local
690    /// coordinates. `None` is the default: all of it.
691    input_region: Option<Vec<RegionOp>>,
692
693    // subsurface
694    pub parent_surface_id: Option<ObjectId>,
695    pending_subsurface_position: Option<(i32, i32)>,
696    pub subsurface_position: (i32, i32),
697    pub children: Vec<ObjectId>,
698
699    // xdg
700    xdg_surface: Option<XdgSurface>,
701    xdg_toplevel: Option<XdgToplevel>,
702    xdg_popup: Option<XdgPopup>,
703    pub xdg_geometry: Option<(i32, i32, i32, i32)>,
704
705    title: String,
706    app_id: String,
707
708    // viewport
709    pending_viewport_destination: Option<(i32, i32)>,
710    /// Committed viewport destination (logical size declared by client via
711    /// `wp_viewport.set_destination`).  Used by fractional-scale-aware clients
712    /// (e.g. Chromium) that render at physical resolution with `buffer_scale=1`
713    /// and rely on the viewport to declare the logical surface size.
714    pub viewport_destination: Option<(i32, i32)>,
715
716    is_cursor: bool,
717    cursor_hotspot: (i32, i32),
718}
719
720struct ShmPool {
721    resource: WlShmPool,
722    fd: OwnedFd,
723    inner: std::sync::Mutex<ShmPoolInner>,
724}
725
726struct ShmPoolInner {
727    size: usize,
728    mmap_ptr: *mut u8,
729}
730
731// Safety: the raw ptr is never shared outside the mutex; the fd and resource
732// are Send by construction.
733unsafe impl Send for ShmPoolInner {}
734
735impl ShmPool {
736    fn new(resource: WlShmPool, fd: OwnedFd, size: i32) -> Self {
737        let sz = size.max(0) as usize;
738        let ptr = if sz > 0 {
739            unsafe {
740                libc::mmap(
741                    std::ptr::null_mut(),
742                    sz,
743                    libc::PROT_READ,
744                    libc::MAP_SHARED,
745                    fd.as_raw_fd(),
746                    0,
747                )
748            }
749        } else {
750            libc::MAP_FAILED
751        };
752        ShmPool {
753            resource,
754            fd,
755            inner: std::sync::Mutex::new(ShmPoolInner {
756                size: sz,
757                mmap_ptr: if ptr == libc::MAP_FAILED {
758                    std::ptr::null_mut()
759                } else {
760                    ptr as *mut u8
761                },
762            }),
763        }
764    }
765
766    fn resize(&self, new_size: i32) {
767        let new_sz = new_size.max(0) as usize;
768        let mut inner = self.inner.lock().unwrap();
769        if new_sz <= inner.size {
770            return;
771        }
772        if !inner.mmap_ptr.is_null() {
773            unsafe {
774                libc::munmap(inner.mmap_ptr as *mut _, inner.size);
775            }
776        }
777        let ptr = unsafe {
778            libc::mmap(
779                std::ptr::null_mut(),
780                new_sz,
781                libc::PROT_READ,
782                libc::MAP_SHARED,
783                self.fd.as_raw_fd(),
784                0,
785            )
786        };
787        inner.mmap_ptr = if ptr == libc::MAP_FAILED {
788            std::ptr::null_mut()
789        } else {
790            ptr as *mut u8
791        };
792        inner.size = new_sz;
793    }
794
795    /// Run `f` with the mapped SHM region as a `&[u8]`, holding the pool
796    /// mutex for the duration. Returns `None` if the mmap is invalid.
797    /// Used by the zero-copy upload path so we can stream bytes straight
798    /// from client-shared memory into Vulkan-mapped memory without going
799    /// through an intermediate owned `Vec`.
800    fn with_mmap<F, R>(&self, f: F) -> Option<R>
801    where
802        F: FnOnce(&[u8]) -> R,
803    {
804        let inner = self.inner.lock().unwrap();
805        if inner.mmap_ptr.is_null() {
806            return None;
807        }
808        let slice = unsafe { std::slice::from_raw_parts(inner.mmap_ptr, inner.size) };
809        Some(f(slice))
810    }
811
812    fn read_buffer(
813        &self,
814        offset: i32,
815        width: i32,
816        height: i32,
817        stride: i32,
818        format: wl_shm::Format,
819    ) -> Option<(u32, u32, PixelData)> {
820        let inner = self.inner.lock().unwrap();
821        if inner.mmap_ptr.is_null() {
822            return None;
823        }
824        // Client-controlled geometry. Reject non-positive/negative values
825        // (a negative i32 cast to usize becomes a huge value) and use checked
826        // arithmetic: a crafted stride/height/offset can otherwise wrap the
827        // bounds check below and trigger an out-of-bounds read of the mmap.
828        if offset < 0 || width <= 0 || height <= 0 || stride < 0 {
829            return None;
830        }
831        let w = width as u32;
832        let h = height as u32;
833        let s = stride as usize;
834        let off = offset as usize;
835        let row_bytes = (w as usize).checked_mul(4)?;
836        let needed = s
837            .checked_mul(h as usize - 1)
838            .and_then(|body| body.checked_add(off))
839            .and_then(|n| n.checked_add(row_bytes))?;
840        if needed > inner.size {
841            return None;
842        }
843        let mut bgra = if s == row_bytes && off == 0 {
844            let total = row_bytes * h as usize;
845            unsafe { std::slice::from_raw_parts(inner.mmap_ptr, total) }.to_vec()
846        } else {
847            let mut packed = Vec::with_capacity(row_bytes * h as usize);
848            for row in 0..h as usize {
849                let src = unsafe {
850                    std::slice::from_raw_parts(inner.mmap_ptr.add(off + row * s), row_bytes)
851                };
852                packed.extend_from_slice(src);
853            }
854            packed
855        };
856        if matches!(format, wl_shm::Format::Xrgb8888 | wl_shm::Format::Xbgr8888) {
857            for px in bgra.chunks_exact_mut(4) {
858                px[3] = 255;
859            }
860        }
861        if matches!(format, wl_shm::Format::Abgr8888 | wl_shm::Format::Xbgr8888) {
862            Some((w, h, PixelData::Rgba(Arc::new(bgra))))
863        } else {
864            Some((w, h, PixelData::Bgra(Arc::new(bgra))))
865        }
866    }
867}
868
869impl Drop for ShmPool {
870    fn drop(&mut self) {
871        let inner = self.inner.get_mut().unwrap();
872        if !inner.mmap_ptr.is_null() {
873            unsafe {
874                libc::munmap(inner.mmap_ptr as *mut _, inner.size);
875            }
876        }
877    }
878}
879
880unsafe impl Send for ShmPool {}
881
882struct ShmBufferData {
883    /// Keep the pool alive for the lifetime of the buffer: wl_shm_pool.destroy
884    /// does NOT invalidate buffers created from the pool (see the wl_shm_pool
885    /// XML — "destruction does not affect wl_shm_pool.create_buffer"). Client
886    /// processes such as Chromium routinely destroy the pool immediately
887    /// after creating a buffer. Holding an Arc here keeps the mmap alive.
888    pool: Arc<ShmPool>,
889    offset: i32,
890    width: i32,
891    height: i32,
892    stride: i32,
893    format: wl_shm::Format,
894}
895
896struct DmaBufBufferData {
897    width: i32,
898    height: i32,
899    fourcc: u32,
900    modifier: u64,
901    planes: Vec<DmaBufPlane>,
902    y_invert: bool,
903}
904
905struct DmaBufPlane {
906    fd: OwnedFd,
907    offset: u32,
908    stride: u32,
909}
910
911struct DmaBufParamsPending {
912    resource: ZwpLinuxBufferParamsV1,
913    planes: Vec<DmaBufPlane>,
914    modifier: u64,
915}
916
917struct ClientState;
918struct XdgSurfaceData {
919    wl_surface_id: ObjectId,
920}
921struct XdgToplevelData {
922    wl_surface_id: ObjectId,
923}
924struct XdgPopupData {
925    wl_surface_id: ObjectId,
926}
927struct SubsurfaceData {
928    wl_surface_id: ObjectId,
929    parent_surface_id: ObjectId,
930}
931
932// -- Clipboard / data device data types --
933
934struct DataSourceData {
935    mime_types: std::sync::Mutex<Vec<String>>,
936}
937
938struct DataOfferData {
939    /// If `true`, the offer represents external (browser/CLI) clipboard data
940    /// stored in `Compositor::external_clipboard`.  Otherwise it is backed by
941    /// a Wayland `wl_data_source`.
942    external: bool,
943}
944
945/// Stored state for the external (browser/CLI) clipboard selection.
946struct ExternalClipboard {
947    mime_type: String,
948    data: Vec<u8>,
949}
950
951struct PrimarySourceData {
952    mime_types: std::sync::Mutex<Vec<String>>,
953}
954struct PrimaryOfferData {
955    external: bool,
956}
957
958// -- Activation token data --
959struct ActivationTokenData {
960    serial: u32,
961}
962
963struct PositionerState {
964    resource: XdgPositioner,
965    geometry: PositionerGeometry,
966}
967
968// ---------------------------------------------------------------------------
969// US-QWERTY character → evdev keycode mapping
970// ---------------------------------------------------------------------------
971
972/// Map an ASCII character to its evdev keycode under a US-QWERTY layout.
973/// Returns `(keycode, needs_shift)`, or `None` for characters not on the
974/// layout (non-ASCII, control chars other than \t/\n).
975fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
976    const KEY_1: u32 = 2;
977    const KEY_2: u32 = 3;
978    const KEY_3: u32 = 4;
979    const KEY_4: u32 = 5;
980    const KEY_5: u32 = 6;
981    const KEY_6: u32 = 7;
982    const KEY_7: u32 = 8;
983    const KEY_8: u32 = 9;
984    const KEY_9: u32 = 10;
985    const KEY_0: u32 = 11;
986    const KEY_MINUS: u32 = 12;
987    const KEY_EQUAL: u32 = 13;
988    const KEY_TAB: u32 = 15;
989    const KEY_Q: u32 = 16;
990    const KEY_W: u32 = 17;
991    const KEY_E: u32 = 18;
992    const KEY_R: u32 = 19;
993    const KEY_T: u32 = 20;
994    const KEY_Y: u32 = 21;
995    const KEY_U: u32 = 22;
996    const KEY_I: u32 = 23;
997    const KEY_O: u32 = 24;
998    const KEY_P: u32 = 25;
999    const KEY_LEFTBRACE: u32 = 26;
1000    const KEY_RIGHTBRACE: u32 = 27;
1001    const KEY_ENTER: u32 = 28;
1002    const KEY_A: u32 = 30;
1003    const KEY_S: u32 = 31;
1004    const KEY_D: u32 = 32;
1005    const KEY_F: u32 = 33;
1006    const KEY_G: u32 = 34;
1007    const KEY_H: u32 = 35;
1008    const KEY_J: u32 = 36;
1009    const KEY_K: u32 = 37;
1010    const KEY_L: u32 = 38;
1011    const KEY_SEMICOLON: u32 = 39;
1012    const KEY_APOSTROPHE: u32 = 40;
1013    const KEY_GRAVE: u32 = 41;
1014    const KEY_BACKSLASH: u32 = 43;
1015    const KEY_Z: u32 = 44;
1016    const KEY_X: u32 = 45;
1017    const KEY_C: u32 = 46;
1018    const KEY_V: u32 = 47;
1019    const KEY_B: u32 = 48;
1020    const KEY_N: u32 = 49;
1021    const KEY_M: u32 = 50;
1022    const KEY_COMMA: u32 = 51;
1023    const KEY_DOT: u32 = 52;
1024    const KEY_SLASH: u32 = 53;
1025    const KEY_SPACE: u32 = 57;
1026
1027    fn letter_kc(ch: char) -> u32 {
1028        match ch {
1029            'a' => KEY_A,
1030            'b' => KEY_B,
1031            'c' => KEY_C,
1032            'd' => KEY_D,
1033            'e' => KEY_E,
1034            'f' => KEY_F,
1035            'g' => KEY_G,
1036            'h' => KEY_H,
1037            'i' => KEY_I,
1038            'j' => KEY_J,
1039            'k' => KEY_K,
1040            'l' => KEY_L,
1041            'm' => KEY_M,
1042            'n' => KEY_N,
1043            'o' => KEY_O,
1044            'p' => KEY_P,
1045            'q' => KEY_Q,
1046            'r' => KEY_R,
1047            's' => KEY_S,
1048            't' => KEY_T,
1049            'u' => KEY_U,
1050            'v' => KEY_V,
1051            'w' => KEY_W,
1052            'x' => KEY_X,
1053            'y' => KEY_Y,
1054            'z' => KEY_Z,
1055            _ => KEY_SPACE,
1056        }
1057    }
1058
1059    let (kc, shift) = match ch {
1060        'a'..='z' => (letter_kc(ch), false),
1061        'A'..='Z' => (letter_kc(ch.to_ascii_lowercase()), true),
1062        '0' => (KEY_0, false),
1063        '1'..='9' => (KEY_1 + (ch as u32 - '1' as u32), false),
1064        ' ' => (KEY_SPACE, false),
1065        '-' => (KEY_MINUS, false),
1066        '=' => (KEY_EQUAL, false),
1067        '[' => (KEY_LEFTBRACE, false),
1068        ']' => (KEY_RIGHTBRACE, false),
1069        ';' => (KEY_SEMICOLON, false),
1070        '\'' => (KEY_APOSTROPHE, false),
1071        ',' => (KEY_COMMA, false),
1072        '.' => (KEY_DOT, false),
1073        '/' => (KEY_SLASH, false),
1074        '\\' => (KEY_BACKSLASH, false),
1075        '`' => (KEY_GRAVE, false),
1076        '\t' => (KEY_TAB, false),
1077        '\n' => (KEY_ENTER, false),
1078        '!' => (KEY_1, true),
1079        '@' => (KEY_2, true),
1080        '#' => (KEY_3, true),
1081        '$' => (KEY_4, true),
1082        '%' => (KEY_5, true),
1083        '^' => (KEY_6, true),
1084        '&' => (KEY_7, true),
1085        '*' => (KEY_8, true),
1086        '(' => (KEY_9, true),
1087        ')' => (KEY_0, true),
1088        '_' => (KEY_MINUS, true),
1089        '+' => (KEY_EQUAL, true),
1090        '{' => (KEY_LEFTBRACE, true),
1091        '}' => (KEY_RIGHTBRACE, true),
1092        ':' => (KEY_SEMICOLON, true),
1093        '"' => (KEY_APOSTROPHE, true),
1094        '<' => (KEY_COMMA, true),
1095        '>' => (KEY_DOT, true),
1096        '?' => (KEY_SLASH, true),
1097        '|' => (KEY_BACKSLASH, true),
1098        '~' => (KEY_GRAVE, true),
1099        _ => return None,
1100    };
1101    Some((kc, shift))
1102}
1103
1104// ---------------------------------------------------------------------------
1105// XKB modifier state tracking
1106// ---------------------------------------------------------------------------
1107
1108/// Bitmask values matching the `modifier_map` in us-qwerty.xkb.
1109const MOD_SHIFT: u32 = 1 << 0;
1110const MOD_LOCK: u32 = 1 << 1;
1111const MOD_CONTROL: u32 = 1 << 2;
1112const MOD_MOD1: u32 = 1 << 3; // Alt
1113const MOD_MOD4: u32 = 1 << 6; // Super / Meta
1114
1115/// Return the XKB modifier bit for an evdev keycode, or 0 if the key is
1116/// not a modifier.
1117fn keycode_to_mod(keycode: u32) -> u32 {
1118    match keycode {
1119        42 | 54 => MOD_SHIFT,   // ShiftLeft, ShiftRight
1120        58 => MOD_LOCK,         // CapsLock (toggled, handled separately)
1121        29 | 97 => MOD_CONTROL, // ControlLeft, ControlRight
1122        56 | 100 => MOD_MOD1,   // AltLeft, AltRight
1123        125 | 126 => MOD_MOD4,  // MetaLeft, MetaRight
1124        _ => 0,
1125    }
1126}
1127
1128/// Per-object state for a `zwp_text_input_v3` resource.
1129struct TextInputState {
1130    resource: ZwpTextInputV3,
1131    /// Whether the client has sent `enable` (text input is active).
1132    enabled: bool,
1133}
1134
1135/// Main compositor state.
1136struct Compositor {
1137    display_handle: DisplayHandle,
1138    surfaces: HashMap<ObjectId, Surface>,
1139    /// Rectangles accumulated per live `wl_region`, until a surface takes a
1140    /// copy via `set_input_region`. The resource is kept so entries from a
1141    /// client that disconnected without destroying its regions can be
1142    /// reclaimed in `cleanup_dead_surfaces`.
1143    regions: HashMap<ObjectId, (WlRegion, Vec<RegionOp>)>,
1144    toplevel_surface_ids: HashMap<u16, ObjectId>,
1145    /// Per-toplevel timestamp (`elapsed_ms`) of the last server-driven
1146    /// `RequestFrame`. Lets `handle_surface_commit` tell whether the server
1147    /// is actively pacing this surface (a viewer is connected): while it is,
1148    /// the eager per-commit frame-callback fire is suppressed so it doesn't
1149    /// drive a nested compositor (e.g. weston) into an unthrottled repaint
1150    /// loop that overruns the server's display-rate pacing. See the fallback
1151    /// note in `handle_surface_commit`.
1152    last_request_frame_ms: HashMap<u16, u32>,
1153    next_surface_id: u16,
1154    shm_pools: HashMap<ObjectId, Arc<ShmPool>>,
1155    /// Per-surface metadata (dimensions, scale, flags) populated at commit time.
1156    /// Replaces the old pixel_cache — pixel data now lives as persistent GPU
1157    /// textures inside VulkanRenderer.
1158    surface_meta: HashMap<ObjectId, super::render::SurfaceMeta>,
1159    dmabuf_params: HashMap<ObjectId, DmaBufParamsPending>,
1160    vulkan_renderer: Option<super::vulkan_render::VulkanRenderer>,
1161    output_width: i32,
1162    output_height: i32,
1163    /// Advertised refresh rate in millihertz.  Derived from the highest
1164    /// `display_fps` among connected browser clients.
1165    output_refresh_mhz: u32,
1166    /// Output scale in 1/120th units (wp_fractional_scale_v1 convention).
1167    /// 120 = 1×, 180 = 1.5×, 240 = 2×.  Derived from the browser's
1168    /// devicePixelRatio sent via C2S_SURFACE_RESIZE.
1169    output_scale_120: u16,
1170    outputs: Vec<WlOutput>,
1171    keyboards: Vec<WlKeyboard>,
1172    pointers: Vec<WlPointer>,
1173    keyboard_keymap_data: Vec<u8>,
1174    /// Currently depressed (held down) XKB modifier mask.
1175    mods_depressed: u32,
1176    /// CapsLock locked modifier mask (toggled on/off by CapsLock key).
1177    mods_locked: u32,
1178    serial: u32,
1179    event_tx: mpsc::Sender<CompositorEvent>,
1180    event_notify: Arc<dyn Fn() + Send + Sync>,
1181    loop_signal: LoopSignal,
1182    /// Pending per-(surface, target) commit data, keyed by `(sid,
1183    /// width, height)`.  Each render of one surface can produce
1184    /// several frames — one per registered per-client encoder target
1185    /// size — and each lands here as its own entry so the server sees
1186    /// one `SurfaceCommit` per target.  Value is `(log_w, log_h, pixels)`
1187    /// where the logicals are derived from the per-target physical size.
1188    pending_commits: HashMap<(u16, u32, u32), (u32, u32, PixelData)>,
1189    /// Bitstreams from compositor-resident encoders awaiting the next
1190    /// flush.  Kept apart from `pending_commits` because these are owned
1191    /// by one client each and must never be coalesced by target size.
1192    pending_encoded: Vec<EncodedFrame>,
1193    /// Latest composited (native) size per surface, used to gate
1194    /// `SurfaceResized` events.  The renderer emits one frame per
1195    /// per-client encoder target (downscaled), but `SurfaceResized`
1196    /// must reflect the compositor's native output so pointer
1197    /// coordinate mapping stays consistent regardless of how many
1198    /// clients are subscribed at what sizes.
1199    pending_native_sizes: HashMap<u16, (u32, u32, u32, u32)>,
1200    /// Toplevels that need a re-composite the next time the GPU
1201    /// pipeline is idle.  Populated when a per-client encoder target
1202    /// is installed (`SetExternalOutputBuffers` /
1203    /// `RegisterDownscaleTarget`) — without a follow-up render the
1204    /// new target buffer never gets pixels and the per-client encoder
1205    /// skips forever.  An immediate `render_tree_sized` from the
1206    /// command handler would early-return whenever the previous
1207    /// commit's GPU submit hasn't completed yet, so the work is
1208    /// deferred here and drained in the main loop after
1209    /// `try_retire_pending` clears `pending_submit`.
1210    /// Toplevels awaiting a deferred recomposite, and whether the request
1211    /// was for the encoders only.  An encoder-only recomposite re-runs the
1212    /// GPU pipeline over unchanged content — the pixels are identical to
1213    /// what the server already has, so publishing them again would only
1214    /// burn a generation and make every other viewer re-encode the frame it
1215    /// is already showing.  A content request (`false`) wins over an
1216    /// encoder-only one for the same toplevel.
1217    pending_recomposite_toplevels: HashMap<u16, bool>,
1218    focused_surface_id: u16,
1219    /// The wl_surface ObjectId the pointer is currently over (None = none).
1220    pointer_entered_id: Option<ObjectId>,
1221    /// Where the cursor last was inside `pointer_entered_id`, in surface-local
1222    /// coordinates, so a pointer created later can be entered there.
1223    pointer_entered_local: (f64, f64),
1224    /// Set after output scale change; triggers keyboard leave/re-enter
1225    /// on the next surface commit so clients have time to process the
1226    /// reconfigure before receiving new input events.
1227    pending_kb_reenter: bool,
1228
1229    gpu_device: String,
1230    verbose: bool,
1231    shutdown: Arc<AtomicBool>,
1232    /// Track last reported size per toplevel surface_id to detect changes.
1233    /// Per-toplevel: (composited_w, composited_h, logical_w, logical_h).
1234    /// Used for pointer coordinate mapping (browser→Wayland).
1235    last_reported_size: HashMap<u16, (u32, u32, u32, u32)>,
1236    /// Per-toplevel configured size.  Each surface can live in a
1237    /// differently-sized BSP pane, so we need to track sizes individually
1238    /// rather than relying on the single `output_width`/`output_height`.
1239    surface_sizes: HashMap<u16, (i32, i32)>,
1240    /// Pending positioner geometry, keyed by XdgPositioner protocol id.
1241    positioners: HashMap<ObjectId, PositionerState>,
1242    /// Active wp_fractional_scale_v1 objects.  When `output_scale_120`
1243    /// changes we send `preferred_scale` to every entry.
1244    fractional_scales: Vec<WpFractionalScaleV1>,
1245
1246    // -- Clipboard --
1247    /// Active wl_data_device objects (one per seat binding).
1248    data_devices: Vec<WlDataDevice>,
1249    /// The wl_data_source that currently owns the clipboard selection (if any).
1250    /// Cleared when the source is destroyed or replaced.
1251    selection_source: Option<WlDataSource>,
1252    /// External clipboard data offered from the browser or CLI.
1253    external_clipboard: Option<ExternalClipboard>,
1254
1255    // -- Primary selection --
1256    primary_devices: Vec<ZwpPrimarySelectionDeviceV1>,
1257    primary_source: Option<ZwpPrimarySelectionSourceV1>,
1258    external_primary: Option<ExternalClipboard>,
1259
1260    // -- Relative pointer --
1261    relative_pointers: Vec<ZwpRelativePointerV1>,
1262
1263    // -- Text input --
1264    /// Active zwp_text_input_v3 objects.  When the compositor receives
1265    /// composed text from the browser it delivers it via `commit_string`
1266    /// + `done` to the text_input object belonging to the focused surface.
1267    text_inputs: Vec<TextInputState>,
1268    /// Serial counter for `zwp_text_input_v3.done` events.  Incremented on
1269    /// every `done` event sent by the compositor.
1270    #[expect(dead_code)]
1271    text_input_serial: u32,
1272
1273    // -- Activation --
1274    next_activation_token: u32,
1275
1276    // -- Popup grab --
1277    /// Stack of grabbed xdg_popup surfaces (outermost first).  When the
1278    /// pointer clicks outside the topmost grabbed popup we send
1279    /// `xdg_popup.popup_done` to dismiss the popup chain.
1280    popup_grab_stack: Vec<ObjectId>,
1281    /// A button whose press dismissed a popup grab, and whose release must
1282    /// therefore be swallowed too.  Delivering the release alone would hand
1283    /// a client a button it never saw pressed.
1284    popup_dismiss_button: Option<u32>,
1285    /// The popup holding keyboard focus, when one does.
1286    ///
1287    /// Keyboard focus is otherwise a `u16` toplevel id resolved through
1288    /// `toplevel_surface_ids`, and a popup is in neither — it has no surface
1289    /// id at all.  So a grabbing menu could never be told it had focus, and
1290    /// GTK gates menu keynav on exactly that: Escape and the arrow keys went
1291    /// to the page behind the menu instead.  This overrides the toplevel for
1292    /// as long as the grab lasts; `keyboard_focus_wl` is the single answer to
1293    /// "who holds it".
1294    kb_focus_popup: Option<ObjectId>,
1295
1296    // -- DMA-BUF buffer hold --
1297    /// Buffers whose DMA-BUF content could not be eagerly snapshotted to
1298    /// CPU memory (e.g. tiled VRAM that cannot be mmap-read linearly, or
1299    /// fence not ready).  We hold the `WlBuffer` alive so the client
1300    /// cannot reuse it while the GPU texture still references the fd.
1301    /// Released when the surface commits a new buffer or is destroyed.
1302    held_buffers: HashMap<ObjectId, WlBuffer>,
1303
1304    // -- Cursor pixel cache --
1305    /// CPU-accessible RGBA pixels for cursor surfaces.  Cursors aren't
1306    /// GPU-composited — they're sent as cursor image events.  Updated
1307    /// at cursor surface commit time.
1308    cursor_rgba: HashMap<ObjectId, (u32, u32, Vec<u8>)>,
1309}
1310
1311impl Compositor {
1312    fn next_serial(&mut self) -> u32 {
1313        self.serial = self.serial.wrapping_add(1);
1314        self.serial
1315    }
1316
1317    /// Update internal modifier state from a key event and send
1318    /// `wl_keyboard.modifiers` to all keyboards belonging to the focused
1319    /// surface's client.  Many Wayland clients (GTK, Chromium) rely on this
1320    /// event rather than tracking modifiers from raw key events.
1321    fn update_and_send_modifiers(&mut self, keycode: u32, pressed: bool) {
1322        let m = keycode_to_mod(keycode);
1323        if m == 0 {
1324            return;
1325        }
1326        if keycode == 58 {
1327            // CapsLock toggles mods_locked on press.
1328            if pressed {
1329                self.mods_locked ^= MOD_LOCK;
1330            }
1331        } else if pressed {
1332            self.mods_depressed |= m;
1333        } else {
1334            self.mods_depressed &= !m;
1335        }
1336        let serial = self.next_serial();
1337        let focused_wl = self
1338            .toplevel_surface_ids
1339            .get(&self.focused_surface_id)
1340            .and_then(|root_id| self.surfaces.get(root_id))
1341            .map(|s| s.wl_surface.clone());
1342        for kb in &self.keyboards {
1343            if let Some(ref wl) = focused_wl
1344                && same_client(kb, wl)
1345            {
1346                kb.modifiers(serial, self.mods_depressed, 0, self.mods_locked, 0);
1347            }
1348        }
1349    }
1350
1351    /// The surface that currently holds `wl_keyboard.enter`.
1352    ///
1353    /// A grabbing popup outranks the focused toplevel: while a menu is up it
1354    /// is the thing the keyboard is talking to.
1355    fn keyboard_focus_wl(&self) -> Option<WlSurface> {
1356        if let Some(ref popup_id) = self.kb_focus_popup
1357            && let Some(surf) = self.surfaces.get(popup_id)
1358        {
1359            return Some(surf.wl_surface.clone());
1360        }
1361        self.toplevel_surface_ids
1362            .get(&self.focused_surface_id)
1363            .and_then(|root| self.surfaces.get(root))
1364            .map(|s| s.wl_surface.clone())
1365    }
1366
1367    /// `wl_keyboard.leave` (and the text-input equivalent) for `wl`.
1368    fn send_keyboard_leave(&mut self, wl: &WlSurface) {
1369        let serial = self.next_serial();
1370        for kb in &self.keyboards {
1371            if same_client(kb, wl) {
1372                kb.leave(serial, wl);
1373            }
1374        }
1375        for ti in &self.text_inputs {
1376            if same_client(&ti.resource, wl) {
1377                ti.resource.leave(wl);
1378            }
1379        }
1380    }
1381
1382    /// `wl_keyboard.enter` (and the text-input equivalent) for `wl`.
1383    fn send_keyboard_enter(&mut self, wl: &WlSurface) {
1384        let serial = self.next_serial();
1385        for kb in &self.keyboards {
1386            if same_client(kb, wl) {
1387                kb.enter(serial, wl, vec![]);
1388            }
1389        }
1390        for ti in &self.text_inputs {
1391            if same_client(&ti.resource, wl) {
1392                ti.resource.enter(wl);
1393            }
1394        }
1395    }
1396
1397    /// Hand keyboard focus to a popup that has taken a grab.
1398    ///
1399    /// The menu, not the page behind it, is what the arrow keys and Escape
1400    /// are meant for. Idempotent: a client that re-grabs an already-focused
1401    /// popup must not be sent a second `enter` with no `leave` between —
1402    /// the hazard `set_keyboard_focus` documents at length.
1403    fn focus_popup(&mut self, popup_id: &ObjectId) {
1404        if self.kb_focus_popup.as_ref() == Some(popup_id) {
1405            return;
1406        }
1407        let Some(popup_wl) = self.surfaces.get(popup_id).map(|s| s.wl_surface.clone()) else {
1408            return;
1409        };
1410        if let Some(previous) = self.keyboard_focus_wl() {
1411            self.send_keyboard_leave(&previous);
1412        }
1413        self.kb_focus_popup = Some(popup_id.clone());
1414        self.send_keyboard_enter(&popup_wl);
1415        let _ = self.display_handle.flush_clients();
1416    }
1417
1418    /// Give keyboard focus back after `popup_id` goes away.
1419    ///
1420    /// Back to the popup still grabbing underneath it, if the chain is nested
1421    /// — closing a submenu returns to its parent menu, not past both — and to
1422    /// the focused toplevel otherwise. A no-op unless that popup actually
1423    /// held focus, so dismissing an unfocused chain disturbs nothing.
1424    fn unfocus_popup(&mut self, popup_id: &ObjectId) {
1425        // The caller has already taken this popup off the stack, so what
1426        // remains is what is still grabbing beneath it.
1427        let Some(next_holder) = keyboard_focus_after_popup_close(
1428            popup_id,
1429            self.kb_focus_popup.as_ref(),
1430            &self.popup_grab_stack,
1431        ) else {
1432            return;
1433        };
1434        if let Some(going) = self.surfaces.get(popup_id).map(|s| s.wl_surface.clone()) {
1435            self.send_keyboard_leave(&going);
1436        }
1437        self.kb_focus_popup = next_holder;
1438        if let Some(next) = self.keyboard_focus_wl() {
1439            self.send_keyboard_enter(&next);
1440        }
1441        let _ = self.display_handle.flush_clients();
1442    }
1443
1444    /// Switch keyboard (and text_input) focus from the current surface to
1445    /// `new_surface_id`.  Sends `wl_keyboard.leave` to the old surface's
1446    /// client and `wl_keyboard.enter` to the new surface's client, which is
1447    /// required by the Wayland protocol when focus changes between clients.
1448    fn set_keyboard_focus(&mut self, new_surface_id: u16) {
1449        let old_id = self.focused_surface_id;
1450        if old_id == new_surface_id {
1451            // Focus unchanged: the surface already holds keyboard focus and
1452            // received its `wl_keyboard.enter` when focus first moved here
1453            // (via the change path below). Do NOT re-send it. A second
1454            // `enter` with no intervening `leave` violates the protocol and
1455            // crashes a nested compositor whose focus is already set — e.g.
1456            // weston's wayland backend takes its "this shouldn't happen"
1457            // path and calls `frame_status(output->frame)` unconditionally;
1458            // a fullscreen output has no decoration frame, so that is a NULL
1459            // deref (SIGSEGV). The browser resends `C2S_SURFACE_FOCUS` for
1460            // the already-focused surface on every click/select, so this is
1461            // hit constantly.
1462            //
1463            // Surface-id reuse after a focused surface is destroyed used to
1464            // rely on this branch to deliver the first `enter` to the new
1465            // owner; that is now handled by resetting `focused_surface_id` to
1466            // 0 on destroy, so the reused id arrives here as a real change.
1467            return;
1468        }
1469
1470        // Leave whoever actually holds focus, which is a grabbing popup if
1471        // there is one — sending the leave to the toplevel instead would
1472        // leave the menu believing it still had the keyboard, and the
1473        // toplevel receiving a leave it was never given.
1474        if let Some(old_wl) = self.keyboard_focus_wl()
1475            && (old_id != 0 || self.kb_focus_popup.is_some())
1476        {
1477            self.send_keyboard_leave(&old_wl);
1478        }
1479        // Focus moving to a toplevel outranks any open menu's grab.
1480        self.kb_focus_popup = None;
1481
1482        self.focused_surface_id = new_surface_id;
1483
1484        // Enter the new surface.
1485        if let Some(root_id) = self.toplevel_surface_ids.get(&new_surface_id)
1486            && let Some(wl_surface) = self.surfaces.get(root_id).map(|s| s.wl_surface.clone())
1487        {
1488            self.send_keyboard_enter(&wl_surface);
1489        }
1490    }
1491
1492    fn allocate_surface_id(&mut self) -> u16 {
1493        let mut id = self.next_surface_id;
1494        let start = id;
1495        loop {
1496            if !self.toplevel_surface_ids.contains_key(&id) {
1497                break;
1498            }
1499            id = id.wrapping_add(1);
1500            if id == 0 {
1501                id = 1;
1502            }
1503            if id == start {
1504                break;
1505            }
1506        }
1507        self.next_surface_id = id.wrapping_add(1);
1508        if self.next_surface_id == 0 {
1509            self.next_surface_id = 1;
1510        }
1511        id
1512    }
1513
1514    fn flush_pending_commits(&mut self) {
1515        // First emit at most one SurfaceResized per surface, derived
1516        // from the compositor's NATIVE composite size (not any
1517        // per-client downscaled target).  The server's pointer
1518        // coordinate mapping depends on the native size staying
1519        // consistent regardless of how many viewers are subscribed at
1520        // what sizes.
1521        for (surface_id, (width, height, log_w, log_h)) in self.pending_native_sizes.drain() {
1522            let prev = self.last_reported_size.get(&surface_id).copied();
1523            // Record unconditionally. The entry is not only the size the
1524            // client was told; its logical half is the denominator the
1525            // pointer path divides browser coordinates by
1526            // (`PointerMotion`). Logical size can change while the physical
1527            // size does not — an output scale change alone does exactly that
1528            // — so gating the *store* on the physical size left the ratio
1529            // stale and scaled every later coordinate by the wrong factor.
1530            // Far enough off and the hit test lands outside the window: no
1531            // hover, no cursor, clicks on nothing. Stateful, and it looks
1532            // like the mouse is simply dead.
1533            self.last_reported_size
1534                .insert(surface_id, (width, height, log_w, log_h));
1535            // The event, on the other hand, is genuinely about the size the
1536            // client sees, so it stays gated — a scale change is not a
1537            // resize to tell anyone about.
1538            if prev.map(|(pw, ph, _, _)| (pw, ph)) != Some((width, height)) {
1539                let _ = self.event_tx.send(CompositorEvent::SurfaceResized {
1540                    surface_id,
1541                    width: width as u16,
1542                    height: height as u16,
1543                });
1544            }
1545        }
1546        // Drain into a stable order so per-surface targets are emitted
1547        // in a deterministic sequence.
1548        let now_ms = elapsed_ms();
1549        #[allow(clippy::type_complexity)]
1550        let mut entries: Vec<((u16, u32, u32), (u32, u32, PixelData))> =
1551            self.pending_commits.drain().collect();
1552        entries.sort_by_key(|((sid, w, h), _)| (*sid, *w, *h));
1553        for ((surface_id, width, height), (_log_w, _log_h, pixels)) in entries {
1554            let _ = self.event_tx.send(CompositorEvent::SurfaceCommit {
1555                surface_id,
1556                width,
1557                height,
1558                pixels,
1559                timestamp_ms: now_ms,
1560            });
1561        }
1562        // Emitted after the commits so a client that owns a compositor
1563        // encoder sees its bitstream no earlier than the raw frame it was
1564        // built from.
1565        for frame in self.pending_encoded.drain(..) {
1566            let _ = self.event_tx.send(CompositorEvent::SurfaceEncoded {
1567                frame,
1568                timestamp_ms: now_ms,
1569            });
1570        }
1571        (self.event_notify)();
1572    }
1573
1574    fn read_shm_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1575        let data = buffer.data::<ShmBufferData>()?;
1576        let r = data.pool.read_buffer(
1577            data.offset,
1578            data.width,
1579            data.height,
1580            data.stride,
1581            data.format,
1582        );
1583        if r.is_none() {
1584            static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1585            let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1586            if n < 10 || n.is_multiple_of(100) {
1587                eprintln!(
1588                    "[read_shm_buffer #{n}] pool.read_buffer=None off={} {}x{} stride={} fmt={:?}",
1589                    data.offset, data.width, data.height, data.stride, data.format,
1590                );
1591            }
1592        }
1593        r
1594    }
1595
1596    fn read_dmabuf_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1597        let data = buffer.data::<DmaBufBufferData>()?;
1598        let width = data.width as u32;
1599        let height = data.height as u32;
1600        if width == 0 || height == 0 || data.planes.is_empty() {
1601            static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1602            let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1603            if n < 10 || n.is_multiple_of(100) {
1604                eprintln!(
1605                    "[read_dmabuf_buffer #{n}] empty: {}x{} planes={}",
1606                    width,
1607                    height,
1608                    data.planes.len()
1609                );
1610            }
1611            return None;
1612        }
1613        let plane = &data.planes[0];
1614        if matches!(
1615            data.fourcc,
1616            drm_fourcc::ARGB8888
1617                | drm_fourcc::XRGB8888
1618                | drm_fourcc::ABGR8888
1619                | drm_fourcc::XBGR8888
1620        ) {
1621            // Check if this is a DRM GEM fd (importable by VA-API) or an
1622            // anonymous /dmabuf heap fd (Vulkan WSI, needs CPU mmap).
1623            use std::os::fd::AsRawFd;
1624            let raw_fd = plane.fd.as_raw_fd();
1625            let _is_drm = {
1626                let mut link_buf = [0u8; 256];
1627                let path = format!("/proc/self/fd/{raw_fd}\0");
1628                let n = unsafe {
1629                    libc::readlink(
1630                        path.as_ptr() as *const _,
1631                        link_buf.as_mut_ptr() as *mut _,
1632                        255,
1633                    )
1634                };
1635                n > 0 && link_buf[..n as usize].starts_with(b"/dev/dri/")
1636            };
1637
1638            // Always dup the fd — the encoder handles both DRM GEM and
1639            // anonymous /dmabuf fds.  For /dmabuf fds, the encoder falls
1640            // back to CPU mmap internally.
1641            let owned = plane.fd.try_clone().ok()?;
1642            return Some((
1643                width,
1644                height,
1645                PixelData::DmaBuf {
1646                    fd: Arc::new(owned),
1647                    fourcc: data.fourcc,
1648                    modifier: data.modifier,
1649                    stride: plane.stride,
1650                    offset: plane.offset,
1651                    y_invert: data.y_invert,
1652                },
1653            ));
1654        }
1655        static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1656        let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1657        if n < 10 || n.is_multiple_of(100) {
1658            eprintln!(
1659                "[read_dmabuf_buffer #{n}] unsupported fourcc=0x{:08x} ({}x{}) modifier=0x{:x}",
1660                data.fourcc, width, height, data.modifier,
1661            );
1662        }
1663        None
1664    }
1665
1666    fn read_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1667        // Try SHM first, then DMA-BUF. Both paths now log their own
1668        // failures, so here we only log when the buffer matches neither
1669        // type (exotic buffer roles we don't recognise at all).
1670        if buffer.data::<ShmBufferData>().is_some() {
1671            return self.read_shm_buffer(buffer);
1672        }
1673        if buffer.data::<DmaBufBufferData>().is_some() {
1674            return self.read_dmabuf_buffer(buffer);
1675        }
1676        static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1677        let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1678        if n < 10 || n.is_multiple_of(100) {
1679            eprintln!(
1680                "[read_buffer #{n}] buffer has unknown role (neither Shm nor DmaBuf data attached)",
1681            );
1682        }
1683        None
1684    }
1685
1686    fn handle_surface_commit(&mut self, surface_id: &ObjectId) {
1687        let (root_id, toplevel_sid) = self.find_toplevel_root(surface_id);
1688
1689        // Always consume the pending buffer so the client gets a release
1690        // event.  Skipping this (e.g. when the surface has no toplevel
1691        // role yet) leaks a buffer from the client's pool on every attach,
1692        // eventually starving it and causing a hang.
1693        let had_buffer = self
1694            .surfaces
1695            .get(surface_id)
1696            .is_some_and(|s| s.pending_buffer.is_some());
1697        {
1698            static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1699            let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1700            if n < 40 || n.is_multiple_of(200) {
1701                let children = self
1702                    .surfaces
1703                    .get(surface_id)
1704                    .map(|s| s.children.len())
1705                    .unwrap_or(0);
1706                eprintln!(
1707                    "[commit-in #{n}] sid={surface_id:?} toplevel={toplevel_sid:?} root={root_id:?} had_buffer={had_buffer} children={children}",
1708                );
1709            }
1710        }
1711        self.apply_pending_state(surface_id);
1712
1713        let toplevel_sid = match toplevel_sid {
1714            Some(sid) => sid,
1715            None => {
1716                // No toplevel yet — release any held DMA-BUF buffer since
1717                // no compositing will run to consume it.
1718                if let Some(held) = self.held_buffers.remove(surface_id) {
1719                    held.release();
1720                }
1721                // Fire any pending frame callbacks so the client doesn't
1722                // stall.
1723                self.fire_surface_frame_callbacks(surface_id);
1724                let _ = self.display_handle.flush_clients();
1725                return;
1726            }
1727        };
1728
1729        self.composite_toplevel_into_pending(&root_id, toplevel_sid, false);
1730
1731        // Compositing is done — the VulkanRenderer holds its own dup'd
1732        // fd reference to the DMA-BUF via the persistent texture cache.
1733        // Release the held buffer so the client can reuse it for the
1734        // next frame.
1735        if let Some(held) = self.held_buffers.remove(surface_id) {
1736            held.release();
1737        }
1738
1739        // Fire frame callbacks after processing a commit so clients can
1740        // continue their render loop — but only as a *fallback*. When a
1741        // viewer is connected the server paces this surface via RequestFrame
1742        // at the display rate (see server tick loop); that path is then the
1743        // sole, throttled driver. Firing again here on every commit would
1744        // drive a nested compositor (e.g. weston) into an unthrottled
1745        // repaint loop — running much faster than the display and, for
1746        // weston, tripping an internal subsurface assertion. So skip the
1747        // eager fire while RequestFrame has paced this surface recently, and
1748        // fall back to it otherwise (during resize, or when no subscriber is
1749        // driving RequestFrame) so the client never stalls. The grace window
1750        // is comfortably larger than the server's slowest pacing interval
1751        // (250 ms idle blanket), so suppression stays stable while any client
1752        // is connected and self-heals within it once pacing stops.
1753        const PACING_GRACE_MS: u32 = 500;
1754        let paced = self
1755            .last_request_frame_ms
1756            .get(&toplevel_sid)
1757            .is_some_and(|&t| elapsed_ms().wrapping_sub(t) < PACING_GRACE_MS);
1758        if !paced {
1759            self.fire_frame_callbacks_for_toplevel(toplevel_sid);
1760        }
1761
1762        // After an output scale change, re-send keyboard leave/enter on
1763        // the first commit so clients (especially Firefox) resume input
1764        // processing.  Deferred to here so the client has processed the
1765        // reconfigure before we re-enter.
1766        if self.pending_kb_reenter {
1767            self.pending_kb_reenter = false;
1768            // Re-enter only the surface that actually holds keyboard focus.
1769            // Re-entering every toplevel would hand phantom focus to
1770            // unfocused clients and leave their focus set, so a later real
1771            // focus change delivers a second `enter` with no matching
1772            // `leave` — the redundant-enter that crashes nested compositors
1773            // (see `set_keyboard_focus`). The leave-then-enter pair below is
1774            // safe: the `leave` clears the client's focus before the `enter`
1775            // re-establishes it.
1776            // Whoever actually holds it, which is a grabbing popup when a
1777            // menu is open: re-entering the toplevel instead would move
1778            // focus out from under the menu on a mere scale change.
1779            if let Some(wl) = self.keyboard_focus_wl() {
1780                self.send_keyboard_leave(&wl);
1781                self.send_keyboard_enter(&wl);
1782            }
1783            let _ = self.display_handle.flush_clients();
1784        }
1785
1786        if self.verbose {
1787            let cache_entries = self.surface_meta.len();
1788            let has_pending = self
1789                .pending_commits
1790                .keys()
1791                .any(|(sid, _, _)| *sid == toplevel_sid);
1792            static COMMIT_COUNT: std::sync::atomic::AtomicU64 =
1793                std::sync::atomic::AtomicU64::new(0);
1794            let n = COMMIT_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1795            if n < 5 || n.is_multiple_of(1000) {
1796                eprintln!(
1797                    "[commit #{n}] sid={surface_id:?} root={root_id:?} cache={cache_entries} pending={has_pending} buf={had_buffer}",
1798                );
1799            }
1800        }
1801    }
1802
1803    /// Run the GPU compositor for `toplevel_sid` and store the produced
1804    /// frames into `pending_commits` / `pending_native_sizes` so they're
1805    /// drained by the next `flush_pending_commits` tick.  Drives both the
1806    /// surface-commit path (after applying a fresh client buffer) and
1807    /// the per-target registration paths (so a freshly-installed
1808    /// downscale target / external buffer pool is populated immediately
1809    /// from the most-recent surface state — without it, an idle wayland
1810    /// client never produces pixels at the new target size and the
1811    /// per-client encoder skips forever, wedging the surface).
1812    fn composite_toplevel_into_pending(
1813        &mut self,
1814        root_id: &ObjectId,
1815        toplevel_sid: u16,
1816        encoder_only: bool,
1817    ) {
1818        // Composite at the output scale so HiDPI clients are rendered
1819        // at full resolution.  Use the browser's requested size as the
1820        // target so the frame fits the canvas without letterboxing.
1821        let s120 = self.output_scale_120;
1822        let target_phys = self.surface_sizes.get(&toplevel_sid).map(|&(lw, lh)| {
1823            let pw = super::render::to_physical(lw as u32, s120 as u32);
1824            let ph = super::render::to_physical(lh as u32, s120 as u32);
1825            (pw, ph)
1826        });
1827        let composited = if let Some(ref mut vk) = self.vulkan_renderer {
1828            let composited = vk.render_tree_sized(
1829                root_id,
1830                &self.surfaces,
1831                &self.surface_meta,
1832                s120,
1833                target_phys,
1834                toplevel_sid,
1835            );
1836            self.pending_encoded.extend(vk.take_encoded_frames());
1837            composited
1838        } else {
1839            Vec::new()
1840        };
1841
1842        // Record the compositor's native size once for this surface
1843        // (used to drive SurfaceResized).  All per-target results carry
1844        // the same toplevel_sid and the native composite is produced at
1845        // `target_phys`; pull native dims from there when present, else
1846        // fall back to the largest result's dims (multi-target frames
1847        // are downscaled from one shared native composite).
1848        let s120_u32 = (s120 as u32).max(120);
1849        if let Some((nw, nh)) = target_phys {
1850            let nlog_w = (nw * 120).div_ceil(s120_u32);
1851            let nlog_h = (nh * 120).div_ceil(s120_u32);
1852            self.pending_native_sizes
1853                .insert(toplevel_sid, (nw, nh, nlog_w, nlog_h));
1854        } else if let Some((sid, nw, nh, _)) = composited
1855            .iter()
1856            .max_by_key(|(_, w, h, _)| (*w as u64) * (*h as u64))
1857        {
1858            let nlog_w = (nw * 120).div_ceil(s120_u32);
1859            let nlog_h = (nh * 120).div_ceil(s120_u32);
1860            self.pending_native_sizes
1861                .insert(*sid, (*nw, *nh, nlog_w, nlog_h));
1862        }
1863
1864        for (result_sid, w, h, pixels) in composited {
1865            if pixels.is_empty() {
1866                continue;
1867            }
1868            // The bitstreams this render produced are already collected.
1869            // The pixels are identical to what the server last saw, so
1870            // publishing them would only burn a generation and make every
1871            // other viewer re-encode the frame it is already showing.
1872            if encoder_only {
1873                continue;
1874            }
1875            let kind = match &pixels {
1876                PixelData::Bgra(_) => "bgra",
1877                PixelData::Rgba(_) => "rgba",
1878                PixelData::Nv12 { .. } => "nv12",
1879                PixelData::VaSurface { .. } => "va-surface",
1880                PixelData::Nv12DmaBuf { .. } => "nv12-dmabuf",
1881                PixelData::DmaBuf { fd, .. } => {
1882                    use std::os::fd::AsRawFd;
1883                    let raw = fd.as_raw_fd();
1884                    let mut lb = [0u8; 128];
1885                    let p = format!("/proc/self/fd/{raw}\0");
1886                    let n = unsafe {
1887                        libc::readlink(p.as_ptr() as *const _, lb.as_mut_ptr() as *mut _, 127)
1888                    };
1889                    if n > 0 && lb[..n as usize].starts_with(b"/dev/dri/") {
1890                        "dmabuf-drm"
1891                    } else {
1892                        "dmabuf-anon"
1893                    }
1894                }
1895            };
1896            if self.verbose {
1897                static LC: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1898                let lc = LC.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1899                if lc < 3 || lc.is_multiple_of(1000) {
1900                    eprintln!("[pending #{lc}] {w}x{h} kind={kind}");
1901                }
1902            }
1903            // Logical size derived from this target's physical size at
1904            // the same scale.  Each entry in `pending_commits` produces
1905            // one `SurfaceCommit` so the server can route the per-target
1906            // frame to the correct per-client encoder.
1907            let log_w = (w * 120).div_ceil(s120_u32);
1908            let log_h = (h * 120).div_ceil(s120_u32);
1909            self.pending_commits
1910                .insert((result_sid, w, h), (log_w, log_h, pixels));
1911        }
1912    }
1913
1914    /// Compute the absolute position of a surface within its toplevel by
1915    /// walking up the parent chain and summing `subsurface_position` offsets.
1916    /// The toplevel root itself has position (0, 0).
1917    fn surface_absolute_position(&self, surface_id: &ObjectId) -> (i32, i32) {
1918        let mut x = 0i32;
1919        let mut y = 0i32;
1920        let mut current = surface_id.clone();
1921        while let Some(surf) = self.surfaces.get(&current) {
1922            x += surf.subsurface_position.0;
1923            y += surf.subsurface_position.1;
1924            match surf.parent_surface_id {
1925                Some(ref parent) => current = parent.clone(),
1926                None => break,
1927            }
1928        }
1929        (x, y)
1930    }
1931
1932    fn find_toplevel_root(&self, surface_id: &ObjectId) -> (ObjectId, Option<u16>) {
1933        let mut current = surface_id.clone();
1934        loop {
1935            match self.surfaces.get(&current) {
1936                Some(surf) => {
1937                    if let Some(ref parent) = surf.parent_surface_id {
1938                        current = parent.clone();
1939                    } else {
1940                        return (
1941                            current,
1942                            if surf.surface_id > 0 {
1943                                Some(surf.surface_id)
1944                            } else {
1945                                None
1946                            },
1947                        );
1948                    }
1949                }
1950                None => return (current, None),
1951            }
1952        }
1953    }
1954
1955    fn collect_surface_tree(&self, root_id: &ObjectId) -> Vec<ObjectId> {
1956        let mut result = Vec::new();
1957        self.collect_tree_recursive(root_id, &mut result);
1958        result
1959    }
1960
1961    fn collect_tree_recursive(&self, surface_id: &ObjectId, result: &mut Vec<ObjectId>) {
1962        result.push(surface_id.clone());
1963        if let Some(surf) = self.surfaces.get(surface_id) {
1964            for child_id in &surf.children {
1965                self.collect_tree_recursive(child_id, result);
1966            }
1967        }
1968    }
1969
1970    /// Walk the surface tree rooted at `root_id` and return the topmost
1971    /// surface whose pixel bounds contain (`x`, `y`).  Returns
1972    /// `(wl_surface, local_x, local_y)` with coordinates relative to the
1973    /// hit surface.  Falls back to the root surface when nothing else matches.
1974    fn hit_test_surface_at(
1975        &self,
1976        root_id: &ObjectId,
1977        x: f64,
1978        y: f64,
1979    ) -> Option<(WlSurface, f64, f64)> {
1980        self.hit_test_recursive(root_id, x, y, 0, 0).or_else(|| {
1981            // Fallback: return the root surface with the original coords.
1982            self.surfaces
1983                .get(root_id)
1984                .map(|s| (s.wl_surface.clone(), x, y))
1985        })
1986    }
1987
1988    fn hit_test_recursive(
1989        &self,
1990        surface_id: &ObjectId,
1991        x: f64,
1992        y: f64,
1993        offset_x: i32,
1994        offset_y: i32,
1995    ) -> Option<(WlSurface, f64, f64)> {
1996        let surf = self.surfaces.get(surface_id)?;
1997        let sx = offset_x + surf.subsurface_position.0;
1998        let sy = offset_y + surf.subsurface_position.1;
1999
2000        // Children are ordered back-to-front; iterate in reverse for topmost.
2001        for child_id in surf.children.iter().rev() {
2002            if let Some(hit) = self.hit_test_recursive(child_id, x, y, sx, sy) {
2003                return Some(hit);
2004            }
2005        }
2006
2007        // Check this surface's bounds (logical coordinates).
2008        if let Some(sm) = self.surface_meta.get(surface_id) {
2009            let s = sm.scale.max(1) as f64;
2010            let (w, h) = (sm.width, sm.height);
2011            // Prefer viewport destination for logical size (fractional-scale
2012            // clients set buffer_scale=1 and declare logical size via viewport).
2013            let (lw, lh) = surf
2014                .viewport_destination
2015                .filter(|&(dw, dh)| dw > 0 && dh > 0)
2016                .map(|(dw, dh)| (dw as f64, dh as f64))
2017                .unwrap_or((w as f64 / s, h as f64 / s));
2018            let lx = x - sx as f64;
2019            let ly = y - sy as f64;
2020            if lx >= 0.0 && ly >= 0.0 && lx < lw && ly < lh {
2021                // A surface can decline input over part or all of itself, and
2022                // then the pointer belongs to whatever is behind it. Firefox
2023                // relies on this: it puts its rendering in a subsurface
2024                // covering the whole window and sets that subsurface's input
2025                // region empty, so input falls through to the toplevel where
2026                // its widget code is listening.
2027                match surf.input_region {
2028                    Some(ref ops) if !input_region::contains(ops, lx, ly) => {}
2029                    _ => return Some((surf.wl_surface.clone(), lx, ly)),
2030                }
2031            }
2032        }
2033        None
2034    }
2035
2036    /// Apply double-buffered pending state and consume the pending buffer.
2037    ///
2038    /// SHM buffers are uploaded as persistent GPU textures and released
2039    /// immediately.  DMA-BUF buffers are imported into VulkanRenderer's
2040    /// persistent texture cache and the wl_buffer is held in
2041    /// `held_buffers` so the client cannot reuse the underlying GPU
2042    /// memory while compositing reads from it.
2043    /// The held buffer is released after compositing completes in
2044    /// `handle_surface_commit`, or immediately if there is no toplevel
2045    /// to composite.  The Vulkan renderer imports DMA-BUFs on the GPU
2046    /// and handles vendor-specific tiled layouts (NVIDIA, AMD) natively
2047    /// — CPU mmap of such buffers would produce garbage or block.
2048    fn apply_pending_state(&mut self, surface_id: &ObjectId) {
2049        let (buffer, scale, is_cursor) = {
2050            let Some(surf) = self.surfaces.get_mut(surface_id) else {
2051                return;
2052            };
2053            let buffer = surf.pending_buffer.take();
2054            let scale = surf.pending_buffer_scale;
2055            surf.buffer_scale = scale;
2056            surf.viewport_destination = surf.pending_viewport_destination;
2057            surf.is_opaque = surf.pending_opaque;
2058            if let Some(region) = surf.pending_input_region.take() {
2059                surf.input_region = region;
2060            }
2061            surf.pending_damage = false;
2062            if let Some(pos) = surf.pending_subsurface_position.take() {
2063                surf.subsurface_position = pos;
2064            }
2065            (buffer, scale, surf.is_cursor)
2066        };
2067        let Some(buf) = buffer else { return };
2068
2069        // Release any previously held buffer for this surface — the new
2070        // commit supersedes it.
2071        if let Some(old) = self.held_buffers.remove(surface_id) {
2072            old.release();
2073        }
2074
2075        // Fast path for non-cursor SHM buffers: the client's mmap'd pool
2076        // has the pixels already; we copy+convert straight into Vulkan
2077        // memory and skip the `read_buffer → Vec<u8>` intermediate. Cursor
2078        // surfaces still go through the slow path because they need an
2079        // owned RGBA copy for the cursor protocol.
2080        if !is_cursor && let Some(shm) = buf.data::<ShmBufferData>() {
2081            let w = shm.width as u32;
2082            let h = shm.height as u32;
2083            let stride = shm.stride as usize;
2084            let offset = shm.offset as usize;
2085            let format = shm.format;
2086            // Reject negative/degenerate client geometry before it is cast to
2087            // usize (a negative i32 becomes a huge value that can wrap the
2088            // bounds check in the closure below).
2089            if shm.width > 0
2090                && shm.height > 0
2091                && shm.stride >= 0
2092                && shm.offset >= 0
2093                && let Some(ref mut vk) = self.vulkan_renderer
2094            {
2095                let swap_rb =
2096                    !matches!(format, wl_shm::Format::Abgr8888 | wl_shm::Format::Xbgr8888);
2097                let force_opaque =
2098                    matches!(format, wl_shm::Format::Xrgb8888 | wl_shm::Format::Xbgr8888);
2099                let row_bytes = w as usize * 4;
2100                let uploaded = shm
2101                    .pool
2102                    .with_mmap(|slice| {
2103                        // Checked arithmetic: a crafted stride/height/offset
2104                        // could otherwise wrap this sum and pass the bounds
2105                        // check, causing an out-of-bounds read of the mmap.
2106                        let needed = stride
2107                            .checked_mul(h as usize - 1)
2108                            .and_then(|body| body.checked_add(offset))
2109                            .and_then(|n| n.checked_add(row_bytes));
2110                        match needed {
2111                            Some(n) if n <= slice.len() => {}
2112                            _ => return false,
2113                        }
2114                        vk.upload_surface_shm_mmap(
2115                            surface_id,
2116                            slice,
2117                            offset,
2118                            stride,
2119                            w,
2120                            h,
2121                            swap_rb,
2122                            force_opaque,
2123                        )
2124                    })
2125                    .unwrap_or(false);
2126                if uploaded {
2127                    self.surface_meta.insert(
2128                        surface_id.clone(),
2129                        super::render::SurfaceMeta {
2130                            width: w,
2131                            height: h,
2132                            scale,
2133                            y_invert: false,
2134                        },
2135                    );
2136                    buf.release();
2137                    return;
2138                }
2139            }
2140        }
2141
2142        if let Some((w, h, pixels)) = self.read_buffer(&buf) {
2143            let y_invert = matches!(pixels, PixelData::DmaBuf { y_invert: true, .. });
2144
2145            // Upload the surface's pixel data as a persistent GPU texture.
2146            if let Some(ref mut vk) = self.vulkan_renderer {
2147                vk.upload_surface(surface_id, &pixels, w, h);
2148            }
2149
2150            // Store per-surface metadata for layout, hit-testing, etc.
2151            self.surface_meta.insert(
2152                surface_id.clone(),
2153                super::render::SurfaceMeta {
2154                    width: w,
2155                    height: h,
2156                    scale,
2157                    y_invert,
2158                },
2159            );
2160
2161            // Cursor surfaces need CPU-accessible RGBA pixels for cursor
2162            // image events (they aren't GPU-composited).
2163            if is_cursor {
2164                let rgba = pixels.to_rgba(w, h);
2165                if !rgba.is_empty() {
2166                    self.cursor_rgba.insert(surface_id.clone(), (w, h, rgba));
2167                }
2168            }
2169
2170            if pixels.is_dmabuf() {
2171                // Hold the wl_buffer alive so the client cannot reuse it
2172                // while the GPU texture still references the DMA-BUF fd.
2173                self.held_buffers.insert(surface_id.clone(), buf);
2174            } else {
2175                // SHM buffers are snapshotted into the GPU texture.
2176                // Release immediately so the client can reuse the buffer.
2177                buf.release();
2178            }
2179        } else {
2180            buf.release();
2181        }
2182    }
2183
2184    fn fire_surface_frame_callbacks(&mut self, surface_id: &ObjectId) {
2185        let (callbacks, feedbacks) = {
2186            let Some(surf) = self.surfaces.get_mut(surface_id) else {
2187                return;
2188            };
2189            (
2190                std::mem::take(&mut surf.pending_frame_callbacks),
2191                std::mem::take(&mut surf.pending_presentation_feedbacks),
2192            )
2193        };
2194        let time = elapsed_ms();
2195        for cb in callbacks {
2196            cb.done(time);
2197        }
2198        if !feedbacks.is_empty() {
2199            let (sec, nsec) = monotonic_timespec();
2200            // Send sync_output for each feedback, then presented().
2201            // refresh=0 means unknown (headless, no real display).
2202            for fb in feedbacks {
2203                for output in &self.outputs {
2204                    if same_client(&fb, output) {
2205                        fb.sync_output(output);
2206                    }
2207                }
2208                // refresh in nanoseconds (millihertz → ns: 1e12 / mhz)
2209                let refresh_ns = if self.output_refresh_mhz > 0 {
2210                    (1_000_000_000_000u64 / self.output_refresh_mhz as u64) as u32
2211                } else {
2212                    0
2213                };
2214                fb.presented(
2215                    (sec >> 32) as u32,
2216                    sec as u32,
2217                    nsec as u32,
2218                    refresh_ns,
2219                    0, // seq_hi
2220                    0, // seq_lo
2221                    WpPresentationFeedbackKind::empty(),
2222                );
2223            }
2224        }
2225    }
2226
2227    /// Forget pointer focus if it named `gone`.
2228    ///
2229    /// The pointer cannot be inside a surface that no longer exists, and
2230    /// nothing else clears this. Buttons survive a dangling id because the
2231    /// server always sends a `PointerMotion` immediately before a
2232    /// `PointerButton`, and that motion re-enters; scroll gets no such
2233    /// escort — `PointerAxis` arrives alone and ignores its own `surface_id`,
2234    /// resolving the stale id to no surface and dropping the event. So a
2235    /// dismissed context menu leaves the wheel dead until the cursor happens
2236    /// to cross into another surface.
2237    ///
2238    /// Keyboard focus is already cleared on both destroy paths for the
2239    /// equivalent reason (`focused_surface_id`); this is its pointer twin.
2240    fn forget_pointer_focus(&mut self, gone: &ObjectId) {
2241        if self.pointer_entered_id.as_ref() == Some(gone) {
2242            self.pointer_entered_id = None;
2243        }
2244    }
2245
2246    /// Remove surfaces whose underlying `WlSurface` is no longer alive.
2247    /// This handles the case where a Wayland client process exits or crashes
2248    /// without explicitly destroying its surfaces — `dispatch_clients()`
2249    /// marks the resources as dead, and we clean up here.
2250    fn cleanup_dead_surfaces(&mut self) {
2251        // Purge stale protocol objects from disconnected clients.
2252        self.fractional_scales.retain(|fs| fs.is_alive());
2253        self.outputs.retain(|o| o.is_alive());
2254        self.keyboards.retain(|k| k.is_alive());
2255        self.pointers.retain(|p| p.is_alive());
2256        self.data_devices.retain(|d| d.is_alive());
2257        self.primary_devices.retain(|d| d.is_alive());
2258        self.relative_pointers.retain(|p| p.is_alive());
2259        self.text_inputs.retain(|ti| ti.resource.is_alive());
2260        self.shm_pools.retain(|_, p| p.resource.is_alive());
2261        self.dmabuf_params.retain(|_, p| p.resource.is_alive());
2262        self.positioners.retain(|_, p| p.resource.is_alive());
2263        // A client that disconnects without destroying its regions never
2264        // sends `wl_region.destroy`, so reclaim its builder entries here.
2265        self.regions.retain(|_, (r, _)| r.is_alive());
2266
2267        let dead: Vec<ObjectId> = self
2268            .surfaces
2269            .iter()
2270            .filter(|(_, surf)| !surf.wl_surface.is_alive())
2271            .map(|(id, _)| id.clone())
2272            .collect();
2273
2274        for proto_id in &dead {
2275            self.surface_meta.remove(proto_id);
2276            if let Some(ref mut vk) = self.vulkan_renderer {
2277                vk.remove_surface(proto_id);
2278            }
2279            if let Some(held) = self.held_buffers.remove(proto_id) {
2280                held.release();
2281            }
2282            self.forget_pointer_focus(proto_id);
2283            // A crashed client's popup takes the keyboard with it otherwise:
2284            // the override would name a surface that no longer exists, and
2285            // `keyboard_focus_wl` would answer None for every later event.
2286            if self.kb_focus_popup.as_ref() == Some(proto_id) {
2287                self.popup_grab_stack.retain(|id| id != proto_id);
2288                self.kb_focus_popup = self.popup_grab_stack.last().cloned();
2289                if let Some(next) = self.keyboard_focus_wl() {
2290                    self.send_keyboard_enter(&next);
2291                }
2292            }
2293            if let Some(surf) = self.surfaces.remove(proto_id) {
2294                // Discard any pending presentation feedbacks — the surface
2295                // died before the frame was ever presented.
2296                for fb in surf.pending_presentation_feedbacks {
2297                    fb.discarded();
2298                }
2299                if let Some(ref parent_id) = surf.parent_surface_id
2300                    && let Some(parent) = self.surfaces.get_mut(parent_id)
2301                {
2302                    parent.children.retain(|c| c != proto_id);
2303                }
2304                if surf.surface_id > 0 {
2305                    self.toplevel_surface_ids.remove(&surf.surface_id);
2306                    // Clear keyboard focus if it pointed at the dead surface,
2307                    // so a reused surface id is treated as a fresh focus
2308                    // change (and gets its first `wl_keyboard.enter`) rather
2309                    // than a redundant re-enter. See `set_keyboard_focus`.
2310                    if self.focused_surface_id == surf.surface_id {
2311                        self.focused_surface_id = 0;
2312                    }
2313                    self.last_request_frame_ms.remove(&surf.surface_id);
2314                    self.last_reported_size.remove(&surf.surface_id);
2315                    self.surface_sizes.remove(&surf.surface_id);
2316                    if let Some(ref mut vk) = self.vulkan_renderer {
2317                        vk.destroy_external_outputs_for_surface(surf.surface_id as u32);
2318                    }
2319                    let _ = self.event_tx.send(CompositorEvent::SurfaceDestroyed {
2320                        surface_id: surf.surface_id,
2321                    });
2322                    (self.event_notify)();
2323                }
2324            }
2325        }
2326    }
2327
2328    fn fire_frame_callbacks_for_toplevel(&mut self, toplevel_sid: u16) {
2329        let Some(root_id) = self.toplevel_surface_ids.get(&toplevel_sid).cloned() else {
2330            return;
2331        };
2332        let tree = self.collect_surface_tree(&root_id);
2333        for sid in &tree {
2334            self.fire_surface_frame_callbacks(sid);
2335        }
2336        let _ = self.display_handle.flush_clients();
2337    }
2338
2339    fn handle_cursor_commit(&mut self, surface_id: &ObjectId) {
2340        self.apply_pending_state(surface_id);
2341        let hotspot = self
2342            .surfaces
2343            .get(surface_id)
2344            .map(|s| s.cursor_hotspot)
2345            .unwrap_or((0, 0));
2346        if let Some((w, h, rgba)) = self.cursor_rgba.get(surface_id)
2347            && !rgba.is_empty()
2348        {
2349            let _ = self.event_tx.send(CompositorEvent::SurfaceCursor {
2350                surface_id: self.focused_surface_id,
2351                cursor: CursorImage::Custom {
2352                    hotspot_x: hotspot.0 as u16,
2353                    hotspot_y: hotspot.1 as u16,
2354                    width: *w as u16,
2355                    height: *h as u16,
2356                    rgba: rgba.clone(),
2357                },
2358            });
2359        }
2360        self.fire_surface_frame_callbacks(surface_id);
2361        let _ = self.display_handle.flush_clients();
2362    }
2363
2364    fn handle_command(&mut self, cmd: CompositorCommand) {
2365        match cmd {
2366            CompositorCommand::KeyInput {
2367                surface_id: _,
2368                keycode,
2369                pressed,
2370            } => {
2371                let serial = self.next_serial();
2372                let time = elapsed_ms();
2373                let state = if pressed {
2374                    wl_keyboard::KeyState::Pressed
2375                } else {
2376                    wl_keyboard::KeyState::Released
2377                };
2378                // Popup-aware: same client either way, so the keys already
2379                // arrived — but asking the same question everywhere keeps
2380                // "who has the keyboard" from having two answers.
2381                let focused_wl = self.keyboard_focus_wl();
2382                for kb in &self.keyboards {
2383                    if let Some(ref wl) = focused_wl
2384                        && same_client(kb, wl)
2385                    {
2386                        kb.key(serial, time, keycode, state);
2387                    }
2388                }
2389                // Send wl_keyboard.modifiers if this key changed modifier
2390                // state.  Many Wayland clients (GTK, Chromium, Qt) rely on
2391                // this event rather than computing modifiers from raw key
2392                // events.
2393                self.update_and_send_modifiers(keycode, pressed);
2394                let _ = self.display_handle.flush_clients();
2395            }
2396            CompositorCommand::TextInput { text } => {
2397                let focused_wl = self
2398                    .toplevel_surface_ids
2399                    .get(&self.focused_surface_id)
2400                    .and_then(|root_id| self.surfaces.get(root_id))
2401                    .map(|s| s.wl_surface.clone());
2402                let Some(focused_wl) = focused_wl else { return };
2403
2404                // Synthesise evdev key sequences for ASCII
2405                // characters that exist on the US-QWERTY layout.
2406                //
2407                // The browser sends text (rather than raw keycodes) for
2408                // printable characters when Ctrl/Alt/Meta are NOT held,
2409                // so that keyboard layout differences are handled by the
2410                // browser.  However, the physical Shift key may still be
2411                // held -- its keydown was already forwarded as a raw evdev
2412                // event, so `mods_depressed` already has MOD_SHIFT.
2413                //
2414                // The synthetic Shift press/release we inject around
2415                // shifted characters must not corrupt the real modifier
2416                // state.  Save and restore `mods_depressed` so that a
2417                // subsequent key combo (e.g. Ctrl+Shift+Q) still sees
2418                // the Shift modifier from the physically-held key.
2419                const KEY_LEFTSHIFT: u32 = 42;
2420                let saved_mods_depressed = self.mods_depressed;
2421                for ch in text.chars() {
2422                    if let Some((kc, need_shift)) = char_to_keycode(ch) {
2423                        let time = elapsed_ms();
2424                        if need_shift {
2425                            let serial = self.next_serial();
2426                            for kb in &self.keyboards {
2427                                if same_client(kb, &focused_wl) {
2428                                    kb.key(
2429                                        serial,
2430                                        time,
2431                                        KEY_LEFTSHIFT,
2432                                        wl_keyboard::KeyState::Pressed,
2433                                    );
2434                                }
2435                            }
2436                            self.update_and_send_modifiers(KEY_LEFTSHIFT, true);
2437                        }
2438                        let serial = self.next_serial();
2439                        for kb in &self.keyboards {
2440                            if same_client(kb, &focused_wl) {
2441                                kb.key(serial, time, kc, wl_keyboard::KeyState::Pressed);
2442                            }
2443                        }
2444                        let serial = self.next_serial();
2445                        for kb in &self.keyboards {
2446                            if same_client(kb, &focused_wl) {
2447                                kb.key(serial, time, kc, wl_keyboard::KeyState::Released);
2448                            }
2449                        }
2450                        if need_shift {
2451                            let serial = self.next_serial();
2452                            for kb in &self.keyboards {
2453                                if same_client(kb, &focused_wl) {
2454                                    kb.key(
2455                                        serial,
2456                                        time,
2457                                        KEY_LEFTSHIFT,
2458                                        wl_keyboard::KeyState::Released,
2459                                    );
2460                                }
2461                            }
2462                            self.update_and_send_modifiers(KEY_LEFTSHIFT, false);
2463                        }
2464                    }
2465                    // Non-ASCII characters without a text_input_v3 path
2466                    // are silently dropped.
2467                }
2468                // Restore the real modifier state that was active before
2469                // text synthesis.  If the user is still holding Shift,
2470                // this puts MOD_SHIFT back into mods_depressed.
2471                if self.mods_depressed != saved_mods_depressed {
2472                    self.mods_depressed = saved_mods_depressed;
2473                    let serial = self.next_serial();
2474                    for kb in &self.keyboards {
2475                        if same_client(kb, &focused_wl) {
2476                            kb.modifiers(serial, self.mods_depressed, 0, self.mods_locked, 0);
2477                        }
2478                    }
2479                }
2480                let _ = self.display_handle.flush_clients();
2481            }
2482            CompositorCommand::PointerMotion { surface_id, x, y } => {
2483                let time = elapsed_ms();
2484                // The browser sends coordinates in the composited frame's
2485                // physical pixel space.  Convert to logical (surface-local)
2486                // coordinates using the actual composited-to-logical ratio
2487                // for this surface.
2488                let (mut x, mut y) =
2489                    if let Some(&(cw, ch, lw, lh)) = self.last_reported_size.get(&surface_id) {
2490                        let sx = if cw > 0 { lw as f64 / cw as f64 } else { 1.0 };
2491                        let sy = if ch > 0 { lh as f64 / ch as f64 } else { 1.0 };
2492                        (x * sx, y * sy)
2493                    } else {
2494                        (x, y)
2495                    };
2496                // The composited frame is cropped to xdg_geometry (if set),
2497                // so the browser's (0,0) corresponds to (geo_x, geo_y) in the
2498                // surface tree.  Offset accordingly.
2499                if let Some((gx, gy, _, _)) = self
2500                    .toplevel_surface_ids
2501                    .get(&surface_id)
2502                    .and_then(|rid| self.surfaces.get(rid))
2503                    .and_then(|s| s.xdg_geometry)
2504                {
2505                    x += gx as f64;
2506                    y += gy as f64;
2507                }
2508                // Hit-test the surface tree to find the actual target
2509                // (may be a subsurface or popup rather than the root).
2510                let target_wl = self
2511                    .toplevel_surface_ids
2512                    .get(&surface_id)
2513                    .and_then(|root_id| self.hit_test_surface_at(root_id, x, y))
2514                    .map(|(wl_surface, lx, ly)| (wl_surface.id(), wl_surface, lx, ly));
2515
2516                static PTR_DBG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2517                let pn = PTR_DBG.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2518                if pn < 5 || pn.is_multiple_of(500) {
2519                    let root = self.toplevel_surface_ids.get(&surface_id).cloned();
2520                    let lrs = self.last_reported_size.get(&surface_id).copied();
2521                    eprintln!(
2522                        "[pointer #{pn}] sid={surface_id} logical=({x:.1},{y:.1}) lrs={lrs:?} root={root:?} hit={:?}",
2523                        target_wl.as_ref().map(|(pid, _, lx, ly)| format!(
2524                            "proto={pid:?} local=({lx:.1},{ly:.1})"
2525                        ))
2526                    );
2527                }
2528                if let Some((proto_id, wl_surface, lx, ly)) = target_wl {
2529                    // Remember where we are inside the surface, so a pointer
2530                    // created *after* this can be entered at the right spot
2531                    // rather than waiting for the next motion (`GetPointer`).
2532                    self.pointer_entered_local = (lx, ly);
2533                    let matching_ptrs = self
2534                        .pointers
2535                        .iter()
2536                        .filter(|p| same_client(*p, &wl_surface))
2537                        .count();
2538                    if let Some(change) = focus_transition(
2539                        self.pointer_entered_id.as_ref(),
2540                        &proto_id,
2541                        matching_ptrs > 0,
2542                    ) {
2543                        let serial = self.next_serial();
2544                        if matching_ptrs == 0 {
2545                            // The client has mapped a surface but has not
2546                            // asked for a pointer yet — Firefox routinely
2547                            // takes long enough to start that the cursor is
2548                            // already over its pane. Say so, and (below)
2549                            // decline to latch: an enter nobody received must
2550                            // not count as having entered.
2551                            eprintln!(
2552                                "[pointer-enter] proto={proto_id:?} has no pointer yet (total_ptrs={}); deferring",
2553                                self.pointers.len()
2554                            );
2555                        }
2556                        // Leave old surface.
2557                        if let Some(ref leaving) = change.leave {
2558                            let old_wl = self
2559                                .surfaces
2560                                .values()
2561                                .find(|s| s.wl_surface.id() == *leaving)
2562                                .map(|s| s.wl_surface.clone());
2563                            if let Some(old_wl) = old_wl {
2564                                for ptr in &self.pointers {
2565                                    if same_client(ptr, &old_wl) {
2566                                        ptr.leave(serial, &old_wl);
2567                                        ptr.frame();
2568                                    }
2569                                }
2570                            }
2571                        }
2572                        for ptr in &self.pointers {
2573                            if same_client(ptr, &wl_surface) {
2574                                ptr.enter(serial, &wl_surface, lx, ly);
2575                            }
2576                        }
2577                        // Whatever `focus_transition` decided — including
2578                        // recording nothing when no pointer received the
2579                        // enter, so the next motion retries rather than the
2580                        // state machine believing the job is done. Its tests
2581                        // enumerate the cases; see pointer_focus.rs.
2582                        self.pointer_entered_id = change.entered;
2583                    }
2584                    for ptr in &self.pointers {
2585                        if same_client(ptr, &wl_surface) {
2586                            ptr.motion(time, lx, ly);
2587                            ptr.frame();
2588                        }
2589                    }
2590                }
2591                // When no surface is hit, don't send motion events —
2592                // there is no valid surface-local coordinate to report.
2593                let _ = self.display_handle.flush_clients();
2594            }
2595            CompositorCommand::PointerButton {
2596                surface_id: _,
2597                button,
2598                pressed,
2599            } => {
2600                let serial = self.next_serial();
2601                let time = elapsed_ms();
2602                let state = if pressed {
2603                    wl_pointer::ButtonState::Pressed
2604                } else {
2605                    wl_pointer::ButtonState::Released
2606                };
2607
2608                // If a popup is grabbed and the pointer clicked outside
2609                // the popup chain, dismiss the topmost grabbed popup.
2610                let mut dismissed = false;
2611                if pressed && !self.popup_grab_stack.is_empty() {
2612                    let click_on_grabbed = self.pointer_entered_id.as_ref().is_some_and(|eid| {
2613                        self.popup_grab_stack.iter().any(|gid| {
2614                            self.surfaces
2615                                .get(gid)
2616                                .is_some_and(|s| s.wl_surface.id() == *eid)
2617                        })
2618                    });
2619                    if !click_on_grabbed {
2620                        // Dismiss from the topmost popup down.
2621                        while let Some(grab_wl_id) = self.popup_grab_stack.pop() {
2622                            if let Some(surf) = self.surfaces.get(&grab_wl_id)
2623                                && let Some(ref popup) = surf.xdg_popup
2624                            {
2625                                popup.popup_done();
2626                            }
2627                            // Pop first, then hand focus back: `unfocus_popup`
2628                            // reads the stack to find what is still grabbing
2629                            // underneath, and on the last iteration that is
2630                            // nothing, so focus lands on the toplevel.
2631                            self.unfocus_popup(&grab_wl_id);
2632                        }
2633                        let _ = self.display_handle.flush_clients();
2634                        dismissed = true;
2635                    }
2636                }
2637
2638                // The click that closed a menu is spent on closing it; see
2639                // `button_routing`, whose tests enumerate the cases.
2640                let (routing, swallow) =
2641                    button_routing(pressed, button, dismissed, self.popup_dismiss_button);
2642                self.popup_dismiss_button = swallow;
2643
2644                if routing == ButtonRouting::Deliver {
2645                    let focused_wl = self
2646                        .surfaces
2647                        .values()
2648                        .find(|s| Some(s.wl_surface.id()) == self.pointer_entered_id)
2649                        .map(|s| s.wl_surface.clone());
2650                    for ptr in &self.pointers {
2651                        if let Some(ref wl) = focused_wl
2652                            && same_client(ptr, wl)
2653                        {
2654                            ptr.button(serial, time, button, state);
2655                            ptr.frame();
2656                        }
2657                    }
2658                }
2659                let _ = self.display_handle.flush_clients();
2660            }
2661            CompositorCommand::PointerAxis {
2662                surface_id: _,
2663                axis,
2664                value,
2665            } => {
2666                let time = elapsed_ms();
2667                let wl_axis = if axis == 0 {
2668                    wl_pointer::Axis::VerticalScroll
2669                } else {
2670                    wl_pointer::Axis::HorizontalScroll
2671                };
2672                let focused_wl = self
2673                    .surfaces
2674                    .values()
2675                    .find(|s| Some(s.wl_surface.id()) == self.pointer_entered_id)
2676                    .map(|s| s.wl_surface.clone());
2677                for ptr in &self.pointers {
2678                    if let Some(ref wl) = focused_wl
2679                        && same_client(ptr, wl)
2680                    {
2681                        ptr.axis(time, wl_axis, value);
2682                        ptr.frame();
2683                    }
2684                }
2685                let _ = self.display_handle.flush_clients();
2686            }
2687            CompositorCommand::SurfaceResize {
2688                surface_id,
2689                width,
2690                height,
2691                scale_120,
2692            } => {
2693                // The browser sends physical pixels (cssW × DPR).  Convert
2694                // to logical (CSS) pixels for use in Wayland configures.
2695                let s_in = (scale_120 as i32).max(120);
2696                let w = (width as i32) * 120 / s_in;
2697                let h = (height as i32) * 120 / s_in;
2698                self.surface_sizes.insert(surface_id, (w, h));
2699
2700                // Track whether output properties changed so we can batch
2701                // all events before a single output.done().
2702                let mut output_changed = false;
2703
2704                // Update output scale (in 1/120th units) from the browser DPR.
2705                if scale_120 > 0 && scale_120 != self.output_scale_120 {
2706                    self.output_scale_120 = scale_120;
2707                    output_changed = true;
2708                }
2709
2710                let s120 = self.output_scale_120 as i32;
2711
2712                // Recompute output dimensions from scratch (start from 0,0)
2713                // so the output can shrink when surfaces get smaller or are
2714                // destroyed.  The previous fold started from (output_width,
2715                // output_height) which meant dimensions could only grow.
2716                let (max_w, max_h) = self
2717                    .surface_sizes
2718                    .values()
2719                    .fold((0i32, 0i32), |(mw, mh), &(sw, sh)| (mw.max(sw), mh.max(sh)));
2720                // Clamp to a sensible minimum so the output is never 0×0.
2721                let max_w = max_w.max(1);
2722                let max_h = max_h.max(1);
2723                if max_w != self.output_width || max_h != self.output_height {
2724                    self.output_width = max_w;
2725                    self.output_height = max_h;
2726                    output_changed = true;
2727                }
2728
2729                // When any output property changed, re-send the full
2730                // sequence so clients see it as a display configuration
2731                // change: geometry → mode → scale → fractional_scale → done.
2732                if output_changed {
2733                    let int_scale = ((s120) + 119) / 120;
2734                    for output in &self.outputs {
2735                        output.geometry(
2736                            0,
2737                            0,
2738                            0,
2739                            0,
2740                            wl_output::Subpixel::None,
2741                            "blit".to_string(),
2742                            "virtual".to_string(),
2743                            wl_output::Transform::Normal,
2744                        );
2745                        // mode() takes physical pixels: logical × scale.
2746                        let mode_w = self.output_width * s120 / 120;
2747                        let mode_h = self.output_height * s120 / 120;
2748                        output.mode(
2749                            wl_output::Mode::Current | wl_output::Mode::Preferred,
2750                            mode_w,
2751                            mode_h,
2752                            self.output_refresh_mhz as i32,
2753                        );
2754                        if output.version() >= 2 {
2755                            output.scale(int_scale);
2756                        }
2757                    }
2758                    for fs in &self.fractional_scales {
2759                        fs.preferred_scale(s120 as u32);
2760                    }
2761                }
2762
2763                // Single output.done() after all property changes, so the
2764                // client sees scale + mode atomically before the configure.
2765                if output_changed {
2766                    for output in &self.outputs {
2767                        if output.version() >= 2 {
2768                            output.done();
2769                        }
2770                    }
2771                }
2772
2773                let states = xdg_toplevel_states(&[
2774                    xdg_toplevel::State::Activated,
2775                    xdg_toplevel::State::Maximized,
2776                ]);
2777
2778                if output_changed {
2779                    // When output scale or dimensions changed, every
2780                    // toplevel needs a new configure so it re-renders at
2781                    // the correct density / size.
2782                    for (&sid, root_id) in &self.toplevel_surface_ids {
2783                        let (lw, lh) = self.surface_sizes.get(&sid).copied().unwrap_or((w, h));
2784                        if let Some(surf) = self.surfaces.get(root_id) {
2785                            if let Some(ref tl) = surf.xdg_toplevel {
2786                                tl.configure(lw, lh, states.clone());
2787                            }
2788                            if let Some(ref xs) = surf.xdg_surface {
2789                                let serial = self.serial.wrapping_add(1);
2790                                self.serial = serial;
2791                                xs.configure(serial);
2792                            }
2793                        }
2794                    }
2795                    // Fire frame callbacks so all clients repaint at new
2796                    // scale.
2797                    let all_sids: Vec<u16> = self.toplevel_surface_ids.keys().copied().collect();
2798                    for sid in all_sids {
2799                        self.fire_frame_callbacks_for_toplevel(sid);
2800                    }
2801
2802                    // Reset pointer/keyboard state — scale change
2803                    // invalidates coordinate mappings.
2804                    self.pointer_entered_id = None;
2805                    self.pending_kb_reenter = true;
2806                } else {
2807                    // Only the target surface changed size — configure just
2808                    // that one.  This avoids disturbing other surfaces'
2809                    // frame callback / render cycle, which would race with
2810                    // the server's RequestFrame mechanism and stall them.
2811                    if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id)
2812                        && let Some(surf) = self.surfaces.get(root_id)
2813                    {
2814                        if let Some(ref tl) = surf.xdg_toplevel {
2815                            tl.configure(w, h, states);
2816                        }
2817                        if let Some(ref xs) = surf.xdg_surface {
2818                            let serial = self.serial.wrapping_add(1);
2819                            self.serial = serial;
2820                            xs.configure(serial);
2821                        }
2822                    }
2823                    self.fire_frame_callbacks_for_toplevel(surface_id);
2824                }
2825                let _ = self.display_handle.flush_clients();
2826            }
2827            CompositorCommand::SurfaceFocus { surface_id } => {
2828                self.set_keyboard_focus(surface_id);
2829                let _ = self.display_handle.flush_clients();
2830            }
2831            CompositorCommand::SurfaceClose { surface_id } => {
2832                if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id)
2833                    && let Some(surf) = self.surfaces.get(root_id)
2834                    && let Some(ref tl) = surf.xdg_toplevel
2835                {
2836                    tl.close();
2837                }
2838                let _ = self.display_handle.flush_clients();
2839            }
2840            CompositorCommand::ClipboardOffer { mime_type, data } => {
2841                self.external_clipboard = Some(ExternalClipboard { mime_type, data });
2842                // Tell the previous selection owner it's no longer selected.
2843                // Without this, apps that set their own selection keep
2844                // thinking they're the owner and paste from their internal
2845                // buffer on Ctrl+V instead of requesting data from the new
2846                // offer we're about to advertise.
2847                if let Some(src) = self.selection_source.take() {
2848                    src.cancelled();
2849                }
2850                self.offer_external_clipboard();
2851            }
2852            CompositorCommand::Capture {
2853                surface_id,
2854                scale_120,
2855                reply,
2856            } => {
2857                // Use the capture-specific scale if provided, otherwise
2858                // fall back to the current output scale.
2859                let cap_s120 = if scale_120 > 0 {
2860                    scale_120
2861                } else {
2862                    self.output_scale_120
2863                };
2864                let result = if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id) {
2865                    if let Some(ref mut vk) = self.vulkan_renderer {
2866                        // Capture asks for the compositor's native
2867                        // composite at `cap_s120`.  No external targets
2868                        // are registered for capture, so the renderer
2869                        // returns 0..1 results — pick the first (or
2870                        // None if the render failed to produce
2871                        // anything).
2872                        let captured = vk
2873                            .render_tree_sized(
2874                                root_id,
2875                                &self.surfaces,
2876                                &self.surface_meta,
2877                                cap_s120,
2878                                None,
2879                                surface_id,
2880                            )
2881                            .into_iter()
2882                            .next()
2883                            .map(|(_sid, w, h, pixels)| {
2884                                let rgba = pixels.to_rgba(w, h);
2885                                (w, h, rgba)
2886                            });
2887                        // Capture registers no external targets, so this is
2888                        // normally empty; drain anyway so a stray bitstream
2889                        // can't leak into the next flush.
2890                        let stray = vk.take_encoded_frames();
2891                        debug_assert!(stray.is_empty());
2892                        captured
2893                    } else {
2894                        None
2895                    }
2896                } else {
2897                    None
2898                };
2899                let _ = reply.send(result);
2900            }
2901            CompositorCommand::RequestFrame { surface_id } => {
2902                // Remember that the server paced this surface, so the eager
2903                // per-commit fire in `handle_surface_commit` can stand down
2904                // while this (display-rate-throttled) path is driving frames.
2905                self.last_request_frame_ms.insert(surface_id, elapsed_ms());
2906                self.fire_frame_callbacks_for_toplevel(surface_id);
2907            }
2908            CompositorCommand::ReleaseKeys { keycodes } => {
2909                let time = elapsed_ms();
2910                let focused_wl = self
2911                    .toplevel_surface_ids
2912                    .get(&self.focused_surface_id)
2913                    .and_then(|root_id| self.surfaces.get(root_id))
2914                    .map(|s| s.wl_surface.clone());
2915                for keycode in &keycodes {
2916                    let serial = self.next_serial();
2917                    for kb in &self.keyboards {
2918                        if let Some(ref wl) = focused_wl
2919                            && same_client(kb, wl)
2920                        {
2921                            kb.key(serial, time, *keycode, wl_keyboard::KeyState::Released);
2922                        }
2923                    }
2924                }
2925                // Update modifier state for any released modifier keys.
2926                for keycode in &keycodes {
2927                    self.update_and_send_modifiers(*keycode, false);
2928                }
2929                let _ = self.display_handle.flush_clients();
2930            }
2931            CompositorCommand::ClipboardListMimes { reply } => {
2932                let mimes = self.collect_clipboard_mime_types();
2933                let _ = reply.send(mimes);
2934            }
2935            CompositorCommand::ClipboardGet { mime_type, reply } => {
2936                let data = self.get_clipboard_content(&mime_type);
2937                let _ = reply.send(data);
2938            }
2939            CompositorCommand::SetExternalOutputBuffers {
2940                surface_id,
2941                target_w,
2942                target_h,
2943                buffers,
2944            } => {
2945                let installed = !buffers.is_empty();
2946                if let Some(ref mut vk) = self.vulkan_renderer {
2947                    vk.set_external_output_buffers(surface_id, target_w, target_h, buffers);
2948                }
2949                // Populate the freshly-installed external buffer pool
2950                // from the most-recent committed surface state.  An
2951                // idle wayland client (steady-state UI after a resize)
2952                // doesn't volunteer another commit, so without this
2953                // re-blit the per-client encoder skips forever waiting
2954                // for `last_pixels[(sid, target)]` to appear.  The
2955                // re-composite is deferred until the GPU is idle —
2956                // calling `render_tree_sized` here while a previous
2957                // submit's fence is still pending would early-return
2958                // and skip the new submit entirely.
2959                if installed && self.toplevel_surface_ids.contains_key(&(surface_id as u16)) {
2960                    self.pending_recomposite_toplevels
2961                        .insert(surface_id as u16, false);
2962                }
2963            }
2964            CompositorCommand::RegisterDownscaleTarget {
2965                surface_id,
2966                target_w,
2967                target_h,
2968            } => {
2969                if let Some(ref mut vk) = self.vulkan_renderer {
2970                    vk.register_downscale_target(surface_id, target_w, target_h);
2971                }
2972                // See the SetExternalOutputBuffers handler above for
2973                // why we re-composite here.
2974                if self.toplevel_surface_ids.contains_key(&(surface_id as u16)) {
2975                    self.pending_recomposite_toplevels
2976                        .insert(surface_id as u16, false);
2977                }
2978            }
2979            CompositorCommand::ClearDownscaleTarget {
2980                surface_id,
2981                target_w,
2982                target_h,
2983            } => {
2984                if let Some(ref mut vk) = self.vulkan_renderer {
2985                    vk.clear_downscale_target(surface_id, target_w, target_h);
2986                }
2987            }
2988            CompositorCommand::SetRefreshRate { mhz } => {
2989                // Only update on meaningful changes (>2 Hz difference) to
2990                // avoid flooding clients with mode events from jittery
2991                // requestAnimationFrame measurements.
2992                let diff = (mhz as i64 - self.output_refresh_mhz as i64).unsigned_abs();
2993                if diff > 2000 && mhz > 0 {
2994                    self.output_refresh_mhz = mhz;
2995                    let s120 = self.output_scale_120 as i32;
2996                    let mode_w = self.output_width * s120 / 120;
2997                    let mode_h = self.output_height * s120 / 120;
2998                    for output in &self.outputs {
2999                        output.mode(
3000                            wl_output::Mode::Current | wl_output::Mode::Preferred,
3001                            mode_w,
3002                            mode_h,
3003                            mhz as i32,
3004                        );
3005                        if output.version() >= 2 {
3006                            output.done();
3007                        }
3008                    }
3009                    let _ = self.display_handle.flush_clients();
3010                }
3011            }
3012            CompositorCommand::SetVulkanEncoder {
3013                surface_id,
3014                client_id,
3015                codec,
3016                qp,
3017                width,
3018                height,
3019            } => {
3020                let created = self.vulkan_renderer.as_mut().is_some_and(|vk| {
3021                    vk.create_vulkan_encoder(surface_id, client_id, codec, qp, width, height)
3022                });
3023                if !created {
3024                    let _ = self
3025                        .event_tx
3026                        .send(CompositorEvent::VulkanEncoderUnavailable {
3027                            surface_id: surface_id as u16,
3028                            client_id,
3029                        });
3030                    (self.event_notify)();
3031                }
3032            }
3033            CompositorCommand::SetVulkanEncoderQp {
3034                surface_id,
3035                client_id,
3036                qp,
3037            } => {
3038                if let Some(ref mut vk) = self.vulkan_renderer {
3039                    vk.set_vulkan_encoder_qp(surface_id, client_id, qp);
3040                }
3041            }
3042            CompositorCommand::RequestVulkanKeyframe {
3043                surface_id,
3044                client_id,
3045            } => {
3046                if let Some(ref mut vk) = self.vulkan_renderer {
3047                    vk.request_encoder_keyframe(surface_id, client_id);
3048                }
3049                // The latch is consumed by the next encode, and encodes only
3050                // happen when this toplevel is composited.  A surface whose
3051                // app has stopped painting would never composite again, so
3052                // the keyframe — and any quantizer change staged with it —
3053                // would wait forever.  Queue a recomposite; the drain runs it
3054                // once the GPU pipeline is idle.  Encoder-only: the content
3055                // is unchanged, so republishing the pixels would just make
3056                // every other viewer re-encode the frame it already has.
3057                if self.toplevel_surface_ids.contains_key(&(surface_id as u16)) {
3058                    self.pending_recomposite_toplevels
3059                        .entry(surface_id as u16)
3060                        .or_insert(true);
3061                }
3062            }
3063            CompositorCommand::DestroyVulkanEncoder {
3064                surface_id,
3065                client_id,
3066            } => {
3067                if let Some(ref mut vk) = self.vulkan_renderer {
3068                    vk.destroy_vulkan_encoder(surface_id, client_id);
3069                }
3070            }
3071            CompositorCommand::Shutdown => {
3072                self.shutdown.store(true, Ordering::Relaxed);
3073                self.loop_signal.stop();
3074            }
3075        }
3076    }
3077
3078    /// Send dmabuf feedback events on a `ZwpLinuxDmabufFeedbackV1` object.
3079    /// Builds the format table from the Vulkan renderer's supported modifiers,
3080    /// then sends main_device, one tranche, and done.
3081    fn send_dmabuf_feedback(&self, fb: &ZwpLinuxDmabufFeedbackV1) {
3082        use std::os::unix::fs::MetadataExt;
3083
3084        // Collect format+modifier pairs from the Vulkan renderer.
3085        let modifiers: &[(u32, u64)] = self
3086            .vulkan_renderer
3087            .as_ref()
3088            .map(|vk| vk.supported_dmabuf_modifiers.as_slice())
3089            .unwrap_or(&[]);
3090
3091        // Build the format table: tightly packed (u32 format, u32 pad, u64 modifier).
3092        let entry_size = 16usize;
3093        let table_size = modifiers.len() * entry_size;
3094        let mut table_data = vec![0u8; table_size];
3095        for (i, &(fmt, modifier)) in modifiers.iter().enumerate() {
3096            let off = i * entry_size;
3097            table_data[off..off + 4].copy_from_slice(&fmt.to_ne_bytes());
3098            // 4 bytes padding (already zero)
3099            table_data[off + 8..off + 16].copy_from_slice(&modifier.to_ne_bytes());
3100        }
3101
3102        // Create a memfd for the format table.
3103        let name = c"dmabuf-feedback-table";
3104        let raw_fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
3105        if raw_fd < 0 {
3106            eprintln!("[compositor] memfd_create for dmabuf feedback failed");
3107            fb.done();
3108            return;
3109        }
3110        let table_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
3111        if !table_data.is_empty() {
3112            use std::io::Write;
3113            let mut file = std::fs::File::from(table_fd.try_clone().unwrap());
3114            if file.write_all(&table_data).is_err() {
3115                eprintln!("[compositor] failed to write dmabuf feedback table");
3116                fb.done();
3117                return;
3118            }
3119        }
3120        fb.format_table(table_fd.as_fd(), table_size as u32);
3121
3122        // Get dev_t for the GPU device.
3123        let dev = std::fs::metadata(&self.gpu_device)
3124            .map(|m| m.rdev())
3125            .unwrap_or(0);
3126        let dev_bytes = dev.to_ne_bytes().to_vec();
3127        fb.main_device(dev_bytes.clone());
3128
3129        // Single tranche with all format+modifier pairs.
3130        fb.tranche_target_device(dev_bytes);
3131
3132        // Indices into the format table (array of u16 in native endianness).
3133        let indices: Vec<u8> = (0..modifiers.len() as u16)
3134            .flat_map(|i| i.to_ne_bytes())
3135            .collect();
3136        fb.tranche_formats(indices);
3137
3138        fb.tranche_flags(
3139            wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags::empty(),
3140        );
3141        fb.tranche_done();
3142        fb.done();
3143    }
3144}
3145
3146impl Compositor {
3147    /// Collect all MIME types available on the current clipboard.
3148    fn collect_clipboard_mime_types(&self) -> Vec<String> {
3149        // If a Wayland app owns the selection, use its MIME types.
3150        if let Some(ref src) = self.selection_source {
3151            let data = src.data::<DataSourceData>().unwrap();
3152            return data.mime_types.lock().unwrap().clone();
3153        }
3154        // Otherwise use the external (browser/CLI) clipboard.
3155        if let Some(ref cb) = self.external_clipboard
3156            && !cb.mime_type.is_empty()
3157        {
3158            let mut mimes = vec![cb.mime_type.clone()];
3159            // Add standard text aliases.
3160            if cb.mime_type.starts_with("text/plain") {
3161                if cb.mime_type != "text/plain" {
3162                    mimes.push("text/plain".to_string());
3163                }
3164                if cb.mime_type != "text/plain;charset=utf-8" {
3165                    mimes.push("text/plain;charset=utf-8".to_string());
3166                }
3167                mimes.push("UTF8_STRING".to_string());
3168            }
3169            return mimes;
3170        }
3171        Vec::new()
3172    }
3173
3174    /// Get clipboard content for a specific MIME type.
3175    fn get_clipboard_content(&mut self, mime_type: &str) -> Option<Vec<u8>> {
3176        // If external clipboard matches, return its data directly.
3177        if let Some(ref cb) = self.external_clipboard
3178            && self.selection_source.is_none()
3179        {
3180            // External clipboard is active.
3181            let matches = cb.mime_type == mime_type
3182                || (cb.mime_type.starts_with("text/plain")
3183                    && (mime_type == "text/plain"
3184                        || mime_type == "text/plain;charset=utf-8"
3185                        || mime_type == "UTF8_STRING"));
3186            if matches {
3187                return Some(cb.data.clone());
3188            }
3189            return None;
3190        }
3191        // If a Wayland app owns the selection, read from it via pipe.
3192        if let Some(src) = self.selection_source.clone() {
3193            return self.read_data_source_sync(&src, mime_type);
3194        }
3195        None
3196    }
3197
3198    /// Synchronously read data from a Wayland data source via pipe.
3199    fn read_data_source_sync(&mut self, source: &WlDataSource, mime_type: &str) -> Option<Vec<u8>> {
3200        let mut fds = [0i32; 2];
3201        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
3202            return None;
3203        }
3204        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
3205        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
3206        source.send(mime_type.to_string(), write_fd.as_fd());
3207        let _ = self.display_handle.flush_clients();
3208        drop(write_fd); // close write end so read gets EOF
3209        // Non-blocking read with a modest limit.
3210        unsafe {
3211            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
3212        }
3213        std::thread::sleep(std::time::Duration::from_millis(5));
3214        let mut buf = Vec::new();
3215        let mut tmp = [0u8; 8192];
3216        loop {
3217            let n = unsafe {
3218                libc::read(
3219                    read_fd.as_raw_fd(),
3220                    tmp.as_mut_ptr() as *mut libc::c_void,
3221                    tmp.len(),
3222                )
3223            };
3224            if n <= 0 {
3225                break;
3226            }
3227            buf.extend_from_slice(&tmp[..n as usize]);
3228            if buf.len() > 1024 * 1024 {
3229                break; // 1 MiB cap
3230            }
3231        }
3232        if buf.is_empty() { None } else { Some(buf) }
3233    }
3234}
3235
3236// ---------------------------------------------------------------------------
3237// Helpers
3238// ---------------------------------------------------------------------------
3239
3240/// Read `CLOCK_MONOTONIC` and return `(tv_sec, tv_nsec)`.
3241fn monotonic_timespec() -> (i64, i64) {
3242    let mut ts = libc::timespec {
3243        tv_sec: 0,
3244        tv_nsec: 0,
3245    };
3246    // SAFETY: clock_gettime with CLOCK_MONOTONIC is always valid.
3247    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
3248    (ts.tv_sec, ts.tv_nsec)
3249}
3250
3251fn elapsed_ms() -> u32 {
3252    // Use CLOCK_MONOTONIC directly so the timestamp matches what Wayland
3253    // clients (especially Chromium/Brave) expect for frame-latency
3254    // calculations.  The previous implementation measured from an arbitrary
3255    // epoch which caused Chromium to report negative frame latency.
3256    let (sec, nsec) = monotonic_timespec();
3257    (sec as u32)
3258        .wrapping_mul(1000)
3259        .wrapping_add(nsec as u32 / 1_000_000)
3260}
3261
3262/// Returns true when two Wayland resources belong to the same still-connected client.
3263fn same_client<R1: Resource, R2: Resource>(a: &R1, b: &R2) -> bool {
3264    match (a.client(), b.client()) {
3265        (Some(ca), Some(cb)) => ca.id() == cb.id(),
3266        _ => false,
3267    }
3268}
3269
3270fn yuv420_to_rgb(y: u8, u: u8, v: u8) -> [u8; 3] {
3271    let y = (y as i32 - 16).max(0);
3272    let u = u as i32 - 128;
3273    let v = v as i32 - 128;
3274    let r = ((298 * y + 409 * v + 128) >> 8).clamp(0, 255) as u8;
3275    let g = ((298 * y - 100 * u - 208 * v + 128) >> 8).clamp(0, 255) as u8;
3276    let b = ((298 * y + 516 * u + 128) >> 8).clamp(0, 255) as u8;
3277    [r, g, b]
3278}
3279
3280/// Encode xdg_toplevel states as the raw byte array expected by the protocol.
3281fn xdg_toplevel_states(states: &[xdg_toplevel::State]) -> Vec<u8> {
3282    let mut bytes = Vec::with_capacity(states.len() * 4);
3283    for state in states {
3284        bytes.extend_from_slice(&(*state as u32).to_ne_bytes());
3285    }
3286    bytes
3287}
3288
3289fn create_keymap_fd(keymap_data: &[u8]) -> Option<OwnedFd> {
3290    use std::io::Write;
3291    let name = c"blit-keymap";
3292    let raw_fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
3293    if raw_fd < 0 {
3294        return None;
3295    }
3296    let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
3297    let mut file = std::fs::File::from(fd);
3298    file.write_all(keymap_data).ok()?;
3299    Some(file.into())
3300}
3301
3302// ---------------------------------------------------------------------------
3303// Protocol dispatch implementations
3304// ---------------------------------------------------------------------------
3305
3306// -- wl_compositor --
3307
3308impl GlobalDispatch<WlCompositor, ()> for Compositor {
3309    fn bind(
3310        _state: &mut Self,
3311        _handle: &DisplayHandle,
3312        _client: &Client,
3313        resource: New<WlCompositor>,
3314        _data: &(),
3315        data_init: &mut DataInit<'_, Self>,
3316    ) {
3317        data_init.init(resource, ());
3318    }
3319}
3320
3321impl Dispatch<WlCompositor, ()> for Compositor {
3322    fn request(
3323        state: &mut Self,
3324        _client: &Client,
3325        _resource: &WlCompositor,
3326        request: <WlCompositor as Resource>::Request,
3327        _data: &(),
3328        _dh: &DisplayHandle,
3329        data_init: &mut DataInit<'_, Self>,
3330    ) {
3331        use wayland_server::protocol::wl_compositor::Request;
3332        match request {
3333            Request::CreateSurface { id } => {
3334                let surface = data_init.init(id, ());
3335                let proto_id = surface.id();
3336                state.surfaces.insert(
3337                    proto_id,
3338                    Surface {
3339                        surface_id: 0,
3340                        wl_surface: surface,
3341                        pending_buffer: None,
3342                        pending_buffer_scale: 1,
3343                        pending_damage: false,
3344                        pending_frame_callbacks: Vec::new(),
3345                        pending_presentation_feedbacks: Vec::new(),
3346                        pending_opaque: false,
3347                        pending_input_region: None,
3348                        input_region: None,
3349                        buffer_scale: 1,
3350                        is_opaque: false,
3351                        parent_surface_id: None,
3352                        pending_subsurface_position: None,
3353                        subsurface_position: (0, 0),
3354                        children: Vec::new(),
3355                        xdg_surface: None,
3356                        xdg_toplevel: None,
3357                        xdg_popup: None,
3358                        xdg_geometry: None,
3359                        title: String::new(),
3360                        app_id: String::new(),
3361                        pending_viewport_destination: None,
3362                        viewport_destination: None,
3363                        is_cursor: false,
3364                        cursor_hotspot: (0, 0),
3365                    },
3366                );
3367            }
3368            Request::CreateRegion { id } => {
3369                data_init.init(id, ());
3370            }
3371            _ => {}
3372        }
3373    }
3374}
3375
3376// -- wl_surface --
3377
3378impl Dispatch<WlSurface, ()> for Compositor {
3379    fn request(
3380        state: &mut Self,
3381        _client: &Client,
3382        resource: &WlSurface,
3383        request: <WlSurface as Resource>::Request,
3384        _data: &(),
3385        _dh: &DisplayHandle,
3386        data_init: &mut DataInit<'_, Self>,
3387    ) {
3388        use wayland_server::protocol::wl_surface::Request;
3389        let sid = resource.id();
3390        match request {
3391            Request::Attach { buffer, x: _, y: _ } => {
3392                if let Some(surf) = state.surfaces.get_mut(&sid) {
3393                    surf.pending_buffer = buffer;
3394                }
3395            }
3396            Request::Damage { .. } | Request::DamageBuffer { .. } => {
3397                if let Some(surf) = state.surfaces.get_mut(&sid) {
3398                    surf.pending_damage = true;
3399                }
3400            }
3401            Request::Frame { callback } => {
3402                let cb = data_init.init(callback, ());
3403                if let Some(surf) = state.surfaces.get_mut(&sid) {
3404                    surf.pending_frame_callbacks.push(cb);
3405                }
3406            }
3407            Request::SetBufferScale { scale } => {
3408                if let Some(surf) = state.surfaces.get_mut(&sid) {
3409                    surf.pending_buffer_scale = scale;
3410                }
3411            }
3412            Request::SetOpaqueRegion { region: _ } => {
3413                if let Some(surf) = state.surfaces.get_mut(&sid) {
3414                    surf.pending_opaque = true;
3415                }
3416            }
3417            Request::SetInputRegion { region } => {
3418                // Double-buffered: takes effect on the next commit. An empty
3419                // list is meaningfully different from nil — nil restores the
3420                // default of accepting input everywhere, an empty region
3421                // declines it everywhere.
3422                let ops = region.map(|r| {
3423                    state
3424                        .regions
3425                        .get(&r.id())
3426                        .map(|(_, ops)| ops.clone())
3427                        .unwrap_or_default()
3428                });
3429                if let Some(surf) = state.surfaces.get_mut(&sid) {
3430                    surf.pending_input_region = Some(ops);
3431                }
3432            }
3433            Request::Commit => {
3434                let is_cursor = state.surfaces.get(&sid).is_some_and(|s| s.is_cursor);
3435                if is_cursor {
3436                    state.handle_cursor_commit(&sid);
3437                } else {
3438                    state.handle_surface_commit(&sid);
3439                }
3440            }
3441            Request::SetBufferTransform { .. } => {}
3442            Request::Offset { .. } => {}
3443            Request::Destroy => {
3444                state.surface_meta.remove(&sid);
3445                state.cursor_rgba.remove(&sid);
3446                if let Some(ref mut vk) = state.vulkan_renderer {
3447                    vk.remove_surface(&sid);
3448                }
3449                if let Some(held) = state.held_buffers.remove(&sid) {
3450                    held.release();
3451                }
3452                state.forget_pointer_focus(&sid);
3453                if let Some(parent_id) = state
3454                    .surfaces
3455                    .get(&sid)
3456                    .and_then(|s| s.parent_surface_id.clone())
3457                    && let Some(parent) = state.surfaces.get_mut(&parent_id)
3458                {
3459                    parent.children.retain(|c| *c != sid);
3460                }
3461                if let Some(surf) = state.surfaces.remove(&sid) {
3462                    for fb in surf.pending_presentation_feedbacks {
3463                        fb.discarded();
3464                    }
3465                    if surf.surface_id > 0 {
3466                        state.toplevel_surface_ids.remove(&surf.surface_id);
3467                        state.last_reported_size.remove(&surf.surface_id);
3468                        state.surface_sizes.remove(&surf.surface_id);
3469                        if let Some(ref mut vk) = state.vulkan_renderer {
3470                            vk.destroy_external_outputs_for_surface(surf.surface_id as u32);
3471                        }
3472                        let _ = state.event_tx.send(CompositorEvent::SurfaceDestroyed {
3473                            surface_id: surf.surface_id,
3474                        });
3475                        (state.event_notify)();
3476                    }
3477                }
3478            }
3479            _ => {}
3480        }
3481    }
3482}
3483
3484// -- wl_callback --
3485impl Dispatch<WlCallback, ()> for Compositor {
3486    fn request(
3487        _: &mut Self,
3488        _: &Client,
3489        _: &WlCallback,
3490        _: <WlCallback as Resource>::Request,
3491        _: &(),
3492        _: &DisplayHandle,
3493        _: &mut DataInit<'_, Self>,
3494    ) {
3495    }
3496}
3497
3498// -- wp_presentation --
3499impl GlobalDispatch<WpPresentation, ()> for Compositor {
3500    fn bind(
3501        _: &mut Self,
3502        _: &DisplayHandle,
3503        _: &Client,
3504        resource: New<WpPresentation>,
3505        _: &(),
3506        data_init: &mut DataInit<'_, Self>,
3507    ) {
3508        let pres = data_init.init(resource, ());
3509        // Tell the client we use CLOCK_MONOTONIC for presentation timestamps.
3510        pres.clock_id(libc::CLOCK_MONOTONIC as u32);
3511    }
3512}
3513
3514impl Dispatch<WpPresentation, ()> for Compositor {
3515    fn request(
3516        state: &mut Self,
3517        _: &Client,
3518        _: &WpPresentation,
3519        request: <WpPresentation as Resource>::Request,
3520        _: &(),
3521        _: &DisplayHandle,
3522        data_init: &mut DataInit<'_, Self>,
3523    ) {
3524        use wp_presentation::Request;
3525        match request {
3526            Request::Feedback { surface, callback } => {
3527                let fb = data_init.init(callback, ());
3528                let sid = surface.id();
3529                if let Some(surf) = state.surfaces.get_mut(&sid) {
3530                    surf.pending_presentation_feedbacks.push(fb);
3531                }
3532            }
3533            Request::Destroy => {}
3534            _ => {}
3535        }
3536    }
3537}
3538
3539// -- wp_presentation_feedback (no client requests) --
3540impl Dispatch<WpPresentationFeedback, ()> for Compositor {
3541    fn request(
3542        _: &mut Self,
3543        _: &Client,
3544        _: &WpPresentationFeedback,
3545        _: <WpPresentationFeedback as Resource>::Request,
3546        _: &(),
3547        _: &DisplayHandle,
3548        _: &mut DataInit<'_, Self>,
3549    ) {
3550    }
3551}
3552
3553// -- wl_region --
3554impl Dispatch<WlRegion, ()> for Compositor {
3555    fn request(
3556        state: &mut Self,
3557        _: &Client,
3558        resource: &WlRegion,
3559        request: <WlRegion as Resource>::Request,
3560        _: &(),
3561        _: &DisplayHandle,
3562        _: &mut DataInit<'_, Self>,
3563    ) {
3564        use wayland_server::protocol::wl_region::Request;
3565        let rid = resource.id();
3566        match request {
3567            Request::Add {
3568                x,
3569                y,
3570                width,
3571                height,
3572            } => {
3573                state
3574                    .regions
3575                    .entry(rid)
3576                    .or_insert_with(|| (resource.clone(), Vec::new()))
3577                    .1
3578                    .push(RegionOp {
3579                        add: true,
3580                        x,
3581                        y,
3582                        w: width,
3583                        h: height,
3584                    });
3585            }
3586            Request::Subtract {
3587                x,
3588                y,
3589                width,
3590                height,
3591            } => {
3592                state
3593                    .regions
3594                    .entry(rid)
3595                    .or_insert_with(|| (resource.clone(), Vec::new()))
3596                    .1
3597                    .push(RegionOp {
3598                        add: false,
3599                        x,
3600                        y,
3601                        w: width,
3602                        h: height,
3603                    });
3604            }
3605            Request::Destroy => {
3606                // Clients destroy the region as soon as they have handed it
3607                // to `set_input_region`, so the surface keeps its own copy
3608                // and this only drops the builder.
3609                state.regions.remove(&rid);
3610            }
3611            _ => {}
3612        }
3613    }
3614}
3615
3616// -- wl_subcompositor --
3617impl GlobalDispatch<WlSubcompositor, ()> for Compositor {
3618    fn bind(
3619        _: &mut Self,
3620        _: &DisplayHandle,
3621        _: &Client,
3622        resource: New<WlSubcompositor>,
3623        _: &(),
3624        data_init: &mut DataInit<'_, Self>,
3625    ) {
3626        data_init.init(resource, ());
3627    }
3628}
3629
3630impl Dispatch<WlSubcompositor, ()> for Compositor {
3631    fn request(
3632        state: &mut Self,
3633        _: &Client,
3634        _: &WlSubcompositor,
3635        request: <WlSubcompositor as Resource>::Request,
3636        _: &(),
3637        _: &DisplayHandle,
3638        data_init: &mut DataInit<'_, Self>,
3639    ) {
3640        use wayland_server::protocol::wl_subcompositor::Request;
3641        match request {
3642            Request::GetSubsurface {
3643                id,
3644                surface,
3645                parent,
3646            } => {
3647                let child_id = surface.id();
3648                let parent_id = parent.id();
3649                data_init.init(
3650                    id,
3651                    SubsurfaceData {
3652                        wl_surface_id: child_id.clone(),
3653                        parent_surface_id: parent_id.clone(),
3654                    },
3655                );
3656                if let Some(surf) = state.surfaces.get_mut(&child_id) {
3657                    surf.parent_surface_id = Some(parent_id.clone());
3658                }
3659                if let Some(parent_surf) = state.surfaces.get_mut(&parent_id)
3660                    && !parent_surf.children.contains(&child_id)
3661                {
3662                    parent_surf.children.push(child_id);
3663                }
3664            }
3665            Request::Destroy => {}
3666            _ => {}
3667        }
3668    }
3669}
3670
3671// -- wl_subsurface --
3672impl Dispatch<WlSubsurface, SubsurfaceData> for Compositor {
3673    fn request(
3674        state: &mut Self,
3675        _: &Client,
3676        _: &WlSubsurface,
3677        request: <WlSubsurface as Resource>::Request,
3678        data: &SubsurfaceData,
3679        _: &DisplayHandle,
3680        _: &mut DataInit<'_, Self>,
3681    ) {
3682        use wayland_server::protocol::wl_subsurface::Request;
3683        match request {
3684            Request::SetPosition { x, y } => {
3685                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3686                    surf.pending_subsurface_position = Some((x, y));
3687                }
3688            }
3689            Request::PlaceAbove { sibling } => {
3690                let sibling_id = sibling.id();
3691                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3692                    let child_id = &data.wl_surface_id;
3693                    parent.children.retain(|c| c != child_id);
3694                    let pos = parent
3695                        .children
3696                        .iter()
3697                        .position(|c| *c == sibling_id)
3698                        .map(|p| p + 1)
3699                        .unwrap_or(parent.children.len());
3700                    parent.children.insert(pos, child_id.clone());
3701                }
3702            }
3703            Request::PlaceBelow { sibling } => {
3704                let sibling_id = sibling.id();
3705                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3706                    let child_id = &data.wl_surface_id;
3707                    parent.children.retain(|c| c != child_id);
3708                    let pos = parent
3709                        .children
3710                        .iter()
3711                        .position(|c| *c == sibling_id)
3712                        .unwrap_or(0);
3713                    parent.children.insert(pos, child_id.clone());
3714                }
3715            }
3716            Request::SetSync | Request::SetDesync => {}
3717            Request::Destroy => {
3718                let child_id = &data.wl_surface_id;
3719                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3720                    parent.children.retain(|c| c != child_id);
3721                }
3722                if let Some(surf) = state.surfaces.get_mut(child_id) {
3723                    surf.parent_surface_id = None;
3724                }
3725            }
3726            _ => {}
3727        }
3728    }
3729}
3730
3731// -- xdg_wm_base --
3732impl GlobalDispatch<XdgWmBase, ()> for Compositor {
3733    fn bind(
3734        _: &mut Self,
3735        _: &DisplayHandle,
3736        _: &Client,
3737        resource: New<XdgWmBase>,
3738        _: &(),
3739        data_init: &mut DataInit<'_, Self>,
3740    ) {
3741        data_init.init(resource, ());
3742    }
3743}
3744
3745impl Dispatch<XdgWmBase, ()> for Compositor {
3746    fn request(
3747        state: &mut Self,
3748        _: &Client,
3749        _: &XdgWmBase,
3750        request: <XdgWmBase as Resource>::Request,
3751        _: &(),
3752        _: &DisplayHandle,
3753        data_init: &mut DataInit<'_, Self>,
3754    ) {
3755        use xdg_wm_base::Request;
3756        match request {
3757            Request::GetXdgSurface { id, surface } => {
3758                let wl_surface_id = surface.id();
3759                let xdg_surface = data_init.init(
3760                    id,
3761                    XdgSurfaceData {
3762                        wl_surface_id: wl_surface_id.clone(),
3763                    },
3764                );
3765                if let Some(surf) = state.surfaces.get_mut(&wl_surface_id) {
3766                    surf.xdg_surface = Some(xdg_surface);
3767                }
3768            }
3769            Request::CreatePositioner { id } => {
3770                let positioner = data_init.init(id, ());
3771                let pos_id = positioner.id();
3772                state.positioners.insert(
3773                    pos_id,
3774                    PositionerState {
3775                        resource: positioner,
3776                        geometry: PositionerGeometry {
3777                            size: (0, 0),
3778                            anchor_rect: (0, 0, 0, 0),
3779                            anchor: 0,
3780                            gravity: 0,
3781                            constraint_adjustment: 0,
3782                            offset: (0, 0),
3783                        },
3784                    },
3785                );
3786            }
3787            Request::Pong { .. } => {}
3788            Request::Destroy => {}
3789            _ => {}
3790        }
3791    }
3792}
3793
3794// -- xdg_surface --
3795impl Dispatch<XdgSurface, XdgSurfaceData> for Compositor {
3796    fn request(
3797        state: &mut Self,
3798        _: &Client,
3799        resource: &XdgSurface,
3800        request: <XdgSurface as Resource>::Request,
3801        data: &XdgSurfaceData,
3802        _: &DisplayHandle,
3803        data_init: &mut DataInit<'_, Self>,
3804    ) {
3805        use xdg_surface::Request;
3806        match request {
3807            Request::GetToplevel { id } => {
3808                let toplevel = data_init.init(
3809                    id,
3810                    XdgToplevelData {
3811                        wl_surface_id: data.wl_surface_id.clone(),
3812                    },
3813                );
3814                let surface_id = state.allocate_surface_id();
3815                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3816                    surf.xdg_toplevel = Some(toplevel.clone());
3817                    surf.surface_id = surface_id;
3818                }
3819                state
3820                    .toplevel_surface_ids
3821                    .insert(surface_id, data.wl_surface_id.clone());
3822
3823                // Use a per-surface size if one was already configured
3824                // (e.g. the browser sent C2S_SURFACE_RESIZE before the
3825                // toplevel was created), otherwise fall back to the global
3826                // output dimensions.  surface_sizes stores logical pixels.
3827                let (cw, ch) = state
3828                    .surface_sizes
3829                    .get(&surface_id)
3830                    .copied()
3831                    .unwrap_or((state.output_width, state.output_height));
3832                let states = xdg_toplevel_states(&[
3833                    xdg_toplevel::State::Activated,
3834                    xdg_toplevel::State::Maximized,
3835                ]);
3836                toplevel.configure(cw, ch, states);
3837                let serial = state.next_serial();
3838                resource.configure(serial);
3839
3840                // Keyboard focus — sends leave to the previously focused
3841                // surface's client before entering the new one.
3842                state.set_keyboard_focus(surface_id);
3843                // Tell the client which output its surface is on so it can
3844                // determine scale and start rendering.
3845                if let Some(surf) = state.surfaces.get(&data.wl_surface_id) {
3846                    for output in &state.outputs {
3847                        if same_client(output, &surf.wl_surface) {
3848                            surf.wl_surface.enter(output);
3849                        }
3850                    }
3851                }
3852                let _ = state.display_handle.flush_clients();
3853
3854                let _ = state.event_tx.send(CompositorEvent::SurfaceCreated {
3855                    surface_id,
3856                    title: String::new(),
3857                    app_id: String::new(),
3858                    parent_id: 0,
3859                    width: 0,
3860                    height: 0,
3861                });
3862                (state.event_notify)();
3863                if state.verbose {
3864                    eprintln!("[compositor] new_toplevel sid={surface_id}");
3865                }
3866            }
3867            Request::GetPopup {
3868                id,
3869                parent,
3870                positioner,
3871            } => {
3872                let popup = data_init.init(
3873                    id,
3874                    XdgPopupData {
3875                        wl_surface_id: data.wl_surface_id.clone(),
3876                    },
3877                );
3878
3879                // Parent relationship: make the popup a child of the parent
3880                // surface so it is composited into the same toplevel frame.
3881                let parent_wl_id: Option<ObjectId> = parent
3882                    .as_ref()
3883                    .and_then(|p| p.data::<XdgSurfaceData>())
3884                    .map(|d| d.wl_surface_id.clone());
3885
3886                // The xdg-shell protocol specifies popup positions relative
3887                // to the parent's *window geometry*, not its surface origin.
3888                // Fetch the parent's geometry offset so we can convert
3889                // between window-geometry space and surface-tree space.
3890                let parent_geom_offset = parent_wl_id
3891                    .as_ref()
3892                    .and_then(|pid| state.surfaces.get(pid))
3893                    .and_then(|s| s.xdg_geometry)
3894                    .map(|(gx, gy, _, _)| (gx, gy))
3895                    .unwrap_or((0, 0));
3896
3897                // Compute the parent's absolute position within the toplevel
3898                // and the logical output bounds for constraint adjustment.
3899                // Add the geometry offset so parent_abs represents the
3900                // window-geometry origin in surface-tree coordinates.
3901                let parent_abs = parent_wl_id
3902                    .as_ref()
3903                    .map(|pid| {
3904                        let abs = state.surface_absolute_position(pid);
3905                        (abs.0 + parent_geom_offset.0, abs.1 + parent_geom_offset.1)
3906                    })
3907                    .unwrap_or((0, 0));
3908                // Use the client's actual surface size for popup bounds,
3909                // not the configured size (client may not have resized yet).
3910                let (_, toplevel_root) = parent_wl_id
3911                    .as_ref()
3912                    .map(|pid| state.find_toplevel_root(pid))
3913                    .unwrap_or_else(|| {
3914                        // Dummy root — no parent.
3915                        (data.wl_surface_id.clone(), None)
3916                    });
3917                let bounds = toplevel_root
3918                    .and_then(|_| {
3919                        let root_wl_id = parent_wl_id.as_ref().map(|pid| {
3920                            let (rid, _) = state.find_toplevel_root(pid);
3921                            rid
3922                        })?;
3923                        let surf = state.surfaces.get(&root_wl_id)?;
3924                        if let Some((gx, gy, gw, gh)) = surf.xdg_geometry
3925                            && gw > 0
3926                            && gh > 0
3927                        {
3928                            return Some((gx, gy, gw, gh));
3929                        }
3930
3931                        // Fall back to the client's actual logical surface
3932                        // size when window geometry is unavailable.
3933                        let sm = state.surface_meta.get(&root_wl_id)?;
3934                        let s = (sm.scale).max(1);
3935                        let (lw, lh) = surf
3936                            .viewport_destination
3937                            .filter(|&(dw, dh)| dw > 0 && dh > 0)
3938                            .unwrap_or((sm.width as i32 / s, sm.height as i32 / s));
3939                        Some((0, 0, lw, lh))
3940                    })
3941                    .unwrap_or((0, 0, state.output_width, state.output_height));
3942
3943                eprintln!(
3944                    "[popup] parent_abs={parent_abs:?} bounds={bounds:?} parent_wl={parent_wl_id:?} geom_off={parent_geom_offset:?}"
3945                );
3946                // Compute geometry from positioner with constraint adjustment.
3947                let pos_id = positioner.id();
3948                let (px, py, pw, ph) = state
3949                    .positioners
3950                    .get(&pos_id)
3951                    .map(|p| p.geometry.compute_position(parent_abs, bounds))
3952                    .unwrap_or((0, 0, 200, 200));
3953                eprintln!("[popup] result=({px},{py},{pw},{ph})");
3954
3955                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3956                    surf.xdg_popup = Some(popup.clone());
3957                    surf.parent_surface_id = parent_wl_id.clone();
3958                    // Convert from window-geometry-relative to surface-
3959                    // relative coords so the popup composites correctly.
3960                    // The rendering crops to xdg_geometry, so the popup
3961                    // must be offset by the parent's geometry origin.
3962                    surf.subsurface_position =
3963                        (parent_geom_offset.0 + px, parent_geom_offset.1 + py);
3964                }
3965                if let Some(ref parent_id) = parent_wl_id
3966                    && let Some(parent_surf) = state.surfaces.get_mut(parent_id)
3967                    && !parent_surf.children.contains(&data.wl_surface_id)
3968                {
3969                    parent_surf.children.push(data.wl_surface_id.clone());
3970                }
3971
3972                popup.configure(px, py, pw, ph);
3973                let serial = state.next_serial();
3974                resource.configure(serial);
3975                let _ = state.display_handle.flush_clients();
3976            }
3977            Request::SetWindowGeometry {
3978                x,
3979                y,
3980                width,
3981                height,
3982            } => {
3983                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3984                    // For popup surfaces, adjust subsurface_position to
3985                    // account for the popup's own geometry offset.  The
3986                    // xdg-shell protocol positions the popup's *geometry*
3987                    // (not its surface origin) relative to the parent's
3988                    // geometry.  Without this adjustment, CSD shadows or
3989                    // borders around the popup cause the visible content
3990                    // to shift by (gx, gy).
3991                    if surf.xdg_popup.is_some() {
3992                        let (old_gx, old_gy) = surf
3993                            .xdg_geometry
3994                            .map(|(gx, gy, _, _)| (gx, gy))
3995                            .unwrap_or((0, 0));
3996                        surf.subsurface_position.0 += old_gx - x;
3997                        surf.subsurface_position.1 += old_gy - y;
3998                    }
3999                    surf.xdg_geometry = Some((x, y, width, height));
4000                }
4001            }
4002            Request::AckConfigure { .. } => {}
4003            Request::Destroy => {}
4004            _ => {}
4005        }
4006    }
4007}
4008
4009// -- xdg_toplevel --
4010impl Dispatch<XdgToplevel, XdgToplevelData> for Compositor {
4011    fn request(
4012        state: &mut Self,
4013        _: &Client,
4014        _: &XdgToplevel,
4015        request: <XdgToplevel as Resource>::Request,
4016        data: &XdgToplevelData,
4017        _: &DisplayHandle,
4018        _: &mut DataInit<'_, Self>,
4019    ) {
4020        use xdg_toplevel::Request;
4021        match request {
4022            Request::SetTitle { title } => {
4023                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
4024                    && surf.title != title
4025                {
4026                    surf.title = title.clone();
4027                    if surf.surface_id > 0 {
4028                        let _ = state.event_tx.send(CompositorEvent::SurfaceTitle {
4029                            surface_id: surf.surface_id,
4030                            title,
4031                        });
4032                        (state.event_notify)();
4033                    }
4034                }
4035            }
4036            Request::SetAppId { app_id } => {
4037                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
4038                    && surf.app_id != app_id
4039                {
4040                    surf.app_id = app_id.clone();
4041                    if surf.surface_id > 0 {
4042                        let _ = state.event_tx.send(CompositorEvent::SurfaceAppId {
4043                            surface_id: surf.surface_id,
4044                            app_id,
4045                        });
4046                        (state.event_notify)();
4047                    }
4048                }
4049            }
4050            Request::Destroy => {
4051                let wl_surface_id = &data.wl_surface_id;
4052                state.surface_meta.remove(wl_surface_id);
4053                state.cursor_rgba.remove(wl_surface_id);
4054                if let Some(ref mut vk) = state.vulkan_renderer {
4055                    vk.remove_surface(wl_surface_id);
4056                }
4057                if let Some(held) = state.held_buffers.remove(wl_surface_id) {
4058                    held.release();
4059                }
4060                if let Some(surf) = state.surfaces.get_mut(wl_surface_id) {
4061                    let sid = surf.surface_id;
4062                    surf.xdg_toplevel = None;
4063                    if sid > 0 {
4064                        state.toplevel_surface_ids.remove(&sid);
4065                        state.last_reported_size.remove(&sid);
4066                        state.surface_sizes.remove(&sid);
4067                        if let Some(ref mut vk) = state.vulkan_renderer {
4068                            vk.destroy_external_outputs_for_surface(sid as u32);
4069                        }
4070                        let _ = state
4071                            .event_tx
4072                            .send(CompositorEvent::SurfaceDestroyed { surface_id: sid });
4073                        (state.event_notify)();
4074                        surf.surface_id = 0;
4075                    }
4076                }
4077            }
4078            _ => {}
4079        }
4080    }
4081}
4082
4083// -- xdg_popup --
4084impl Dispatch<XdgPopup, XdgPopupData> for Compositor {
4085    fn request(
4086        state: &mut Self,
4087        _: &Client,
4088        _: &XdgPopup,
4089        request: <XdgPopup as Resource>::Request,
4090        data: &XdgPopupData,
4091        _: &DisplayHandle,
4092        _: &mut DataInit<'_, Self>,
4093    ) {
4094        use xdg_popup::Request;
4095        match request {
4096            Request::Grab { seat: _, serial: _ } => {
4097                // Add this popup to the grab stack so we can send
4098                // popup_done when the user clicks outside.
4099                state
4100                    .popup_grab_stack
4101                    .retain(|id| *id != data.wl_surface_id);
4102                state.popup_grab_stack.push(data.wl_surface_id.clone());
4103                // A grab is the client saying this menu now owns the input
4104                // it named. Keyboard focus has to follow, or the menu is on
4105                // screen while the keyboard still talks to the page.
4106                state.focus_popup(&data.wl_surface_id);
4107            }
4108            Request::Reposition { positioner, token } => {
4109                // Recompute the popup position using the new positioner.
4110                let pos_id = positioner.id();
4111                if let Some(surf) = state.surfaces.get(&data.wl_surface_id)
4112                    && let Some(parent_id) = surf.parent_surface_id.clone()
4113                {
4114                    let parent_geom_offset = state
4115                        .surfaces
4116                        .get(&parent_id)
4117                        .and_then(|s| s.xdg_geometry)
4118                        .map(|(gx, gy, _, _)| (gx, gy))
4119                        .unwrap_or((0, 0));
4120                    let parent_abs = {
4121                        let abs = state.surface_absolute_position(&parent_id);
4122                        (abs.0 + parent_geom_offset.0, abs.1 + parent_geom_offset.1)
4123                    };
4124                    let (root_id, toplevel_root) = state.find_toplevel_root(&parent_id);
4125                    let bounds = toplevel_root
4126                        .and_then(|_| {
4127                            let surf = state.surfaces.get(&root_id)?;
4128                            if let Some((gx, gy, gw, gh)) = surf.xdg_geometry
4129                                && gw > 0
4130                                && gh > 0
4131                            {
4132                                return Some((gx, gy, gw, gh));
4133                            }
4134                            let sm = state.surface_meta.get(&root_id)?;
4135                            let s = (sm.scale).max(1);
4136                            let (lw, lh) = surf
4137                                .viewport_destination
4138                                .filter(|&(dw, dh)| dw > 0 && dh > 0)
4139                                .unwrap_or((sm.width as i32 / s, sm.height as i32 / s));
4140                            Some((0, 0, lw, lh))
4141                        })
4142                        .unwrap_or((0, 0, state.output_width, state.output_height));
4143                    let (px, py, pw, ph) = state
4144                        .positioners
4145                        .get(&pos_id)
4146                        .map(|p| p.geometry.compute_position(parent_abs, bounds))
4147                        .unwrap_or((0, 0, 200, 200));
4148                    if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
4149                        // Undo the previous geometry adjustment before
4150                        // applying the new position.
4151                        let old_gx = surf.xdg_geometry.map(|(gx, _, _, _)| gx).unwrap_or(0);
4152                        let old_gy = surf.xdg_geometry.map(|(_, gy, _, _)| gy).unwrap_or(0);
4153                        surf.subsurface_position = (
4154                            parent_geom_offset.0 + px - old_gx,
4155                            parent_geom_offset.1 + py - old_gy,
4156                        );
4157                        if let Some(ref popup) = surf.xdg_popup {
4158                            popup.configure(px, py, pw, ph);
4159                            popup.repositioned(token);
4160                        }
4161                        if let Some(ref xs) = surf.xdg_surface {
4162                            let serial = state.serial.wrapping_add(1);
4163                            state.serial = serial;
4164                            xs.configure(serial);
4165                        }
4166                    }
4167                }
4168            }
4169            Request::Destroy => {
4170                // Remove from grab stack.
4171                state
4172                    .popup_grab_stack
4173                    .retain(|id| *id != data.wl_surface_id);
4174                // Hand the keyboard back — a menu closed by its own client
4175                // (picking an item, pressing Escape) never goes through the
4176                // click-outside path, so this is the ordinary way a popup
4177                // ends. Off the stack first, so what remains is what is
4178                // still grabbing beneath it.
4179                state.unfocus_popup(&data.wl_surface_id);
4180                // Remove from parent's children list.
4181                if let Some(parent_id) = state
4182                    .surfaces
4183                    .get(&data.wl_surface_id)
4184                    .and_then(|s| s.parent_surface_id.clone())
4185                    && let Some(parent) = state.surfaces.get_mut(&parent_id)
4186                {
4187                    parent.children.retain(|c| *c != data.wl_surface_id);
4188                }
4189                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
4190                    surf.xdg_popup = None;
4191                    surf.parent_surface_id = None;
4192                }
4193            }
4194            _ => {}
4195        }
4196    }
4197}
4198
4199// -- xdg_positioner --
4200use wayland_protocols::xdg::shell::server::xdg_positioner;
4201impl Dispatch<XdgPositioner, ()> for Compositor {
4202    fn request(
4203        state: &mut Self,
4204        _: &Client,
4205        resource: &XdgPositioner,
4206        request: <XdgPositioner as Resource>::Request,
4207        _: &(),
4208        _: &DisplayHandle,
4209        _: &mut DataInit<'_, Self>,
4210    ) {
4211        use xdg_positioner::Request;
4212        let pos_id = resource.id();
4213        let Some(pos) = state.positioners.get_mut(&pos_id) else {
4214            return;
4215        };
4216        match request {
4217            Request::SetSize { width, height } => {
4218                pos.geometry.size = (width, height);
4219            }
4220            Request::SetAnchorRect {
4221                x,
4222                y,
4223                width,
4224                height,
4225            } => {
4226                pos.geometry.anchor_rect = (x, y, width, height);
4227            }
4228            Request::SetAnchor {
4229                anchor: wayland_server::WEnum::Value(v),
4230            } => {
4231                pos.geometry.anchor = v as u32;
4232            }
4233            Request::SetGravity {
4234                gravity: wayland_server::WEnum::Value(v),
4235            } => {
4236                pos.geometry.gravity = v as u32;
4237            }
4238            Request::SetOffset { x, y } => {
4239                pos.geometry.offset = (x, y);
4240            }
4241            Request::SetConstraintAdjustment {
4242                constraint_adjustment,
4243            } => {
4244                pos.geometry.constraint_adjustment = constraint_adjustment.into();
4245            }
4246            Request::Destroy => {
4247                state.positioners.remove(&pos_id);
4248            }
4249            _ => {}
4250        }
4251    }
4252}
4253
4254// -- xdg_decoration --
4255impl GlobalDispatch<ZxdgDecorationManagerV1, ()> for Compositor {
4256    fn bind(
4257        _: &mut Self,
4258        _: &DisplayHandle,
4259        _: &Client,
4260        resource: New<ZxdgDecorationManagerV1>,
4261        _: &(),
4262        data_init: &mut DataInit<'_, Self>,
4263    ) {
4264        data_init.init(resource, ());
4265    }
4266}
4267
4268impl Dispatch<ZxdgDecorationManagerV1, ()> for Compositor {
4269    fn request(
4270        _: &mut Self,
4271        _: &Client,
4272        _: &ZxdgDecorationManagerV1,
4273        request: <ZxdgDecorationManagerV1 as Resource>::Request,
4274        _: &(),
4275        _: &DisplayHandle,
4276        data_init: &mut DataInit<'_, Self>,
4277    ) {
4278        use zxdg_decoration_manager_v1::Request;
4279        match request {
4280            Request::GetToplevelDecoration { id, toplevel: _ } => {
4281                let decoration = data_init.init(id, ());
4282                // Always request server-side (i.e. no) decorations.
4283                decoration.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
4284            }
4285            Request::Destroy => {}
4286            _ => {}
4287        }
4288    }
4289}
4290
4291impl Dispatch<ZxdgToplevelDecorationV1, ()> for Compositor {
4292    fn request(
4293        _: &mut Self,
4294        _: &Client,
4295        resource: &ZxdgToplevelDecorationV1,
4296        request: <ZxdgToplevelDecorationV1 as Resource>::Request,
4297        _: &(),
4298        _: &DisplayHandle,
4299        _: &mut DataInit<'_, Self>,
4300    ) {
4301        use zxdg_toplevel_decoration_v1::Request;
4302        match request {
4303            Request::SetMode { .. } | Request::UnsetMode => {
4304                resource.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
4305            }
4306            Request::Destroy => {}
4307            _ => {}
4308        }
4309    }
4310}
4311
4312// -- wl_shm --
4313impl GlobalDispatch<WlShm, ()> for Compositor {
4314    fn bind(
4315        _: &mut Self,
4316        _: &DisplayHandle,
4317        _: &Client,
4318        resource: New<WlShm>,
4319        _: &(),
4320        data_init: &mut DataInit<'_, Self>,
4321    ) {
4322        let shm = data_init.init(resource, ());
4323        shm.format(wl_shm::Format::Argb8888);
4324        shm.format(wl_shm::Format::Xrgb8888);
4325        shm.format(wl_shm::Format::Abgr8888);
4326        shm.format(wl_shm::Format::Xbgr8888);
4327    }
4328}
4329
4330impl Dispatch<WlShm, ()> for Compositor {
4331    fn request(
4332        state: &mut Self,
4333        _: &Client,
4334        _: &WlShm,
4335        request: <WlShm as Resource>::Request,
4336        _: &(),
4337        _: &DisplayHandle,
4338        data_init: &mut DataInit<'_, Self>,
4339    ) {
4340        use wayland_server::protocol::wl_shm::Request;
4341        if let Request::CreatePool { id, fd, size } = request {
4342            let pool = data_init.init(id, ());
4343            let pool_id = pool.id();
4344            state
4345                .shm_pools
4346                .insert(pool_id, Arc::new(ShmPool::new(pool, fd, size)));
4347        }
4348    }
4349}
4350
4351// -- wl_shm_pool --
4352impl Dispatch<WlShmPool, ()> for Compositor {
4353    fn request(
4354        state: &mut Self,
4355        _: &Client,
4356        resource: &WlShmPool,
4357        request: <WlShmPool as Resource>::Request,
4358        _: &(),
4359        _: &DisplayHandle,
4360        data_init: &mut DataInit<'_, Self>,
4361    ) {
4362        use wayland_server::protocol::wl_shm_pool::Request;
4363        let pool_id = resource.id();
4364        match request {
4365            Request::CreateBuffer {
4366                id,
4367                offset,
4368                width,
4369                height,
4370                stride,
4371                format,
4372            } => {
4373                // format comes as WEnum<Format>, extract the known value.
4374                let fmt = match format {
4375                    wayland_server::WEnum::Value(f) => f,
4376                    _ => wl_shm::Format::Argb8888, // fallback
4377                };
4378                let Some(pool) = state.shm_pools.get(&pool_id).cloned() else {
4379                    return;
4380                };
4381                data_init.init(
4382                    id,
4383                    ShmBufferData {
4384                        pool,
4385                        offset,
4386                        width,
4387                        height,
4388                        stride,
4389                        format: fmt,
4390                    },
4391                );
4392            }
4393            Request::Resize { size } => {
4394                if let Some(pool) = state.shm_pools.get(&pool_id) {
4395                    pool.resize(size);
4396                }
4397            }
4398            Request::Destroy => {
4399                // Drop the map entry — Arc keeps the ShmPool alive while
4400                // wl_buffers created from it still reference it.
4401                state.shm_pools.remove(&pool_id);
4402            }
4403            _ => {}
4404        }
4405    }
4406}
4407
4408// -- wl_buffer (SHM) --
4409impl Dispatch<WlBuffer, ShmBufferData> for Compositor {
4410    fn request(
4411        _: &mut Self,
4412        _: &Client,
4413        _: &WlBuffer,
4414        _: <WlBuffer as Resource>::Request,
4415        _: &ShmBufferData,
4416        _: &DisplayHandle,
4417        _: &mut DataInit<'_, Self>,
4418    ) {
4419    }
4420}
4421
4422// -- wl_buffer (DMA-BUF) --
4423impl Dispatch<WlBuffer, DmaBufBufferData> for Compositor {
4424    fn request(
4425        _: &mut Self,
4426        _: &Client,
4427        _: &WlBuffer,
4428        _: <WlBuffer as Resource>::Request,
4429        _: &DmaBufBufferData,
4430        _: &DisplayHandle,
4431        _: &mut DataInit<'_, Self>,
4432    ) {
4433    }
4434}
4435
4436// -- wl_output --
4437impl GlobalDispatch<WlOutput, ()> for Compositor {
4438    fn bind(
4439        state: &mut Self,
4440        _: &DisplayHandle,
4441        _: &Client,
4442        resource: New<WlOutput>,
4443        _: &(),
4444        data_init: &mut DataInit<'_, Self>,
4445    ) {
4446        let output = data_init.init(resource, ());
4447        output.geometry(
4448            0,
4449            0,
4450            0,
4451            0,
4452            wl_output::Subpixel::Unknown,
4453            "Virtual".to_string(),
4454            "Headless".to_string(),
4455            wl_output::Transform::Normal,
4456        );
4457        let s120 = state.output_scale_120 as i32;
4458        let mode_w = state.output_width * s120 / 120;
4459        let mode_h = state.output_height * s120 / 120;
4460        output.mode(
4461            wl_output::Mode::Current | wl_output::Mode::Preferred,
4462            mode_w,
4463            mode_h,
4464            state.output_refresh_mhz as i32,
4465        );
4466        if output.version() >= 2 {
4467            output.scale(((state.output_scale_120 as i32) + 119) / 120);
4468        }
4469        if output.version() >= 2 {
4470            output.done();
4471        }
4472        state.outputs.push(output);
4473    }
4474}
4475
4476impl Dispatch<WlOutput, ()> for Compositor {
4477    fn request(
4478        state: &mut Self,
4479        _: &Client,
4480        resource: &WlOutput,
4481        request: <WlOutput as Resource>::Request,
4482        _: &(),
4483        _: &DisplayHandle,
4484        _: &mut DataInit<'_, Self>,
4485    ) {
4486        use wayland_server::protocol::wl_output::Request;
4487        if let Request::Release = request {
4488            state.outputs.retain(|o| o.id() != resource.id());
4489        }
4490    }
4491}
4492
4493// -- wl_seat --
4494impl GlobalDispatch<WlSeat, ()> for Compositor {
4495    fn bind(
4496        _: &mut Self,
4497        _: &DisplayHandle,
4498        _: &Client,
4499        resource: New<WlSeat>,
4500        _: &(),
4501        data_init: &mut DataInit<'_, Self>,
4502    ) {
4503        let seat = data_init.init(resource, ());
4504        seat.capabilities(wl_seat::Capability::Keyboard | wl_seat::Capability::Pointer);
4505        if seat.version() >= 2 {
4506            seat.name("headless".to_string());
4507        }
4508    }
4509}
4510
4511impl Dispatch<WlSeat, ()> for Compositor {
4512    fn request(
4513        state: &mut Self,
4514        _: &Client,
4515        _: &WlSeat,
4516        request: <WlSeat as Resource>::Request,
4517        _: &(),
4518        _: &DisplayHandle,
4519        data_init: &mut DataInit<'_, Self>,
4520    ) {
4521        use wayland_server::protocol::wl_seat::Request;
4522        match request {
4523            Request::GetKeyboard { id } => {
4524                let kb = data_init.init(id, ());
4525                if let Some(fd) = create_keymap_fd(&state.keyboard_keymap_data) {
4526                    kb.keymap(
4527                        wl_keyboard::KeymapFormat::XkbV1,
4528                        fd.as_fd(),
4529                        state.keyboard_keymap_data.len() as u32,
4530                    );
4531                }
4532                if kb.version() >= 4 {
4533                    kb.repeat_info(25, 200);
4534                }
4535                state.keyboards.push(kb);
4536            }
4537            Request::GetPointer { id } => {
4538                let ptr = data_init.init(id, ());
4539                // If the cursor is already inside one of this client's
4540                // surfaces, this pointer has missed its `enter` — the client
4541                // asked for it too late. Nothing else will deliver one until
4542                // the pointer crosses into a *different* surface, so a user
4543                // whose cursor is simply resting over the pane would click
4544                // into a void. Send it now, at the position we last saw.
4545                let entered = state.pointer_entered_id.as_ref().and_then(|eid| {
4546                    state
4547                        .surfaces
4548                        .values()
4549                        .find(|s| s.wl_surface.id() == *eid)
4550                        .map(|s| s.wl_surface.clone())
4551                });
4552                if let Some(wl) = entered
4553                    && same_client(&ptr, &wl)
4554                {
4555                    let serial = state.next_serial();
4556                    let (lx, ly) = state.pointer_entered_local;
4557                    ptr.enter(serial, &wl, lx, ly);
4558                    // Mandatory here: with no motion following, the frame is
4559                    // what tells a v5+ client the group is complete.
4560                    ptr.frame();
4561                    let _ = state.display_handle.flush_clients();
4562                }
4563                state.pointers.push(ptr);
4564            }
4565            Request::GetTouch { id } => {
4566                data_init.init(id, ());
4567            }
4568            Request::Release => {}
4569            _ => {}
4570        }
4571    }
4572}
4573
4574// -- wl_keyboard --
4575impl Dispatch<WlKeyboard, ()> for Compositor {
4576    fn request(
4577        state: &mut Self,
4578        _: &Client,
4579        resource: &WlKeyboard,
4580        request: <WlKeyboard as Resource>::Request,
4581        _: &(),
4582        _: &DisplayHandle,
4583        _: &mut DataInit<'_, Self>,
4584    ) {
4585        if let wl_keyboard::Request::Release = request {
4586            state.keyboards.retain(|k| k.id() != resource.id());
4587        }
4588    }
4589}
4590
4591// -- wl_pointer --
4592impl Dispatch<WlPointer, ()> for Compositor {
4593    fn request(
4594        state: &mut Self,
4595        _: &Client,
4596        resource: &WlPointer,
4597        request: <WlPointer as Resource>::Request,
4598        _: &(),
4599        _: &DisplayHandle,
4600        _: &mut DataInit<'_, Self>,
4601    ) {
4602        use wl_pointer::Request;
4603        match request {
4604            Request::SetCursor {
4605                serial: _,
4606                surface,
4607                hotspot_x,
4608                hotspot_y,
4609            } => {
4610                if let Some(surface) = surface {
4611                    let sid = surface.id();
4612                    if let Some(surf) = state.surfaces.get_mut(&sid) {
4613                        surf.is_cursor = true;
4614                        surf.cursor_hotspot = (hotspot_x, hotspot_y);
4615                    }
4616                } else {
4617                    let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
4618                        surface_id: state.focused_surface_id,
4619                        cursor: CursorImage::Hidden,
4620                    });
4621                }
4622            }
4623            Request::Release => {
4624                state.pointers.retain(|p| p.id() != resource.id());
4625            }
4626            _ => {}
4627        }
4628    }
4629}
4630
4631// -- wl_touch (stub) --
4632impl Dispatch<wayland_server::protocol::wl_touch::WlTouch, ()> for Compositor {
4633    fn request(
4634        _: &mut Self,
4635        _: &Client,
4636        _: &wayland_server::protocol::wl_touch::WlTouch,
4637        _: <wayland_server::protocol::wl_touch::WlTouch as Resource>::Request,
4638        _: &(),
4639        _: &DisplayHandle,
4640        _: &mut DataInit<'_, Self>,
4641    ) {
4642    }
4643}
4644
4645// -- zwp_linux_dmabuf_v1 --
4646impl GlobalDispatch<ZwpLinuxDmabufV1, ()> for Compositor {
4647    fn bind(
4648        state: &mut Self,
4649        _: &DisplayHandle,
4650        _: &Client,
4651        resource: New<ZwpLinuxDmabufV1>,
4652        _: &(),
4653        data_init: &mut DataInit<'_, Self>,
4654    ) {
4655        let dmabuf = data_init.init(resource, ());
4656        // v4+ clients use get_default_feedback / get_surface_feedback
4657        // instead of the deprecated format/modifier events.
4658        if dmabuf.version() >= 4 {
4659            return;
4660        }
4661        if dmabuf.version() >= 3 {
4662            // Advertise DRM format modifiers that the Vulkan device can
4663            // actually import.  This ensures clients (Chromium, mpv, …)
4664            // allocate DMA-BUFs with a tiling layout the compositor can
4665            // handle natively on the GPU, avoiding broken CPU mmap
4666            // fallbacks for vendor-specific tiled VRAM.
4667            if let Some(ref vk) = state.vulkan_renderer
4668                && !vk.supported_dmabuf_modifiers.is_empty()
4669            {
4670                for &(drm_fmt, modifier) in &vk.supported_dmabuf_modifiers {
4671                    let mod_hi = (modifier >> 32) as u32;
4672                    let mod_lo = (modifier & 0xFFFFFFFF) as u32;
4673                    dmabuf.modifier(drm_fmt, mod_hi, mod_lo);
4674                }
4675            }
4676            // When Vulkan has no DMA-BUF extensions (SHM-only mode) we
4677            // intentionally advertise zero modifiers so clients fall back
4678            // to wl_shm.
4679        } else if state
4680            .vulkan_renderer
4681            .as_ref()
4682            .is_some_and(|vk| vk.has_dmabuf())
4683        {
4684            dmabuf.format(drm_fourcc::ARGB8888);
4685            dmabuf.format(drm_fourcc::XRGB8888);
4686            dmabuf.format(drm_fourcc::ABGR8888);
4687            dmabuf.format(drm_fourcc::XBGR8888);
4688        }
4689    }
4690}
4691
4692impl Dispatch<ZwpLinuxDmabufV1, ()> for Compositor {
4693    fn request(
4694        state: &mut Self,
4695        _: &Client,
4696        _: &ZwpLinuxDmabufV1,
4697        request: <ZwpLinuxDmabufV1 as Resource>::Request,
4698        _: &(),
4699        _: &DisplayHandle,
4700        data_init: &mut DataInit<'_, Self>,
4701    ) {
4702        use zwp_linux_dmabuf_v1::Request;
4703        match request {
4704            Request::CreateParams { params_id } => {
4705                data_init.init(params_id, ());
4706            }
4707            Request::GetDefaultFeedback { id } => {
4708                let fb = data_init.init(id, ());
4709                state.send_dmabuf_feedback(&fb);
4710            }
4711            Request::GetSurfaceFeedback { id, .. } => {
4712                let fb = data_init.init(id, ());
4713                state.send_dmabuf_feedback(&fb);
4714            }
4715            Request::Destroy => {}
4716            _ => {}
4717        }
4718    }
4719}
4720
4721impl Dispatch<ZwpLinuxDmabufFeedbackV1, ()> for Compositor {
4722    fn request(
4723        _: &mut Self,
4724        _: &Client,
4725        _: &ZwpLinuxDmabufFeedbackV1,
4726        _request: <ZwpLinuxDmabufFeedbackV1 as Resource>::Request,
4727        _: &(),
4728        _: &DisplayHandle,
4729        _data_init: &mut DataInit<'_, Self>,
4730    ) {
4731        // Only request is Destroy, handled automatically.
4732    }
4733}
4734
4735// -- zwp_linux_buffer_params_v1 --
4736impl Dispatch<ZwpLinuxBufferParamsV1, ()> for Compositor {
4737    fn request(
4738        state: &mut Self,
4739        client: &Client,
4740        resource: &ZwpLinuxBufferParamsV1,
4741        request: <ZwpLinuxBufferParamsV1 as Resource>::Request,
4742        _: &(),
4743        dh: &DisplayHandle,
4744        data_init: &mut DataInit<'_, Self>,
4745    ) {
4746        use zwp_linux_buffer_params_v1::Request;
4747        let params_id = resource.id();
4748        match request {
4749            Request::Add {
4750                fd,
4751                plane_idx: _,
4752                offset,
4753                stride,
4754                modifier_hi,
4755                modifier_lo,
4756            } => {
4757                let modifier = ((modifier_hi as u64) << 32) | (modifier_lo as u64);
4758                let entry = state
4759                    .dmabuf_params
4760                    .entry(params_id.clone())
4761                    .or_insert_with(|| DmaBufParamsPending {
4762                        resource: resource.clone(),
4763                        planes: Vec::new(),
4764                        modifier,
4765                    });
4766                entry.modifier = modifier;
4767                entry.planes.push(DmaBufPlane { fd, offset, stride });
4768            }
4769            Request::Create {
4770                width,
4771                height,
4772                format,
4773                flags,
4774            } => {
4775                let pending = state.dmabuf_params.remove(&params_id);
4776                let (planes, modifier) = match pending {
4777                    Some(p) => (p.planes, p.modifier),
4778                    None => {
4779                        resource.failed();
4780                        return;
4781                    }
4782                };
4783                let y_invert = flags
4784                    .into_result()
4785                    .ok()
4786                    .is_some_and(|f| f.contains(zwp_linux_buffer_params_v1::Flags::YInvert));
4787                match client.create_resource::<WlBuffer, DmaBufBufferData, Compositor>(
4788                    dh,
4789                    1,
4790                    DmaBufBufferData {
4791                        width,
4792                        height,
4793                        fourcc: format,
4794                        modifier,
4795                        planes,
4796                        y_invert,
4797                    },
4798                ) {
4799                    Ok(buffer) => resource.created(&buffer),
4800                    Err(_) => resource.failed(),
4801                }
4802            }
4803            Request::CreateImmed {
4804                buffer_id,
4805                width,
4806                height,
4807                format,
4808                flags,
4809            } => {
4810                let (planes, modifier) = state
4811                    .dmabuf_params
4812                    .remove(&params_id)
4813                    .map(|p| (p.planes, p.modifier))
4814                    .unwrap_or_default();
4815                let y_invert = flags
4816                    .into_result()
4817                    .ok()
4818                    .is_some_and(|f| f.contains(zwp_linux_buffer_params_v1::Flags::YInvert));
4819                data_init.init(
4820                    buffer_id,
4821                    DmaBufBufferData {
4822                        width,
4823                        height,
4824                        fourcc: format,
4825                        modifier,
4826                        planes,
4827                        y_invert,
4828                    },
4829                );
4830            }
4831            Request::Destroy => {
4832                state.dmabuf_params.remove(&params_id);
4833            }
4834            _ => {}
4835        }
4836    }
4837}
4838
4839// -- wp_fractional_scale_manager_v1 --
4840impl GlobalDispatch<WpFractionalScaleManagerV1, ()> for Compositor {
4841    fn bind(
4842        _: &mut Self,
4843        _: &DisplayHandle,
4844        _: &Client,
4845        resource: New<WpFractionalScaleManagerV1>,
4846        _: &(),
4847        data_init: &mut DataInit<'_, Self>,
4848    ) {
4849        data_init.init(resource, ());
4850    }
4851}
4852
4853impl Dispatch<WpFractionalScaleManagerV1, ()> for Compositor {
4854    fn request(
4855        state: &mut Self,
4856        _: &Client,
4857        _: &WpFractionalScaleManagerV1,
4858        request: <WpFractionalScaleManagerV1 as Resource>::Request,
4859        _: &(),
4860        _: &DisplayHandle,
4861        data_init: &mut DataInit<'_, Self>,
4862    ) {
4863        use wp_fractional_scale_manager_v1::Request;
4864        match request {
4865            Request::GetFractionalScale { id, surface: _ } => {
4866                let fs = data_init.init(id, ());
4867                // Send the current preferred scale immediately.
4868                fs.preferred_scale(state.output_scale_120 as u32);
4869                state.fractional_scales.push(fs);
4870            }
4871            Request::Destroy => {}
4872            _ => {}
4873        }
4874    }
4875}
4876
4877// -- wp_fractional_scale_v1 --
4878impl Dispatch<WpFractionalScaleV1, ()> for Compositor {
4879    fn request(
4880        state: &mut Self,
4881        _: &Client,
4882        resource: &WpFractionalScaleV1,
4883        _: <WpFractionalScaleV1 as Resource>::Request,
4884        _: &(),
4885        _: &DisplayHandle,
4886        _: &mut DataInit<'_, Self>,
4887    ) {
4888        // Only request is Destroy.
4889        state
4890            .fractional_scales
4891            .retain(|fs| fs.id() != resource.id());
4892    }
4893}
4894
4895// -- wp_viewporter --
4896impl GlobalDispatch<WpViewporter, ()> for Compositor {
4897    fn bind(
4898        _: &mut Self,
4899        _: &DisplayHandle,
4900        _: &Client,
4901        resource: New<WpViewporter>,
4902        _: &(),
4903        data_init: &mut DataInit<'_, Self>,
4904    ) {
4905        data_init.init(resource, ());
4906    }
4907}
4908
4909impl Dispatch<WpViewporter, ()> for Compositor {
4910    fn request(
4911        _: &mut Self,
4912        _: &Client,
4913        _: &WpViewporter,
4914        request: <WpViewporter as Resource>::Request,
4915        _: &(),
4916        _: &DisplayHandle,
4917        data_init: &mut DataInit<'_, Self>,
4918    ) {
4919        use wp_viewporter::Request;
4920        match request {
4921            Request::GetViewport { id, surface } => {
4922                // Associate the viewport with the surface's ObjectId so
4923                // SetDestination can update the right Surface.
4924                let obj_id = surface.id();
4925                data_init.init(id, obj_id);
4926            }
4927            Request::Destroy => {}
4928            _ => {}
4929        }
4930    }
4931}
4932
4933// -- wp_viewport --
4934impl Dispatch<WpViewport, ObjectId> for Compositor {
4935    fn request(
4936        state: &mut Self,
4937        _: &Client,
4938        _: &WpViewport,
4939        request: <WpViewport as Resource>::Request,
4940        surface_obj_id: &ObjectId,
4941        _: &DisplayHandle,
4942        _: &mut DataInit<'_, Self>,
4943    ) {
4944        use wayland_protocols::wp::viewporter::server::wp_viewport::Request;
4945        match request {
4946            Request::SetDestination { width, height } => {
4947                if let Some(surf) = state.surfaces.get_mut(surface_obj_id) {
4948                    // width/height of -1 means unset (revert to buffer size).
4949                    if width > 0 && height > 0 {
4950                        surf.pending_viewport_destination = Some((width, height));
4951                    } else {
4952                        surf.pending_viewport_destination = None;
4953                    }
4954                }
4955            }
4956            Request::SetSource { .. } => {
4957                // Source crop — not needed for headless compositor.
4958            }
4959            Request::Destroy => {}
4960            _ => {}
4961        }
4962    }
4963}
4964
4965// =========================================================================
4966// NEW PROTOCOLS
4967// =========================================================================
4968
4969// -- wl_data_device_manager (clipboard) --
4970
4971impl GlobalDispatch<WlDataDeviceManager, ()> for Compositor {
4972    fn bind(
4973        _: &mut Self,
4974        _: &DisplayHandle,
4975        _: &Client,
4976        resource: New<WlDataDeviceManager>,
4977        _: &(),
4978        data_init: &mut DataInit<'_, Self>,
4979    ) {
4980        data_init.init(resource, ());
4981    }
4982}
4983
4984impl Dispatch<WlDataDeviceManager, ()> for Compositor {
4985    fn request(
4986        state: &mut Self,
4987        _: &Client,
4988        _: &WlDataDeviceManager,
4989        request: <WlDataDeviceManager as Resource>::Request,
4990        _: &(),
4991        _: &DisplayHandle,
4992        data_init: &mut DataInit<'_, Self>,
4993    ) {
4994        use wl_data_device_manager::Request;
4995        match request {
4996            Request::CreateDataSource { id } => {
4997                data_init.init(
4998                    id,
4999                    DataSourceData {
5000                        mime_types: std::sync::Mutex::new(Vec::new()),
5001                    },
5002                );
5003            }
5004            Request::GetDataDevice { id, seat: _ } => {
5005                let dd = data_init.init(id, ());
5006                state.data_devices.push(dd);
5007            }
5008            _ => {}
5009        }
5010    }
5011}
5012
5013impl Dispatch<WlDataSource, DataSourceData> for Compositor {
5014    fn request(
5015        _: &mut Self,
5016        _: &Client,
5017        _: &WlDataSource,
5018        request: <WlDataSource as Resource>::Request,
5019        data: &DataSourceData,
5020        _: &DisplayHandle,
5021        _: &mut DataInit<'_, Self>,
5022    ) {
5023        use wl_data_source::Request;
5024        match request {
5025            Request::Offer { mime_type } => {
5026                data.mime_types.lock().unwrap().push(mime_type);
5027            }
5028            Request::Destroy => {}
5029            _ => {} // SetActions — DnD, ignored
5030        }
5031    }
5032
5033    fn destroyed(
5034        state: &mut Self,
5035        _: wayland_server::backend::ClientId,
5036        resource: &WlDataSource,
5037        _: &DataSourceData,
5038    ) {
5039        if state
5040            .selection_source
5041            .as_ref()
5042            .is_some_and(|s| s.id() == resource.id())
5043        {
5044            state.selection_source = None;
5045        }
5046    }
5047}
5048
5049impl Dispatch<WlDataDevice, ()> for Compositor {
5050    fn request(
5051        state: &mut Self,
5052        _: &Client,
5053        _: &WlDataDevice,
5054        request: <WlDataDevice as Resource>::Request,
5055        _: &(),
5056        _: &DisplayHandle,
5057        _: &mut DataInit<'_, Self>,
5058    ) {
5059        use wl_data_device::Request;
5060        match request {
5061            Request::SetSelection { source, serial: _ } => {
5062                state.selection_source = source.clone();
5063                // Try to read text content and emit an event.
5064                if let Some(ref src) = source {
5065                    let data = src.data::<DataSourceData>().unwrap();
5066                    let mimes = data.mime_types.lock().unwrap();
5067                    let text_mime = mimes
5068                        .iter()
5069                        .find(|m| {
5070                            m.as_str() == "text/plain;charset=utf-8"
5071                                || m.as_str() == "text/plain"
5072                                || m.as_str() == "UTF8_STRING"
5073                        })
5074                        .cloned();
5075                    drop(mimes);
5076                    if let Some(mime) = text_mime {
5077                        state.read_data_source_and_emit(src, &mime);
5078                    }
5079                }
5080            }
5081            Request::Release => {}
5082            _ => {} // StartDrag — ignored
5083        }
5084    }
5085
5086    fn destroyed(
5087        state: &mut Self,
5088        _: wayland_server::backend::ClientId,
5089        resource: &WlDataDevice,
5090        _: &(),
5091    ) {
5092        state.data_devices.retain(|d| d.id() != resource.id());
5093    }
5094}
5095
5096impl Dispatch<WlDataOffer, DataOfferData> for Compositor {
5097    fn request(
5098        state: &mut Self,
5099        _: &Client,
5100        _: &WlDataOffer,
5101        request: <WlDataOffer as Resource>::Request,
5102        data: &DataOfferData,
5103        _: &DisplayHandle,
5104        _: &mut DataInit<'_, Self>,
5105    ) {
5106        use wl_data_offer::Request;
5107        match request {
5108            Request::Receive { mime_type, fd } => {
5109                if data.external {
5110                    // Write external clipboard data to the fd.
5111                    if let Some(ref cb) = state.external_clipboard
5112                        && (cb.mime_type == mime_type
5113                            || mime_type == "text/plain"
5114                            || mime_type == "text/plain;charset=utf-8"
5115                            || mime_type == "UTF8_STRING")
5116                    {
5117                        use std::io::Write;
5118                        let mut f = std::fs::File::from(fd);
5119                        let _ = f.write_all(&cb.data);
5120                    }
5121                } else if let Some(ref src) = state.selection_source {
5122                    // Forward to the Wayland data source.
5123                    src.send(mime_type, fd.as_fd());
5124                }
5125            }
5126            Request::Destroy => {}
5127            _ => {} // Accept, Finish, SetActions — DnD
5128        }
5129    }
5130}
5131
5132impl Compositor {
5133    /// Create a pipe, ask the data source to write into it, read the result,
5134    /// and emit a `ClipboardContent` event.
5135    fn read_data_source_and_emit(&mut self, source: &WlDataSource, mime_type: &str) {
5136        let mut fds = [0i32; 2];
5137        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
5138            return;
5139        }
5140        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
5141        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
5142        source.send(mime_type.to_string(), write_fd.as_fd());
5143        let _ = self.display_handle.flush_clients();
5144        // Non-blocking read with a modest limit.
5145        unsafe {
5146            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
5147        }
5148        // Give the client a moment to write.
5149        std::thread::sleep(std::time::Duration::from_millis(5));
5150        let mut buf = Vec::new();
5151        let mut tmp = [0u8; 8192];
5152        loop {
5153            let n = unsafe {
5154                libc::read(
5155                    read_fd.as_raw_fd(),
5156                    tmp.as_mut_ptr() as *mut libc::c_void,
5157                    tmp.len(),
5158                )
5159            };
5160            if n <= 0 {
5161                break;
5162            }
5163            buf.extend_from_slice(&tmp[..n as usize]);
5164            if buf.len() > 1024 * 1024 {
5165                break; // 1 MiB cap
5166            }
5167        }
5168        if !buf.is_empty() {
5169            let _ = self.event_tx.send(CompositorEvent::ClipboardContent {
5170                mime_type: mime_type.to_string(),
5171                data: buf,
5172            });
5173            (self.event_notify)();
5174        }
5175    }
5176
5177    /// Push external clipboard to all connected wl_data_device objects.
5178    fn offer_external_clipboard(&mut self) {
5179        let Some(ref cb) = self.external_clipboard else {
5180            return;
5181        };
5182        let mime = cb.mime_type.clone();
5183        for dd in &self.data_devices {
5184            if let Some(client) = dd.client() {
5185                let offer = client
5186                    .create_resource::<WlDataOffer, DataOfferData, Compositor>(
5187                        &self.display_handle,
5188                        dd.version(),
5189                        DataOfferData { external: true },
5190                    )
5191                    .unwrap();
5192                dd.data_offer(&offer);
5193                offer.offer(mime.clone());
5194                // Offer standard text aliases.
5195                if mime.starts_with("text/plain") {
5196                    if mime != "text/plain" {
5197                        offer.offer("text/plain".to_string());
5198                    }
5199                    if mime != "text/plain;charset=utf-8" {
5200                        offer.offer("text/plain;charset=utf-8".to_string());
5201                    }
5202                    offer.offer("UTF8_STRING".to_string());
5203                }
5204                dd.selection(Some(&offer));
5205            }
5206        }
5207        let _ = self.display_handle.flush_clients();
5208    }
5209}
5210
5211// -- zwp_primary_selection --
5212
5213impl GlobalDispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
5214    fn bind(
5215        _: &mut Self,
5216        _: &DisplayHandle,
5217        _: &Client,
5218        resource: New<ZwpPrimarySelectionDeviceManagerV1>,
5219        _: &(),
5220        data_init: &mut DataInit<'_, Self>,
5221    ) {
5222        data_init.init(resource, ());
5223    }
5224}
5225
5226impl Dispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
5227    fn request(
5228        state: &mut Self,
5229        _: &Client,
5230        _: &ZwpPrimarySelectionDeviceManagerV1,
5231        request: <ZwpPrimarySelectionDeviceManagerV1 as Resource>::Request,
5232        _: &(),
5233        _: &DisplayHandle,
5234        data_init: &mut DataInit<'_, Self>,
5235    ) {
5236        use zwp_primary_selection_device_manager_v1::Request;
5237        match request {
5238            Request::CreateSource { id } => {
5239                data_init.init(
5240                    id,
5241                    PrimarySourceData {
5242                        mime_types: std::sync::Mutex::new(Vec::new()),
5243                    },
5244                );
5245            }
5246            Request::GetDevice { id, seat: _ } => {
5247                let pd = data_init.init(id, ());
5248                state.primary_devices.push(pd);
5249            }
5250            Request::Destroy => {}
5251            _ => {}
5252        }
5253    }
5254}
5255
5256impl Dispatch<ZwpPrimarySelectionSourceV1, PrimarySourceData> for Compositor {
5257    fn request(
5258        _: &mut Self,
5259        _: &Client,
5260        _: &ZwpPrimarySelectionSourceV1,
5261        request: <ZwpPrimarySelectionSourceV1 as Resource>::Request,
5262        data: &PrimarySourceData,
5263        _: &DisplayHandle,
5264        _: &mut DataInit<'_, Self>,
5265    ) {
5266        use zwp_primary_selection_source_v1::Request;
5267        match request {
5268            Request::Offer { mime_type } => {
5269                data.mime_types.lock().unwrap().push(mime_type);
5270            }
5271            Request::Destroy => {}
5272            _ => {}
5273        }
5274    }
5275
5276    fn destroyed(
5277        state: &mut Self,
5278        _: wayland_server::backend::ClientId,
5279        resource: &ZwpPrimarySelectionSourceV1,
5280        _: &PrimarySourceData,
5281    ) {
5282        if state
5283            .primary_source
5284            .as_ref()
5285            .is_some_and(|s| s.id() == resource.id())
5286        {
5287            state.primary_source = None;
5288        }
5289    }
5290}
5291
5292impl Dispatch<ZwpPrimarySelectionDeviceV1, ()> for Compositor {
5293    fn request(
5294        state: &mut Self,
5295        _: &Client,
5296        _: &ZwpPrimarySelectionDeviceV1,
5297        request: <ZwpPrimarySelectionDeviceV1 as Resource>::Request,
5298        _: &(),
5299        _: &DisplayHandle,
5300        _: &mut DataInit<'_, Self>,
5301    ) {
5302        use zwp_primary_selection_device_v1::Request;
5303        match request {
5304            Request::SetSelection { source, serial: _ } => {
5305                state.primary_source = source;
5306            }
5307            Request::Destroy => {}
5308            _ => {}
5309        }
5310    }
5311
5312    fn destroyed(
5313        state: &mut Self,
5314        _: wayland_server::backend::ClientId,
5315        resource: &ZwpPrimarySelectionDeviceV1,
5316        _: &(),
5317    ) {
5318        state.primary_devices.retain(|d| d.id() != resource.id());
5319    }
5320}
5321
5322impl Dispatch<ZwpPrimarySelectionOfferV1, PrimaryOfferData> for Compositor {
5323    fn request(
5324        state: &mut Self,
5325        _: &Client,
5326        _: &ZwpPrimarySelectionOfferV1,
5327        request: <ZwpPrimarySelectionOfferV1 as Resource>::Request,
5328        data: &PrimaryOfferData,
5329        _: &DisplayHandle,
5330        _: &mut DataInit<'_, Self>,
5331    ) {
5332        use zwp_primary_selection_offer_v1::Request;
5333        match request {
5334            Request::Receive { mime_type, fd } => {
5335                if data.external {
5336                    if let Some(ref cb) = state.external_primary {
5337                        use std::io::Write;
5338                        let mut f = std::fs::File::from(fd);
5339                        let _ = f.write_all(&cb.data);
5340                        let _ = mime_type; // accepted regardless
5341                    }
5342                } else if let Some(ref src) = state.primary_source {
5343                    src.send(mime_type, fd.as_fd());
5344                }
5345            }
5346            Request::Destroy => {}
5347            _ => {}
5348        }
5349    }
5350}
5351
5352// -- zwp_pointer_constraints_v1 --
5353
5354impl GlobalDispatch<ZwpPointerConstraintsV1, ()> for Compositor {
5355    fn bind(
5356        _: &mut Self,
5357        _: &DisplayHandle,
5358        _: &Client,
5359        resource: New<ZwpPointerConstraintsV1>,
5360        _: &(),
5361        data_init: &mut DataInit<'_, Self>,
5362    ) {
5363        data_init.init(resource, ());
5364    }
5365}
5366
5367impl Dispatch<ZwpPointerConstraintsV1, ()> for Compositor {
5368    fn request(
5369        _: &mut Self,
5370        _: &Client,
5371        _: &ZwpPointerConstraintsV1,
5372        request: <ZwpPointerConstraintsV1 as Resource>::Request,
5373        _: &(),
5374        _: &DisplayHandle,
5375        data_init: &mut DataInit<'_, Self>,
5376    ) {
5377        use zwp_pointer_constraints_v1::Request;
5378        match request {
5379            Request::LockPointer {
5380                id,
5381                surface: _,
5382                pointer: _,
5383                region: _,
5384                lifetime: _,
5385            } => {
5386                let lp = data_init.init(id, ());
5387                // Immediately grant the lock (headless — no physical pointer to contest).
5388                lp.locked();
5389            }
5390            Request::ConfinePointer {
5391                id,
5392                surface: _,
5393                pointer: _,
5394                region: _,
5395                lifetime: _,
5396            } => {
5397                let cp = data_init.init(id, ());
5398                cp.confined();
5399            }
5400            Request::Destroy => {}
5401            _ => {}
5402        }
5403    }
5404}
5405
5406impl Dispatch<ZwpLockedPointerV1, ()> for Compositor {
5407    fn request(
5408        _: &mut Self,
5409        _: &Client,
5410        _: &ZwpLockedPointerV1,
5411        _: <ZwpLockedPointerV1 as Resource>::Request,
5412        _: &(),
5413        _: &DisplayHandle,
5414        _: &mut DataInit<'_, Self>,
5415    ) {
5416        // SetCursorPositionHint, SetRegion, Destroy — no-ops for headless.
5417    }
5418}
5419
5420impl Dispatch<ZwpConfinedPointerV1, ()> for Compositor {
5421    fn request(
5422        _: &mut Self,
5423        _: &Client,
5424        _: &ZwpConfinedPointerV1,
5425        _: <ZwpConfinedPointerV1 as Resource>::Request,
5426        _: &(),
5427        _: &DisplayHandle,
5428        _: &mut DataInit<'_, Self>,
5429    ) {
5430        // SetRegion, Destroy — no-ops for headless.
5431    }
5432}
5433
5434// -- zwp_relative_pointer_manager_v1 --
5435
5436impl GlobalDispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
5437    fn bind(
5438        _: &mut Self,
5439        _: &DisplayHandle,
5440        _: &Client,
5441        resource: New<ZwpRelativePointerManagerV1>,
5442        _: &(),
5443        data_init: &mut DataInit<'_, Self>,
5444    ) {
5445        data_init.init(resource, ());
5446    }
5447}
5448
5449impl Dispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
5450    fn request(
5451        state: &mut Self,
5452        _: &Client,
5453        _: &ZwpRelativePointerManagerV1,
5454        request: <ZwpRelativePointerManagerV1 as Resource>::Request,
5455        _: &(),
5456        _: &DisplayHandle,
5457        data_init: &mut DataInit<'_, Self>,
5458    ) {
5459        use zwp_relative_pointer_manager_v1::Request;
5460        match request {
5461            Request::GetRelativePointer { id, pointer: _ } => {
5462                let rp = data_init.init(id, ());
5463                state.relative_pointers.push(rp);
5464            }
5465            Request::Destroy => {}
5466            _ => {}
5467        }
5468    }
5469}
5470
5471impl Dispatch<ZwpRelativePointerV1, ()> for Compositor {
5472    fn request(
5473        state: &mut Self,
5474        _: &Client,
5475        resource: &ZwpRelativePointerV1,
5476        _: <ZwpRelativePointerV1 as Resource>::Request,
5477        _: &(),
5478        _: &DisplayHandle,
5479        _: &mut DataInit<'_, Self>,
5480    ) {
5481        // Only request is Destroy.
5482        state
5483            .relative_pointers
5484            .retain(|rp| rp.id() != resource.id());
5485    }
5486}
5487
5488// -- zwp_text_input_v3 --
5489
5490impl GlobalDispatch<ZwpTextInputManagerV3, ()> for Compositor {
5491    fn bind(
5492        _: &mut Self,
5493        _: &DisplayHandle,
5494        _: &Client,
5495        resource: New<ZwpTextInputManagerV3>,
5496        _: &(),
5497        data_init: &mut DataInit<'_, Self>,
5498    ) {
5499        data_init.init(resource, ());
5500    }
5501}
5502
5503impl Dispatch<ZwpTextInputManagerV3, ()> for Compositor {
5504    fn request(
5505        state: &mut Self,
5506        _: &Client,
5507        _: &ZwpTextInputManagerV3,
5508        request: <ZwpTextInputManagerV3 as Resource>::Request,
5509        _: &(),
5510        _: &DisplayHandle,
5511        data_init: &mut DataInit<'_, Self>,
5512    ) {
5513        use zwp_text_input_manager_v3::Request;
5514        match request {
5515            Request::GetTextInput { id, seat: _ } => {
5516                let ti = data_init.init(id, ());
5517                state.text_inputs.push(TextInputState {
5518                    resource: ti,
5519                    enabled: false,
5520                });
5521            }
5522            Request::Destroy => {}
5523            _ => {}
5524        }
5525    }
5526}
5527
5528impl Dispatch<ZwpTextInputV3, ()> for Compositor {
5529    fn request(
5530        state: &mut Self,
5531        _: &Client,
5532        resource: &ZwpTextInputV3,
5533        request: <ZwpTextInputV3 as Resource>::Request,
5534        _: &(),
5535        _: &DisplayHandle,
5536        _: &mut DataInit<'_, Self>,
5537    ) {
5538        use zwp_text_input_v3::Request;
5539        match request {
5540            Request::Enable => {
5541                if let Some(ti) = state
5542                    .text_inputs
5543                    .iter_mut()
5544                    .find(|t| t.resource.id() == resource.id())
5545                {
5546                    ti.enabled = true;
5547                }
5548            }
5549            Request::Disable => {
5550                if let Some(ti) = state
5551                    .text_inputs
5552                    .iter_mut()
5553                    .find(|t| t.resource.id() == resource.id())
5554                {
5555                    ti.enabled = false;
5556                }
5557            }
5558            Request::Commit => {
5559                // Client acknowledges our last done; nothing to do.
5560            }
5561            Request::Destroy => {
5562                state
5563                    .text_inputs
5564                    .retain(|t| t.resource.id() != resource.id());
5565            }
5566            // SetSurroundingText, SetTextChangeCause, SetContentType,
5567            // SetCursorRectangle — informational; ignored for now.
5568            _ => {}
5569        }
5570    }
5571}
5572
5573// -- xdg_activation_v1 --
5574
5575impl GlobalDispatch<XdgActivationV1, ()> for Compositor {
5576    fn bind(
5577        _: &mut Self,
5578        _: &DisplayHandle,
5579        _: &Client,
5580        resource: New<XdgActivationV1>,
5581        _: &(),
5582        data_init: &mut DataInit<'_, Self>,
5583    ) {
5584        data_init.init(resource, ());
5585    }
5586}
5587
5588impl Dispatch<XdgActivationV1, ()> for Compositor {
5589    fn request(
5590        state: &mut Self,
5591        _: &Client,
5592        _: &XdgActivationV1,
5593        request: <XdgActivationV1 as Resource>::Request,
5594        _: &(),
5595        _: &DisplayHandle,
5596        data_init: &mut DataInit<'_, Self>,
5597    ) {
5598        use xdg_activation_v1::Request;
5599        match request {
5600            Request::GetActivationToken { id } => {
5601                let serial = state.next_activation_token;
5602                state.next_activation_token = serial.wrapping_add(1);
5603                data_init.init(id, ActivationTokenData { serial });
5604            }
5605            Request::Activate {
5606                token: _,
5607                surface: _,
5608            } => {
5609                // In a headless compositor, activation requests are always
5610                // granted (focus is managed externally by the browser/CLI).
5611            }
5612            Request::Destroy => {}
5613            _ => {}
5614        }
5615    }
5616}
5617
5618impl Dispatch<XdgActivationTokenV1, ActivationTokenData> for Compositor {
5619    fn request(
5620        _: &mut Self,
5621        _: &Client,
5622        resource: &XdgActivationTokenV1,
5623        request: <XdgActivationTokenV1 as Resource>::Request,
5624        data: &ActivationTokenData,
5625        _: &DisplayHandle,
5626        _: &mut DataInit<'_, Self>,
5627    ) {
5628        use xdg_activation_token_v1::Request;
5629        match request {
5630            Request::Commit => {
5631                // Issue a token immediately — the headless compositor doesn't
5632                // need to validate app_id / surface / serial.
5633                resource.done(format!("blit-token-{}", data.serial));
5634            }
5635            Request::SetSerial { .. } | Request::SetAppId { .. } | Request::SetSurface { .. } => {}
5636            Request::Destroy => {}
5637            _ => {}
5638        }
5639    }
5640}
5641
5642// -- wp_cursor_shape_manager_v1 --
5643
5644impl GlobalDispatch<WpCursorShapeManagerV1, ()> for Compositor {
5645    fn bind(
5646        _: &mut Self,
5647        _: &DisplayHandle,
5648        _: &Client,
5649        resource: New<WpCursorShapeManagerV1>,
5650        _: &(),
5651        data_init: &mut DataInit<'_, Self>,
5652    ) {
5653        data_init.init(resource, ());
5654    }
5655}
5656
5657impl Dispatch<WpCursorShapeManagerV1, ()> for Compositor {
5658    fn request(
5659        _: &mut Self,
5660        _: &Client,
5661        _: &WpCursorShapeManagerV1,
5662        request: <WpCursorShapeManagerV1 as Resource>::Request,
5663        _: &(),
5664        _: &DisplayHandle,
5665        data_init: &mut DataInit<'_, Self>,
5666    ) {
5667        use wp_cursor_shape_manager_v1::Request;
5668        match request {
5669            Request::GetPointer {
5670                cursor_shape_device,
5671                pointer: _,
5672            } => {
5673                data_init.init(cursor_shape_device, ());
5674            }
5675            Request::GetTabletToolV2 {
5676                cursor_shape_device,
5677                tablet_tool: _,
5678            } => {
5679                data_init.init(cursor_shape_device, ());
5680            }
5681            Request::Destroy => {}
5682            _ => {}
5683        }
5684    }
5685}
5686
5687impl Dispatch<WpCursorShapeDeviceV1, ()> for Compositor {
5688    fn request(
5689        state: &mut Self,
5690        _: &Client,
5691        _: &WpCursorShapeDeviceV1,
5692        request: <WpCursorShapeDeviceV1 as Resource>::Request,
5693        _: &(),
5694        _: &DisplayHandle,
5695        _: &mut DataInit<'_, Self>,
5696    ) {
5697        use wp_cursor_shape_device_v1::Request;
5698        match request {
5699            Request::SetShape { serial: _, shape } => {
5700                use wayland_server::WEnum;
5701                use wp_cursor_shape_device_v1::Shape;
5702                let name = match shape {
5703                    WEnum::Value(Shape::Default) => "default",
5704                    WEnum::Value(Shape::ContextMenu) => "context-menu",
5705                    WEnum::Value(Shape::Help) => "help",
5706                    WEnum::Value(Shape::Pointer) => "pointer",
5707                    WEnum::Value(Shape::Progress) => "progress",
5708                    WEnum::Value(Shape::Wait) => "wait",
5709                    WEnum::Value(Shape::Cell) => "cell",
5710                    WEnum::Value(Shape::Crosshair) => "crosshair",
5711                    WEnum::Value(Shape::Text) => "text",
5712                    WEnum::Value(Shape::VerticalText) => "vertical-text",
5713                    WEnum::Value(Shape::Alias) => "alias",
5714                    WEnum::Value(Shape::Copy) => "copy",
5715                    WEnum::Value(Shape::Move) => "move",
5716                    WEnum::Value(Shape::NoDrop) => "no-drop",
5717                    WEnum::Value(Shape::NotAllowed) => "not-allowed",
5718                    WEnum::Value(Shape::Grab) => "grab",
5719                    WEnum::Value(Shape::Grabbing) => "grabbing",
5720                    WEnum::Value(Shape::EResize) => "e-resize",
5721                    WEnum::Value(Shape::NResize) => "n-resize",
5722                    WEnum::Value(Shape::NeResize) => "ne-resize",
5723                    WEnum::Value(Shape::NwResize) => "nw-resize",
5724                    WEnum::Value(Shape::SResize) => "s-resize",
5725                    WEnum::Value(Shape::SeResize) => "se-resize",
5726                    WEnum::Value(Shape::SwResize) => "sw-resize",
5727                    WEnum::Value(Shape::WResize) => "w-resize",
5728                    WEnum::Value(Shape::EwResize) => "ew-resize",
5729                    WEnum::Value(Shape::NsResize) => "ns-resize",
5730                    WEnum::Value(Shape::NeswResize) => "nesw-resize",
5731                    WEnum::Value(Shape::NwseResize) => "nwse-resize",
5732                    WEnum::Value(Shape::ColResize) => "col-resize",
5733                    WEnum::Value(Shape::RowResize) => "row-resize",
5734                    WEnum::Value(Shape::AllScroll) => "all-scroll",
5735                    WEnum::Value(Shape::ZoomIn) => "zoom-in",
5736                    WEnum::Value(Shape::ZoomOut) => "zoom-out",
5737                    _ => "default",
5738                };
5739                let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
5740                    surface_id: state.focused_surface_id,
5741                    cursor: CursorImage::Named(name.to_string()),
5742                });
5743                (state.event_notify)();
5744            }
5745            Request::Destroy => {}
5746            _ => {}
5747        }
5748    }
5749}
5750
5751// -- Client data --
5752impl wayland_server::backend::ClientData for ClientState {
5753    fn initialized(&self, _: wayland_server::backend::ClientId) {}
5754    fn disconnected(
5755        &self,
5756        _: wayland_server::backend::ClientId,
5757        _: wayland_server::backend::DisconnectReason,
5758    ) {
5759    }
5760}
5761
5762// ---------------------------------------------------------------------------
5763// Public API
5764// ---------------------------------------------------------------------------
5765
5766pub struct CompositorHandle {
5767    pub event_rx: mpsc::Receiver<CompositorEvent>,
5768    pub command_tx: mpsc::Sender<CompositorCommand>,
5769    pub socket_name: String,
5770    pub thread: std::thread::JoinHandle<()>,
5771    pub shutdown: Arc<AtomicBool>,
5772    /// Whether the compositor's Vulkan renderer supports Vulkan Video encode.
5773    pub vulkan_video_encode: bool,
5774    /// Whether the compositor's Vulkan renderer supports Vulkan Video AV1 encode.
5775    pub vulkan_video_encode_av1: bool,
5776    loop_signal: LoopSignal,
5777}
5778
5779impl CompositorHandle {
5780    pub fn wake(&self) {
5781        self.loop_signal.wakeup();
5782    }
5783}
5784
5785pub fn spawn_compositor(
5786    verbose: bool,
5787    event_notify: Arc<dyn Fn() + Send + Sync>,
5788    gpu_device: &str,
5789) -> CompositorHandle {
5790    let _gpu_device = gpu_device.to_string();
5791    let (event_tx, event_rx) = mpsc::channel();
5792    let (command_tx, command_rx) = mpsc::channel();
5793    let (socket_tx, socket_rx) = mpsc::sync_channel(1);
5794    let (signal_tx, signal_rx) = mpsc::sync_channel::<LoopSignal>(1);
5795    let (caps_tx, caps_rx) = mpsc::sync_channel::<(bool, bool)>(1);
5796    let shutdown = Arc::new(AtomicBool::new(false));
5797    let shutdown_clone = shutdown.clone();
5798
5799    let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")
5800        .map(std::path::PathBuf::from)
5801        .filter(|p| {
5802            let probe = p.join(".blit-probe");
5803            if std::fs::write(&probe, b"").is_ok() {
5804                let _ = std::fs::remove_file(&probe);
5805                true
5806            } else {
5807                false
5808            }
5809        })
5810        .unwrap_or_else(std::env::temp_dir);
5811
5812    let runtime_dir_clone = runtime_dir.clone();
5813    let thread = std::thread::Builder::new()
5814        .name("compositor".into())
5815        .spawn(move || {
5816            unsafe { std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir_clone) };
5817            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5818                run_compositor(
5819                    event_tx,
5820                    command_rx,
5821                    socket_tx,
5822                    signal_tx,
5823                    caps_tx,
5824                    event_notify,
5825                    shutdown_clone,
5826                    verbose,
5827                    _gpu_device,
5828                );
5829            }));
5830            if let Err(e) = result {
5831                let msg = if let Some(s) = e.downcast_ref::<&str>() {
5832                    s.to_string()
5833                } else if let Some(s) = e.downcast_ref::<String>() {
5834                    s.clone()
5835                } else {
5836                    "unknown panic".to_string()
5837                };
5838                eprintln!("[compositor] PANIC: {msg}");
5839            }
5840        })
5841        .expect("failed to spawn compositor thread");
5842
5843    let socket_name = socket_rx.recv().expect("compositor failed to start");
5844    let socket_name = runtime_dir
5845        .join(&socket_name)
5846        .to_string_lossy()
5847        .into_owned();
5848    let loop_signal = signal_rx
5849        .recv()
5850        .expect("compositor failed to send loop signal");
5851    let (vulkan_video_encode, vulkan_video_encode_av1) = caps_rx.recv().unwrap_or((false, false));
5852
5853    CompositorHandle {
5854        event_rx,
5855        command_tx,
5856        socket_name,
5857        thread,
5858        shutdown,
5859        vulkan_video_encode,
5860        vulkan_video_encode_av1,
5861        loop_signal,
5862    }
5863}
5864
5865#[allow(clippy::too_many_arguments)]
5866fn run_compositor(
5867    event_tx: mpsc::Sender<CompositorEvent>,
5868    command_rx: mpsc::Receiver<CompositorCommand>,
5869    socket_tx: mpsc::SyncSender<String>,
5870    signal_tx: mpsc::SyncSender<LoopSignal>,
5871    caps_tx: mpsc::SyncSender<(bool, bool)>,
5872    event_notify: Arc<dyn Fn() + Send + Sync>,
5873    shutdown: Arc<AtomicBool>,
5874    verbose: bool,
5875    gpu_device: String,
5876) {
5877    let mut event_loop: EventLoop<Compositor> =
5878        EventLoop::try_new().expect("failed to create event loop");
5879    let loop_signal = event_loop.get_signal();
5880
5881    let display: Display<Compositor> = Display::new().expect("failed to create display");
5882    let dh = display.handle();
5883
5884    // Probe Vulkan early so we know whether DMA-BUF is available
5885    // before registering Wayland globals.
5886    eprintln!("[compositor] trying Vulkan renderer for {gpu_device}");
5887    let vulkan_renderer = super::vulkan_render::VulkanRenderer::try_new(&gpu_device);
5888    let has_dmabuf = vulkan_renderer.as_ref().is_some_and(|vk| vk.has_dmabuf());
5889    eprintln!(
5890        "[compositor] Vulkan renderer: {} (dmabuf={})",
5891        vulkan_renderer.is_some(),
5892        has_dmabuf,
5893    );
5894    if vulkan_renderer.is_none() {
5895        eprintln!(
5896            "[compositor] WARNING: no Vulkan renderer — clients can connect but NO frames will be composited (windows will never appear)."
5897        );
5898        eprintln!(
5899            "[compositor] WARNING: install a Vulkan driver; on GPU-less hosts a software driver works (e.g. `apt install mesa-vulkan-drivers` for lavapipe)."
5900        );
5901    }
5902
5903    // Create globals.
5904    dh.create_global::<Compositor, WlCompositor, ()>(6, ());
5905    dh.create_global::<Compositor, WlSubcompositor, ()>(1, ());
5906    dh.create_global::<Compositor, XdgWmBase, ()>(6, ());
5907    dh.create_global::<Compositor, WlShm, ()>(1, ());
5908    dh.create_global::<Compositor, WlOutput, ()>(4, ());
5909    dh.create_global::<Compositor, WlSeat, ()>(9, ());
5910    // Only advertise zwp_linux_dmabuf_v1 when the Vulkan device can
5911    // actually import DMA-BUFs.  Advertising the global with zero
5912    // formats confuses clients (Chrome, mpv) into not falling back to
5913    // wl_shm.
5914    if has_dmabuf {
5915        dh.create_global::<Compositor, ZwpLinuxDmabufV1, ()>(4, ());
5916    }
5917    dh.create_global::<Compositor, WpViewporter, ()>(1, ());
5918    dh.create_global::<Compositor, WpFractionalScaleManagerV1, ()>(1, ());
5919    dh.create_global::<Compositor, ZxdgDecorationManagerV1, ()>(1, ());
5920    dh.create_global::<Compositor, WlDataDeviceManager, ()>(3, ());
5921    dh.create_global::<Compositor, ZwpPointerConstraintsV1, ()>(1, ());
5922    dh.create_global::<Compositor, ZwpRelativePointerManagerV1, ()>(1, ());
5923    dh.create_global::<Compositor, XdgActivationV1, ()>(1, ());
5924    dh.create_global::<Compositor, WpCursorShapeManagerV1, ()>(1, ());
5925    dh.create_global::<Compositor, ZwpPrimarySelectionDeviceManagerV1, ()>(1, ());
5926    dh.create_global::<Compositor, WpPresentation, ()>(1, ());
5927    dh.create_global::<Compositor, ZwpTextInputManagerV3, ()>(1, ());
5928
5929    // XKB keymap.
5930    let keymap_string = include_str!("../data/us-qwerty.xkb");
5931    let mut keymap_data = keymap_string.as_bytes().to_vec();
5932    keymap_data.push(0); // null-terminate
5933
5934    // Listening socket.
5935    let listening_socket = wayland_server::ListeningSocket::bind_auto("wayland", 0..33)
5936        .unwrap_or_else(|e| {
5937            let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "(unset)".into());
5938            panic!("failed to create wayland socket in XDG_RUNTIME_DIR={dir}: {e}\nhint: ensure the directory exists and is writable by the current user");
5939        });
5940    let socket_name = listening_socket
5941        .socket_name()
5942        .unwrap()
5943        .to_string_lossy()
5944        .into_owned();
5945    socket_tx.send(socket_name).unwrap();
5946    let _ = signal_tx.send(loop_signal.clone());
5947
5948    let mut compositor = Compositor {
5949        display_handle: dh,
5950        surfaces: HashMap::new(),
5951        regions: HashMap::new(),
5952        toplevel_surface_ids: HashMap::new(),
5953        last_request_frame_ms: HashMap::new(),
5954        next_surface_id: 1,
5955        shm_pools: HashMap::new(),
5956        surface_meta: HashMap::new(),
5957        dmabuf_params: HashMap::new(),
5958        vulkan_renderer,
5959        output_width: 1920,
5960        output_height: 1080,
5961        output_refresh_mhz: 60_000,
5962        output_scale_120: 120,
5963        outputs: Vec::new(),
5964        keyboards: Vec::new(),
5965        pointers: Vec::new(),
5966        keyboard_keymap_data: keymap_data,
5967        mods_depressed: 0,
5968        mods_locked: 0,
5969        serial: 0,
5970        event_tx,
5971        event_notify,
5972        loop_signal: loop_signal.clone(),
5973        pending_commits: HashMap::new(),
5974        pending_encoded: Vec::new(),
5975        pending_native_sizes: HashMap::new(),
5976        pending_recomposite_toplevels: HashMap::new(),
5977        focused_surface_id: 0,
5978        pointer_entered_id: None,
5979        pointer_entered_local: (0.0, 0.0),
5980        pending_kb_reenter: false,
5981        gpu_device,
5982        verbose,
5983        shutdown: shutdown.clone(),
5984        last_reported_size: HashMap::new(),
5985        surface_sizes: HashMap::new(),
5986        positioners: HashMap::new(),
5987        fractional_scales: Vec::new(),
5988        data_devices: Vec::new(),
5989        selection_source: None,
5990        external_clipboard: None,
5991        primary_devices: Vec::new(),
5992        primary_source: None,
5993        external_primary: None,
5994        relative_pointers: Vec::new(),
5995        text_inputs: Vec::new(),
5996        text_input_serial: 0,
5997        next_activation_token: 1,
5998        popup_grab_stack: Vec::new(),
5999        kb_focus_popup: None,
6000        popup_dismiss_button: None,
6001        held_buffers: HashMap::new(),
6002        cursor_rgba: HashMap::new(),
6003    };
6004
6005    // Report Vulkan Video encode capabilities to the server.
6006    {
6007        let (vve, vve_av1) = compositor
6008            .vulkan_renderer
6009            .as_ref()
6010            .map(|vk| (vk.has_video_encode(), vk.has_video_encode_av1()))
6011            .unwrap_or((false, false));
6012        let _ = caps_tx.send((vve, vve_av1));
6013    }
6014
6015    let handle = event_loop.handle();
6016
6017    // Insert display fd source.
6018    let display_source = Generic::new(display, Interest::READ, calloop::Mode::Level);
6019    handle
6020        .insert_source(display_source, |_, display, state| {
6021            let d = unsafe { display.get_mut() };
6022            if let Err(e) = d.dispatch_clients(state)
6023                && state.verbose
6024            {
6025                eprintln!("[compositor] dispatch_clients error: {e}");
6026            }
6027            state.cleanup_dead_surfaces();
6028            if let Err(e) = d.flush_clients()
6029                && state.verbose
6030            {
6031                eprintln!("[compositor] flush_clients error: {e}");
6032            }
6033            Ok(PostAction::Continue)
6034        })
6035        .expect("failed to insert display source");
6036
6037    // Insert listening socket.
6038    let socket_source = Generic::new(listening_socket, Interest::READ, calloop::Mode::Level);
6039    handle
6040        .insert_source(socket_source, |_, socket, state| {
6041            let ls = unsafe { socket.get_mut() };
6042            if let Some(client_stream) = ls.accept().ok().flatten()
6043                && let Err(e) = state
6044                    .display_handle
6045                    .insert_client(client_stream, Arc::new(ClientState))
6046                && state.verbose
6047            {
6048                eprintln!("[compositor] insert_client error: {e}");
6049            }
6050            Ok(PostAction::Continue)
6051        })
6052        .expect("failed to insert listening socket");
6053
6054    if verbose {
6055        eprintln!("[compositor] entering event loop");
6056    }
6057
6058    while !shutdown.load(Ordering::Relaxed) {
6059        // Process commands.
6060        while let Ok(cmd) = command_rx.try_recv() {
6061            match cmd {
6062                CompositorCommand::Shutdown => {
6063                    shutdown.store(true, Ordering::Relaxed);
6064                    return;
6065                }
6066                other => compositor.handle_command(other),
6067            }
6068        }
6069
6070        // Shorten the dispatch timeout when the Vulkan renderer has
6071        // in-flight GPU work so we poll for completion promptly.
6072        let poll_timeout = if compositor
6073            .vulkan_renderer
6074            .as_ref()
6075            .is_some_and(|vk| vk.has_pending())
6076        {
6077            std::time::Duration::from_millis(1)
6078        } else {
6079            std::time::Duration::from_secs(1)
6080        };
6081
6082        if let Err(e) = event_loop.dispatch(Some(poll_timeout), &mut compositor)
6083            && verbose
6084        {
6085            eprintln!("[compositor] event loop error: {e}");
6086        }
6087
6088        // Check for completed Vulkan GPU work.  This runs independently
6089        // of surface commits so completed frames are flushed to the
6090        // server without waiting for the next Wayland event.  One submit
6091        // can yield multiple results (one per per-client downscale target
6092        // plus the native composite).
6093        if let Some(ref mut vk) = compositor.vulkan_renderer {
6094            let retired = vk.try_retire_pending();
6095            if !retired.is_empty() {
6096                let s120_u32 = (compositor.output_scale_120 as u32).max(120);
6097                // The largest result is the compositor's native
6098                // composite — drive SurfaceResized off it when no fresh
6099                // handle_surface_commit has populated pending_native_sizes.
6100                if let Some(&(sid, nw, nh, _)) = retired
6101                    .iter()
6102                    .max_by_key(|&&(_, w, h, _)| (w as u64) * (h as u64))
6103                {
6104                    let log_w = (nw * 120).div_ceil(s120_u32);
6105                    let log_h = (nh * 120).div_ceil(s120_u32);
6106                    compositor
6107                        .pending_native_sizes
6108                        .entry(sid)
6109                        .or_insert((nw, nh, log_w, log_h));
6110                }
6111                for (sid, w, h, pixels) in retired {
6112                    let log_w = (w * 120).div_ceil(s120_u32);
6113                    let log_h = (h * 120).div_ceil(s120_u32);
6114                    compositor
6115                        .pending_commits
6116                        .insert((sid, w, h), (log_w, log_h, pixels));
6117                }
6118            }
6119        }
6120
6121        // Drain deferred recomposites queued by per-client target
6122        // installs (Set/RegisterDownscaleTarget).  Only run when the
6123        // GPU pipeline is idle — `render_tree_sized` early-returns
6124        // when `pending_submit` is held, which would silently drop
6125        // the new submit and leave the freshly-installed target
6126        // empty.  Each recomposite submits one render, so process
6127        // one toplevel per iteration; remaining queued toplevels get
6128        // their turn after the next retire.
6129        let can_recomposite = compositor
6130            .vulkan_renderer
6131            .as_ref()
6132            .is_some_and(|vk| !vk.has_pending());
6133        if can_recomposite
6134            && let Some((&sid, &encoder_only)) =
6135                compositor.pending_recomposite_toplevels.iter().next()
6136        {
6137            compositor.pending_recomposite_toplevels.remove(&sid);
6138            if let Some(root_id) = compositor.toplevel_surface_ids.get(&sid).cloned() {
6139                compositor.composite_toplevel_into_pending(&root_id, sid, encoder_only);
6140                // Wake the loop so the retire path runs again
6141                // promptly — without an explicit wakeup the loop
6142                // would idle on its 1s dispatch timeout instead of
6143                // the 1ms has_pending poll.
6144                loop_signal.wakeup();
6145            }
6146        }
6147
6148        if !compositor.pending_commits.is_empty()
6149            || !compositor.pending_encoded.is_empty()
6150            || !compositor.pending_native_sizes.is_empty()
6151        {
6152            compositor.flush_pending_commits();
6153        }
6154
6155        if let Err(e) = compositor.display_handle.flush_clients()
6156            && verbose
6157        {
6158            eprintln!("[compositor] flush error: {e}");
6159        }
6160    }
6161
6162    if verbose {
6163        eprintln!("[compositor] event loop exited");
6164    }
6165}