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