Skip to main content

blit_compositor/
imp.rs

1use std::collections::HashMap;
2use std::os::fd::OwnedFd;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::mpsc;
6use std::time::Instant;
7
8use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufMappingMode};
9use smithay::backend::allocator::{Buffer, Fourcc, Modifier, Format as DmabufFormat};
10use smithay::backend::input::{Axis, ButtonState, KeyState};
11use smithay::delegate_compositor;
12use smithay::delegate_cursor_shape;
13use smithay::delegate_data_device;
14use smithay::delegate_dmabuf;
15use smithay::delegate_fractional_scale;
16use smithay::delegate_output;
17use smithay::delegate_text_input_manager;
18use smithay::delegate_primary_selection;
19use smithay::delegate_seat;
20use smithay::delegate_shm;
21use smithay::delegate_viewporter;
22use smithay::delegate_xdg_activation;
23use smithay::delegate_xdg_decoration;
24use smithay::delegate_xdg_shell;
25use smithay::delegate_xdg_toplevel_icon;
26use smithay::desktop::{Space, Window};
27use smithay::input::keyboard::FilterResult;
28use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent};
29use smithay::input::{Seat, SeatHandler, SeatState};
30use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
31use smithay::reexports::calloop::generic::Generic;
32use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, PostAction};
33use smithay::reexports::wayland_server::protocol::wl_buffer;
34use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
35use smithay::reexports::wayland_server::protocol::wl_shm;
36use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
37use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, Resource};
38use smithay::utils::{Serial, Transform, SERIAL_COUNTER};
39use smithay::wayland::buffer::BufferHandler;
40use smithay::wayland::compositor::{
41    self, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
42    with_states, with_surface_tree_downward, TraversalAction, get_parent,
43};
44use smithay::wayland::output::OutputHandler;
45use smithay::wayland::selection::data_device::{
46    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
47    set_data_device_focus, set_data_device_selection, request_data_device_client_selection,
48};
49use smithay::wayland::selection::{SelectionHandler, SelectionSource, SelectionTarget};
50use smithay::wayland::shell::xdg::{
51    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
52    XdgToplevelSurfaceData,
53};
54use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf};
55use smithay::wayland::drm_syncobj::{DrmSyncobjHandler, DrmSyncobjState, supports_syncobj_eventfd};
56use smithay::backend::drm::DrmDeviceFd;
57use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
58use smithay::wayland::shm::{BufferData, ShmHandler, ShmState, with_buffer_contents};
59use smithay::wayland::socket::ListeningSocketSource;
60use smithay::wayland::cursor_shape::CursorShapeManagerState;
61use smithay::wayland::tablet_manager::TabletSeatHandler;
62use smithay::wayland::fractional_scale::{FractionalScaleHandler, FractionalScaleManagerState};
63use smithay::wayland::selection::primary_selection::{PrimarySelectionHandler, PrimarySelectionState, set_primary_focus};
64use smithay::wayland::text_input::TextInputManagerState;
65use smithay::wayland::viewporter::ViewporterState;
66use smithay::wayland::xdg_activation::{
67    XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
68};
69use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconHandler;
70use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
71
72/// Pixel data in its native format, avoiding unnecessary colorspace conversions.
73///
74/// The inner buffers are wrapped in `Arc` so that cloning `PixelData` is O(1)
75/// (refcount bump) rather than a multi-megabyte memcpy.
76#[derive(Clone)]
77pub enum PixelData {
78    /// BGRA packed pixels (from Wayland SHM buffers).
79    /// Layout: [B, G, R, A] per pixel, row-major, no padding.
80    Bgra(Arc<Vec<u8>>),
81    /// RGBA packed pixels (legacy path / capture conversions).
82    /// Layout: [R, G, B, A] per pixel, row-major, no padding.
83    Rgba(Arc<Vec<u8>>),
84    /// NV12 planar: Y plane followed by interleaved UV plane.
85    /// `y_stride` and `uv_stride` may differ from width (DMA-BUF padding).
86    /// Stored contiguously: `data[..y_stride*height]` is Y,
87    /// `data[y_stride*height..]` is UV.
88    Nv12 {
89        data: Arc<Vec<u8>>,
90        y_stride: usize,
91        uv_stride: usize,
92    },
93    /// DMA-BUF file descriptor for zero-copy GPU encoding.
94    ///
95    /// The fd is dup'd from the Wayland buffer — the original wl_buffer has
96    /// been released, but this fd keeps the DMA-BUF kernel object alive.
97    /// The encoder imports this directly into GPU memory (VA-API VPP or
98    /// CUDA-EGL) without any CPU-side pixel copies.
99    DmaBuf {
100        fd: Arc<std::os::fd::OwnedFd>,
101        /// DRM fourcc (e.g. DRM_FORMAT_ARGB8888)
102        fourcc: u32,
103        /// DRM format modifier (e.g. DRM_FORMAT_MOD_LINEAR)
104        modifier: u64,
105        stride: u32,
106        offset: u32,
107    },
108}
109
110/// A single layer in a composited surface tree.
111#[derive(Clone)]
112pub struct PixelLayer {
113    pub x: i32,
114    pub y: i32,
115    pub width: u32,
116    pub height: u32,
117    pub pixels: PixelData,
118}
119
120/// DRM fourcc constants used in PixelData::DmaBuf.
121pub mod drm_fourcc {
122    /// DRM_FORMAT_ARGB8888 — [B,G,R,A] in memory on little-endian.
123    pub const ARGB8888: u32 = u32::from_le_bytes(*b"AR24");
124    /// DRM_FORMAT_XRGB8888 — [B,G,R,X] in memory on little-endian.
125    pub const XRGB8888: u32 = u32::from_le_bytes(*b"XR24");
126    /// DRM_FORMAT_ABGR8888 — [R,G,B,A] in memory on little-endian.
127    pub const ABGR8888: u32 = u32::from_le_bytes(*b"AB24");
128    /// DRM_FORMAT_XBGR8888 — [R,G,B,X] in memory on little-endian.
129    pub const XBGR8888: u32 = u32::from_le_bytes(*b"XB24");
130    /// DRM_FORMAT_NV12
131    pub const NV12: u32 = u32::from_le_bytes(*b"NV12");
132}
133
134impl PixelData {
135    /// Convert to RGBA for consumers that require it (screenshots, etc.).
136    /// Panics for DmaBuf variant — callers must handle that case separately.
137    pub fn to_rgba(&self, width: u32, height: u32) -> Vec<u8> {
138        let w = width as usize;
139        let h = height as usize;
140        match self {
141            PixelData::Rgba(data) => data.as_ref().clone(),
142            PixelData::Bgra(data) => {
143                let mut rgba = Vec::with_capacity(w * h * 4);
144                for px in data.chunks_exact(4) {
145                    rgba.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
146                }
147                rgba
148            }
149            PixelData::Nv12 {
150                data,
151                y_stride,
152                uv_stride,
153            } => {
154                let y_plane_size = *y_stride * h;
155                let uv_h = h.div_ceil(2);
156                let uv_plane_size = *uv_stride * uv_h;
157                if data.len() < y_plane_size + uv_plane_size {
158                    return Vec::new();
159                }
160                let y_plane = &data[..y_plane_size];
161                let uv_plane = &data[y_plane_size..];
162                let mut rgba = Vec::with_capacity(w * h * 4);
163                for row in 0..h {
164                    for col in 0..w {
165                        let y = y_plane[row * y_stride + col];
166                        let uv_idx = (row / 2) * uv_stride + (col / 2) * 2;
167                        if uv_idx + 1 >= uv_plane.len() {
168                            rgba.extend_from_slice(&[0, 0, 0, 255]);
169                            continue;
170                        }
171                        let u = uv_plane[uv_idx];
172                        let v = uv_plane[uv_idx + 1];
173                        let [r, g, b] = yuv420_to_rgb(y, u, v);
174                        rgba.extend_from_slice(&[r, g, b, 255]);
175                    }
176                }
177                rgba
178            }
179            PixelData::DmaBuf {
180                fd, fourcc, stride, ..
181            } => {
182                // Try CPU readback of DMA-BUF via sync + mmap.
183                use std::os::fd::AsRawFd;
184                let raw = fd.as_raw_fd();
185                let stride_usize = *stride as usize;
186                let map_size = stride_usize * h;
187                if map_size == 0 {
188                    return Vec::new();
189                }
190                // Sync to ensure GPU writes are visible to CPU.
191                let sync_start: u64 = 1 | 4; // DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ
192                unsafe { libc::ioctl(raw, 0x40086200u64 as _, &sync_start) };
193                let ptr = unsafe {
194                    libc::mmap(
195                        std::ptr::null_mut(),
196                        map_size,
197                        libc::PROT_READ,
198                        libc::MAP_SHARED,
199                        raw,
200                        0,
201                    )
202                };
203                if ptr == libc::MAP_FAILED {
204                    return Vec::new();
205                }
206                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
207                let row_bytes = w * 4;
208                let mut pixels = Vec::with_capacity(w * h * 4);
209                for row in 0..h {
210                    let start = row * stride_usize;
211                    if start + row_bytes <= slice.len() {
212                        pixels.extend_from_slice(&slice[start..start + row_bytes]);
213                    }
214                }
215                // Force alpha=255 for XRGB/XBGR formats.
216                if matches!(*fourcc, drm_fourcc::XRGB8888 | drm_fourcc::XBGR8888) {
217                    for px in pixels.chunks_exact_mut(4) {
218                        px[3] = 255;
219                    }
220                }
221                let sync_end: u64 = 2 | 4; // DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ
222                unsafe {
223                    libc::ioctl(raw, 0x40086200u64 as _, &sync_end);
224                    libc::munmap(ptr, map_size);
225                }
226                pixels
227            }
228        }
229    }
230
231    /// Return the pixel data as BGRA bytes (for compositing).
232    /// For DmaBuf, attempts mmap readback.  Returns empty on failure.
233    pub fn to_bgra_vec(&self, width: u32, height: u32) -> Vec<u8> {
234        match self {
235            PixelData::Bgra(v) => v.as_ref().to_vec(),
236            PixelData::Rgba(v) => {
237                // RGBA → BGRA swap
238                let mut bgra = v.as_ref().to_vec();
239                for px in bgra.chunks_exact_mut(4) {
240                    px.swap(0, 2);
241                }
242                bgra
243            }
244            PixelData::DmaBuf {
245                fd, stride, fourcc, ..
246            } => {
247                // Try mmap readback for linear DMA-BUFs.
248                use std::os::fd::AsRawFd;
249                let raw = fd.as_raw_fd();
250                let s = *stride as usize;
251                let h = height as usize;
252                let w = width as usize;
253                let map_size = s * h;
254                if map_size == 0 {
255                    return Vec::new();
256                }
257                let sync_start: u64 = 1 | 4;
258                unsafe { libc::ioctl(raw, 0x40086200u64 as _, &sync_start) };
259                let ptr = unsafe {
260                    libc::mmap(
261                        std::ptr::null_mut(),
262                        map_size,
263                        libc::PROT_READ,
264                        libc::MAP_SHARED,
265                        raw,
266                        0,
267                    )
268                };
269                if ptr == libc::MAP_FAILED {
270                    return Vec::new();
271                }
272                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
273                let row_bytes = w * 4;
274                let is_bgr = matches!(fourcc, 0x34325241 | 0x34325258);
275                let mut bgra = Vec::with_capacity(w * h * 4);
276                for row in 0..h {
277                    let src = &slice[row * s..row * s + row_bytes.min(slice.len() - row * s)];
278                    if is_bgr {
279                        bgra.extend_from_slice(src);
280                    } else {
281                        for px in src.chunks_exact(4) {
282                            bgra.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
283                        }
284                    }
285                }
286                let sync_end: u64 = 2 | 4;
287                unsafe {
288                    libc::ioctl(raw, 0x40086200u64 as _, &sync_end);
289                    libc::munmap(ptr, map_size);
290                }
291                bgra
292            }
293            _ => Vec::new(),
294        }
295    }
296
297    pub fn is_empty(&self) -> bool {
298        match self {
299            PixelData::Bgra(v) | PixelData::Rgba(v) => v.is_empty(),
300            PixelData::Nv12 { data, .. } => data.is_empty(),
301            PixelData::DmaBuf { .. } => false,
302        }
303    }
304
305    /// Returns true if this is a DMA-BUF reference (GPU-resident).
306    pub fn is_dmabuf(&self) -> bool {
307        matches!(self, PixelData::DmaBuf { .. })
308    }
309}
310
311pub enum CompositorEvent {
312    SurfaceCreated {
313        surface_id: u16,
314        title: String,
315        app_id: String,
316        parent_id: u16,
317        width: u16,
318        height: u16,
319    },
320    SurfaceDestroyed {
321        surface_id: u16,
322    },
323    SurfaceCommit {
324        surface_id: u16,
325        width: u32,
326        height: u32,
327        pixels: PixelData,
328    },
329    SurfaceTitle {
330        surface_id: u16,
331        title: String,
332    },
333    SurfaceAppId {
334        surface_id: u16,
335        app_id: String,
336    },
337    SurfaceResized {
338        surface_id: u16,
339        width: u16,
340        height: u16,
341    },
342    ClipboardContent {
343        surface_id: u16,
344        mime_type: String,
345        data: Vec<u8>,
346    },
347}
348
349pub enum CompositorCommand {
350    KeyInput {
351        surface_id: u16,
352        keycode: u32,
353        pressed: bool,
354    },
355    PointerMotion {
356        surface_id: u16,
357        x: f64,
358        y: f64,
359    },
360    PointerButton {
361        surface_id: u16,
362        button: u32,
363        pressed: bool,
364    },
365    PointerAxis {
366        surface_id: u16,
367        axis: u8,
368        value: f64,
369    },
370    SurfaceResize {
371        surface_id: u16,
372        width: u16,
373        height: u16,
374        /// DPR in 1/120th units (Wayland convention): 120 = 1×, 240 = 2×.  0 = unchanged.
375        scale_120: u16,
376    },
377    SurfaceFocus {
378        surface_id: u16,
379    },
380    SurfaceClose {
381        surface_id: u16,
382    },
383    ClipboardOffer {
384        surface_id: u16,
385        mime_type: String,
386        data: Vec<u8>,
387    },
388    Capture {
389        surface_id: u16,
390        reply: mpsc::SyncSender<Option<(u32, u32, Vec<u8>)>>,
391    },
392    /// Fire pending wl_surface.frame callbacks for a surface so the
393    /// client will paint and commit its next frame.  Send this when
394    /// the server is ready to consume a new frame (streaming or capture).
395    RequestFrame {
396        surface_id: u16,
397    },
398    /// Release the given keys (evdev keycodes) in the compositor's XKB
399    /// state.  Sent by the server when a transport client disconnects so
400    /// that stuck modifiers / runaway key-repeat don't persist.
401    ReleaseKeys {
402        keycodes: Vec<u32>,
403    },
404    Shutdown,
405}
406
407struct SurfaceInfo {
408    surface_id: u16,
409    window: Window,
410    last_width: u32,
411    last_height: u32,
412    last_title: String,
413    last_app_id: String,
414    /// Suppress repeated "commit with no readable buffer" warnings.
415    unreadable_warned: bool,
416}
417
418struct ClientData {
419    compositor_state: CompositorClientState,
420}
421
422impl smithay::reexports::wayland_server::backend::ClientData for ClientData {
423    fn initialized(&self, _client_id: smithay::reexports::wayland_server::backend::ClientId) {}
424    fn disconnected(
425        &self,
426        _client_id: smithay::reexports::wayland_server::backend::ClientId,
427        _reason: smithay::reexports::wayland_server::backend::DisconnectReason,
428    ) {
429    }
430}
431
432pub struct Compositor {
433    display_handle: DisplayHandle,
434    compositor_state: CompositorState,
435    xdg_shell_state: XdgShellState,
436    shm_state: ShmState,
437    seat_state: SeatState<Self>,
438    data_device_state: DataDeviceState,
439    #[allow(dead_code)]
440    viewporter_state: ViewporterState,
441    #[allow(dead_code)]
442    xdg_decoration_state: XdgDecorationState,
443    dmabuf_state: DmabufState,
444    #[allow(dead_code)]
445    dmabuf_global: DmabufGlobal,
446    syncobj_state: Option<DrmSyncobjState>,
447    primary_selection_state: PrimarySelectionState,
448    activation_state: XdgActivationState,
449    seat: Seat<Self>,
450    output: Output,
451    space: Space<Window>,
452
453    surfaces: HashMap<u64, SurfaceInfo>,
454    surface_lookup: HashMap<u16, u64>,
455    next_surface_id: u16,
456
457    event_tx: mpsc::Sender<CompositorEvent>,
458    event_notify: Arc<dyn Fn() + Send + Sync>,
459    loop_signal: LoopSignal,
460
461    verbose: bool,
462
463    /// The surface_id of the currently keyboard-focused surface (0 = none).
464    focused_surface_id: u16,
465
466    /// Buffered pixel data from commits within the current event-loop
467    /// dispatch.  Flushed after dispatch returns so that only the LAST
468    /// commit per surface per iteration is sent to the server.
469    pending_commits: HashMap<u16, (u32, u32, PixelData)>,
470
471    /// Per-wl_surface cache of the last successfully read pixel data.
472    /// Keyed by wl_surface protocol_id.  When a toplevel commits, its
473    /// subsurfaces may not have re-committed since the last read;
474    /// `BufferAssignment::NewBuffer` is only set on the subsurface's own
475    /// commit.  Without this cache, subsurface content silently disappears
476    /// from the composite whenever the parent commits independently.
477    surface_pixel_cache: HashMap<u64, (u32, u32, PixelData)>,
478
479    /// GPU renderer for compositing surfaces.  None if GPU unavailable.
480    gpu_renderer: Option<super::render::SurfaceRenderer>,
481}
482
483impl Compositor {
484    /// Send buffered SurfaceCommit events to the server.  Called after
485    /// event_loop.dispatch() returns so that multiple commits within a
486    /// single dispatch cycle are coalesced into one event per surface.
487    fn flush_pending_commits(&mut self) {
488        for (surface_id, (width, height, pixels)) in self.pending_commits.drain() {
489            let _ = self.event_tx.send(CompositorEvent::SurfaceCommit {
490                surface_id,
491                width,
492                height,
493                pixels,
494            });
495        }
496        (self.event_notify)();
497    }
498
499    fn allocate_surface_id(&mut self) -> u16 {
500        // Skip IDs already in use to prevent collisions after u16 wraparound
501        // (possible in long-running sessions with >65535 surface creates).
502        let mut id = self.next_surface_id;
503        let start = id;
504        loop {
505            if !self.surface_lookup.contains_key(&id) {
506                break;
507            }
508            id = id.wrapping_add(1);
509            if id == 0 {
510                id = 1;
511            }
512            if id == start {
513                // Exhausted all IDs — extremely unlikely (65535 concurrent surfaces).
514                break;
515            }
516        }
517        self.next_surface_id = id.wrapping_add(1);
518        if self.next_surface_id == 0 {
519            self.next_surface_id = 1;
520        }
521        id
522    }
523
524    fn handle_command(&mut self, cmd: CompositorCommand) {
525        match cmd {
526            CompositorCommand::KeyInput {
527                surface_id,
528                keycode,
529                pressed,
530            } => {
531                if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
532                    && let Some(info) = self.surfaces.get(&obj_id)
533                    && let Some(toplevel) = info.window.toplevel()
534                    && let Some(keyboard) = self.seat.get_keyboard()
535                {
536                    if self.verbose {
537                        eprintln!(
538                            "[compositor] key: sid={surface_id} evdev={keycode} pressed={pressed}"
539                        );
540                    }
541                    let serial = SERIAL_COUNTER.next_serial();
542                    let time = elapsed_ms();
543                    let state = if pressed {
544                        KeyState::Pressed
545                    } else {
546                        KeyState::Released
547                    };
548                    keyboard.set_focus(self, Some(toplevel.wl_surface().clone()), serial);
549                    // smithay expects XKB keycodes (evdev + 8).  The browser
550                    // sends raw evdev scancodes, so we add the offset here,
551                    // matching what smithay's libinput and winit backends do.
552                    keyboard.input::<(), _>(
553                        self,
554                        (keycode + 8).into(),
555                        state,
556                        serial,
557                        time,
558                        |_, _, _| FilterResult::Forward,
559                    );
560                }
561            }
562            CompositorCommand::PointerMotion { surface_id, x, y } => {
563                if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
564                    && let Some(info) = self.surfaces.get(&obj_id)
565                    && let Some(toplevel) = info.window.toplevel()
566                    && let Some(pointer) = self.seat.get_pointer()
567                {
568                    let serial = SERIAL_COUNTER.next_serial();
569                    let time = elapsed_ms();
570                    let wl_surface = toplevel.wl_surface().clone();
571                    // smithay's ClickGrab redirects all motion events to the
572                    // surface that received the original button-press, ignoring
573                    // the focus we pass here.  In blit every surface has its
574                    // own canvas, so a stale grab (e.g. mouseup lost when the
575                    // user switched surfaces) would permanently block input to
576                    // every other surface.  Clear the grab when the target
577                    // surface differs from the grabbed one.
578                    if pointer.is_grabbed() {
579                        let stale = pointer
580                            .grab_start_data()
581                            .and_then(|d| d.focus.as_ref().map(|(s, _)| s.id() != wl_surface.id()))
582                            .unwrap_or(false);
583                        if stale {
584                            pointer.unset_grab(self, serial, time);
585                        }
586                    }
587                    pointer.motion(
588                        self,
589                        Some((wl_surface, (0.0, 0.0).into())),
590                        &MotionEvent {
591                            location: (x, y).into(),
592                            serial,
593                            time,
594                        },
595                    );
596                    pointer.frame(self);
597                }
598            }
599            CompositorCommand::PointerButton {
600                surface_id,
601                button,
602                pressed,
603            } => {
604                if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
605                    && self.surfaces.contains_key(&obj_id)
606                    && let Some(pointer) = self.seat.get_pointer()
607                {
608                    let serial = SERIAL_COUNTER.next_serial();
609                    let state = if pressed {
610                        ButtonState::Pressed
611                    } else {
612                        ButtonState::Released
613                    };
614                    pointer.button(
615                        self,
616                        &ButtonEvent {
617                            button,
618                            state,
619                            serial,
620                            time: elapsed_ms(),
621                        },
622                    );
623                    pointer.frame(self);
624                }
625            }
626            CompositorCommand::PointerAxis {
627                surface_id: _,
628                axis,
629                value,
630            } => {
631                let Some(pointer) = self.seat.get_pointer() else {
632                    return;
633                };
634                let ax = if axis == 0 {
635                    Axis::Vertical
636                } else {
637                    Axis::Horizontal
638                };
639                pointer.axis(self, AxisFrame::new(elapsed_ms()).value(ax, value));
640                pointer.frame(self);
641            }
642            CompositorCommand::SurfaceResize {
643                surface_id: _,
644                width,
645                height,
646                scale_120,
647            } => {
648                // Update output scale if the client reported a DPR.
649                // scale_120 is already in Wayland fractional_scale units (1/120th).
650                let scale_frac = if scale_120 >= 120 {
651                    scale_120 as f64
652                } else {
653                    120.0
654                };
655                let cur = self.output.current_scale().fractional_scale();
656                if (cur - scale_frac).abs() > 0.01 {
657                    // Integer scale for wl_output: round to nearest.
658                    let int_scale = ((scale_frac / 120.0) + 0.5) as i32;
659                    self.output.change_current_state(
660                        None,
661                        None,
662                        Some(smithay::output::Scale::Custom {
663                            advertised_integer: int_scale.max(1),
664                            fractional: scale_frac,
665                        }),
666                        None,
667                    );
668                }
669
670                // width/height are in physical pixels.  Convert to logical
671                // pixels for the toplevel configure (Wayland uses logical).
672                let scale_f = scale_frac / 120.0;
673                let logical_w = ((width as f64) / scale_f).round() as i32;
674                let logical_h = ((height as f64) / scale_f).round() as i32;
675
676                // Update the output mode to match the physical size.
677                let mode = smithay::output::Mode {
678                    size: (width as i32, height as i32).into(),
679                    refresh: 60_000,
680                };
681                self.output
682                    .change_current_state(Some(mode), None, None, None);
683                self.output.set_preferred(mode);
684
685                // Configure all toplevel surfaces to fill the output.
686                for info in self.surfaces.values() {
687                    if let Some(toplevel) = info.window.toplevel() {
688                        toplevel.with_pending_state(|state| {
689                            state.size = Some((logical_w.max(1), logical_h.max(1)).into());
690                        });
691                        toplevel.send_pending_configure();
692                    }
693                }
694                // Refresh so surfaces receive the updated output scale via
695                // wl_surface.enter and wp_fractional_scale_v1.preferred_scale.
696                self.space.refresh();
697            }
698            CompositorCommand::SurfaceFocus { surface_id } => {
699                if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
700                    && let Some(info) = self.surfaces.get(&obj_id)
701                    && let Some(toplevel) = info.window.toplevel()
702                    && let Some(keyboard) = self.seat.get_keyboard()
703                {
704                    let serial = SERIAL_COUNTER.next_serial();
705                    keyboard.set_focus(self, Some(toplevel.wl_surface().clone()), serial);
706                }
707            }
708            CompositorCommand::SurfaceClose { surface_id } => {
709                if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
710                    && let Some(info) = self.surfaces.get(&obj_id)
711                    && let Some(toplevel) = info.window.toplevel()
712                {
713                    toplevel.send_close();
714                }
715            }
716            CompositorCommand::ClipboardOffer {
717                surface_id: _,
718                mime_type,
719                data,
720            } => {
721                // Inject the browser/remote clipboard as a compositor-owned
722                // selection so that focused Wayland clients can paste it.
723                let mime_types = if mime_type == "text/plain" {
724                    vec![
725                        "text/plain".to_string(),
726                        "text/plain;charset=utf-8".to_string(),
727                        "UTF8_STRING".to_string(),
728                        "TEXT".to_string(),
729                        "STRING".to_string(),
730                    ]
731                } else {
732                    vec![mime_type]
733                };
734                set_data_device_selection(
735                    &self.display_handle,
736                    &self.seat,
737                    mime_types,
738                    Arc::new(data),
739                );
740            }
741            CompositorCommand::Capture { surface_id, reply } => {
742                let result = if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
743                    && let Some(info) = self.surfaces.get(&obj_id)
744                    && let Some(toplevel) = info.window.toplevel()
745                {
746                    let wl_surface = toplevel.wl_surface().clone();
747                    self.read_surface_pixels(&wl_surface)
748                } else {
749                    None
750                };
751                let _ = reply.send(result);
752            }
753            CompositorCommand::RequestFrame { surface_id } => {
754                self.fire_frame_callbacks(surface_id);
755            }
756            CompositorCommand::ReleaseKeys { keycodes } => {
757                if let Some(keyboard) = self.seat.get_keyboard() {
758                    let serial = SERIAL_COUNTER.next_serial();
759                    let time = elapsed_ms();
760                    for keycode in keycodes {
761                        keyboard.input::<(), _>(
762                            self,
763                            (keycode + 8).into(),
764                            KeyState::Released,
765                            serial,
766                            time,
767                            |_, _, _| FilterResult::Forward,
768                        );
769                    }
770                }
771            }
772            CompositorCommand::Shutdown => {
773                self.loop_signal.stop();
774            }
775        }
776    }
777
778    /// Fire pending `wl_surface.frame` callbacks for a specific surface.
779    fn fire_frame_callbacks(&self, surface_id: u16) {
780        if let Some(&obj_id) = self.surface_lookup.get(&surface_id)
781            && let Some(info) = self.surfaces.get(&obj_id)
782            && let Some(toplevel) = info.window.toplevel()
783        {
784            let surface = toplevel.wl_surface().clone();
785            let time = elapsed_ms();
786            let mut fired = 0u32;
787            with_surface_tree_downward(
788                &surface,
789                (),
790                |_, _, &()| TraversalAction::DoChildren(()),
791                |_, states, &()| {
792                    for callback in states
793                        .cached_state
794                        .get::<SurfaceAttributes>()
795                        .current()
796                        .frame_callbacks
797                        .drain(..)
798                    {
799                        callback.done(time);
800                        fired += 1;
801                    }
802                },
803                |_, _, &()| true,
804            );
805            let _ = fired;
806        }
807    }
808
809    fn read_surface_pixels(&mut self, surface: &WlSurface) -> Option<(u32, u32, Vec<u8>)> {
810        let mut result: Option<(u32, u32, PixelData)> = None;
811        with_states(surface, |states| {
812            let mut guard = states.cached_state.get::<SurfaceAttributes>();
813            let attrs = guard.current();
814            if let Some(compositor::BufferAssignment::NewBuffer(buffer)) = attrs.buffer.as_ref() {
815                result = read_shm_buffer(buffer);
816                if result.is_none()
817                    && let Ok(dmabuf) = get_dmabuf(buffer)
818                {
819                    result = read_dmabuf_pixels(dmabuf);
820                }
821            }
822        });
823        // Convert to RGBA for the capture path (used only by dead-code Capture command).
824        result.map(|(w, h, pd)| (w, h, pd.to_rgba(w, h)))
825    }
826}
827
828/// Read an SHM buffer into `PixelData::Bgra`, forcing alpha=255 for
829/// XRGB8888 format (where the alpha byte is undefined and typically 0).
830fn read_shm_buffer(buffer: &wl_buffer::WlBuffer) -> Option<(u32, u32, PixelData)> {
831    let mut result = None;
832    let _ = with_buffer_contents(buffer, |ptr, len, data: BufferData| {
833        let width = data.width as u32;
834        let height = data.height as u32;
835        let stride = data.stride as usize;
836        let offset = data.offset as usize;
837        let pixel_data = unsafe { std::slice::from_raw_parts(ptr, len) };
838        let row_bytes = width as usize * 4;
839        let mut bgra = if stride == row_bytes
840            && offset == 0
841            && pixel_data.len() >= row_bytes * height as usize
842        {
843            pixel_data[..row_bytes * height as usize].to_vec()
844        } else {
845            let mut packed = Vec::with_capacity(row_bytes * height as usize);
846            for row in 0..height as usize {
847                let row_start = offset + row * stride;
848                let row_end = row_start + row_bytes;
849                if row_end <= pixel_data.len() {
850                    packed.extend_from_slice(&pixel_data[row_start..row_end]);
851                }
852            }
853            packed
854        };
855        // XRGB8888: the alpha byte is undefined (clients typically leave it
856        // as 0).  Force opaque so that captures and any alpha-aware path
857        // treat pixels correctly.
858        if data.format == wl_shm::Format::Xrgb8888 || data.format == wl_shm::Format::Xbgr8888 {
859            for px in bgra.chunks_exact_mut(4) {
860                px[3] = 255;
861            }
862        }
863        // ABGR/XBGR are [R,G,B,A] in memory — store as PixelData::Rgba.
864        if matches!(
865            data.format,
866            wl_shm::Format::Abgr8888 | wl_shm::Format::Xbgr8888
867        ) {
868            result = Some((width, height, PixelData::Rgba(Arc::new(bgra))));
869        } else {
870            result = Some((width, height, PixelData::Bgra(Arc::new(bgra))));
871        }
872    });
873    result
874}
875
876fn elapsed_ms() -> u32 {
877    use std::sync::OnceLock;
878    static START: OnceLock<Instant> = OnceLock::new();
879    START.get_or_init(Instant::now).elapsed().as_millis() as u32
880}
881
882impl CompositorHandler for Compositor {
883    fn compositor_state(&mut self) -> &mut CompositorState {
884        &mut self.compositor_state
885    }
886
887    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
888        &client.get_data::<ClientData>().unwrap().compositor_state
889    }
890
891    fn commit(&mut self, surface: &WlSurface) {
892        // Update RendererSurfaceState so Smithay's render element pipeline
893        // can produce elements from imported textures.
894        smithay::backend::renderer::utils::on_commit_buffer_handler::<Self>(surface);
895
896        // If this is a subsurface, walk up to the toplevel root.
897        let (root_surface, key, surface_id) = {
898            let key = surface.id().protocol_id() as u64;
899            if let Some(info) = self.surfaces.get(&key) {
900                (surface.clone(), key, info.surface_id)
901            } else {
902                // Not a toplevel — walk up to find the parent toplevel.
903                let mut current = surface.clone();
904                while let Some(parent) = get_parent(&current) {
905                    current = parent;
906                }
907                let parent_key = current.id().protocol_id() as u64;
908                match self.surfaces.get(&parent_key) {
909                    Some(info) => (current, parent_key, info.surface_id),
910                    None => return, // orphan subsurface
911                }
912            }
913        };
914
915        let mut committed_pixels: Option<(u32, u32, PixelData)> = None;
916        let mut new_title = String::new();
917        let mut new_app_id = String::new();
918
919        let have_pending = self.pending_commits.contains_key(&surface_id);
920        let warned_unreadable = self.surfaces.get(&key).is_none_or(|i| i.unreadable_warned);
921
922        if !have_pending {
923            // Use the xdg_toplevel geometry as the render region — this
924            // is the content rect EXCLUDING CSD shadows.  Clients like
925            // Chromium draw shadows as part of the surface buffer, but
926            // the geometry tells us exactly where the real window is.
927            let geometry = self.surfaces.get(&key).map(|i| i.window.geometry());
928
929            // Try GPU renderer (handles SHM + DMA-BUF + subsurfaces).
930            if let Some(renderer) = &mut self.gpu_renderer
931                && let Some((w, h, bgra)) =
932                    renderer.render_surface(&root_surface, geometry.as_ref())
933            {
934                committed_pixels = Some((w, h, PixelData::Bgra(Arc::new(bgra))));
935            }
936
937            // GPU unavailable or failed — fall back to manual SHM readback
938            // for the root surface only (no subsurface compositing).
939            if committed_pixels.is_none() {
940                with_states(&root_surface, |states| {
941                    let mut guard = states.cached_state.get::<SurfaceAttributes>();
942                    let attrs = guard.current();
943                    if let Some(compositor::BufferAssignment::NewBuffer(buffer)) =
944                        attrs.buffer.as_ref()
945                        && let Some((w, h, pd)) = read_shm_buffer(buffer)
946                    {
947                        committed_pixels = Some((w, h, pd));
948                    }
949                });
950            }
951
952            if committed_pixels.is_none() && !warned_unreadable {
953                eprintln!(
954                    "compositor: commit with no readable buffer — suppressing further warnings for this surface"
955                );
956            }
957        }
958
959        // Release all buffers in the tree so clients can reuse them.
960        with_surface_tree_downward(
961            &root_surface,
962            (),
963            |_, _, _| TraversalAction::DoChildren(()),
964            |_, states, _| {
965                let mut guard = states.cached_state.get::<SurfaceAttributes>();
966                let attrs = guard.current();
967                if let Some(compositor::BufferAssignment::NewBuffer(buffer)) = attrs.buffer.as_ref()
968                {
969                    buffer.release();
970                }
971            },
972            |_, _, _| true,
973        );
974
975        // Read title/app_id from the toplevel.
976        with_states(&root_surface, |states| {
977            if let Some(data) = states.data_map.get::<XdgToplevelSurfaceData>() {
978                let lock = data.lock().unwrap();
979                new_title = lock.title.clone().unwrap_or_default();
980                new_app_id = lock.app_id.clone().unwrap_or_default();
981            }
982        });
983
984        // Mark surface as warned if we hit an unreadable buffer.
985        if committed_pixels.is_none()
986            && !have_pending
987            && !warned_unreadable
988            && let Some(info) = self.surfaces.get_mut(&key)
989        {
990            info.unreadable_warned = true;
991        }
992
993        if let Some(info) = self.surfaces.get_mut(&key)
994            && new_title != info.last_title
995        {
996            info.last_title = new_title.clone();
997            let _ = self.event_tx.send(CompositorEvent::SurfaceTitle {
998                surface_id,
999                title: new_title,
1000            });
1001        }
1002
1003        if let Some(info) = self.surfaces.get_mut(&key)
1004            && new_app_id != info.last_app_id
1005        {
1006            info.last_app_id = new_app_id.clone();
1007            let _ = self.event_tx.send(CompositorEvent::SurfaceAppId {
1008                surface_id,
1009                app_id: new_app_id,
1010            });
1011        }
1012
1013        if let Some((width, height, pixels)) = committed_pixels {
1014            let info = self.surfaces.get_mut(&key).unwrap();
1015            if width != info.last_width || height != info.last_height {
1016                info.last_width = width;
1017                info.last_height = height;
1018                let _ = self.event_tx.send(CompositorEvent::SurfaceResized {
1019                    surface_id,
1020                    width: width as u16,
1021                    height: height as u16,
1022                });
1023            }
1024
1025            if !pixels.is_empty() {
1026                self.pending_commits
1027                    .insert(surface_id, (width, height, pixels));
1028            }
1029        }
1030
1031        // Frame callbacks are fired in flush_pending_commits() — once per
1032        // dispatch cycle — rather than on every commit to avoid hot loops.
1033    }
1034}
1035
1036impl BufferHandler for Compositor {
1037    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
1038}
1039
1040impl ShmHandler for Compositor {
1041    fn shm_state(&self) -> &ShmState {
1042        &self.shm_state
1043    }
1044}
1045
1046impl XdgShellHandler for Compositor {
1047    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
1048        &mut self.xdg_shell_state
1049    }
1050
1051    fn new_toplevel(&mut self, surface: ToplevelSurface) {
1052        if self.verbose {
1053            eprintln!("[compositor] new_toplevel");
1054        }
1055        let window = Window::new_wayland_window(surface.clone());
1056        let wl_surface = surface.wl_surface().clone();
1057        let key = wl_surface.id().protocol_id() as u64;
1058        let surface_id = self.allocate_surface_id();
1059
1060        self.space.map_element(window.clone(), (0, 0), false);
1061        self.space.refresh();
1062
1063        let info = SurfaceInfo {
1064            surface_id,
1065            window,
1066            last_width: 0,
1067            last_height: 0,
1068            last_title: String::new(),
1069            last_app_id: String::new(),
1070            unreadable_warned: false,
1071        };
1072        self.surfaces.insert(key, info);
1073        self.surface_lookup.insert(surface_id, key);
1074
1075        surface.with_pending_state(|state| {
1076            state.states.set(
1077                smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Activated,
1078            );
1079        });
1080        surface.send_configure();
1081
1082        let _ = self.event_tx.send(CompositorEvent::SurfaceCreated {
1083            surface_id,
1084            title: String::new(),
1085            app_id: String::new(),
1086            parent_id: 0,
1087            width: 0,
1088            height: 0,
1089        });
1090    }
1091
1092    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
1093        let wl_surface = surface.wl_surface();
1094        let key = wl_surface.id().protocol_id() as u64;
1095        self.surface_pixel_cache.remove(&key);
1096        if let Some(info) = self.surfaces.remove(&key) {
1097            self.surface_lookup.remove(&info.surface_id);
1098            self.space.unmap_elem(&info.window);
1099            let _ = self.event_tx.send(CompositorEvent::SurfaceDestroyed {
1100                surface_id: info.surface_id,
1101            });
1102        }
1103    }
1104
1105    fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {}
1106
1107    fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {}
1108
1109    fn reposition_request(
1110        &mut self,
1111        _surface: PopupSurface,
1112        _positioner: PositionerState,
1113        _token: u32,
1114    ) {
1115    }
1116}
1117
1118impl OutputHandler for Compositor {}
1119
1120impl SeatHandler for Compositor {
1121    type KeyboardFocus = WlSurface;
1122    type PointerFocus = WlSurface;
1123    type TouchFocus = WlSurface;
1124
1125    fn seat_state(&mut self) -> &mut SeatState<Self> {
1126        &mut self.seat_state
1127    }
1128
1129    fn cursor_image(
1130        &mut self,
1131        _seat: &Seat<Self>,
1132        _image: smithay::input::pointer::CursorImageStatus,
1133    ) {
1134    }
1135
1136    fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
1137        if let Some(surface) = focused {
1138            let key = surface.id().protocol_id() as u64;
1139            if let Some(info) = self.surfaces.get(&key) {
1140                self.focused_surface_id = info.surface_id;
1141            }
1142        }
1143        let client = focused.and_then(|s| self.display_handle.get_client(s.id()).ok());
1144        set_data_device_focus(&self.display_handle, seat, client.clone());
1145        set_primary_focus(&self.display_handle, seat, client);
1146    }
1147}
1148
1149impl SelectionHandler for Compositor {
1150    type SelectionUserData = Arc<Vec<u8>>;
1151
1152    fn new_selection(
1153        &mut self,
1154        ty: SelectionTarget,
1155        source: Option<SelectionSource>,
1156        seat: Seat<Self>,
1157    ) {
1158        if ty != SelectionTarget::Clipboard {
1159            return;
1160        }
1161        let Some(source) = source else { return };
1162        let mime_types = source.mime_types();
1163
1164        // Pick the best text mime type the source offers.
1165        let preferred = [
1166            "text/plain;charset=utf-8",
1167            "text/plain",
1168            "UTF8_STRING",
1169            "TEXT",
1170            "STRING",
1171        ];
1172        let Some(mime) = preferred
1173            .iter()
1174            .map(|m| m.to_string())
1175            .find(|m| mime_types.contains(m))
1176        else {
1177            return;
1178        };
1179
1180        // Create a socketpair: the write end goes to the Wayland client,
1181        // we read clipboard data from the read end on a helper thread.
1182        let (mut read_stream, write_stream) = match std::os::unix::net::UnixStream::pair() {
1183            Ok(pair) => pair,
1184            Err(_) => return,
1185        };
1186        let write_fd: OwnedFd = write_stream.into();
1187
1188        if request_data_device_client_selection::<Self>(&seat, mime.clone(), write_fd).is_err() {
1189            return;
1190        }
1191
1192        let event_tx = self.event_tx.clone();
1193        let event_notify = self.event_notify.clone();
1194        let surface_id = self.focused_surface_id;
1195        std::thread::Builder::new()
1196            .name("clipboard-read".into())
1197            .spawn(move || {
1198                use std::io::Read;
1199                const MAX_CLIPBOARD_SIZE: usize = 16 * 1024 * 1024; // 16 MiB
1200                let _ = read_stream.set_read_timeout(Some(std::time::Duration::from_secs(1)));
1201                let mut data = Vec::new();
1202                let mut buf = [0u8; 8192];
1203                loop {
1204                    match read_stream.read(&mut buf) {
1205                        Ok(0) => break,
1206                        Ok(n) => {
1207                            data.extend_from_slice(&buf[..n]);
1208                            if data.len() > MAX_CLIPBOARD_SIZE {
1209                                break;
1210                            }
1211                        }
1212                        Err(_) => break,
1213                    }
1214                }
1215                data.truncate(MAX_CLIPBOARD_SIZE);
1216                if !data.is_empty() {
1217                    let _ = event_tx.send(CompositorEvent::ClipboardContent {
1218                        surface_id,
1219                        mime_type: mime,
1220                        data,
1221                    });
1222                    (event_notify)();
1223                }
1224            })
1225            .expect("failed to spawn clipboard-read thread");
1226    }
1227
1228    fn send_selection(
1229        &mut self,
1230        _ty: SelectionTarget,
1231        _mime_type: String,
1232        fd: OwnedFd,
1233        _seat: Seat<Self>,
1234        user_data: &Self::SelectionUserData,
1235    ) {
1236        // Write the compositor-owned clipboard data to the requesting client's fd.
1237        let data = user_data.clone();
1238        std::thread::Builder::new()
1239            .name("clipboard-write".into())
1240            .spawn(move || {
1241                use std::io::Write;
1242                let mut file = std::fs::File::from(fd);
1243                let _ = file.write_all(&data);
1244            })
1245            .expect("failed to spawn clipboard-write thread");
1246    }
1247}
1248
1249impl DataDeviceHandler for Compositor {
1250    fn data_device_state(&self) -> &DataDeviceState {
1251        &self.data_device_state
1252    }
1253}
1254
1255impl ClientDndGrabHandler for Compositor {}
1256impl ServerDndGrabHandler for Compositor {}
1257
1258impl XdgDecorationHandler for Compositor {
1259    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
1260        toplevel.with_pending_state(|state| {
1261            state.decoration_mode = Some(DecorationMode::ServerSide);
1262        });
1263        toplevel.send_configure();
1264    }
1265
1266    fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
1267        toplevel.with_pending_state(|state| {
1268            state.decoration_mode = Some(DecorationMode::ServerSide);
1269        });
1270        toplevel.send_configure();
1271    }
1272
1273    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
1274        toplevel.with_pending_state(|state| {
1275            state.decoration_mode = Some(DecorationMode::ServerSide);
1276        });
1277        toplevel.send_configure();
1278    }
1279}
1280
1281impl DmabufHandler for Compositor {
1282    fn dmabuf_state(&mut self) -> &mut DmabufState {
1283        &mut self.dmabuf_state
1284    }
1285
1286    fn dmabuf_imported(
1287        &mut self,
1288        _global: &DmabufGlobal,
1289        _dmabuf: Dmabuf,
1290        notifier: ImportNotifier,
1291    ) {
1292        let _ = notifier.successful::<Compositor>();
1293    }
1294}
1295
1296impl DrmSyncobjHandler for Compositor {
1297    fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> {
1298        self.syncobj_state.as_mut()
1299    }
1300}
1301
1302fn with_dmabuf_plane_bytes<T>(
1303    dmabuf: &Dmabuf,
1304    plane_idx: usize,
1305    f: impl FnOnce(&[u8]) -> Option<T>,
1306) -> Option<T> {
1307    let _ = dmabuf.sync_plane(
1308        plane_idx,
1309        smithay::backend::allocator::dmabuf::DmabufSyncFlags::START
1310            | smithay::backend::allocator::dmabuf::DmabufSyncFlags::READ,
1311    );
1312    struct PlaneSyncGuard<'a> {
1313        dmabuf: &'a Dmabuf,
1314        plane_idx: usize,
1315    }
1316
1317    impl Drop for PlaneSyncGuard<'_> {
1318        fn drop(&mut self) {
1319            let _ = self.dmabuf.sync_plane(
1320                self.plane_idx,
1321                smithay::backend::allocator::dmabuf::DmabufSyncFlags::END
1322                    | smithay::backend::allocator::dmabuf::DmabufSyncFlags::READ,
1323            );
1324        }
1325    }
1326
1327    let _sync_guard = PlaneSyncGuard { dmabuf, plane_idx };
1328    let mapping = dmabuf.map_plane(plane_idx, DmabufMappingMode::READ).ok()?;
1329    let ptr = mapping.ptr() as *const u8;
1330    let len = mapping.length();
1331    let plane_data = unsafe { std::slice::from_raw_parts(ptr, len) };
1332    f(plane_data)
1333}
1334
1335fn yuv420_to_rgb(y: u8, u: u8, v: u8) -> [u8; 3] {
1336    let y = (y as i32 - 16).max(0);
1337    let u = u as i32 - 128;
1338    let v = v as i32 - 128;
1339
1340    let r = ((298 * y + 409 * v + 128) >> 8).clamp(0, 255) as u8;
1341    let g = ((298 * y - 100 * u - 208 * v + 128) >> 8).clamp(0, 255) as u8;
1342    let b = ((298 * y + 516 * u + 128) >> 8).clamp(0, 255) as u8;
1343
1344    [r, g, b]
1345}
1346
1347fn read_le_u16(bytes: &[u8], offset: usize) -> Option<u16> {
1348    let end = offset.checked_add(2)?;
1349    let raw = bytes.get(offset..end)?;
1350    Some(u16::from_le_bytes([raw[0], raw[1]]))
1351}
1352
1353/// Direct mmap readback of a packed BGRA/RGBA DMA-BUF fd.
1354/// Used for anonymous heap fds (Vulkan wsi) where Smithay's map_plane fails.
1355/// Must be called BEFORE wl_buffer.release() to avoid a race with the client.
1356#[allow(dead_code)]
1357fn read_packed_dmabuf_fd(
1358    raw_fd: std::os::fd::RawFd,
1359    stride: usize,
1360    width: usize,
1361    height: usize,
1362    format: Fourcc,
1363) -> Option<PixelData> {
1364    let file_size = unsafe { libc::lseek(raw_fd, 0, libc::SEEK_END) };
1365    if file_size <= 0 {
1366        return None;
1367    }
1368    let map_len = file_size as usize;
1369
1370    #[repr(C)]
1371    struct DmaBufSync {
1372        flags: u64,
1373    }
1374    const DMA_BUF_SYNC_READ: u64 = 1;
1375    const DMA_BUF_SYNC_START: u64 = 0;
1376    const DMA_BUF_SYNC_END: u64 = 4;
1377    const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
1378
1379    let sync_start = DmaBufSync {
1380        flags: DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ,
1381    };
1382    unsafe {
1383        libc::ioctl(raw_fd, DMA_BUF_IOCTL_SYNC as _, &sync_start);
1384    }
1385
1386    let ptr = unsafe {
1387        libc::mmap(
1388            std::ptr::null_mut(),
1389            map_len,
1390            libc::PROT_READ,
1391            libc::MAP_SHARED,
1392            raw_fd,
1393            0,
1394        )
1395    };
1396
1397    let result = if ptr == libc::MAP_FAILED {
1398        None
1399    } else {
1400        let plane_data = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_len) };
1401        let r = read_packed_dmabuf(plane_data, stride, width, height, false, format);
1402        unsafe {
1403            libc::munmap(ptr, map_len);
1404        }
1405        r
1406    };
1407
1408    let sync_end = DmaBufSync {
1409        flags: DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ,
1410    };
1411    unsafe {
1412        libc::ioctl(raw_fd, DMA_BUF_IOCTL_SYNC as _, &sync_end);
1413    }
1414
1415    result
1416}
1417
1418/// Read a packed 4-byte DMA-BUF into native pixel data.
1419///
1420/// ARGB8888 / XRGB8888 are BGRA in memory on little-endian — pass straight
1421/// through as `PixelData::Bgra` with zero per-pixel work.  ABGR8888 /
1422/// XBGR8888 are RGBA in memory — likewise zero swizzle.
1423fn read_packed_dmabuf(
1424    plane_data: &[u8],
1425    stride: usize,
1426    width: usize,
1427    height: usize,
1428    y_inverted: bool,
1429    format: Fourcc,
1430) -> Option<PixelData> {
1431    let row_bytes = width * 4;
1432    let total = row_bytes * height;
1433
1434    // Determine whether the raw bytes are BGRA or RGBA in memory, and
1435    // whether the alpha channel is real or must be forced to 0xFF.
1436    let (is_bgra, force_opaque) = match format {
1437        Fourcc::Argb8888 => (true, false),  // BGRA in memory, real alpha
1438        Fourcc::Xrgb8888 => (true, true),   // BGRX — alpha undefined
1439        Fourcc::Abgr8888 => (false, false), // RGBA in memory, real alpha
1440        Fourcc::Xbgr8888 => (false, true),  // RGBX — alpha undefined
1441        _ => return None,
1442    };
1443
1444    // Fast path: contiguous, not y-inverted, no padding.
1445    if !y_inverted && stride == row_bytes && plane_data.len() >= total {
1446        let mut buf = plane_data[..total].to_vec();
1447        if force_opaque {
1448            // Stamp alpha = 255 on every 4th byte.
1449            for px in buf.chunks_exact_mut(4) {
1450                px[3] = 255;
1451            }
1452        }
1453        return Some(if is_bgra {
1454            PixelData::Bgra(Arc::new(buf))
1455        } else {
1456            PixelData::Rgba(Arc::new(buf))
1457        });
1458    }
1459
1460    // Slow path: stride padding or y-inversion — pack rows.
1461    let mut buf = Vec::with_capacity(total);
1462    for row in 0..height {
1463        let src_row = if y_inverted { height - 1 - row } else { row };
1464        let row_start = src_row * stride;
1465        let row_end = row_start + row_bytes;
1466        if row_end > plane_data.len() {
1467            return None;
1468        }
1469        buf.extend_from_slice(&plane_data[row_start..row_end]);
1470    }
1471    if force_opaque {
1472        for px in buf.chunks_exact_mut(4) {
1473            px[3] = 255;
1474        }
1475    }
1476    Some(if is_bgra {
1477        PixelData::Bgra(Arc::new(buf))
1478    } else {
1479        PixelData::Rgba(Arc::new(buf))
1480    })
1481}
1482
1483#[cfg(test)]
1484fn read_nv12_dmabuf(
1485    y_plane: &[u8],
1486    y_stride: usize,
1487    uv_plane: &[u8],
1488    uv_stride: usize,
1489    width: usize,
1490    height: usize,
1491    y_inverted: bool,
1492) -> Option<Vec<u8>> {
1493    if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
1494        return None;
1495    }
1496
1497    let mut rgba = Vec::with_capacity(width * height * 4);
1498    for row in 0..height {
1499        let src_row = if y_inverted { height - 1 - row } else { row };
1500        let y_row_start = src_row * y_stride;
1501        let uv_row_start = (src_row / 2) * uv_stride;
1502        for col in 0..width {
1503            let y = y_plane[y_row_start + col];
1504            let uv_idx = uv_row_start + (col / 2) * 2;
1505            let u = uv_plane[uv_idx];
1506            let v = uv_plane[uv_idx + 1];
1507            let [r, g, b] = yuv420_to_rgb(y, u, v);
1508            rgba.extend_from_slice(&[r, g, b, 255]);
1509        }
1510    }
1511    Some(rgba)
1512}
1513
1514#[cfg(test)]
1515fn read_p010_dmabuf(
1516    y_plane: &[u8],
1517    y_stride: usize,
1518    uv_plane: &[u8],
1519    uv_stride: usize,
1520    width: usize,
1521    height: usize,
1522    y_inverted: bool,
1523) -> Option<Vec<u8>> {
1524    if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
1525        return None;
1526    }
1527
1528    let mut rgba = Vec::with_capacity(width * height * 4);
1529    for row in 0..height {
1530        let src_row = if y_inverted { height - 1 - row } else { row };
1531        let y_row_start = src_row * y_stride;
1532        let y_row_end = y_row_start + width * 2;
1533        if y_row_end > y_plane.len() {
1534            return None;
1535        }
1536
1537        let uv_row_start = (src_row / 2) * uv_stride;
1538        let uv_row_end = uv_row_start + width * 2;
1539        if uv_row_end > uv_plane.len() {
1540            return None;
1541        }
1542
1543        for col in 0..width {
1544            let y = (read_le_u16(y_plane, y_row_start + col * 2)? >> 8) as u8;
1545            let uv_idx = uv_row_start + (col / 2) * 4;
1546            let u = (read_le_u16(uv_plane, uv_idx)? >> 8) as u8;
1547            let v = (read_le_u16(uv_plane, uv_idx + 2)? >> 8) as u8;
1548            let [r, g, b] = yuv420_to_rgb(y, u, v);
1549            rgba.extend_from_slice(&[r, g, b, 255]);
1550        }
1551    }
1552
1553    Some(rgba)
1554}
1555
1556/// Copy NV12 planes into a contiguous buffer without colorspace conversion.
1557/// Returns (data, y_stride, uv_stride) where data = Y rows ++ UV rows.
1558fn read_nv12_dmabuf_passthrough(
1559    y_plane: &[u8],
1560    y_stride: usize,
1561    uv_plane: &[u8],
1562    uv_stride: usize,
1563    width: usize,
1564    height: usize,
1565    y_inverted: bool,
1566) -> Option<(Vec<u8>, usize, usize)> {
1567    if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
1568        return None;
1569    }
1570
1571    let uv_height = height / 2;
1572    // Use width as the output stride (pack rows tightly).
1573    let out_y_stride = width;
1574    // UV plane has interleaved U,V pairs: width bytes per row.
1575    let out_uv_stride = width;
1576    let mut data = vec![0u8; out_y_stride * height + out_uv_stride * uv_height];
1577
1578    // Copy Y plane
1579    for row in 0..height {
1580        let src_row = if y_inverted { height - 1 - row } else { row };
1581        let src_start = src_row * y_stride;
1582        let src_end = src_start + width;
1583        if src_end > y_plane.len() {
1584            return None;
1585        }
1586        let dst_start = row * out_y_stride;
1587        data[dst_start..dst_start + width].copy_from_slice(&y_plane[src_start..src_end]);
1588    }
1589
1590    // Copy UV plane
1591    let uv_dst_offset = out_y_stride * height;
1592    for row in 0..uv_height {
1593        let src_row = if y_inverted { uv_height - 1 - row } else { row };
1594        let src_start = src_row * uv_stride;
1595        let src_end = src_start + width; // width bytes of interleaved UV
1596        if src_end > uv_plane.len() {
1597            return None;
1598        }
1599        let dst_start = uv_dst_offset + row * out_uv_stride;
1600        data[dst_start..dst_start + width].copy_from_slice(&uv_plane[src_start..src_end]);
1601    }
1602
1603    Some((data, out_y_stride, out_uv_stride))
1604}
1605
1606/// Convert P010 (10-bit) DMA-BUF to 8-bit NV12 without going through RGBA.
1607/// Returns (data, y_stride, uv_stride).
1608fn read_p010_to_nv12(
1609    y_plane: &[u8],
1610    y_stride: usize,
1611    uv_plane: &[u8],
1612    uv_stride: usize,
1613    width: usize,
1614    height: usize,
1615    y_inverted: bool,
1616) -> Option<(Vec<u8>, usize, usize)> {
1617    if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
1618        return None;
1619    }
1620
1621    let uv_height = height / 2;
1622    let out_y_stride = width;
1623    let out_uv_stride = width;
1624    let mut data = vec![0u8; out_y_stride * height + out_uv_stride * uv_height];
1625
1626    // Convert Y plane: P010 stores 16-bit LE values, take high 8 bits
1627    for row in 0..height {
1628        let src_row = if y_inverted { height - 1 - row } else { row };
1629        let dst_start = row * out_y_stride;
1630        for col in 0..width {
1631            let src_offset = src_row * y_stride + col * 2;
1632            let val = read_le_u16(y_plane, src_offset)?;
1633            data[dst_start + col] = (val >> 8) as u8;
1634        }
1635    }
1636
1637    // Convert UV plane: P010 stores 16-bit LE U,V pairs
1638    let uv_dst_offset = out_y_stride * height;
1639    for row in 0..uv_height {
1640        let src_row = if y_inverted { uv_height - 1 - row } else { row };
1641        let dst_start = uv_dst_offset + row * out_uv_stride;
1642        for col in 0..width / 2 {
1643            let src_offset = src_row * uv_stride + col * 4;
1644            let u = (read_le_u16(uv_plane, src_offset)? >> 8) as u8;
1645            let v = (read_le_u16(uv_plane, src_offset + 2)? >> 8) as u8;
1646            data[dst_start + col * 2] = u;
1647            data[dst_start + col * 2 + 1] = v;
1648        }
1649    }
1650
1651    Some((data, out_y_stride, out_uv_stride))
1652}
1653
1654/// Convert Smithay Fourcc (DrmFourcc enum) to raw DRM fourcc u32.
1655fn fourcc_to_drm(code: Fourcc) -> u32 {
1656    code as u32
1657}
1658
1659fn read_dmabuf_pixels(dmabuf: &Dmabuf) -> Option<(u32, u32, PixelData)> {
1660    let size = dmabuf.size();
1661    let width = size.w as u32;
1662    let height = size.h as u32;
1663    if width == 0 || height == 0 {
1664        return None;
1665    }
1666
1667    let format = dmabuf.format();
1668
1669    // --- Path selection ---
1670    //
1671    // For packed BGRA/RGBA formats (ARGB8888, XRGB8888, ABGR8888, XBGR8888)
1672    // we take the zero-copy path: dup the DMA-BUF fd and pass it through as
1673    // PixelData::DmaBuf.  The encoder imports it directly into a VASurface via
1674    // VA-API VPP (BGRA→NV12 on the GPU) without any CPU mmap.  This is safe
1675    // because the buffer release happens after we've already submitted the fd
1676    // to the encoder's VPP pipeline — the kernel DMA-BUF object stays alive
1677    // as long as the fd is open.
1678    //
1679    // For NV12/P010 (hardware video decoder output) we CPU-mmap immediately
1680    // in the commit handler, before releasing the buffer, because these
1681    // typically come from the VA-API decoder's own surfaces and are already
1682    // CPU-accessible (system RAM).  The CPU path avoids a redundant GPU round-trip.
1683    //
1684    // Non-Linear modifiers are fine for the GPU zero-copy path (VA-API imports
1685    // by DRM fd regardless of modifier).  For the CPU path we require Linear.
1686
1687    let y_inverted = dmabuf.y_inverted();
1688
1689    // For packed BGRA/RGBA DMA-BUFs, choose between two paths:
1690    //
1691    // 1. Zero-copy GPU path (PixelData::DmaBuf): dup the fd and let the
1692    //    encoder import it into VA-API via PRIME_2.  Only safe for DRM
1693    //    device-backed fds (from GBM/KMS).  The encoder detects anonymous fds
1694    //    and rejects them.
1695    //
1696    // 2. Immediate CPU mmap (PixelData::Bgra): mmap NOW, before wl_buffer
1697    //    release, so we capture the frame before mpv overwrites it.  Required
1698    //    for anonymous /dmabuf heap fds (Vulkan wsi) where the content is
1699    //    CPU-accessible but may be reused immediately after release.
1700    //
1701    // Zero-copy GPU path: dup the DMA-BUF fd and pass it through.
1702    // The server imports it via VA-API VPP (BGRA→NV12 on the GPU)
1703    // without any CPU mmap.  The dup'd fd keeps the kernel DMA-BUF
1704    // object alive even after wl_buffer.release().
1705    //
1706    // Previously this path did an eager CPU mmap+copy as a fallback,
1707    // but that consumed 87% of CPU at 60fps (1.9MB memcpy per frame).
1708    if !y_inverted
1709        && matches!(
1710            format.code,
1711            Fourcc::Argb8888 | Fourcc::Xrgb8888 | Fourcc::Abgr8888 | Fourcc::Xbgr8888
1712        )
1713        && let Some(borrowed_fd) = dmabuf.handles().next()
1714        && let Ok(owned) = borrowed_fd.try_clone_to_owned()
1715    {
1716        let stride = dmabuf.strides().next().unwrap_or(width * 4);
1717        let offset = dmabuf.offsets().next().unwrap_or(0);
1718        return Some((
1719            width,
1720            height,
1721            PixelData::DmaBuf {
1722                fd: Arc::new(owned),
1723                fourcc: fourcc_to_drm(format.code),
1724                modifier: format.modifier.into(),
1725                stride,
1726                offset,
1727            },
1728        ));
1729    }
1730
1731    // CPU readback path for NV12/P010 (and y_inverted BGRA as fallback).
1732    // Must happen before wl_buffer.release() since the client may reuse the
1733    // buffer immediately after release.
1734    let modifier_is_linear = matches!(format.modifier, Modifier::Linear);
1735    if !modifier_is_linear {
1736        return None;
1737    }
1738
1739    let width_usize = width as usize;
1740    let height_usize = height as usize;
1741    let pixel_data = match format.code {
1742        Fourcc::Argb8888 | Fourcc::Xrgb8888 | Fourcc::Abgr8888 | Fourcc::Xbgr8888 => {
1743            // Fallback CPU path for y_inverted buffers.
1744            let stride = dmabuf.strides().next().unwrap_or(width * 4) as usize;
1745            with_dmabuf_plane_bytes(dmabuf, 0, |plane_data| {
1746                read_packed_dmabuf(
1747                    plane_data,
1748                    stride,
1749                    width_usize,
1750                    height_usize,
1751                    y_inverted,
1752                    format.code,
1753                )
1754            })?
1755        }
1756        Fourcc::Nv12 => {
1757            // Pass NV12 straight through — no YUV->RGBA conversion.
1758            let mut strides = dmabuf.strides();
1759            let y_stride = strides.next().unwrap_or(width) as usize;
1760            let uv_stride = strides.next().unwrap_or(width) as usize;
1761            let nv12 = with_dmabuf_plane_bytes(dmabuf, 0, |y_plane_data| {
1762                with_dmabuf_plane_bytes(dmabuf, 1, |uv_plane_data| {
1763                    read_nv12_dmabuf_passthrough(
1764                        y_plane_data,
1765                        y_stride,
1766                        uv_plane_data,
1767                        uv_stride,
1768                        width_usize,
1769                        height_usize,
1770                        y_inverted,
1771                    )
1772                })
1773            })?;
1774            PixelData::Nv12 {
1775                data: Arc::new(nv12.0),
1776                y_stride: nv12.1,
1777                uv_stride: nv12.2,
1778            }
1779        }
1780        Fourcc::P010 => {
1781            // P010 requires conversion to 8-bit NV12 since our encoders don't
1782            // accept 10-bit input.  Convert to NV12 directly (not RGBA).
1783            let mut strides = dmabuf.strides();
1784            let y_stride = strides.next().unwrap_or(width * 2) as usize;
1785            let uv_stride = strides.next().unwrap_or(width * 2) as usize;
1786            let nv12 = with_dmabuf_plane_bytes(dmabuf, 0, |y_plane| {
1787                with_dmabuf_plane_bytes(dmabuf, 1, |uv_plane| {
1788                    read_p010_to_nv12(
1789                        y_plane,
1790                        y_stride,
1791                        uv_plane,
1792                        uv_stride,
1793                        width_usize,
1794                        height_usize,
1795                        y_inverted,
1796                    )
1797                })
1798            })?;
1799            PixelData::Nv12 {
1800                data: Arc::new(nv12.0),
1801                y_stride: nv12.1,
1802                uv_stride: nv12.2,
1803            }
1804        }
1805        _ => return None,
1806    };
1807
1808    Some((width, height, pixel_data))
1809}
1810
1811impl PrimarySelectionHandler for Compositor {
1812    fn primary_selection_state(&self) -> &PrimarySelectionState {
1813        &self.primary_selection_state
1814    }
1815}
1816
1817impl XdgActivationHandler for Compositor {
1818    fn activation_state(&mut self) -> &mut XdgActivationState {
1819        &mut self.activation_state
1820    }
1821
1822    fn request_activation(
1823        &mut self,
1824        _token: XdgActivationToken,
1825        _token_data: XdgActivationTokenData,
1826        _surface: WlSurface,
1827    ) {
1828    }
1829}
1830
1831impl FractionalScaleHandler for Compositor {
1832    fn new_fractional_scale(&mut self, _surface: WlSurface) {}
1833}
1834
1835impl XdgToplevelIconHandler for Compositor {}
1836impl TabletSeatHandler for Compositor {}
1837
1838delegate_compositor!(Compositor);
1839delegate_cursor_shape!(Compositor);
1840delegate_shm!(Compositor);
1841delegate_xdg_shell!(Compositor);
1842delegate_seat!(Compositor);
1843delegate_data_device!(Compositor);
1844delegate_primary_selection!(Compositor);
1845delegate_output!(Compositor);
1846delegate_dmabuf!(Compositor);
1847smithay::delegate_drm_syncobj!(Compositor);
1848delegate_fractional_scale!(Compositor);
1849delegate_viewporter!(Compositor);
1850delegate_xdg_activation!(Compositor);
1851delegate_xdg_decoration!(Compositor);
1852delegate_xdg_toplevel_icon!(Compositor);
1853delegate_text_input_manager!(Compositor);
1854
1855pub struct CompositorHandle {
1856    pub event_rx: mpsc::Receiver<CompositorEvent>,
1857    pub command_tx: mpsc::Sender<CompositorCommand>,
1858    pub socket_name: String,
1859    pub thread: std::thread::JoinHandle<()>,
1860    pub shutdown: Arc<AtomicBool>,
1861    loop_signal: LoopSignal,
1862}
1863
1864impl CompositorHandle {
1865    /// Wake the compositor event loop immediately so it processes
1866    /// pending commands without waiting for the idle timeout.
1867    pub fn wake(&self) {
1868        self.loop_signal.wakeup();
1869    }
1870}
1871
1872pub fn spawn_compositor(
1873    verbose: bool,
1874    event_notify: Arc<dyn Fn() + Send + Sync>,
1875    gpu_device: &str,
1876) -> CompositorHandle {
1877    let gpu_device = gpu_device.to_string();
1878    let (event_tx, event_rx) = mpsc::channel();
1879    let (command_tx, command_rx) = mpsc::channel();
1880    let (socket_tx, socket_rx) = mpsc::sync_channel(1);
1881    let (signal_tx, signal_rx) = mpsc::sync_channel::<LoopSignal>(1);
1882    let shutdown = Arc::new(AtomicBool::new(false));
1883    let shutdown_clone = shutdown.clone();
1884
1885    let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")
1886        .map(std::path::PathBuf::from)
1887        .filter(|p| {
1888            // Verify the directory is actually writable by the current user
1889            // before using it. A stale or inaccessible XDG_RUNTIME_DIR (e.g.
1890            // in containers, after su/sudo, or in CI) causes PermissionDenied
1891            // when smithay tries to bind the Wayland socket. A probe write is
1892            // more reliable than inspecting mode bits, which don't account for
1893            // the effective uid.
1894            let probe = p.join(".blit-probe");
1895            if std::fs::write(&probe, b"").is_ok() {
1896                let _ = std::fs::remove_file(&probe);
1897                true
1898            } else {
1899                false
1900            }
1901        })
1902        .unwrap_or_else(std::env::temp_dir);
1903
1904    let runtime_dir_clone = runtime_dir.clone();
1905    let thread = std::thread::Builder::new()
1906        .name("compositor".into())
1907        .spawn(move || {
1908            unsafe { std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir_clone) };
1909            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1910                run_compositor(
1911                    event_tx,
1912                    command_rx,
1913                    socket_tx,
1914                    signal_tx,
1915                    event_notify,
1916                    shutdown_clone,
1917                    verbose,
1918                    gpu_device,
1919                );
1920            }));
1921            if let Err(e) = result {
1922                let msg = if let Some(s) = e.downcast_ref::<&str>() {
1923                    s.to_string()
1924                } else if let Some(s) = e.downcast_ref::<String>() {
1925                    s.clone()
1926                } else {
1927                    "unknown panic".to_string()
1928                };
1929                eprintln!("[compositor] PANIC: {msg}");
1930            }
1931        })
1932        .expect("failed to spawn compositor thread");
1933
1934    let socket_name = socket_rx.recv().expect("compositor failed to start");
1935    let socket_name = runtime_dir
1936        .join(&socket_name)
1937        .to_string_lossy()
1938        .into_owned();
1939    let loop_signal = signal_rx
1940        .recv()
1941        .expect("compositor failed to send loop signal");
1942
1943    CompositorHandle {
1944        event_rx,
1945        command_tx,
1946        socket_name,
1947        thread,
1948        shutdown,
1949        loop_signal,
1950    }
1951}
1952
1953#[allow(clippy::too_many_arguments)]
1954fn run_compositor(
1955    event_tx: mpsc::Sender<CompositorEvent>,
1956    command_rx: mpsc::Receiver<CompositorCommand>,
1957    socket_tx: mpsc::SyncSender<String>,
1958    signal_tx: mpsc::SyncSender<LoopSignal>,
1959    event_notify: Arc<dyn Fn() + Send + Sync>,
1960    shutdown: Arc<AtomicBool>,
1961    verbose: bool,
1962    gpu_device: String,
1963) {
1964    let mut event_loop: EventLoop<Compositor> =
1965        EventLoop::try_new().expect("failed to create event loop");
1966    let display: Display<Compositor> = Display::new().expect("failed to create display");
1967    let dh = display.handle();
1968
1969    let compositor_state = CompositorState::new::<Compositor>(&dh);
1970    let xdg_shell_state = XdgShellState::new::<Compositor>(&dh);
1971    let shm_state = ShmState::new::<Compositor>(&dh, vec![]);
1972    let data_device_state = DataDeviceState::new::<Compositor>(&dh);
1973    let viewporter_state = ViewporterState::new::<Compositor>(&dh);
1974    let xdg_decoration_state = XdgDecorationState::new::<Compositor>(&dh);
1975    let primary_selection_state = PrimarySelectionState::new::<Compositor>(&dh);
1976    let activation_state = XdgActivationState::new::<Compositor>(&dh);
1977    FractionalScaleManagerState::new::<Compositor>(&dh);
1978    CursorShapeManagerState::new::<Compositor>(&dh);
1979    // Disabled: smithay 0.7 has a bug in ShmBufferUserData::remove_destruction_hook
1980    // (uses != instead of ==) that causes a protocol error when clients destroy icon
1981    // buffers, killing Chromium-based browsers.
1982    TextInputManagerState::new::<Compositor>(&dh);
1983
1984    let mut dmabuf_state = DmabufState::new();
1985    // DMA-BUF format advertisement.
1986    //
1987    // Advertise packed BGRA/RGBA and YUV formats with both Linear and Invalid
1988    // (implicit) modifiers.  Modifier::Invalid tells clients they may use any
1989    // modifier (driver-chosen), which is how Vulkan/EGL allocates on AMD/radv.
1990    // The VA-API VPP zero-copy path imports buffers by DRM fd regardless of
1991    // modifier, so tiled GPU allocations work fine.
1992    // NV12/P010 use the CPU readback path and must be Linear.
1993    // Only advertise Linear modifier.  This forces clients to allocate
1994    // CPU-mappable buffers, ensuring the mmap readback path and VA-API
1995    // PRIME import both work.  Clients still render on the GPU — they
1996    // just use a linear memory layout instead of GPU-tiled.
1997    // Advertise packed BGRA/RGBA and YUV formats with both Linear and
1998    // Invalid (implicit) modifiers.  Invalid tells clients they may use
1999    // any modifier (driver-chosen tiling), which is how GPU compositors
2000    // like Chromium allocate surfaces.  The VA-API VPP path imports
2001    // buffers by DRM fd regardless of modifier.
2002    let dmabuf_formats = {
2003        use Fourcc::*;
2004        let packed = [Argb8888, Xrgb8888, Abgr8888, Xbgr8888];
2005        let yuv = [Fourcc::Nv12, Fourcc::P010];
2006        let mut fmts = Vec::new();
2007        for code in packed {
2008            fmts.push(DmabufFormat {
2009                code,
2010                modifier: Modifier::Linear,
2011            });
2012            fmts.push(DmabufFormat {
2013                code,
2014                modifier: Modifier::Invalid,
2015            });
2016        }
2017        for code in yuv {
2018            fmts.push(DmabufFormat {
2019                code,
2020                modifier: Modifier::Linear,
2021            });
2022        }
2023        fmts
2024    };
2025    // Build DMA-BUF feedback with the render node's dev_t so clients
2026    // allocate GEM-backed DMA-BUFs on the right GPU (instead of anonymous
2027    // /dmabuf: heap fds that EGL can't import).
2028    let dmabuf_global = match std::fs::metadata(&gpu_device).ok().and_then(|m| {
2029        use std::os::unix::fs::MetadataExt;
2030        let dev = m.rdev();
2031        let formats = dmabuf_formats.clone();
2032        let feedback = DmabufFeedbackBuilder::new(dev, formats).build().ok()?;
2033        Some(dmabuf_state.create_global_with_default_feedback::<Compositor>(&dh, &feedback))
2034    }) {
2035        Some(global) => {
2036            if verbose {
2037                eprintln!("[compositor] DMA-BUF feedback enabled ({gpu_device})");
2038            }
2039            global
2040        }
2041        None => {
2042            if verbose {
2043                eprintln!("[compositor] DMA-BUF feedback unavailable, using basic global");
2044            }
2045            dmabuf_state.create_global::<Compositor>(&dh, dmabuf_formats)
2046        }
2047    };
2048
2049    let mut seat_state = SeatState::new();
2050    let mut seat = seat_state.new_wl_seat(&dh, "headless");
2051    let keymap = include_str!("../data/us-qwerty.xkb").to_string();
2052    seat.add_keyboard_from_keymap_string(keymap, 200, 25)
2053        .expect("failed to add keyboard from embedded keymap");
2054    seat.add_pointer();
2055
2056    let output = Output::new(
2057        "headless-0".to_string(),
2058        PhysicalProperties {
2059            size: (0, 0).into(),
2060            subpixel: Subpixel::Unknown,
2061            make: "Virtual".into(),
2062            model: "Headless".into(),
2063        },
2064    );
2065    let mode = Mode {
2066        size: (1920, 1080).into(),
2067        refresh: 60_000,
2068    };
2069    output.create_global::<Compositor>(&dh);
2070    output.change_current_state(
2071        Some(mode),
2072        Some(Transform::Normal),
2073        Some(smithay::output::Scale::Integer(1)),
2074        Some((0, 0).into()),
2075    );
2076    output.set_preferred(mode);
2077
2078    let mut space = Space::default();
2079    space.map_output(&output, (0, 0));
2080
2081    let listening_socket = ListeningSocketSource::new_auto().unwrap_or_else(|e| {
2082        let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "(unset)".into());
2083        panic!(
2084            "failed to create wayland socket in XDG_RUNTIME_DIR={dir}: {e}\n\
2085             hint: ensure the directory exists and is writable by the current user"
2086        );
2087    });
2088    let socket_name = listening_socket
2089        .socket_name()
2090        .to_string_lossy()
2091        .into_owned();
2092    socket_tx.send(socket_name).unwrap();
2093
2094    let handle = event_loop.handle();
2095
2096    handle
2097        .insert_source(listening_socket, |client_stream, _, state| {
2098            if let Err(e) = state.display_handle.insert_client(
2099                client_stream,
2100                Arc::new(ClientData {
2101                    compositor_state: CompositorClientState::default(),
2102                }),
2103            ) && verbose
2104            {
2105                eprintln!("[compositor] insert_client error: {e}");
2106            }
2107        })
2108        .expect("failed to insert listening socket");
2109
2110    let loop_signal = event_loop.get_signal();
2111
2112    let mut compositor = Compositor {
2113        display_handle: dh.clone(),
2114        compositor_state,
2115        xdg_shell_state,
2116        shm_state,
2117        seat_state,
2118        data_device_state,
2119        viewporter_state,
2120        xdg_decoration_state,
2121        dmabuf_state,
2122        dmabuf_global,
2123        syncobj_state: {
2124            // Enable explicit sync (linux-drm-syncobj-v1) if the GPU supports it.
2125            // This replaces implicit DMA-BUF fencing which hangs on AMD.
2126
2127            std::fs::OpenOptions::new()
2128                .read(true)
2129                .write(true)
2130                .open(&gpu_device)
2131                .ok()
2132                .and_then(|f| {
2133                    use std::os::fd::OwnedFd;
2134                    let owned: OwnedFd = f.into();
2135                    let dev_fd = smithay::utils::DeviceFd::from(owned);
2136                    let drm_fd = DrmDeviceFd::new(dev_fd);
2137                    if supports_syncobj_eventfd(&drm_fd) {
2138                        let state = DrmSyncobjState::new::<Compositor>(&dh, drm_fd);
2139                        eprintln!("[compositor] explicit sync (drm-syncobj) enabled");
2140                        Some(state)
2141                    } else {
2142                        eprintln!("[compositor] explicit sync not supported by GPU");
2143                        None
2144                    }
2145                })
2146        },
2147        primary_selection_state,
2148        activation_state,
2149        seat,
2150        output,
2151        space,
2152        surfaces: HashMap::new(),
2153        surface_lookup: HashMap::new(),
2154        next_surface_id: 1,
2155        event_tx,
2156        event_notify,
2157        loop_signal: loop_signal.clone(),
2158        verbose,
2159        focused_surface_id: 0,
2160        pending_commits: HashMap::new(),
2161        surface_pixel_cache: HashMap::new(),
2162        gpu_renderer: super::render::SurfaceRenderer::try_new(&gpu_device),
2163    };
2164
2165    // Send the loop signal back so the server can wake us.
2166    let _ = signal_tx.send(loop_signal.clone());
2167
2168    let display_source = Generic::new(display, Interest::READ, calloop::Mode::Level);
2169    handle
2170        .insert_source(display_source, |_, display, state| {
2171            let d = unsafe { display.get_mut() };
2172            if let Err(e) = d.dispatch_clients(state)
2173                && verbose
2174            {
2175                eprintln!("[compositor] dispatch_clients error: {e}");
2176            }
2177            if let Err(e) = d.flush_clients()
2178                && verbose
2179            {
2180                eprintln!("[compositor] flush_clients error: {e}");
2181            }
2182            Ok(PostAction::Continue)
2183        })
2184        .expect("failed to insert display source");
2185
2186    if verbose {
2187        eprintln!("[compositor] entering event loop");
2188    }
2189    while !shutdown.load(Ordering::Relaxed) {
2190        while let Ok(cmd) = command_rx.try_recv() {
2191            match cmd {
2192                CompositorCommand::Shutdown => {
2193                    shutdown.store(true, Ordering::Relaxed);
2194                    return;
2195                }
2196                other => compositor.handle_command(other),
2197            }
2198        }
2199
2200        // No rate limit — the loop wakes instantly on Wayland client
2201        // traffic (fd readable) or server commands (loop_signal.wakeup()).
2202        // The 1s ceiling is only a liveness fallback for shutdown polling.
2203        if let Err(e) =
2204            event_loop.dispatch(Some(std::time::Duration::from_secs(1)), &mut compositor)
2205            && verbose
2206        {
2207            eprintln!("[compositor] event loop error: {e}");
2208        }
2209
2210        // Flush coalesced commits — only the latest per surface is sent.
2211        if !compositor.pending_commits.is_empty() {
2212            compositor.flush_pending_commits();
2213        }
2214
2215        if let Err(e) = compositor.display_handle.flush_clients()
2216            && verbose
2217        {
2218            eprintln!("[compositor] flush error: {e}");
2219        }
2220    }
2221    if verbose {
2222        eprintln!("[compositor] event loop exited");
2223    }
2224}
2225
2226#[cfg(test)]
2227mod tests {
2228    use super::{Fourcc, read_nv12_dmabuf, read_p010_dmabuf, read_packed_dmabuf};
2229
2230    /// Test-only wrapper that returns RGBA like the old `read_packed_rgba_dmabuf`.
2231    fn read_packed_rgba_dmabuf(
2232        plane_data: &[u8],
2233        stride: usize,
2234        width: usize,
2235        height: usize,
2236        y_inverted: bool,
2237        format: Fourcc,
2238    ) -> Option<Vec<u8>> {
2239        let pd = read_packed_dmabuf(plane_data, stride, width, height, y_inverted, format)?;
2240        Some(pd.to_rgba(width as u32, height as u32))
2241    }
2242
2243    #[test]
2244    fn xrgb_dmabuf_forces_opaque_alpha() {
2245        let pixels = [
2246            0x10, 0x20, 0x30, 0x00, //
2247            0x40, 0x50, 0x60, 0x7f,
2248        ];
2249
2250        let rgba = read_packed_rgba_dmabuf(&pixels, 8, 2, 1, false, Fourcc::Xrgb8888).unwrap();
2251
2252        assert_eq!(rgba, vec![0x30, 0x20, 0x10, 0xff, 0x60, 0x50, 0x40, 0xff]);
2253    }
2254
2255    #[test]
2256    fn nv12_black_decodes_to_opaque_black() {
2257        let y_plane = [16, 16, 16, 16];
2258        let uv_plane = [128, 128];
2259
2260        let rgba = read_nv12_dmabuf(&y_plane, 2, &uv_plane, 2, 2, 2, false).unwrap();
2261
2262        assert_eq!(rgba, [0, 0, 0, 255].repeat(4));
2263    }
2264
2265    #[test]
2266    fn p010_white_decodes_to_opaque_white() {
2267        let y_plane = [0x00, 0xeb, 0x00, 0xeb, 0x00, 0xeb, 0x00, 0xeb];
2268        let uv_plane = [0x00, 0x80, 0x00, 0x80];
2269
2270        let rgba = read_p010_dmabuf(&y_plane, 4, &uv_plane, 4, 2, 2, false).unwrap();
2271
2272        assert_eq!(rgba, [255, 255, 255, 255].repeat(4));
2273    }
2274}