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