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