Skip to main content

blit_compositor/
imp.rs

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