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