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