Skip to main content

blit_compositor/
imp.rs

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