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                // Invalidate the Wayland-side selection — external takes over.
2431                self.selection_source = None;
2432                self.offer_external_clipboard();
2433            }
2434            CompositorCommand::Capture {
2435                surface_id,
2436                scale_120,
2437                reply,
2438            } => {
2439                // Use the capture-specific scale if provided, otherwise
2440                // fall back to the current output scale.
2441                let cap_s120 = if scale_120 > 0 {
2442                    scale_120
2443                } else {
2444                    self.output_scale_120
2445                };
2446                let result = if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id) {
2447                    if let Some(ref mut vk) = self.vulkan_renderer {
2448                        vk.render_tree_sized(
2449                            root_id,
2450                            &self.surfaces,
2451                            &self.surface_meta,
2452                            cap_s120,
2453                            None,
2454                            surface_id,
2455                        )
2456                        .map(|(_sid, w, h, pixels)| {
2457                            let rgba = pixels.to_rgba(w, h);
2458                            (w, h, rgba)
2459                        })
2460                    } else {
2461                        None
2462                    }
2463                } else {
2464                    None
2465                };
2466                let _ = reply.send(result);
2467            }
2468            CompositorCommand::RequestFrame { surface_id } => {
2469                self.fire_frame_callbacks_for_toplevel(surface_id);
2470            }
2471            CompositorCommand::ReleaseKeys { keycodes } => {
2472                let time = elapsed_ms();
2473                let focused_wl = self
2474                    .toplevel_surface_ids
2475                    .get(&self.focused_surface_id)
2476                    .and_then(|root_id| self.surfaces.get(root_id))
2477                    .map(|s| s.wl_surface.clone());
2478                for keycode in &keycodes {
2479                    let serial = self.next_serial();
2480                    for kb in &self.keyboards {
2481                        if let Some(ref wl) = focused_wl
2482                            && same_client(kb, wl)
2483                        {
2484                            kb.key(serial, time, *keycode, wl_keyboard::KeyState::Released);
2485                        }
2486                    }
2487                }
2488                // Update modifier state for any released modifier keys.
2489                for keycode in &keycodes {
2490                    self.update_and_send_modifiers(*keycode, false);
2491                }
2492                let _ = self.display_handle.flush_clients();
2493            }
2494            CompositorCommand::ClipboardListMimes { reply } => {
2495                let mimes = self.collect_clipboard_mime_types();
2496                let _ = reply.send(mimes);
2497            }
2498            CompositorCommand::ClipboardGet { mime_type, reply } => {
2499                let data = self.get_clipboard_content(&mime_type);
2500                let _ = reply.send(data);
2501            }
2502            CompositorCommand::SetExternalOutputBuffers {
2503                surface_id,
2504                buffers,
2505            } => {
2506                if let Some(ref mut vk) = self.vulkan_renderer {
2507                    vk.set_external_output_buffers(surface_id, buffers);
2508                }
2509            }
2510            CompositorCommand::SetRefreshRate { mhz } => {
2511                // Only update on meaningful changes (>2 Hz difference) to
2512                // avoid flooding clients with mode events from jittery
2513                // requestAnimationFrame measurements.
2514                let diff = (mhz as i64 - self.output_refresh_mhz as i64).unsigned_abs();
2515                if diff > 2000 && mhz > 0 {
2516                    self.output_refresh_mhz = mhz;
2517                    let s120 = self.output_scale_120 as i32;
2518                    let mode_w = self.output_width * s120 / 120;
2519                    let mode_h = self.output_height * s120 / 120;
2520                    for output in &self.outputs {
2521                        output.mode(
2522                            wl_output::Mode::Current | wl_output::Mode::Preferred,
2523                            mode_w,
2524                            mode_h,
2525                            mhz as i32,
2526                        );
2527                        if output.version() >= 2 {
2528                            output.done();
2529                        }
2530                    }
2531                    let _ = self.display_handle.flush_clients();
2532                }
2533            }
2534            CompositorCommand::SetVulkanEncoder {
2535                surface_id,
2536                codec,
2537                qp,
2538                width,
2539                height,
2540            } => {
2541                if let Some(ref mut vk) = self.vulkan_renderer {
2542                    vk.create_vulkan_encoder(surface_id, codec, qp, width, height);
2543                }
2544            }
2545            CompositorCommand::RequestVulkanKeyframe { surface_id } => {
2546                if let Some(ref mut vk) = self.vulkan_renderer {
2547                    vk.request_encoder_keyframe(surface_id);
2548                }
2549            }
2550            CompositorCommand::DestroyVulkanEncoder { surface_id } => {
2551                if let Some(ref mut vk) = self.vulkan_renderer {
2552                    vk.destroy_vulkan_encoder(surface_id);
2553                }
2554            }
2555            CompositorCommand::Shutdown => {
2556                self.shutdown.store(true, Ordering::Relaxed);
2557                self.loop_signal.stop();
2558            }
2559        }
2560    }
2561
2562    /// Send dmabuf feedback events on a `ZwpLinuxDmabufFeedbackV1` object.
2563    /// Builds the format table from the Vulkan renderer's supported modifiers,
2564    /// then sends main_device, one tranche, and done.
2565    fn send_dmabuf_feedback(&self, fb: &ZwpLinuxDmabufFeedbackV1) {
2566        use std::os::unix::fs::MetadataExt;
2567
2568        // Collect format+modifier pairs from the Vulkan renderer.
2569        let modifiers: &[(u32, u64)] = self
2570            .vulkan_renderer
2571            .as_ref()
2572            .map(|vk| vk.supported_dmabuf_modifiers.as_slice())
2573            .unwrap_or(&[]);
2574
2575        // Build the format table: tightly packed (u32 format, u32 pad, u64 modifier).
2576        let entry_size = 16usize;
2577        let table_size = modifiers.len() * entry_size;
2578        let mut table_data = vec![0u8; table_size];
2579        for (i, &(fmt, modifier)) in modifiers.iter().enumerate() {
2580            let off = i * entry_size;
2581            table_data[off..off + 4].copy_from_slice(&fmt.to_ne_bytes());
2582            // 4 bytes padding (already zero)
2583            table_data[off + 8..off + 16].copy_from_slice(&modifier.to_ne_bytes());
2584        }
2585
2586        // Create a memfd for the format table.
2587        let name = c"dmabuf-feedback-table";
2588        let raw_fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
2589        if raw_fd < 0 {
2590            eprintln!("[compositor] memfd_create for dmabuf feedback failed");
2591            fb.done();
2592            return;
2593        }
2594        let table_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
2595        if !table_data.is_empty() {
2596            use std::io::Write;
2597            let mut file = std::fs::File::from(table_fd.try_clone().unwrap());
2598            if file.write_all(&table_data).is_err() {
2599                eprintln!("[compositor] failed to write dmabuf feedback table");
2600                fb.done();
2601                return;
2602            }
2603        }
2604        fb.format_table(table_fd.as_fd(), table_size as u32);
2605
2606        // Get dev_t for the GPU device.
2607        let dev = std::fs::metadata(&self.gpu_device)
2608            .map(|m| m.rdev())
2609            .unwrap_or(0);
2610        let dev_bytes = dev.to_ne_bytes().to_vec();
2611        fb.main_device(dev_bytes.clone());
2612
2613        // Single tranche with all format+modifier pairs.
2614        fb.tranche_target_device(dev_bytes);
2615
2616        // Indices into the format table (array of u16 in native endianness).
2617        let indices: Vec<u8> = (0..modifiers.len() as u16)
2618            .flat_map(|i| i.to_ne_bytes())
2619            .collect();
2620        fb.tranche_formats(indices);
2621
2622        fb.tranche_flags(
2623            wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags::empty(),
2624        );
2625        fb.tranche_done();
2626        fb.done();
2627    }
2628}
2629
2630impl Compositor {
2631    /// Collect all MIME types available on the current clipboard.
2632    fn collect_clipboard_mime_types(&self) -> Vec<String> {
2633        // If a Wayland app owns the selection, use its MIME types.
2634        if let Some(ref src) = self.selection_source {
2635            let data = src.data::<DataSourceData>().unwrap();
2636            return data.mime_types.lock().unwrap().clone();
2637        }
2638        // Otherwise use the external (browser/CLI) clipboard.
2639        if let Some(ref cb) = self.external_clipboard
2640            && !cb.mime_type.is_empty()
2641        {
2642            let mut mimes = vec![cb.mime_type.clone()];
2643            // Add standard text aliases.
2644            if cb.mime_type.starts_with("text/plain") {
2645                if cb.mime_type != "text/plain" {
2646                    mimes.push("text/plain".to_string());
2647                }
2648                if cb.mime_type != "text/plain;charset=utf-8" {
2649                    mimes.push("text/plain;charset=utf-8".to_string());
2650                }
2651                mimes.push("UTF8_STRING".to_string());
2652            }
2653            return mimes;
2654        }
2655        Vec::new()
2656    }
2657
2658    /// Get clipboard content for a specific MIME type.
2659    fn get_clipboard_content(&mut self, mime_type: &str) -> Option<Vec<u8>> {
2660        // If external clipboard matches, return its data directly.
2661        if let Some(ref cb) = self.external_clipboard
2662            && self.selection_source.is_none()
2663        {
2664            // External clipboard is active.
2665            let matches = cb.mime_type == mime_type
2666                || (cb.mime_type.starts_with("text/plain")
2667                    && (mime_type == "text/plain"
2668                        || mime_type == "text/plain;charset=utf-8"
2669                        || mime_type == "UTF8_STRING"));
2670            if matches {
2671                return Some(cb.data.clone());
2672            }
2673            return None;
2674        }
2675        // If a Wayland app owns the selection, read from it via pipe.
2676        if let Some(src) = self.selection_source.clone() {
2677            return self.read_data_source_sync(&src, mime_type);
2678        }
2679        None
2680    }
2681
2682    /// Synchronously read data from a Wayland data source via pipe.
2683    fn read_data_source_sync(&mut self, source: &WlDataSource, mime_type: &str) -> Option<Vec<u8>> {
2684        let mut fds = [0i32; 2];
2685        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
2686            return None;
2687        }
2688        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
2689        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
2690        source.send(mime_type.to_string(), write_fd.as_fd());
2691        let _ = self.display_handle.flush_clients();
2692        drop(write_fd); // close write end so read gets EOF
2693        // Non-blocking read with a modest limit.
2694        unsafe {
2695            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
2696        }
2697        std::thread::sleep(std::time::Duration::from_millis(5));
2698        let mut buf = Vec::new();
2699        let mut tmp = [0u8; 8192];
2700        loop {
2701            let n = unsafe {
2702                libc::read(
2703                    read_fd.as_raw_fd(),
2704                    tmp.as_mut_ptr() as *mut libc::c_void,
2705                    tmp.len(),
2706                )
2707            };
2708            if n <= 0 {
2709                break;
2710            }
2711            buf.extend_from_slice(&tmp[..n as usize]);
2712            if buf.len() > 1024 * 1024 {
2713                break; // 1 MiB cap
2714            }
2715        }
2716        if buf.is_empty() { None } else { Some(buf) }
2717    }
2718}
2719
2720// ---------------------------------------------------------------------------
2721// Helpers
2722// ---------------------------------------------------------------------------
2723
2724/// Read `CLOCK_MONOTONIC` and return `(tv_sec, tv_nsec)`.
2725fn monotonic_timespec() -> (i64, i64) {
2726    let mut ts = libc::timespec {
2727        tv_sec: 0,
2728        tv_nsec: 0,
2729    };
2730    // SAFETY: clock_gettime with CLOCK_MONOTONIC is always valid.
2731    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
2732    (ts.tv_sec, ts.tv_nsec)
2733}
2734
2735fn elapsed_ms() -> u32 {
2736    // Use CLOCK_MONOTONIC directly so the timestamp matches what Wayland
2737    // clients (especially Chromium/Brave) expect for frame-latency
2738    // calculations.  The previous implementation measured from an arbitrary
2739    // epoch which caused Chromium to report negative frame latency.
2740    let (sec, nsec) = monotonic_timespec();
2741    (sec as u32)
2742        .wrapping_mul(1000)
2743        .wrapping_add(nsec as u32 / 1_000_000)
2744}
2745
2746/// Returns true when two Wayland resources belong to the same still-connected client.
2747fn same_client<R1: Resource, R2: Resource>(a: &R1, b: &R2) -> bool {
2748    match (a.client(), b.client()) {
2749        (Some(ca), Some(cb)) => ca.id() == cb.id(),
2750        _ => false,
2751    }
2752}
2753
2754fn yuv420_to_rgb(y: u8, u: u8, v: u8) -> [u8; 3] {
2755    let y = (y as i32 - 16).max(0);
2756    let u = u as i32 - 128;
2757    let v = v as i32 - 128;
2758    let r = ((298 * y + 409 * v + 128) >> 8).clamp(0, 255) as u8;
2759    let g = ((298 * y - 100 * u - 208 * v + 128) >> 8).clamp(0, 255) as u8;
2760    let b = ((298 * y + 516 * u + 128) >> 8).clamp(0, 255) as u8;
2761    [r, g, b]
2762}
2763
2764/// Encode xdg_toplevel states as the raw byte array expected by the protocol.
2765fn xdg_toplevel_states(states: &[xdg_toplevel::State]) -> Vec<u8> {
2766    let mut bytes = Vec::with_capacity(states.len() * 4);
2767    for state in states {
2768        bytes.extend_from_slice(&(*state as u32).to_ne_bytes());
2769    }
2770    bytes
2771}
2772
2773fn create_keymap_fd(keymap_data: &[u8]) -> Option<OwnedFd> {
2774    use std::io::Write;
2775    let name = c"blit-keymap";
2776    let raw_fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
2777    if raw_fd < 0 {
2778        return None;
2779    }
2780    let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
2781    let mut file = std::fs::File::from(fd);
2782    file.write_all(keymap_data).ok()?;
2783    Some(file.into())
2784}
2785
2786// ---------------------------------------------------------------------------
2787// Protocol dispatch implementations
2788// ---------------------------------------------------------------------------
2789
2790// -- wl_compositor --
2791
2792impl GlobalDispatch<WlCompositor, ()> for Compositor {
2793    fn bind(
2794        _state: &mut Self,
2795        _handle: &DisplayHandle,
2796        _client: &Client,
2797        resource: New<WlCompositor>,
2798        _data: &(),
2799        data_init: &mut DataInit<'_, Self>,
2800    ) {
2801        data_init.init(resource, ());
2802    }
2803}
2804
2805impl Dispatch<WlCompositor, ()> for Compositor {
2806    fn request(
2807        state: &mut Self,
2808        _client: &Client,
2809        _resource: &WlCompositor,
2810        request: <WlCompositor as Resource>::Request,
2811        _data: &(),
2812        _dh: &DisplayHandle,
2813        data_init: &mut DataInit<'_, Self>,
2814    ) {
2815        use wayland_server::protocol::wl_compositor::Request;
2816        match request {
2817            Request::CreateSurface { id } => {
2818                let surface = data_init.init(id, ());
2819                let proto_id = surface.id();
2820                state.surfaces.insert(
2821                    proto_id,
2822                    Surface {
2823                        surface_id: 0,
2824                        wl_surface: surface,
2825                        pending_buffer: None,
2826                        pending_buffer_scale: 1,
2827                        pending_damage: false,
2828                        pending_frame_callbacks: Vec::new(),
2829                        pending_presentation_feedbacks: Vec::new(),
2830                        pending_opaque: false,
2831                        buffer_scale: 1,
2832                        is_opaque: false,
2833                        parent_surface_id: None,
2834                        pending_subsurface_position: None,
2835                        subsurface_position: (0, 0),
2836                        children: Vec::new(),
2837                        xdg_surface: None,
2838                        xdg_toplevel: None,
2839                        xdg_popup: None,
2840                        xdg_geometry: None,
2841                        title: String::new(),
2842                        app_id: String::new(),
2843                        pending_viewport_destination: None,
2844                        viewport_destination: None,
2845                        is_cursor: false,
2846                        cursor_hotspot: (0, 0),
2847                    },
2848                );
2849            }
2850            Request::CreateRegion { id } => {
2851                data_init.init(id, ());
2852            }
2853            _ => {}
2854        }
2855    }
2856}
2857
2858// -- wl_surface --
2859
2860impl Dispatch<WlSurface, ()> for Compositor {
2861    fn request(
2862        state: &mut Self,
2863        _client: &Client,
2864        resource: &WlSurface,
2865        request: <WlSurface as Resource>::Request,
2866        _data: &(),
2867        _dh: &DisplayHandle,
2868        data_init: &mut DataInit<'_, Self>,
2869    ) {
2870        use wayland_server::protocol::wl_surface::Request;
2871        let sid = resource.id();
2872        match request {
2873            Request::Attach { buffer, x: _, y: _ } => {
2874                if let Some(surf) = state.surfaces.get_mut(&sid) {
2875                    surf.pending_buffer = buffer;
2876                }
2877            }
2878            Request::Damage { .. } | Request::DamageBuffer { .. } => {
2879                if let Some(surf) = state.surfaces.get_mut(&sid) {
2880                    surf.pending_damage = true;
2881                }
2882            }
2883            Request::Frame { callback } => {
2884                let cb = data_init.init(callback, ());
2885                if let Some(surf) = state.surfaces.get_mut(&sid) {
2886                    surf.pending_frame_callbacks.push(cb);
2887                }
2888            }
2889            Request::SetBufferScale { scale } => {
2890                if let Some(surf) = state.surfaces.get_mut(&sid) {
2891                    surf.pending_buffer_scale = scale;
2892                }
2893            }
2894            Request::SetOpaqueRegion { region: _ } => {
2895                if let Some(surf) = state.surfaces.get_mut(&sid) {
2896                    surf.pending_opaque = true;
2897                }
2898            }
2899            Request::SetInputRegion { .. } => {}
2900            Request::Commit => {
2901                let is_cursor = state.surfaces.get(&sid).is_some_and(|s| s.is_cursor);
2902                if is_cursor {
2903                    state.handle_cursor_commit(&sid);
2904                } else {
2905                    state.handle_surface_commit(&sid);
2906                }
2907            }
2908            Request::SetBufferTransform { .. } => {}
2909            Request::Offset { .. } => {}
2910            Request::Destroy => {
2911                state.surface_meta.remove(&sid);
2912                state.cursor_rgba.remove(&sid);
2913                if let Some(ref mut vk) = state.vulkan_renderer {
2914                    vk.remove_surface(&sid);
2915                }
2916                if let Some(held) = state.held_buffers.remove(&sid) {
2917                    held.release();
2918                }
2919                if let Some(parent_id) = state
2920                    .surfaces
2921                    .get(&sid)
2922                    .and_then(|s| s.parent_surface_id.clone())
2923                    && let Some(parent) = state.surfaces.get_mut(&parent_id)
2924                {
2925                    parent.children.retain(|c| *c != sid);
2926                }
2927                if let Some(surf) = state.surfaces.remove(&sid) {
2928                    for fb in surf.pending_presentation_feedbacks {
2929                        fb.discarded();
2930                    }
2931                    if surf.surface_id > 0 {
2932                        state.toplevel_surface_ids.remove(&surf.surface_id);
2933                        state.last_reported_size.remove(&surf.surface_id);
2934                        state.surface_sizes.remove(&surf.surface_id);
2935                        let _ = state.event_tx.send(CompositorEvent::SurfaceDestroyed {
2936                            surface_id: surf.surface_id,
2937                        });
2938                        (state.event_notify)();
2939                    }
2940                }
2941            }
2942            _ => {}
2943        }
2944    }
2945}
2946
2947// -- wl_callback --
2948impl Dispatch<WlCallback, ()> for Compositor {
2949    fn request(
2950        _: &mut Self,
2951        _: &Client,
2952        _: &WlCallback,
2953        _: <WlCallback as Resource>::Request,
2954        _: &(),
2955        _: &DisplayHandle,
2956        _: &mut DataInit<'_, Self>,
2957    ) {
2958    }
2959}
2960
2961// -- wp_presentation --
2962impl GlobalDispatch<WpPresentation, ()> for Compositor {
2963    fn bind(
2964        _: &mut Self,
2965        _: &DisplayHandle,
2966        _: &Client,
2967        resource: New<WpPresentation>,
2968        _: &(),
2969        data_init: &mut DataInit<'_, Self>,
2970    ) {
2971        let pres = data_init.init(resource, ());
2972        // Tell the client we use CLOCK_MONOTONIC for presentation timestamps.
2973        pres.clock_id(libc::CLOCK_MONOTONIC as u32);
2974    }
2975}
2976
2977impl Dispatch<WpPresentation, ()> for Compositor {
2978    fn request(
2979        state: &mut Self,
2980        _: &Client,
2981        _: &WpPresentation,
2982        request: <WpPresentation as Resource>::Request,
2983        _: &(),
2984        _: &DisplayHandle,
2985        data_init: &mut DataInit<'_, Self>,
2986    ) {
2987        use wp_presentation::Request;
2988        match request {
2989            Request::Feedback { surface, callback } => {
2990                let fb = data_init.init(callback, ());
2991                let sid = surface.id();
2992                if let Some(surf) = state.surfaces.get_mut(&sid) {
2993                    surf.pending_presentation_feedbacks.push(fb);
2994                }
2995            }
2996            Request::Destroy => {}
2997            _ => {}
2998        }
2999    }
3000}
3001
3002// -- wp_presentation_feedback (no client requests) --
3003impl Dispatch<WpPresentationFeedback, ()> for Compositor {
3004    fn request(
3005        _: &mut Self,
3006        _: &Client,
3007        _: &WpPresentationFeedback,
3008        _: <WpPresentationFeedback as Resource>::Request,
3009        _: &(),
3010        _: &DisplayHandle,
3011        _: &mut DataInit<'_, Self>,
3012    ) {
3013    }
3014}
3015
3016// -- wl_region --
3017impl Dispatch<WlRegion, ()> for Compositor {
3018    fn request(
3019        _: &mut Self,
3020        _: &Client,
3021        _: &WlRegion,
3022        _: <WlRegion as Resource>::Request,
3023        _: &(),
3024        _: &DisplayHandle,
3025        _: &mut DataInit<'_, Self>,
3026    ) {
3027    }
3028}
3029
3030// -- wl_subcompositor --
3031impl GlobalDispatch<WlSubcompositor, ()> for Compositor {
3032    fn bind(
3033        _: &mut Self,
3034        _: &DisplayHandle,
3035        _: &Client,
3036        resource: New<WlSubcompositor>,
3037        _: &(),
3038        data_init: &mut DataInit<'_, Self>,
3039    ) {
3040        data_init.init(resource, ());
3041    }
3042}
3043
3044impl Dispatch<WlSubcompositor, ()> for Compositor {
3045    fn request(
3046        state: &mut Self,
3047        _: &Client,
3048        _: &WlSubcompositor,
3049        request: <WlSubcompositor as Resource>::Request,
3050        _: &(),
3051        _: &DisplayHandle,
3052        data_init: &mut DataInit<'_, Self>,
3053    ) {
3054        use wayland_server::protocol::wl_subcompositor::Request;
3055        match request {
3056            Request::GetSubsurface {
3057                id,
3058                surface,
3059                parent,
3060            } => {
3061                let child_id = surface.id();
3062                let parent_id = parent.id();
3063                data_init.init(
3064                    id,
3065                    SubsurfaceData {
3066                        wl_surface_id: child_id.clone(),
3067                        parent_surface_id: parent_id.clone(),
3068                    },
3069                );
3070                if let Some(surf) = state.surfaces.get_mut(&child_id) {
3071                    surf.parent_surface_id = Some(parent_id.clone());
3072                }
3073                if let Some(parent_surf) = state.surfaces.get_mut(&parent_id)
3074                    && !parent_surf.children.contains(&child_id)
3075                {
3076                    parent_surf.children.push(child_id);
3077                }
3078            }
3079            Request::Destroy => {}
3080            _ => {}
3081        }
3082    }
3083}
3084
3085// -- wl_subsurface --
3086impl Dispatch<WlSubsurface, SubsurfaceData> for Compositor {
3087    fn request(
3088        state: &mut Self,
3089        _: &Client,
3090        _: &WlSubsurface,
3091        request: <WlSubsurface as Resource>::Request,
3092        data: &SubsurfaceData,
3093        _: &DisplayHandle,
3094        _: &mut DataInit<'_, Self>,
3095    ) {
3096        use wayland_server::protocol::wl_subsurface::Request;
3097        match request {
3098            Request::SetPosition { x, y } => {
3099                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3100                    surf.pending_subsurface_position = Some((x, y));
3101                }
3102            }
3103            Request::PlaceAbove { sibling } => {
3104                let sibling_id = sibling.id();
3105                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3106                    let child_id = &data.wl_surface_id;
3107                    parent.children.retain(|c| c != child_id);
3108                    let pos = parent
3109                        .children
3110                        .iter()
3111                        .position(|c| *c == sibling_id)
3112                        .map(|p| p + 1)
3113                        .unwrap_or(parent.children.len());
3114                    parent.children.insert(pos, child_id.clone());
3115                }
3116            }
3117            Request::PlaceBelow { sibling } => {
3118                let sibling_id = sibling.id();
3119                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3120                    let child_id = &data.wl_surface_id;
3121                    parent.children.retain(|c| c != child_id);
3122                    let pos = parent
3123                        .children
3124                        .iter()
3125                        .position(|c| *c == sibling_id)
3126                        .unwrap_or(0);
3127                    parent.children.insert(pos, child_id.clone());
3128                }
3129            }
3130            Request::SetSync | Request::SetDesync => {}
3131            Request::Destroy => {
3132                let child_id = &data.wl_surface_id;
3133                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
3134                    parent.children.retain(|c| c != child_id);
3135                }
3136                if let Some(surf) = state.surfaces.get_mut(child_id) {
3137                    surf.parent_surface_id = None;
3138                }
3139            }
3140            _ => {}
3141        }
3142    }
3143}
3144
3145// -- xdg_wm_base --
3146impl GlobalDispatch<XdgWmBase, ()> for Compositor {
3147    fn bind(
3148        _: &mut Self,
3149        _: &DisplayHandle,
3150        _: &Client,
3151        resource: New<XdgWmBase>,
3152        _: &(),
3153        data_init: &mut DataInit<'_, Self>,
3154    ) {
3155        data_init.init(resource, ());
3156    }
3157}
3158
3159impl Dispatch<XdgWmBase, ()> for Compositor {
3160    fn request(
3161        state: &mut Self,
3162        _: &Client,
3163        _: &XdgWmBase,
3164        request: <XdgWmBase as Resource>::Request,
3165        _: &(),
3166        _: &DisplayHandle,
3167        data_init: &mut DataInit<'_, Self>,
3168    ) {
3169        use xdg_wm_base::Request;
3170        match request {
3171            Request::GetXdgSurface { id, surface } => {
3172                let wl_surface_id = surface.id();
3173                let xdg_surface = data_init.init(
3174                    id,
3175                    XdgSurfaceData {
3176                        wl_surface_id: wl_surface_id.clone(),
3177                    },
3178                );
3179                if let Some(surf) = state.surfaces.get_mut(&wl_surface_id) {
3180                    surf.xdg_surface = Some(xdg_surface);
3181                }
3182            }
3183            Request::CreatePositioner { id } => {
3184                let positioner = data_init.init(id, ());
3185                let pos_id = positioner.id();
3186                state.positioners.insert(
3187                    pos_id,
3188                    PositionerState {
3189                        resource: positioner,
3190                        geometry: PositionerGeometry {
3191                            size: (0, 0),
3192                            anchor_rect: (0, 0, 0, 0),
3193                            anchor: 0,
3194                            gravity: 0,
3195                            constraint_adjustment: 0,
3196                            offset: (0, 0),
3197                        },
3198                    },
3199                );
3200            }
3201            Request::Pong { .. } => {}
3202            Request::Destroy => {}
3203            _ => {}
3204        }
3205    }
3206}
3207
3208// -- xdg_surface --
3209impl Dispatch<XdgSurface, XdgSurfaceData> for Compositor {
3210    fn request(
3211        state: &mut Self,
3212        _: &Client,
3213        resource: &XdgSurface,
3214        request: <XdgSurface as Resource>::Request,
3215        data: &XdgSurfaceData,
3216        _: &DisplayHandle,
3217        data_init: &mut DataInit<'_, Self>,
3218    ) {
3219        use xdg_surface::Request;
3220        match request {
3221            Request::GetToplevel { id } => {
3222                let toplevel = data_init.init(
3223                    id,
3224                    XdgToplevelData {
3225                        wl_surface_id: data.wl_surface_id.clone(),
3226                    },
3227                );
3228                let surface_id = state.allocate_surface_id();
3229                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3230                    surf.xdg_toplevel = Some(toplevel.clone());
3231                    surf.surface_id = surface_id;
3232                }
3233                state
3234                    .toplevel_surface_ids
3235                    .insert(surface_id, data.wl_surface_id.clone());
3236
3237                // Use a per-surface size if one was already configured
3238                // (e.g. the browser sent C2S_SURFACE_RESIZE before the
3239                // toplevel was created), otherwise fall back to the global
3240                // output dimensions.  surface_sizes stores logical pixels.
3241                let (cw, ch) = state
3242                    .surface_sizes
3243                    .get(&surface_id)
3244                    .copied()
3245                    .unwrap_or((state.output_width, state.output_height));
3246                let states = xdg_toplevel_states(&[
3247                    xdg_toplevel::State::Activated,
3248                    xdg_toplevel::State::Maximized,
3249                ]);
3250                toplevel.configure(cw, ch, states);
3251                let serial = state.next_serial();
3252                resource.configure(serial);
3253
3254                // Keyboard focus — sends leave to the previously focused
3255                // surface's client before entering the new one.
3256                state.set_keyboard_focus(surface_id);
3257                // Tell the client which output its surface is on so it can
3258                // determine scale and start rendering.
3259                if let Some(surf) = state.surfaces.get(&data.wl_surface_id) {
3260                    for output in &state.outputs {
3261                        if same_client(output, &surf.wl_surface) {
3262                            surf.wl_surface.enter(output);
3263                        }
3264                    }
3265                }
3266                let _ = state.display_handle.flush_clients();
3267
3268                let _ = state.event_tx.send(CompositorEvent::SurfaceCreated {
3269                    surface_id,
3270                    title: String::new(),
3271                    app_id: String::new(),
3272                    parent_id: 0,
3273                    width: 0,
3274                    height: 0,
3275                });
3276                (state.event_notify)();
3277                if state.verbose {
3278                    eprintln!("[compositor] new_toplevel sid={surface_id}");
3279                }
3280            }
3281            Request::GetPopup {
3282                id,
3283                parent,
3284                positioner,
3285            } => {
3286                let popup = data_init.init(
3287                    id,
3288                    XdgPopupData {
3289                        wl_surface_id: data.wl_surface_id.clone(),
3290                    },
3291                );
3292
3293                // Parent relationship: make the popup a child of the parent
3294                // surface so it is composited into the same toplevel frame.
3295                let parent_wl_id: Option<ObjectId> = parent
3296                    .as_ref()
3297                    .and_then(|p| p.data::<XdgSurfaceData>())
3298                    .map(|d| d.wl_surface_id.clone());
3299
3300                // The xdg-shell protocol specifies popup positions relative
3301                // to the parent's *window geometry*, not its surface origin.
3302                // Fetch the parent's geometry offset so we can convert
3303                // between window-geometry space and surface-tree space.
3304                let parent_geom_offset = parent_wl_id
3305                    .as_ref()
3306                    .and_then(|pid| state.surfaces.get(pid))
3307                    .and_then(|s| s.xdg_geometry)
3308                    .map(|(gx, gy, _, _)| (gx, gy))
3309                    .unwrap_or((0, 0));
3310
3311                // Compute the parent's absolute position within the toplevel
3312                // and the logical output bounds for constraint adjustment.
3313                // Add the geometry offset so parent_abs represents the
3314                // window-geometry origin in surface-tree coordinates.
3315                let parent_abs = parent_wl_id
3316                    .as_ref()
3317                    .map(|pid| {
3318                        let abs = state.surface_absolute_position(pid);
3319                        (abs.0 + parent_geom_offset.0, abs.1 + parent_geom_offset.1)
3320                    })
3321                    .unwrap_or((0, 0));
3322                // Use the client's actual surface size for popup bounds,
3323                // not the configured size (client may not have resized yet).
3324                let (_, toplevel_root) = parent_wl_id
3325                    .as_ref()
3326                    .map(|pid| state.find_toplevel_root(pid))
3327                    .unwrap_or_else(|| {
3328                        // Dummy root — no parent.
3329                        (data.wl_surface_id.clone(), None)
3330                    });
3331                let bounds = toplevel_root
3332                    .and_then(|_| {
3333                        let root_wl_id = parent_wl_id.as_ref().map(|pid| {
3334                            let (rid, _) = state.find_toplevel_root(pid);
3335                            rid
3336                        })?;
3337                        let surf = state.surfaces.get(&root_wl_id)?;
3338                        if let Some((gx, gy, gw, gh)) = surf.xdg_geometry
3339                            && gw > 0
3340                            && gh > 0
3341                        {
3342                            return Some((gx, gy, gw, gh));
3343                        }
3344
3345                        // Fall back to the client's actual logical surface
3346                        // size when window geometry is unavailable.
3347                        let sm = state.surface_meta.get(&root_wl_id)?;
3348                        let s = (sm.scale).max(1);
3349                        let (lw, lh) = surf
3350                            .viewport_destination
3351                            .filter(|&(dw, dh)| dw > 0 && dh > 0)
3352                            .unwrap_or((sm.width as i32 / s, sm.height as i32 / s));
3353                        Some((0, 0, lw, lh))
3354                    })
3355                    .unwrap_or((0, 0, state.output_width, state.output_height));
3356
3357                eprintln!(
3358                    "[popup] parent_abs={parent_abs:?} bounds={bounds:?} parent_wl={parent_wl_id:?} geom_off={parent_geom_offset:?}"
3359                );
3360                // Compute geometry from positioner with constraint adjustment.
3361                let pos_id = positioner.id();
3362                let (px, py, pw, ph) = state
3363                    .positioners
3364                    .get(&pos_id)
3365                    .map(|p| p.geometry.compute_position(parent_abs, bounds))
3366                    .unwrap_or((0, 0, 200, 200));
3367                eprintln!("[popup] result=({px},{py},{pw},{ph})");
3368
3369                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3370                    surf.xdg_popup = Some(popup.clone());
3371                    surf.parent_surface_id = parent_wl_id.clone();
3372                    // Convert from window-geometry-relative to surface-
3373                    // relative coords so the popup composites correctly.
3374                    // The rendering crops to xdg_geometry, so the popup
3375                    // must be offset by the parent's geometry origin.
3376                    surf.subsurface_position =
3377                        (parent_geom_offset.0 + px, parent_geom_offset.1 + py);
3378                }
3379                if let Some(ref parent_id) = parent_wl_id
3380                    && let Some(parent_surf) = state.surfaces.get_mut(parent_id)
3381                    && !parent_surf.children.contains(&data.wl_surface_id)
3382                {
3383                    parent_surf.children.push(data.wl_surface_id.clone());
3384                }
3385
3386                popup.configure(px, py, pw, ph);
3387                let serial = state.next_serial();
3388                resource.configure(serial);
3389                let _ = state.display_handle.flush_clients();
3390            }
3391            Request::SetWindowGeometry {
3392                x,
3393                y,
3394                width,
3395                height,
3396            } => {
3397                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3398                    // For popup surfaces, adjust subsurface_position to
3399                    // account for the popup's own geometry offset.  The
3400                    // xdg-shell protocol positions the popup's *geometry*
3401                    // (not its surface origin) relative to the parent's
3402                    // geometry.  Without this adjustment, CSD shadows or
3403                    // borders around the popup cause the visible content
3404                    // to shift by (gx, gy).
3405                    if surf.xdg_popup.is_some() {
3406                        let (old_gx, old_gy) = surf
3407                            .xdg_geometry
3408                            .map(|(gx, gy, _, _)| (gx, gy))
3409                            .unwrap_or((0, 0));
3410                        surf.subsurface_position.0 += old_gx - x;
3411                        surf.subsurface_position.1 += old_gy - y;
3412                    }
3413                    surf.xdg_geometry = Some((x, y, width, height));
3414                }
3415            }
3416            Request::AckConfigure { .. } => {}
3417            Request::Destroy => {}
3418            _ => {}
3419        }
3420    }
3421}
3422
3423// -- xdg_toplevel --
3424impl Dispatch<XdgToplevel, XdgToplevelData> for Compositor {
3425    fn request(
3426        state: &mut Self,
3427        _: &Client,
3428        _: &XdgToplevel,
3429        request: <XdgToplevel as Resource>::Request,
3430        data: &XdgToplevelData,
3431        _: &DisplayHandle,
3432        _: &mut DataInit<'_, Self>,
3433    ) {
3434        use xdg_toplevel::Request;
3435        match request {
3436            Request::SetTitle { title } => {
3437                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
3438                    && surf.title != title
3439                {
3440                    surf.title = title.clone();
3441                    if surf.surface_id > 0 {
3442                        let _ = state.event_tx.send(CompositorEvent::SurfaceTitle {
3443                            surface_id: surf.surface_id,
3444                            title,
3445                        });
3446                        (state.event_notify)();
3447                    }
3448                }
3449            }
3450            Request::SetAppId { app_id } => {
3451                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
3452                    && surf.app_id != app_id
3453                {
3454                    surf.app_id = app_id.clone();
3455                    if surf.surface_id > 0 {
3456                        let _ = state.event_tx.send(CompositorEvent::SurfaceAppId {
3457                            surface_id: surf.surface_id,
3458                            app_id,
3459                        });
3460                        (state.event_notify)();
3461                    }
3462                }
3463            }
3464            Request::Destroy => {
3465                let wl_surface_id = &data.wl_surface_id;
3466                state.surface_meta.remove(wl_surface_id);
3467                state.cursor_rgba.remove(wl_surface_id);
3468                if let Some(ref mut vk) = state.vulkan_renderer {
3469                    vk.remove_surface(wl_surface_id);
3470                }
3471                if let Some(held) = state.held_buffers.remove(wl_surface_id) {
3472                    held.release();
3473                }
3474                if let Some(surf) = state.surfaces.get_mut(wl_surface_id) {
3475                    let sid = surf.surface_id;
3476                    surf.xdg_toplevel = None;
3477                    if sid > 0 {
3478                        state.toplevel_surface_ids.remove(&sid);
3479                        state.last_reported_size.remove(&sid);
3480                        state.surface_sizes.remove(&sid);
3481                        let _ = state
3482                            .event_tx
3483                            .send(CompositorEvent::SurfaceDestroyed { surface_id: sid });
3484                        (state.event_notify)();
3485                        surf.surface_id = 0;
3486                    }
3487                }
3488            }
3489            _ => {}
3490        }
3491    }
3492}
3493
3494// -- xdg_popup --
3495impl Dispatch<XdgPopup, XdgPopupData> for Compositor {
3496    fn request(
3497        state: &mut Self,
3498        _: &Client,
3499        _: &XdgPopup,
3500        request: <XdgPopup as Resource>::Request,
3501        data: &XdgPopupData,
3502        _: &DisplayHandle,
3503        _: &mut DataInit<'_, Self>,
3504    ) {
3505        use xdg_popup::Request;
3506        match request {
3507            Request::Grab { seat: _, serial: _ } => {
3508                // Add this popup to the grab stack so we can send
3509                // popup_done when the user clicks outside.
3510                state
3511                    .popup_grab_stack
3512                    .retain(|id| *id != data.wl_surface_id);
3513                state.popup_grab_stack.push(data.wl_surface_id.clone());
3514            }
3515            Request::Reposition { positioner, token } => {
3516                // Recompute the popup position using the new positioner.
3517                let pos_id = positioner.id();
3518                if let Some(surf) = state.surfaces.get(&data.wl_surface_id)
3519                    && let Some(parent_id) = surf.parent_surface_id.clone()
3520                {
3521                    let parent_geom_offset = state
3522                        .surfaces
3523                        .get(&parent_id)
3524                        .and_then(|s| s.xdg_geometry)
3525                        .map(|(gx, gy, _, _)| (gx, gy))
3526                        .unwrap_or((0, 0));
3527                    let parent_abs = {
3528                        let abs = state.surface_absolute_position(&parent_id);
3529                        (abs.0 + parent_geom_offset.0, abs.1 + parent_geom_offset.1)
3530                    };
3531                    let (root_id, toplevel_root) = state.find_toplevel_root(&parent_id);
3532                    let bounds = toplevel_root
3533                        .and_then(|_| {
3534                            let surf = state.surfaces.get(&root_id)?;
3535                            if let Some((gx, gy, gw, gh)) = surf.xdg_geometry
3536                                && gw > 0
3537                                && gh > 0
3538                            {
3539                                return Some((gx, gy, gw, gh));
3540                            }
3541                            let sm = state.surface_meta.get(&root_id)?;
3542                            let s = (sm.scale).max(1);
3543                            let (lw, lh) = surf
3544                                .viewport_destination
3545                                .filter(|&(dw, dh)| dw > 0 && dh > 0)
3546                                .unwrap_or((sm.width as i32 / s, sm.height as i32 / s));
3547                            Some((0, 0, lw, lh))
3548                        })
3549                        .unwrap_or((0, 0, state.output_width, state.output_height));
3550                    let (px, py, pw, ph) = state
3551                        .positioners
3552                        .get(&pos_id)
3553                        .map(|p| p.geometry.compute_position(parent_abs, bounds))
3554                        .unwrap_or((0, 0, 200, 200));
3555                    if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3556                        // Undo the previous geometry adjustment before
3557                        // applying the new position.
3558                        let old_gx = surf.xdg_geometry.map(|(gx, _, _, _)| gx).unwrap_or(0);
3559                        let old_gy = surf.xdg_geometry.map(|(_, gy, _, _)| gy).unwrap_or(0);
3560                        surf.subsurface_position = (
3561                            parent_geom_offset.0 + px - old_gx,
3562                            parent_geom_offset.1 + py - old_gy,
3563                        );
3564                        if let Some(ref popup) = surf.xdg_popup {
3565                            popup.configure(px, py, pw, ph);
3566                            popup.repositioned(token);
3567                        }
3568                        if let Some(ref xs) = surf.xdg_surface {
3569                            let serial = state.serial.wrapping_add(1);
3570                            state.serial = serial;
3571                            xs.configure(serial);
3572                        }
3573                    }
3574                }
3575            }
3576            Request::Destroy => {
3577                // Remove from grab stack.
3578                state
3579                    .popup_grab_stack
3580                    .retain(|id| *id != data.wl_surface_id);
3581                // Remove from parent's children list.
3582                if let Some(parent_id) = state
3583                    .surfaces
3584                    .get(&data.wl_surface_id)
3585                    .and_then(|s| s.parent_surface_id.clone())
3586                    && let Some(parent) = state.surfaces.get_mut(&parent_id)
3587                {
3588                    parent.children.retain(|c| *c != data.wl_surface_id);
3589                }
3590                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3591                    surf.xdg_popup = None;
3592                    surf.parent_surface_id = None;
3593                }
3594            }
3595            _ => {}
3596        }
3597    }
3598}
3599
3600// -- xdg_positioner --
3601use wayland_protocols::xdg::shell::server::xdg_positioner;
3602impl Dispatch<XdgPositioner, ()> for Compositor {
3603    fn request(
3604        state: &mut Self,
3605        _: &Client,
3606        resource: &XdgPositioner,
3607        request: <XdgPositioner as Resource>::Request,
3608        _: &(),
3609        _: &DisplayHandle,
3610        _: &mut DataInit<'_, Self>,
3611    ) {
3612        use xdg_positioner::Request;
3613        let pos_id = resource.id();
3614        let Some(pos) = state.positioners.get_mut(&pos_id) else {
3615            return;
3616        };
3617        match request {
3618            Request::SetSize { width, height } => {
3619                pos.geometry.size = (width, height);
3620            }
3621            Request::SetAnchorRect {
3622                x,
3623                y,
3624                width,
3625                height,
3626            } => {
3627                pos.geometry.anchor_rect = (x, y, width, height);
3628            }
3629            Request::SetAnchor {
3630                anchor: wayland_server::WEnum::Value(v),
3631            } => {
3632                pos.geometry.anchor = v as u32;
3633            }
3634            Request::SetGravity {
3635                gravity: wayland_server::WEnum::Value(v),
3636            } => {
3637                pos.geometry.gravity = v as u32;
3638            }
3639            Request::SetOffset { x, y } => {
3640                pos.geometry.offset = (x, y);
3641            }
3642            Request::SetConstraintAdjustment {
3643                constraint_adjustment,
3644            } => {
3645                pos.geometry.constraint_adjustment = constraint_adjustment.into();
3646            }
3647            Request::Destroy => {
3648                state.positioners.remove(&pos_id);
3649            }
3650            _ => {}
3651        }
3652    }
3653}
3654
3655// -- xdg_decoration --
3656impl GlobalDispatch<ZxdgDecorationManagerV1, ()> for Compositor {
3657    fn bind(
3658        _: &mut Self,
3659        _: &DisplayHandle,
3660        _: &Client,
3661        resource: New<ZxdgDecorationManagerV1>,
3662        _: &(),
3663        data_init: &mut DataInit<'_, Self>,
3664    ) {
3665        data_init.init(resource, ());
3666    }
3667}
3668
3669impl Dispatch<ZxdgDecorationManagerV1, ()> for Compositor {
3670    fn request(
3671        _: &mut Self,
3672        _: &Client,
3673        _: &ZxdgDecorationManagerV1,
3674        request: <ZxdgDecorationManagerV1 as Resource>::Request,
3675        _: &(),
3676        _: &DisplayHandle,
3677        data_init: &mut DataInit<'_, Self>,
3678    ) {
3679        use zxdg_decoration_manager_v1::Request;
3680        match request {
3681            Request::GetToplevelDecoration { id, toplevel: _ } => {
3682                let decoration = data_init.init(id, ());
3683                // Always request server-side (i.e. no) decorations.
3684                decoration.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
3685            }
3686            Request::Destroy => {}
3687            _ => {}
3688        }
3689    }
3690}
3691
3692impl Dispatch<ZxdgToplevelDecorationV1, ()> for Compositor {
3693    fn request(
3694        _: &mut Self,
3695        _: &Client,
3696        resource: &ZxdgToplevelDecorationV1,
3697        request: <ZxdgToplevelDecorationV1 as Resource>::Request,
3698        _: &(),
3699        _: &DisplayHandle,
3700        _: &mut DataInit<'_, Self>,
3701    ) {
3702        use zxdg_toplevel_decoration_v1::Request;
3703        match request {
3704            Request::SetMode { .. } | Request::UnsetMode => {
3705                resource.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
3706            }
3707            Request::Destroy => {}
3708            _ => {}
3709        }
3710    }
3711}
3712
3713// -- wl_shm --
3714impl GlobalDispatch<WlShm, ()> for Compositor {
3715    fn bind(
3716        _: &mut Self,
3717        _: &DisplayHandle,
3718        _: &Client,
3719        resource: New<WlShm>,
3720        _: &(),
3721        data_init: &mut DataInit<'_, Self>,
3722    ) {
3723        let shm = data_init.init(resource, ());
3724        shm.format(wl_shm::Format::Argb8888);
3725        shm.format(wl_shm::Format::Xrgb8888);
3726        shm.format(wl_shm::Format::Abgr8888);
3727        shm.format(wl_shm::Format::Xbgr8888);
3728    }
3729}
3730
3731impl Dispatch<WlShm, ()> for Compositor {
3732    fn request(
3733        state: &mut Self,
3734        _: &Client,
3735        _: &WlShm,
3736        request: <WlShm as Resource>::Request,
3737        _: &(),
3738        _: &DisplayHandle,
3739        data_init: &mut DataInit<'_, Self>,
3740    ) {
3741        use wayland_server::protocol::wl_shm::Request;
3742        if let Request::CreatePool { id, fd, size } = request {
3743            let pool = data_init.init(id, ());
3744            let pool_id = pool.id();
3745            state
3746                .shm_pools
3747                .insert(pool_id, Arc::new(ShmPool::new(pool, fd, size)));
3748        }
3749    }
3750}
3751
3752// -- wl_shm_pool --
3753impl Dispatch<WlShmPool, ()> for Compositor {
3754    fn request(
3755        state: &mut Self,
3756        _: &Client,
3757        resource: &WlShmPool,
3758        request: <WlShmPool as Resource>::Request,
3759        _: &(),
3760        _: &DisplayHandle,
3761        data_init: &mut DataInit<'_, Self>,
3762    ) {
3763        use wayland_server::protocol::wl_shm_pool::Request;
3764        let pool_id = resource.id();
3765        match request {
3766            Request::CreateBuffer {
3767                id,
3768                offset,
3769                width,
3770                height,
3771                stride,
3772                format,
3773            } => {
3774                // format comes as WEnum<Format>, extract the known value.
3775                let fmt = match format {
3776                    wayland_server::WEnum::Value(f) => f,
3777                    _ => wl_shm::Format::Argb8888, // fallback
3778                };
3779                let Some(pool) = state.shm_pools.get(&pool_id).cloned() else {
3780                    return;
3781                };
3782                data_init.init(
3783                    id,
3784                    ShmBufferData {
3785                        pool,
3786                        offset,
3787                        width,
3788                        height,
3789                        stride,
3790                        format: fmt,
3791                    },
3792                );
3793            }
3794            Request::Resize { size } => {
3795                if let Some(pool) = state.shm_pools.get(&pool_id) {
3796                    pool.resize(size);
3797                }
3798            }
3799            Request::Destroy => {
3800                // Drop the map entry — Arc keeps the ShmPool alive while
3801                // wl_buffers created from it still reference it.
3802                state.shm_pools.remove(&pool_id);
3803            }
3804            _ => {}
3805        }
3806    }
3807}
3808
3809// -- wl_buffer (SHM) --
3810impl Dispatch<WlBuffer, ShmBufferData> for Compositor {
3811    fn request(
3812        _: &mut Self,
3813        _: &Client,
3814        _: &WlBuffer,
3815        _: <WlBuffer as Resource>::Request,
3816        _: &ShmBufferData,
3817        _: &DisplayHandle,
3818        _: &mut DataInit<'_, Self>,
3819    ) {
3820    }
3821}
3822
3823// -- wl_buffer (DMA-BUF) --
3824impl Dispatch<WlBuffer, DmaBufBufferData> for Compositor {
3825    fn request(
3826        _: &mut Self,
3827        _: &Client,
3828        _: &WlBuffer,
3829        _: <WlBuffer as Resource>::Request,
3830        _: &DmaBufBufferData,
3831        _: &DisplayHandle,
3832        _: &mut DataInit<'_, Self>,
3833    ) {
3834    }
3835}
3836
3837// -- wl_output --
3838impl GlobalDispatch<WlOutput, ()> for Compositor {
3839    fn bind(
3840        state: &mut Self,
3841        _: &DisplayHandle,
3842        _: &Client,
3843        resource: New<WlOutput>,
3844        _: &(),
3845        data_init: &mut DataInit<'_, Self>,
3846    ) {
3847        let output = data_init.init(resource, ());
3848        output.geometry(
3849            0,
3850            0,
3851            0,
3852            0,
3853            wl_output::Subpixel::Unknown,
3854            "Virtual".to_string(),
3855            "Headless".to_string(),
3856            wl_output::Transform::Normal,
3857        );
3858        let s120 = state.output_scale_120 as i32;
3859        let mode_w = state.output_width * s120 / 120;
3860        let mode_h = state.output_height * s120 / 120;
3861        output.mode(
3862            wl_output::Mode::Current | wl_output::Mode::Preferred,
3863            mode_w,
3864            mode_h,
3865            state.output_refresh_mhz as i32,
3866        );
3867        if output.version() >= 2 {
3868            output.scale(((state.output_scale_120 as i32) + 119) / 120);
3869        }
3870        if output.version() >= 2 {
3871            output.done();
3872        }
3873        state.outputs.push(output);
3874    }
3875}
3876
3877impl Dispatch<WlOutput, ()> for Compositor {
3878    fn request(
3879        state: &mut Self,
3880        _: &Client,
3881        resource: &WlOutput,
3882        request: <WlOutput as Resource>::Request,
3883        _: &(),
3884        _: &DisplayHandle,
3885        _: &mut DataInit<'_, Self>,
3886    ) {
3887        use wayland_server::protocol::wl_output::Request;
3888        if let Request::Release = request {
3889            state.outputs.retain(|o| o.id() != resource.id());
3890        }
3891    }
3892}
3893
3894// -- wl_seat --
3895impl GlobalDispatch<WlSeat, ()> for Compositor {
3896    fn bind(
3897        _: &mut Self,
3898        _: &DisplayHandle,
3899        _: &Client,
3900        resource: New<WlSeat>,
3901        _: &(),
3902        data_init: &mut DataInit<'_, Self>,
3903    ) {
3904        let seat = data_init.init(resource, ());
3905        seat.capabilities(wl_seat::Capability::Keyboard | wl_seat::Capability::Pointer);
3906        if seat.version() >= 2 {
3907            seat.name("headless".to_string());
3908        }
3909    }
3910}
3911
3912impl Dispatch<WlSeat, ()> for Compositor {
3913    fn request(
3914        state: &mut Self,
3915        _: &Client,
3916        _: &WlSeat,
3917        request: <WlSeat as Resource>::Request,
3918        _: &(),
3919        _: &DisplayHandle,
3920        data_init: &mut DataInit<'_, Self>,
3921    ) {
3922        use wayland_server::protocol::wl_seat::Request;
3923        match request {
3924            Request::GetKeyboard { id } => {
3925                let kb = data_init.init(id, ());
3926                if let Some(fd) = create_keymap_fd(&state.keyboard_keymap_data) {
3927                    kb.keymap(
3928                        wl_keyboard::KeymapFormat::XkbV1,
3929                        fd.as_fd(),
3930                        state.keyboard_keymap_data.len() as u32,
3931                    );
3932                }
3933                if kb.version() >= 4 {
3934                    kb.repeat_info(25, 200);
3935                }
3936                state.keyboards.push(kb);
3937            }
3938            Request::GetPointer { id } => {
3939                let ptr = data_init.init(id, ());
3940                state.pointers.push(ptr);
3941            }
3942            Request::GetTouch { id } => {
3943                data_init.init(id, ());
3944            }
3945            Request::Release => {}
3946            _ => {}
3947        }
3948    }
3949}
3950
3951// -- wl_keyboard --
3952impl Dispatch<WlKeyboard, ()> for Compositor {
3953    fn request(
3954        state: &mut Self,
3955        _: &Client,
3956        resource: &WlKeyboard,
3957        request: <WlKeyboard as Resource>::Request,
3958        _: &(),
3959        _: &DisplayHandle,
3960        _: &mut DataInit<'_, Self>,
3961    ) {
3962        if let wl_keyboard::Request::Release = request {
3963            state.keyboards.retain(|k| k.id() != resource.id());
3964        }
3965    }
3966}
3967
3968// -- wl_pointer --
3969impl Dispatch<WlPointer, ()> for Compositor {
3970    fn request(
3971        state: &mut Self,
3972        _: &Client,
3973        resource: &WlPointer,
3974        request: <WlPointer as Resource>::Request,
3975        _: &(),
3976        _: &DisplayHandle,
3977        _: &mut DataInit<'_, Self>,
3978    ) {
3979        use wl_pointer::Request;
3980        match request {
3981            Request::SetCursor {
3982                serial: _,
3983                surface,
3984                hotspot_x,
3985                hotspot_y,
3986            } => {
3987                if let Some(surface) = surface {
3988                    let sid = surface.id();
3989                    if let Some(surf) = state.surfaces.get_mut(&sid) {
3990                        surf.is_cursor = true;
3991                        surf.cursor_hotspot = (hotspot_x, hotspot_y);
3992                    }
3993                } else {
3994                    let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
3995                        surface_id: state.focused_surface_id,
3996                        cursor: CursorImage::Hidden,
3997                    });
3998                }
3999            }
4000            Request::Release => {
4001                state.pointers.retain(|p| p.id() != resource.id());
4002            }
4003            _ => {}
4004        }
4005    }
4006}
4007
4008// -- wl_touch (stub) --
4009impl Dispatch<wayland_server::protocol::wl_touch::WlTouch, ()> for Compositor {
4010    fn request(
4011        _: &mut Self,
4012        _: &Client,
4013        _: &wayland_server::protocol::wl_touch::WlTouch,
4014        _: <wayland_server::protocol::wl_touch::WlTouch as Resource>::Request,
4015        _: &(),
4016        _: &DisplayHandle,
4017        _: &mut DataInit<'_, Self>,
4018    ) {
4019    }
4020}
4021
4022// -- zwp_linux_dmabuf_v1 --
4023impl GlobalDispatch<ZwpLinuxDmabufV1, ()> for Compositor {
4024    fn bind(
4025        state: &mut Self,
4026        _: &DisplayHandle,
4027        _: &Client,
4028        resource: New<ZwpLinuxDmabufV1>,
4029        _: &(),
4030        data_init: &mut DataInit<'_, Self>,
4031    ) {
4032        let dmabuf = data_init.init(resource, ());
4033        // v4+ clients use get_default_feedback / get_surface_feedback
4034        // instead of the deprecated format/modifier events.
4035        if dmabuf.version() >= 4 {
4036            return;
4037        }
4038        if dmabuf.version() >= 3 {
4039            // Advertise DRM format modifiers that the Vulkan device can
4040            // actually import.  This ensures clients (Chromium, mpv, …)
4041            // allocate DMA-BUFs with a tiling layout the compositor can
4042            // handle natively on the GPU, avoiding broken CPU mmap
4043            // fallbacks for vendor-specific tiled VRAM.
4044            if let Some(ref vk) = state.vulkan_renderer
4045                && !vk.supported_dmabuf_modifiers.is_empty()
4046            {
4047                for &(drm_fmt, modifier) in &vk.supported_dmabuf_modifiers {
4048                    let mod_hi = (modifier >> 32) as u32;
4049                    let mod_lo = (modifier & 0xFFFFFFFF) as u32;
4050                    dmabuf.modifier(drm_fmt, mod_hi, mod_lo);
4051                }
4052            }
4053            // When Vulkan has no DMA-BUF extensions (SHM-only mode) we
4054            // intentionally advertise zero modifiers so clients fall back
4055            // to wl_shm.
4056        } else if state
4057            .vulkan_renderer
4058            .as_ref()
4059            .is_some_and(|vk| vk.has_dmabuf())
4060        {
4061            dmabuf.format(drm_fourcc::ARGB8888);
4062            dmabuf.format(drm_fourcc::XRGB8888);
4063            dmabuf.format(drm_fourcc::ABGR8888);
4064            dmabuf.format(drm_fourcc::XBGR8888);
4065        }
4066    }
4067}
4068
4069impl Dispatch<ZwpLinuxDmabufV1, ()> for Compositor {
4070    fn request(
4071        state: &mut Self,
4072        _: &Client,
4073        _: &ZwpLinuxDmabufV1,
4074        request: <ZwpLinuxDmabufV1 as Resource>::Request,
4075        _: &(),
4076        _: &DisplayHandle,
4077        data_init: &mut DataInit<'_, Self>,
4078    ) {
4079        use zwp_linux_dmabuf_v1::Request;
4080        match request {
4081            Request::CreateParams { params_id } => {
4082                data_init.init(params_id, ());
4083            }
4084            Request::GetDefaultFeedback { id } => {
4085                let fb = data_init.init(id, ());
4086                state.send_dmabuf_feedback(&fb);
4087            }
4088            Request::GetSurfaceFeedback { id, .. } => {
4089                let fb = data_init.init(id, ());
4090                state.send_dmabuf_feedback(&fb);
4091            }
4092            Request::Destroy => {}
4093            _ => {}
4094        }
4095    }
4096}
4097
4098impl Dispatch<ZwpLinuxDmabufFeedbackV1, ()> for Compositor {
4099    fn request(
4100        _: &mut Self,
4101        _: &Client,
4102        _: &ZwpLinuxDmabufFeedbackV1,
4103        _request: <ZwpLinuxDmabufFeedbackV1 as Resource>::Request,
4104        _: &(),
4105        _: &DisplayHandle,
4106        _data_init: &mut DataInit<'_, Self>,
4107    ) {
4108        // Only request is Destroy, handled automatically.
4109    }
4110}
4111
4112// -- zwp_linux_buffer_params_v1 --
4113impl Dispatch<ZwpLinuxBufferParamsV1, ()> for Compositor {
4114    fn request(
4115        state: &mut Self,
4116        client: &Client,
4117        resource: &ZwpLinuxBufferParamsV1,
4118        request: <ZwpLinuxBufferParamsV1 as Resource>::Request,
4119        _: &(),
4120        dh: &DisplayHandle,
4121        data_init: &mut DataInit<'_, Self>,
4122    ) {
4123        use zwp_linux_buffer_params_v1::Request;
4124        let params_id = resource.id();
4125        match request {
4126            Request::Add {
4127                fd,
4128                plane_idx: _,
4129                offset,
4130                stride,
4131                modifier_hi,
4132                modifier_lo,
4133            } => {
4134                let modifier = ((modifier_hi as u64) << 32) | (modifier_lo as u64);
4135                let entry = state
4136                    .dmabuf_params
4137                    .entry(params_id.clone())
4138                    .or_insert_with(|| DmaBufParamsPending {
4139                        resource: resource.clone(),
4140                        planes: Vec::new(),
4141                        modifier,
4142                    });
4143                entry.modifier = modifier;
4144                entry.planes.push(DmaBufPlane { fd, offset, stride });
4145            }
4146            Request::Create {
4147                width,
4148                height,
4149                format,
4150                flags,
4151            } => {
4152                let pending = state.dmabuf_params.remove(&params_id);
4153                let (planes, modifier) = match pending {
4154                    Some(p) => (p.planes, p.modifier),
4155                    None => {
4156                        resource.failed();
4157                        return;
4158                    }
4159                };
4160                let y_invert = flags
4161                    .into_result()
4162                    .ok()
4163                    .is_some_and(|f| f.contains(zwp_linux_buffer_params_v1::Flags::YInvert));
4164                match client.create_resource::<WlBuffer, DmaBufBufferData, Compositor>(
4165                    dh,
4166                    1,
4167                    DmaBufBufferData {
4168                        width,
4169                        height,
4170                        fourcc: format,
4171                        modifier,
4172                        planes,
4173                        y_invert,
4174                    },
4175                ) {
4176                    Ok(buffer) => resource.created(&buffer),
4177                    Err(_) => resource.failed(),
4178                }
4179            }
4180            Request::CreateImmed {
4181                buffer_id,
4182                width,
4183                height,
4184                format,
4185                flags,
4186            } => {
4187                let (planes, modifier) = state
4188                    .dmabuf_params
4189                    .remove(&params_id)
4190                    .map(|p| (p.planes, p.modifier))
4191                    .unwrap_or_default();
4192                let y_invert = flags
4193                    .into_result()
4194                    .ok()
4195                    .is_some_and(|f| f.contains(zwp_linux_buffer_params_v1::Flags::YInvert));
4196                data_init.init(
4197                    buffer_id,
4198                    DmaBufBufferData {
4199                        width,
4200                        height,
4201                        fourcc: format,
4202                        modifier,
4203                        planes,
4204                        y_invert,
4205                    },
4206                );
4207            }
4208            Request::Destroy => {
4209                state.dmabuf_params.remove(&params_id);
4210            }
4211            _ => {}
4212        }
4213    }
4214}
4215
4216// -- wp_fractional_scale_manager_v1 --
4217impl GlobalDispatch<WpFractionalScaleManagerV1, ()> for Compositor {
4218    fn bind(
4219        _: &mut Self,
4220        _: &DisplayHandle,
4221        _: &Client,
4222        resource: New<WpFractionalScaleManagerV1>,
4223        _: &(),
4224        data_init: &mut DataInit<'_, Self>,
4225    ) {
4226        data_init.init(resource, ());
4227    }
4228}
4229
4230impl Dispatch<WpFractionalScaleManagerV1, ()> for Compositor {
4231    fn request(
4232        state: &mut Self,
4233        _: &Client,
4234        _: &WpFractionalScaleManagerV1,
4235        request: <WpFractionalScaleManagerV1 as Resource>::Request,
4236        _: &(),
4237        _: &DisplayHandle,
4238        data_init: &mut DataInit<'_, Self>,
4239    ) {
4240        use wp_fractional_scale_manager_v1::Request;
4241        match request {
4242            Request::GetFractionalScale { id, surface: _ } => {
4243                let fs = data_init.init(id, ());
4244                // Send the current preferred scale immediately.
4245                fs.preferred_scale(state.output_scale_120 as u32);
4246                state.fractional_scales.push(fs);
4247            }
4248            Request::Destroy => {}
4249            _ => {}
4250        }
4251    }
4252}
4253
4254// -- wp_fractional_scale_v1 --
4255impl Dispatch<WpFractionalScaleV1, ()> for Compositor {
4256    fn request(
4257        state: &mut Self,
4258        _: &Client,
4259        resource: &WpFractionalScaleV1,
4260        _: <WpFractionalScaleV1 as Resource>::Request,
4261        _: &(),
4262        _: &DisplayHandle,
4263        _: &mut DataInit<'_, Self>,
4264    ) {
4265        // Only request is Destroy.
4266        state
4267            .fractional_scales
4268            .retain(|fs| fs.id() != resource.id());
4269    }
4270}
4271
4272// -- wp_viewporter --
4273impl GlobalDispatch<WpViewporter, ()> for Compositor {
4274    fn bind(
4275        _: &mut Self,
4276        _: &DisplayHandle,
4277        _: &Client,
4278        resource: New<WpViewporter>,
4279        _: &(),
4280        data_init: &mut DataInit<'_, Self>,
4281    ) {
4282        data_init.init(resource, ());
4283    }
4284}
4285
4286impl Dispatch<WpViewporter, ()> for Compositor {
4287    fn request(
4288        _: &mut Self,
4289        _: &Client,
4290        _: &WpViewporter,
4291        request: <WpViewporter as Resource>::Request,
4292        _: &(),
4293        _: &DisplayHandle,
4294        data_init: &mut DataInit<'_, Self>,
4295    ) {
4296        use wp_viewporter::Request;
4297        match request {
4298            Request::GetViewport { id, surface } => {
4299                // Associate the viewport with the surface's ObjectId so
4300                // SetDestination can update the right Surface.
4301                let obj_id = surface.id();
4302                data_init.init(id, obj_id);
4303            }
4304            Request::Destroy => {}
4305            _ => {}
4306        }
4307    }
4308}
4309
4310// -- wp_viewport --
4311impl Dispatch<WpViewport, ObjectId> for Compositor {
4312    fn request(
4313        state: &mut Self,
4314        _: &Client,
4315        _: &WpViewport,
4316        request: <WpViewport as Resource>::Request,
4317        surface_obj_id: &ObjectId,
4318        _: &DisplayHandle,
4319        _: &mut DataInit<'_, Self>,
4320    ) {
4321        use wayland_protocols::wp::viewporter::server::wp_viewport::Request;
4322        match request {
4323            Request::SetDestination { width, height } => {
4324                if let Some(surf) = state.surfaces.get_mut(surface_obj_id) {
4325                    // width/height of -1 means unset (revert to buffer size).
4326                    if width > 0 && height > 0 {
4327                        surf.pending_viewport_destination = Some((width, height));
4328                    } else {
4329                        surf.pending_viewport_destination = None;
4330                    }
4331                }
4332            }
4333            Request::SetSource { .. } => {
4334                // Source crop — not needed for headless compositor.
4335            }
4336            Request::Destroy => {}
4337            _ => {}
4338        }
4339    }
4340}
4341
4342// =========================================================================
4343// NEW PROTOCOLS
4344// =========================================================================
4345
4346// -- wl_data_device_manager (clipboard) --
4347
4348impl GlobalDispatch<WlDataDeviceManager, ()> for Compositor {
4349    fn bind(
4350        _: &mut Self,
4351        _: &DisplayHandle,
4352        _: &Client,
4353        resource: New<WlDataDeviceManager>,
4354        _: &(),
4355        data_init: &mut DataInit<'_, Self>,
4356    ) {
4357        data_init.init(resource, ());
4358    }
4359}
4360
4361impl Dispatch<WlDataDeviceManager, ()> for Compositor {
4362    fn request(
4363        state: &mut Self,
4364        _: &Client,
4365        _: &WlDataDeviceManager,
4366        request: <WlDataDeviceManager as Resource>::Request,
4367        _: &(),
4368        _: &DisplayHandle,
4369        data_init: &mut DataInit<'_, Self>,
4370    ) {
4371        use wl_data_device_manager::Request;
4372        match request {
4373            Request::CreateDataSource { id } => {
4374                data_init.init(
4375                    id,
4376                    DataSourceData {
4377                        mime_types: std::sync::Mutex::new(Vec::new()),
4378                    },
4379                );
4380            }
4381            Request::GetDataDevice { id, seat: _ } => {
4382                let dd = data_init.init(id, ());
4383                state.data_devices.push(dd);
4384            }
4385            _ => {}
4386        }
4387    }
4388}
4389
4390impl Dispatch<WlDataSource, DataSourceData> for Compositor {
4391    fn request(
4392        _: &mut Self,
4393        _: &Client,
4394        _: &WlDataSource,
4395        request: <WlDataSource as Resource>::Request,
4396        data: &DataSourceData,
4397        _: &DisplayHandle,
4398        _: &mut DataInit<'_, Self>,
4399    ) {
4400        use wl_data_source::Request;
4401        match request {
4402            Request::Offer { mime_type } => {
4403                data.mime_types.lock().unwrap().push(mime_type);
4404            }
4405            Request::Destroy => {}
4406            _ => {} // SetActions — DnD, ignored
4407        }
4408    }
4409
4410    fn destroyed(
4411        state: &mut Self,
4412        _: wayland_server::backend::ClientId,
4413        resource: &WlDataSource,
4414        _: &DataSourceData,
4415    ) {
4416        if state
4417            .selection_source
4418            .as_ref()
4419            .is_some_and(|s| s.id() == resource.id())
4420        {
4421            state.selection_source = None;
4422        }
4423    }
4424}
4425
4426impl Dispatch<WlDataDevice, ()> for Compositor {
4427    fn request(
4428        state: &mut Self,
4429        _: &Client,
4430        _: &WlDataDevice,
4431        request: <WlDataDevice as Resource>::Request,
4432        _: &(),
4433        _: &DisplayHandle,
4434        _: &mut DataInit<'_, Self>,
4435    ) {
4436        use wl_data_device::Request;
4437        match request {
4438            Request::SetSelection { source, serial: _ } => {
4439                state.selection_source = source.clone();
4440                // Try to read text content and emit an event.
4441                if let Some(ref src) = source {
4442                    let data = src.data::<DataSourceData>().unwrap();
4443                    let mimes = data.mime_types.lock().unwrap();
4444                    let text_mime = mimes
4445                        .iter()
4446                        .find(|m| {
4447                            m.as_str() == "text/plain;charset=utf-8"
4448                                || m.as_str() == "text/plain"
4449                                || m.as_str() == "UTF8_STRING"
4450                        })
4451                        .cloned();
4452                    drop(mimes);
4453                    if let Some(mime) = text_mime {
4454                        state.read_data_source_and_emit(src, &mime);
4455                    }
4456                }
4457            }
4458            Request::Release => {}
4459            _ => {} // StartDrag — ignored
4460        }
4461    }
4462
4463    fn destroyed(
4464        state: &mut Self,
4465        _: wayland_server::backend::ClientId,
4466        resource: &WlDataDevice,
4467        _: &(),
4468    ) {
4469        state.data_devices.retain(|d| d.id() != resource.id());
4470    }
4471}
4472
4473impl Dispatch<WlDataOffer, DataOfferData> for Compositor {
4474    fn request(
4475        state: &mut Self,
4476        _: &Client,
4477        _: &WlDataOffer,
4478        request: <WlDataOffer as Resource>::Request,
4479        data: &DataOfferData,
4480        _: &DisplayHandle,
4481        _: &mut DataInit<'_, Self>,
4482    ) {
4483        use wl_data_offer::Request;
4484        match request {
4485            Request::Receive { mime_type, fd } => {
4486                if data.external {
4487                    // Write external clipboard data to the fd.
4488                    if let Some(ref cb) = state.external_clipboard
4489                        && (cb.mime_type == mime_type
4490                            || mime_type == "text/plain"
4491                            || mime_type == "text/plain;charset=utf-8"
4492                            || mime_type == "UTF8_STRING")
4493                    {
4494                        use std::io::Write;
4495                        let mut f = std::fs::File::from(fd);
4496                        let _ = f.write_all(&cb.data);
4497                    }
4498                } else if let Some(ref src) = state.selection_source {
4499                    // Forward to the Wayland data source.
4500                    src.send(mime_type, fd.as_fd());
4501                }
4502            }
4503            Request::Destroy => {}
4504            _ => {} // Accept, Finish, SetActions — DnD
4505        }
4506    }
4507}
4508
4509impl Compositor {
4510    /// Create a pipe, ask the data source to write into it, read the result,
4511    /// and emit a `ClipboardContent` event.
4512    fn read_data_source_and_emit(&mut self, source: &WlDataSource, mime_type: &str) {
4513        let mut fds = [0i32; 2];
4514        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4515            return;
4516        }
4517        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
4518        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
4519        source.send(mime_type.to_string(), write_fd.as_fd());
4520        let _ = self.display_handle.flush_clients();
4521        // Non-blocking read with a modest limit.
4522        unsafe {
4523            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
4524        }
4525        // Give the client a moment to write.
4526        std::thread::sleep(std::time::Duration::from_millis(5));
4527        let mut buf = Vec::new();
4528        let mut tmp = [0u8; 8192];
4529        loop {
4530            let n = unsafe {
4531                libc::read(
4532                    read_fd.as_raw_fd(),
4533                    tmp.as_mut_ptr() as *mut libc::c_void,
4534                    tmp.len(),
4535                )
4536            };
4537            if n <= 0 {
4538                break;
4539            }
4540            buf.extend_from_slice(&tmp[..n as usize]);
4541            if buf.len() > 1024 * 1024 {
4542                break; // 1 MiB cap
4543            }
4544        }
4545        if !buf.is_empty() {
4546            let _ = self.event_tx.send(CompositorEvent::ClipboardContent {
4547                mime_type: mime_type.to_string(),
4548                data: buf,
4549            });
4550            (self.event_notify)();
4551        }
4552    }
4553
4554    /// Push external clipboard to all connected wl_data_device objects.
4555    fn offer_external_clipboard(&mut self) {
4556        let Some(ref cb) = self.external_clipboard else {
4557            return;
4558        };
4559        let mime = cb.mime_type.clone();
4560        for dd in &self.data_devices {
4561            if let Some(client) = dd.client() {
4562                let offer = client
4563                    .create_resource::<WlDataOffer, DataOfferData, Compositor>(
4564                        &self.display_handle,
4565                        dd.version(),
4566                        DataOfferData { external: true },
4567                    )
4568                    .unwrap();
4569                dd.data_offer(&offer);
4570                offer.offer(mime.clone());
4571                // Offer standard text aliases.
4572                if mime.starts_with("text/plain") {
4573                    if mime != "text/plain" {
4574                        offer.offer("text/plain".to_string());
4575                    }
4576                    if mime != "text/plain;charset=utf-8" {
4577                        offer.offer("text/plain;charset=utf-8".to_string());
4578                    }
4579                    offer.offer("UTF8_STRING".to_string());
4580                }
4581                dd.selection(Some(&offer));
4582            }
4583        }
4584        let _ = self.display_handle.flush_clients();
4585    }
4586}
4587
4588// -- zwp_primary_selection --
4589
4590impl GlobalDispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
4591    fn bind(
4592        _: &mut Self,
4593        _: &DisplayHandle,
4594        _: &Client,
4595        resource: New<ZwpPrimarySelectionDeviceManagerV1>,
4596        _: &(),
4597        data_init: &mut DataInit<'_, Self>,
4598    ) {
4599        data_init.init(resource, ());
4600    }
4601}
4602
4603impl Dispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
4604    fn request(
4605        state: &mut Self,
4606        _: &Client,
4607        _: &ZwpPrimarySelectionDeviceManagerV1,
4608        request: <ZwpPrimarySelectionDeviceManagerV1 as Resource>::Request,
4609        _: &(),
4610        _: &DisplayHandle,
4611        data_init: &mut DataInit<'_, Self>,
4612    ) {
4613        use zwp_primary_selection_device_manager_v1::Request;
4614        match request {
4615            Request::CreateSource { id } => {
4616                data_init.init(
4617                    id,
4618                    PrimarySourceData {
4619                        mime_types: std::sync::Mutex::new(Vec::new()),
4620                    },
4621                );
4622            }
4623            Request::GetDevice { id, seat: _ } => {
4624                let pd = data_init.init(id, ());
4625                state.primary_devices.push(pd);
4626            }
4627            Request::Destroy => {}
4628            _ => {}
4629        }
4630    }
4631}
4632
4633impl Dispatch<ZwpPrimarySelectionSourceV1, PrimarySourceData> for Compositor {
4634    fn request(
4635        _: &mut Self,
4636        _: &Client,
4637        _: &ZwpPrimarySelectionSourceV1,
4638        request: <ZwpPrimarySelectionSourceV1 as Resource>::Request,
4639        data: &PrimarySourceData,
4640        _: &DisplayHandle,
4641        _: &mut DataInit<'_, Self>,
4642    ) {
4643        use zwp_primary_selection_source_v1::Request;
4644        match request {
4645            Request::Offer { mime_type } => {
4646                data.mime_types.lock().unwrap().push(mime_type);
4647            }
4648            Request::Destroy => {}
4649            _ => {}
4650        }
4651    }
4652
4653    fn destroyed(
4654        state: &mut Self,
4655        _: wayland_server::backend::ClientId,
4656        resource: &ZwpPrimarySelectionSourceV1,
4657        _: &PrimarySourceData,
4658    ) {
4659        if state
4660            .primary_source
4661            .as_ref()
4662            .is_some_and(|s| s.id() == resource.id())
4663        {
4664            state.primary_source = None;
4665        }
4666    }
4667}
4668
4669impl Dispatch<ZwpPrimarySelectionDeviceV1, ()> for Compositor {
4670    fn request(
4671        state: &mut Self,
4672        _: &Client,
4673        _: &ZwpPrimarySelectionDeviceV1,
4674        request: <ZwpPrimarySelectionDeviceV1 as Resource>::Request,
4675        _: &(),
4676        _: &DisplayHandle,
4677        _: &mut DataInit<'_, Self>,
4678    ) {
4679        use zwp_primary_selection_device_v1::Request;
4680        match request {
4681            Request::SetSelection { source, serial: _ } => {
4682                state.primary_source = source;
4683            }
4684            Request::Destroy => {}
4685            _ => {}
4686        }
4687    }
4688
4689    fn destroyed(
4690        state: &mut Self,
4691        _: wayland_server::backend::ClientId,
4692        resource: &ZwpPrimarySelectionDeviceV1,
4693        _: &(),
4694    ) {
4695        state.primary_devices.retain(|d| d.id() != resource.id());
4696    }
4697}
4698
4699impl Dispatch<ZwpPrimarySelectionOfferV1, PrimaryOfferData> for Compositor {
4700    fn request(
4701        state: &mut Self,
4702        _: &Client,
4703        _: &ZwpPrimarySelectionOfferV1,
4704        request: <ZwpPrimarySelectionOfferV1 as Resource>::Request,
4705        data: &PrimaryOfferData,
4706        _: &DisplayHandle,
4707        _: &mut DataInit<'_, Self>,
4708    ) {
4709        use zwp_primary_selection_offer_v1::Request;
4710        match request {
4711            Request::Receive { mime_type, fd } => {
4712                if data.external {
4713                    if let Some(ref cb) = state.external_primary {
4714                        use std::io::Write;
4715                        let mut f = std::fs::File::from(fd);
4716                        let _ = f.write_all(&cb.data);
4717                        let _ = mime_type; // accepted regardless
4718                    }
4719                } else if let Some(ref src) = state.primary_source {
4720                    src.send(mime_type, fd.as_fd());
4721                }
4722            }
4723            Request::Destroy => {}
4724            _ => {}
4725        }
4726    }
4727}
4728
4729// -- zwp_pointer_constraints_v1 --
4730
4731impl GlobalDispatch<ZwpPointerConstraintsV1, ()> for Compositor {
4732    fn bind(
4733        _: &mut Self,
4734        _: &DisplayHandle,
4735        _: &Client,
4736        resource: New<ZwpPointerConstraintsV1>,
4737        _: &(),
4738        data_init: &mut DataInit<'_, Self>,
4739    ) {
4740        data_init.init(resource, ());
4741    }
4742}
4743
4744impl Dispatch<ZwpPointerConstraintsV1, ()> for Compositor {
4745    fn request(
4746        _: &mut Self,
4747        _: &Client,
4748        _: &ZwpPointerConstraintsV1,
4749        request: <ZwpPointerConstraintsV1 as Resource>::Request,
4750        _: &(),
4751        _: &DisplayHandle,
4752        data_init: &mut DataInit<'_, Self>,
4753    ) {
4754        use zwp_pointer_constraints_v1::Request;
4755        match request {
4756            Request::LockPointer {
4757                id,
4758                surface: _,
4759                pointer: _,
4760                region: _,
4761                lifetime: _,
4762            } => {
4763                let lp = data_init.init(id, ());
4764                // Immediately grant the lock (headless — no physical pointer to contest).
4765                lp.locked();
4766            }
4767            Request::ConfinePointer {
4768                id,
4769                surface: _,
4770                pointer: _,
4771                region: _,
4772                lifetime: _,
4773            } => {
4774                let cp = data_init.init(id, ());
4775                cp.confined();
4776            }
4777            Request::Destroy => {}
4778            _ => {}
4779        }
4780    }
4781}
4782
4783impl Dispatch<ZwpLockedPointerV1, ()> for Compositor {
4784    fn request(
4785        _: &mut Self,
4786        _: &Client,
4787        _: &ZwpLockedPointerV1,
4788        _: <ZwpLockedPointerV1 as Resource>::Request,
4789        _: &(),
4790        _: &DisplayHandle,
4791        _: &mut DataInit<'_, Self>,
4792    ) {
4793        // SetCursorPositionHint, SetRegion, Destroy — no-ops for headless.
4794    }
4795}
4796
4797impl Dispatch<ZwpConfinedPointerV1, ()> for Compositor {
4798    fn request(
4799        _: &mut Self,
4800        _: &Client,
4801        _: &ZwpConfinedPointerV1,
4802        _: <ZwpConfinedPointerV1 as Resource>::Request,
4803        _: &(),
4804        _: &DisplayHandle,
4805        _: &mut DataInit<'_, Self>,
4806    ) {
4807        // SetRegion, Destroy — no-ops for headless.
4808    }
4809}
4810
4811// -- zwp_relative_pointer_manager_v1 --
4812
4813impl GlobalDispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
4814    fn bind(
4815        _: &mut Self,
4816        _: &DisplayHandle,
4817        _: &Client,
4818        resource: New<ZwpRelativePointerManagerV1>,
4819        _: &(),
4820        data_init: &mut DataInit<'_, Self>,
4821    ) {
4822        data_init.init(resource, ());
4823    }
4824}
4825
4826impl Dispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
4827    fn request(
4828        state: &mut Self,
4829        _: &Client,
4830        _: &ZwpRelativePointerManagerV1,
4831        request: <ZwpRelativePointerManagerV1 as Resource>::Request,
4832        _: &(),
4833        _: &DisplayHandle,
4834        data_init: &mut DataInit<'_, Self>,
4835    ) {
4836        use zwp_relative_pointer_manager_v1::Request;
4837        match request {
4838            Request::GetRelativePointer { id, pointer: _ } => {
4839                let rp = data_init.init(id, ());
4840                state.relative_pointers.push(rp);
4841            }
4842            Request::Destroy => {}
4843            _ => {}
4844        }
4845    }
4846}
4847
4848impl Dispatch<ZwpRelativePointerV1, ()> for Compositor {
4849    fn request(
4850        state: &mut Self,
4851        _: &Client,
4852        resource: &ZwpRelativePointerV1,
4853        _: <ZwpRelativePointerV1 as Resource>::Request,
4854        _: &(),
4855        _: &DisplayHandle,
4856        _: &mut DataInit<'_, Self>,
4857    ) {
4858        // Only request is Destroy.
4859        state
4860            .relative_pointers
4861            .retain(|rp| rp.id() != resource.id());
4862    }
4863}
4864
4865// -- zwp_text_input_v3 --
4866
4867impl GlobalDispatch<ZwpTextInputManagerV3, ()> for Compositor {
4868    fn bind(
4869        _: &mut Self,
4870        _: &DisplayHandle,
4871        _: &Client,
4872        resource: New<ZwpTextInputManagerV3>,
4873        _: &(),
4874        data_init: &mut DataInit<'_, Self>,
4875    ) {
4876        data_init.init(resource, ());
4877    }
4878}
4879
4880impl Dispatch<ZwpTextInputManagerV3, ()> for Compositor {
4881    fn request(
4882        state: &mut Self,
4883        _: &Client,
4884        _: &ZwpTextInputManagerV3,
4885        request: <ZwpTextInputManagerV3 as Resource>::Request,
4886        _: &(),
4887        _: &DisplayHandle,
4888        data_init: &mut DataInit<'_, Self>,
4889    ) {
4890        use zwp_text_input_manager_v3::Request;
4891        match request {
4892            Request::GetTextInput { id, seat: _ } => {
4893                let ti = data_init.init(id, ());
4894                state.text_inputs.push(TextInputState {
4895                    resource: ti,
4896                    enabled: false,
4897                });
4898            }
4899            Request::Destroy => {}
4900            _ => {}
4901        }
4902    }
4903}
4904
4905impl Dispatch<ZwpTextInputV3, ()> for Compositor {
4906    fn request(
4907        state: &mut Self,
4908        _: &Client,
4909        resource: &ZwpTextInputV3,
4910        request: <ZwpTextInputV3 as Resource>::Request,
4911        _: &(),
4912        _: &DisplayHandle,
4913        _: &mut DataInit<'_, Self>,
4914    ) {
4915        use zwp_text_input_v3::Request;
4916        match request {
4917            Request::Enable => {
4918                if let Some(ti) = state
4919                    .text_inputs
4920                    .iter_mut()
4921                    .find(|t| t.resource.id() == resource.id())
4922                {
4923                    ti.enabled = true;
4924                }
4925            }
4926            Request::Disable => {
4927                if let Some(ti) = state
4928                    .text_inputs
4929                    .iter_mut()
4930                    .find(|t| t.resource.id() == resource.id())
4931                {
4932                    ti.enabled = false;
4933                }
4934            }
4935            Request::Commit => {
4936                // Client acknowledges our last done; nothing to do.
4937            }
4938            Request::Destroy => {
4939                state
4940                    .text_inputs
4941                    .retain(|t| t.resource.id() != resource.id());
4942            }
4943            // SetSurroundingText, SetTextChangeCause, SetContentType,
4944            // SetCursorRectangle — informational; ignored for now.
4945            _ => {}
4946        }
4947    }
4948}
4949
4950// -- xdg_activation_v1 --
4951
4952impl GlobalDispatch<XdgActivationV1, ()> for Compositor {
4953    fn bind(
4954        _: &mut Self,
4955        _: &DisplayHandle,
4956        _: &Client,
4957        resource: New<XdgActivationV1>,
4958        _: &(),
4959        data_init: &mut DataInit<'_, Self>,
4960    ) {
4961        data_init.init(resource, ());
4962    }
4963}
4964
4965impl Dispatch<XdgActivationV1, ()> for Compositor {
4966    fn request(
4967        state: &mut Self,
4968        _: &Client,
4969        _: &XdgActivationV1,
4970        request: <XdgActivationV1 as Resource>::Request,
4971        _: &(),
4972        _: &DisplayHandle,
4973        data_init: &mut DataInit<'_, Self>,
4974    ) {
4975        use xdg_activation_v1::Request;
4976        match request {
4977            Request::GetActivationToken { id } => {
4978                let serial = state.next_activation_token;
4979                state.next_activation_token = serial.wrapping_add(1);
4980                data_init.init(id, ActivationTokenData { serial });
4981            }
4982            Request::Activate {
4983                token: _,
4984                surface: _,
4985            } => {
4986                // In a headless compositor, activation requests are always
4987                // granted (focus is managed externally by the browser/CLI).
4988            }
4989            Request::Destroy => {}
4990            _ => {}
4991        }
4992    }
4993}
4994
4995impl Dispatch<XdgActivationTokenV1, ActivationTokenData> for Compositor {
4996    fn request(
4997        _: &mut Self,
4998        _: &Client,
4999        resource: &XdgActivationTokenV1,
5000        request: <XdgActivationTokenV1 as Resource>::Request,
5001        data: &ActivationTokenData,
5002        _: &DisplayHandle,
5003        _: &mut DataInit<'_, Self>,
5004    ) {
5005        use xdg_activation_token_v1::Request;
5006        match request {
5007            Request::Commit => {
5008                // Issue a token immediately — the headless compositor doesn't
5009                // need to validate app_id / surface / serial.
5010                resource.done(format!("blit-token-{}", data.serial));
5011            }
5012            Request::SetSerial { .. } | Request::SetAppId { .. } | Request::SetSurface { .. } => {}
5013            Request::Destroy => {}
5014            _ => {}
5015        }
5016    }
5017}
5018
5019// -- wp_cursor_shape_manager_v1 --
5020
5021impl GlobalDispatch<WpCursorShapeManagerV1, ()> for Compositor {
5022    fn bind(
5023        _: &mut Self,
5024        _: &DisplayHandle,
5025        _: &Client,
5026        resource: New<WpCursorShapeManagerV1>,
5027        _: &(),
5028        data_init: &mut DataInit<'_, Self>,
5029    ) {
5030        data_init.init(resource, ());
5031    }
5032}
5033
5034impl Dispatch<WpCursorShapeManagerV1, ()> for Compositor {
5035    fn request(
5036        _: &mut Self,
5037        _: &Client,
5038        _: &WpCursorShapeManagerV1,
5039        request: <WpCursorShapeManagerV1 as Resource>::Request,
5040        _: &(),
5041        _: &DisplayHandle,
5042        data_init: &mut DataInit<'_, Self>,
5043    ) {
5044        use wp_cursor_shape_manager_v1::Request;
5045        match request {
5046            Request::GetPointer {
5047                cursor_shape_device,
5048                pointer: _,
5049            } => {
5050                data_init.init(cursor_shape_device, ());
5051            }
5052            Request::GetTabletToolV2 {
5053                cursor_shape_device,
5054                tablet_tool: _,
5055            } => {
5056                data_init.init(cursor_shape_device, ());
5057            }
5058            Request::Destroy => {}
5059            _ => {}
5060        }
5061    }
5062}
5063
5064impl Dispatch<WpCursorShapeDeviceV1, ()> for Compositor {
5065    fn request(
5066        state: &mut Self,
5067        _: &Client,
5068        _: &WpCursorShapeDeviceV1,
5069        request: <WpCursorShapeDeviceV1 as Resource>::Request,
5070        _: &(),
5071        _: &DisplayHandle,
5072        _: &mut DataInit<'_, Self>,
5073    ) {
5074        use wp_cursor_shape_device_v1::Request;
5075        match request {
5076            Request::SetShape { serial: _, shape } => {
5077                use wayland_server::WEnum;
5078                use wp_cursor_shape_device_v1::Shape;
5079                let name = match shape {
5080                    WEnum::Value(Shape::Default) => "default",
5081                    WEnum::Value(Shape::ContextMenu) => "context-menu",
5082                    WEnum::Value(Shape::Help) => "help",
5083                    WEnum::Value(Shape::Pointer) => "pointer",
5084                    WEnum::Value(Shape::Progress) => "progress",
5085                    WEnum::Value(Shape::Wait) => "wait",
5086                    WEnum::Value(Shape::Cell) => "cell",
5087                    WEnum::Value(Shape::Crosshair) => "crosshair",
5088                    WEnum::Value(Shape::Text) => "text",
5089                    WEnum::Value(Shape::VerticalText) => "vertical-text",
5090                    WEnum::Value(Shape::Alias) => "alias",
5091                    WEnum::Value(Shape::Copy) => "copy",
5092                    WEnum::Value(Shape::Move) => "move",
5093                    WEnum::Value(Shape::NoDrop) => "no-drop",
5094                    WEnum::Value(Shape::NotAllowed) => "not-allowed",
5095                    WEnum::Value(Shape::Grab) => "grab",
5096                    WEnum::Value(Shape::Grabbing) => "grabbing",
5097                    WEnum::Value(Shape::EResize) => "e-resize",
5098                    WEnum::Value(Shape::NResize) => "n-resize",
5099                    WEnum::Value(Shape::NeResize) => "ne-resize",
5100                    WEnum::Value(Shape::NwResize) => "nw-resize",
5101                    WEnum::Value(Shape::SResize) => "s-resize",
5102                    WEnum::Value(Shape::SeResize) => "se-resize",
5103                    WEnum::Value(Shape::SwResize) => "sw-resize",
5104                    WEnum::Value(Shape::WResize) => "w-resize",
5105                    WEnum::Value(Shape::EwResize) => "ew-resize",
5106                    WEnum::Value(Shape::NsResize) => "ns-resize",
5107                    WEnum::Value(Shape::NeswResize) => "nesw-resize",
5108                    WEnum::Value(Shape::NwseResize) => "nwse-resize",
5109                    WEnum::Value(Shape::ColResize) => "col-resize",
5110                    WEnum::Value(Shape::RowResize) => "row-resize",
5111                    WEnum::Value(Shape::AllScroll) => "all-scroll",
5112                    WEnum::Value(Shape::ZoomIn) => "zoom-in",
5113                    WEnum::Value(Shape::ZoomOut) => "zoom-out",
5114                    _ => "default",
5115                };
5116                let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
5117                    surface_id: state.focused_surface_id,
5118                    cursor: CursorImage::Named(name.to_string()),
5119                });
5120                (state.event_notify)();
5121            }
5122            Request::Destroy => {}
5123            _ => {}
5124        }
5125    }
5126}
5127
5128// -- Client data --
5129impl wayland_server::backend::ClientData for ClientState {
5130    fn initialized(&self, _: wayland_server::backend::ClientId) {}
5131    fn disconnected(
5132        &self,
5133        _: wayland_server::backend::ClientId,
5134        _: wayland_server::backend::DisconnectReason,
5135    ) {
5136    }
5137}
5138
5139// ---------------------------------------------------------------------------
5140// Public API
5141// ---------------------------------------------------------------------------
5142
5143pub struct CompositorHandle {
5144    pub event_rx: mpsc::Receiver<CompositorEvent>,
5145    pub command_tx: mpsc::Sender<CompositorCommand>,
5146    pub socket_name: String,
5147    pub thread: std::thread::JoinHandle<()>,
5148    pub shutdown: Arc<AtomicBool>,
5149    /// Whether the compositor's Vulkan renderer supports Vulkan Video encode.
5150    pub vulkan_video_encode: bool,
5151    /// Whether the compositor's Vulkan renderer supports Vulkan Video AV1 encode.
5152    pub vulkan_video_encode_av1: bool,
5153    loop_signal: LoopSignal,
5154}
5155
5156impl CompositorHandle {
5157    pub fn wake(&self) {
5158        self.loop_signal.wakeup();
5159    }
5160}
5161
5162pub fn spawn_compositor(
5163    verbose: bool,
5164    event_notify: Arc<dyn Fn() + Send + Sync>,
5165    gpu_device: &str,
5166) -> CompositorHandle {
5167    let _gpu_device = gpu_device.to_string();
5168    let (event_tx, event_rx) = mpsc::channel();
5169    let (command_tx, command_rx) = mpsc::channel();
5170    let (socket_tx, socket_rx) = mpsc::sync_channel(1);
5171    let (signal_tx, signal_rx) = mpsc::sync_channel::<LoopSignal>(1);
5172    let (caps_tx, caps_rx) = mpsc::sync_channel::<(bool, bool)>(1);
5173    let shutdown = Arc::new(AtomicBool::new(false));
5174    let shutdown_clone = shutdown.clone();
5175
5176    let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")
5177        .map(std::path::PathBuf::from)
5178        .filter(|p| {
5179            let probe = p.join(".blit-probe");
5180            if std::fs::write(&probe, b"").is_ok() {
5181                let _ = std::fs::remove_file(&probe);
5182                true
5183            } else {
5184                false
5185            }
5186        })
5187        .unwrap_or_else(std::env::temp_dir);
5188
5189    let runtime_dir_clone = runtime_dir.clone();
5190    let thread = std::thread::Builder::new()
5191        .name("compositor".into())
5192        .spawn(move || {
5193            unsafe { std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir_clone) };
5194            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5195                run_compositor(
5196                    event_tx,
5197                    command_rx,
5198                    socket_tx,
5199                    signal_tx,
5200                    caps_tx,
5201                    event_notify,
5202                    shutdown_clone,
5203                    verbose,
5204                    _gpu_device,
5205                );
5206            }));
5207            if let Err(e) = result {
5208                let msg = if let Some(s) = e.downcast_ref::<&str>() {
5209                    s.to_string()
5210                } else if let Some(s) = e.downcast_ref::<String>() {
5211                    s.clone()
5212                } else {
5213                    "unknown panic".to_string()
5214                };
5215                eprintln!("[compositor] PANIC: {msg}");
5216            }
5217        })
5218        .expect("failed to spawn compositor thread");
5219
5220    let socket_name = socket_rx.recv().expect("compositor failed to start");
5221    let socket_name = runtime_dir
5222        .join(&socket_name)
5223        .to_string_lossy()
5224        .into_owned();
5225    let loop_signal = signal_rx
5226        .recv()
5227        .expect("compositor failed to send loop signal");
5228    let (vulkan_video_encode, vulkan_video_encode_av1) = caps_rx.recv().unwrap_or((false, false));
5229
5230    CompositorHandle {
5231        event_rx,
5232        command_tx,
5233        socket_name,
5234        thread,
5235        shutdown,
5236        vulkan_video_encode,
5237        vulkan_video_encode_av1,
5238        loop_signal,
5239    }
5240}
5241
5242#[allow(clippy::too_many_arguments)]
5243fn run_compositor(
5244    event_tx: mpsc::Sender<CompositorEvent>,
5245    command_rx: mpsc::Receiver<CompositorCommand>,
5246    socket_tx: mpsc::SyncSender<String>,
5247    signal_tx: mpsc::SyncSender<LoopSignal>,
5248    caps_tx: mpsc::SyncSender<(bool, bool)>,
5249    event_notify: Arc<dyn Fn() + Send + Sync>,
5250    shutdown: Arc<AtomicBool>,
5251    verbose: bool,
5252    gpu_device: String,
5253) {
5254    let mut event_loop: EventLoop<Compositor> =
5255        EventLoop::try_new().expect("failed to create event loop");
5256    let loop_signal = event_loop.get_signal();
5257
5258    let display: Display<Compositor> = Display::new().expect("failed to create display");
5259    let dh = display.handle();
5260
5261    // Probe Vulkan early so we know whether DMA-BUF is available
5262    // before registering Wayland globals.
5263    eprintln!("[compositor] trying Vulkan renderer for {gpu_device}");
5264    let vulkan_renderer = super::vulkan_render::VulkanRenderer::try_new(&gpu_device);
5265    let has_dmabuf = vulkan_renderer.as_ref().is_some_and(|vk| vk.has_dmabuf());
5266    eprintln!(
5267        "[compositor] Vulkan renderer: {} (dmabuf={})",
5268        vulkan_renderer.is_some(),
5269        has_dmabuf,
5270    );
5271
5272    // Create globals.
5273    dh.create_global::<Compositor, WlCompositor, ()>(6, ());
5274    dh.create_global::<Compositor, WlSubcompositor, ()>(1, ());
5275    dh.create_global::<Compositor, XdgWmBase, ()>(6, ());
5276    dh.create_global::<Compositor, WlShm, ()>(1, ());
5277    dh.create_global::<Compositor, WlOutput, ()>(4, ());
5278    dh.create_global::<Compositor, WlSeat, ()>(9, ());
5279    // Only advertise zwp_linux_dmabuf_v1 when the Vulkan device can
5280    // actually import DMA-BUFs.  Advertising the global with zero
5281    // formats confuses clients (Chrome, mpv) into not falling back to
5282    // wl_shm.
5283    if has_dmabuf {
5284        dh.create_global::<Compositor, ZwpLinuxDmabufV1, ()>(4, ());
5285    }
5286    dh.create_global::<Compositor, WpViewporter, ()>(1, ());
5287    dh.create_global::<Compositor, WpFractionalScaleManagerV1, ()>(1, ());
5288    dh.create_global::<Compositor, ZxdgDecorationManagerV1, ()>(1, ());
5289    dh.create_global::<Compositor, WlDataDeviceManager, ()>(3, ());
5290    dh.create_global::<Compositor, ZwpPointerConstraintsV1, ()>(1, ());
5291    dh.create_global::<Compositor, ZwpRelativePointerManagerV1, ()>(1, ());
5292    dh.create_global::<Compositor, XdgActivationV1, ()>(1, ());
5293    dh.create_global::<Compositor, WpCursorShapeManagerV1, ()>(1, ());
5294    dh.create_global::<Compositor, ZwpPrimarySelectionDeviceManagerV1, ()>(1, ());
5295    dh.create_global::<Compositor, WpPresentation, ()>(1, ());
5296    dh.create_global::<Compositor, ZwpTextInputManagerV3, ()>(1, ());
5297
5298    // XKB keymap.
5299    let keymap_string = include_str!("../data/us-qwerty.xkb");
5300    let mut keymap_data = keymap_string.as_bytes().to_vec();
5301    keymap_data.push(0); // null-terminate
5302
5303    // Listening socket.
5304    let listening_socket = wayland_server::ListeningSocket::bind_auto("wayland", 0..33)
5305        .unwrap_or_else(|e| {
5306            let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "(unset)".into());
5307            panic!("failed to create wayland socket in XDG_RUNTIME_DIR={dir}: {e}\nhint: ensure the directory exists and is writable by the current user");
5308        });
5309    let socket_name = listening_socket
5310        .socket_name()
5311        .unwrap()
5312        .to_string_lossy()
5313        .into_owned();
5314    socket_tx.send(socket_name).unwrap();
5315    let _ = signal_tx.send(loop_signal.clone());
5316
5317    let mut compositor = Compositor {
5318        display_handle: dh,
5319        surfaces: HashMap::new(),
5320        toplevel_surface_ids: HashMap::new(),
5321        next_surface_id: 1,
5322        shm_pools: HashMap::new(),
5323        surface_meta: HashMap::new(),
5324        dmabuf_params: HashMap::new(),
5325        vulkan_renderer,
5326        output_width: 1920,
5327        output_height: 1080,
5328        output_refresh_mhz: 60_000,
5329        output_scale_120: 120,
5330        outputs: Vec::new(),
5331        keyboards: Vec::new(),
5332        pointers: Vec::new(),
5333        keyboard_keymap_data: keymap_data,
5334        mods_depressed: 0,
5335        mods_locked: 0,
5336        serial: 0,
5337        event_tx,
5338        event_notify,
5339        loop_signal: loop_signal.clone(),
5340        pending_commits: HashMap::new(),
5341        focused_surface_id: 0,
5342        pointer_entered_id: None,
5343        pending_kb_reenter: false,
5344        gpu_device,
5345        verbose,
5346        shutdown: shutdown.clone(),
5347        last_reported_size: HashMap::new(),
5348        surface_sizes: HashMap::new(),
5349        positioners: HashMap::new(),
5350        fractional_scales: Vec::new(),
5351        data_devices: Vec::new(),
5352        selection_source: None,
5353        external_clipboard: None,
5354        primary_devices: Vec::new(),
5355        primary_source: None,
5356        external_primary: None,
5357        relative_pointers: Vec::new(),
5358        text_inputs: Vec::new(),
5359        text_input_serial: 0,
5360        next_activation_token: 1,
5361        popup_grab_stack: Vec::new(),
5362        held_buffers: HashMap::new(),
5363        cursor_rgba: HashMap::new(),
5364    };
5365
5366    // Report Vulkan Video encode capabilities to the server.
5367    {
5368        let (vve, vve_av1) = compositor
5369            .vulkan_renderer
5370            .as_ref()
5371            .map(|vk| (vk.has_video_encode(), vk.has_video_encode_av1()))
5372            .unwrap_or((false, false));
5373        let _ = caps_tx.send((vve, vve_av1));
5374    }
5375
5376    let handle = event_loop.handle();
5377
5378    // Insert display fd source.
5379    let display_source = Generic::new(display, Interest::READ, calloop::Mode::Level);
5380    handle
5381        .insert_source(display_source, |_, display, state| {
5382            let d = unsafe { display.get_mut() };
5383            if let Err(e) = d.dispatch_clients(state)
5384                && state.verbose
5385            {
5386                eprintln!("[compositor] dispatch_clients error: {e}");
5387            }
5388            state.cleanup_dead_surfaces();
5389            if let Err(e) = d.flush_clients()
5390                && state.verbose
5391            {
5392                eprintln!("[compositor] flush_clients error: {e}");
5393            }
5394            Ok(PostAction::Continue)
5395        })
5396        .expect("failed to insert display source");
5397
5398    // Insert listening socket.
5399    let socket_source = Generic::new(listening_socket, Interest::READ, calloop::Mode::Level);
5400    handle
5401        .insert_source(socket_source, |_, socket, state| {
5402            let ls = unsafe { socket.get_mut() };
5403            if let Some(client_stream) = ls.accept().ok().flatten()
5404                && let Err(e) = state
5405                    .display_handle
5406                    .insert_client(client_stream, Arc::new(ClientState))
5407                && state.verbose
5408            {
5409                eprintln!("[compositor] insert_client error: {e}");
5410            }
5411            Ok(PostAction::Continue)
5412        })
5413        .expect("failed to insert listening socket");
5414
5415    if verbose {
5416        eprintln!("[compositor] entering event loop");
5417    }
5418
5419    while !shutdown.load(Ordering::Relaxed) {
5420        // Process commands.
5421        while let Ok(cmd) = command_rx.try_recv() {
5422            match cmd {
5423                CompositorCommand::Shutdown => {
5424                    shutdown.store(true, Ordering::Relaxed);
5425                    return;
5426                }
5427                other => compositor.handle_command(other),
5428            }
5429        }
5430
5431        // Shorten the dispatch timeout when the Vulkan renderer has
5432        // in-flight GPU work so we poll for completion promptly.
5433        let poll_timeout = if compositor
5434            .vulkan_renderer
5435            .as_ref()
5436            .is_some_and(|vk| vk.has_pending())
5437        {
5438            std::time::Duration::from_millis(1)
5439        } else {
5440            std::time::Duration::from_secs(1)
5441        };
5442
5443        if let Err(e) = event_loop.dispatch(Some(poll_timeout), &mut compositor)
5444            && verbose
5445        {
5446            eprintln!("[compositor] event loop error: {e}");
5447        }
5448
5449        // Check for completed Vulkan GPU work.  This runs independently
5450        // of surface commits so completed frames are flushed to the
5451        // server without waiting for the next Wayland event.
5452        if let Some(ref mut vk) = compositor.vulkan_renderer
5453            && let Some((sid, w, h, pixels)) = vk.try_retire_pending()
5454        {
5455            let s120_u32 = (compositor.output_scale_120 as u32).max(120);
5456            let log_w = (w * 120).div_ceil(s120_u32);
5457            let log_h = (h * 120).div_ceil(s120_u32);
5458            compositor
5459                .pending_commits
5460                .insert(sid, (w, h, log_w, log_h, pixels));
5461        }
5462
5463        if !compositor.pending_commits.is_empty() {
5464            compositor.flush_pending_commits();
5465        }
5466
5467        if let Err(e) = compositor.display_handle.flush_clients()
5468            && verbose
5469        {
5470            eprintln!("[compositor] flush error: {e}");
5471        }
5472    }
5473
5474    if verbose {
5475        eprintln!("[compositor] event loop exited");
5476    }
5477}