Skip to main content

blit_compositor/
imp.rs

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