use std::collections::HashMap;
use std::error::Error;
use std::os::fd::OwnedFd;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, io, process, ptr};

use _linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use indexmap::IndexSet;
use libc::dev_t as DeviceId;
#[cfg(feature = "profiling")]
use profiling::puffin::GlobalProfiler;
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBuffer, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::{Fourcc, Modifier as DrmModifier};
use smithay::backend::drm::exporter::gbm::{GbmFramebufferExporter, NodeFilter};
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmNode, DrmSurface};
use smithay::backend::egl::context::EGLContext;
use smithay::backend::egl::display::EGLDisplay;
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::element::RenderElementStates;
use smithay::backend::renderer::gles::{ffi, GlesRenderbuffer, GlesRenderer};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{
    self, utils, Bind, BufferType, Frame, ImportDma, Offscreen, Renderer,
};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{UdevBackend, UdevEvent};
use smithay::output::{Mode, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{
    EventLoop, Interest, LoopHandle, Mode as TriggerMode, PostAction, RegistrationToken,
};
use smithay::reexports::drm::control::connector::{
    Handle as ConnectorHandle, Info as ConnectorInfo, Interface, State as ConnectorState,
};
use smithay::reexports::drm::control::property::{
    Handle as PropertyHandle, Value as PropertyValue,
};
use smithay::reexports::drm::control::crtc;
use smithay::reexports::drm::control::{Device, Mode as DrmMode, ModeTypeFlags, ResourceHandles};
use smithay::reexports::input::Libinput;
use smithay::reexports::rustix::fs::OFlags;
use smithay::reexports::wayland_protocols::wp::linux_dmabuf as _linux_dmabuf;
use smithay::reexports::gbm;
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::DisplayHandle;
use smithay::utils::{DevPath, DeviceFd, Physical, Rectangle, Size};
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder};
use smithay::wayland::{dmabuf, shm};
use tracing::{debug, error, info, warn};

use crate::backend::Backend;
use crate::drawing::Graphics;
use crate::forge::Forge;
use crate::output::Output;
use calloop::channel::Sender;

use crate::kms::{KmsCmd, KmsHandle};
use crate::present::{Committer, Presenter};
use crate::protocols::export_dmabuf::ExportDmabufCapture;
use crate::protocols::image_copy_capture::ImageCopyCapture;
use crate::protocols::screencopy::frame::Screencopy;
use crate::text::TextRenderer;
use crate::trace_error;
use crate::windows::Windows;

const CLEAR_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
const DRM_RETRY_DELAY: Duration = Duration::from_millis(500);
const DRM_MAX_RETRIES: u32 = 10;

pub fn run(exec_override: Option<PathBuf>) {
    // Only set Panfrost workaround if a Mali/Panfrost GPU is detected.
    if is_panfrost_gpu() {
        unsafe { env::set_var("PAN_MESA_DEBUG", "noafbc") };
    }



    let mut event_loop = EventLoop::try_new().expect("event loop");
    let udev = Udev::new(event_loop.handle());
    let mut forge = Forge::new(event_loop.handle(), Backend::Udev(udev), exec_override);

    let backend = UdevBackend::new(&forge.seat_name).expect("init udev");

    for (device_id, path) in backend.device_list() {
        info!("Found DRM device: {:?} (id: {})", path, device_id);
        add_device(&mut forge, path.into());
    }

    // The wayland socket is already listening at this point. Spawn the
    // session command unconditionally — if libseat hasn't granted us DRM
    // master yet (e.g. we were forked on an inactive VT by loginmanager)
    // the device init queued itself on `pending_paths` and the wayland
    // clients can still connect, build their first frame, and signal
    // readiness while we wait for `ActivateSession`.
    forge.run_init_if_needed();

    if let Ok(dmabuf_feedback) = forge.backend.udev().default_dmabuf_feedback(&backend) {
        forge
            .dmabuf_state
            .create_global_with_default_feedback::<Forge>(&forge.display_handle, &dmabuf_feedback);
    }

    event_loop
        .handle()
        .insert_source(backend, move |event, _, forge| match event {
            UdevEvent::Added { path, .. } => add_device(forge, path),
            UdevEvent::Changed { device_id } => {
                // Hotplug (monitor connect/disconnect, EDID re-probe) needs DRM
                // master. If libseat has us paused — e.g. the user is on a text
                // console after Ctrl-Alt-Fn, or screen-locker is mid-resume —
                // every ioctl in `change_device` returns EACCES and we end up
                // with a half-rebuilt drm_compositor we can't recover from.
                // Defer the rescan until `ActivateSession` instead.
                if forge.backend.udev().session.is_active() {
                    trace_error!(forge.backend.udev().change_device(
                        &forge.display_handle,
                        &mut forge.windows,
                        device_id,
                    ));
                } else {
                    forge.backend.udev().pending_change.push(device_id);
                    warn!(
                        "udev change for {device_id} arrived while session paused; deferring to ActivateSession"
                    );
                }
            },
            UdevEvent::Removed { device_id } => forge.backend.udev().remove_device(device_id),
        })
        .expect("insert udev source");

    while !forge.terminated {
        if let Err(e) = event_loop.dispatch(None, &mut forge) {
            error!("Event loop error: {e}");
            break;
        }
        forge.display_handle.flush_clients().expect("flushing clients");
    }

    info!("Compositor shutting down");
}

fn add_device(forge: &mut Forge, path: PathBuf) {
    let session_active = forge.backend.udev().session.is_active();
    match forge.backend.udev().add_device(&forge.display_handle, &mut forge.windows, &path, DRM_MAX_RETRIES) {
        Ok(true) => {
            info!("Display device initialized: {:?}", path);
            forge.run_init_if_needed();
            forge.create_frame();
        },
        Ok(false) => {
            if session_active {
                debug!("Skipped render-only device: {:?}", path);
            } else {
                // Connector probe needs DRM master; queue for retry when the
                // session becomes active so we don't decide "no display" off a
                // probe that was destined to fail.
                debug!(
                    "Skipped {:?} while session inactive — queued for retry on session activate",
                    path
                );
                forge.backend.udev().pending_paths.push(path);
            }
        },
        Err(e) => {
            if session_active {
                warn!(
                    "Could not add device {:?} now: {} — queued for retry on session activate",
                    path, e
                );
            } else {
                debug!(
                    "Skipped {:?} (session inactive: {}) — queued for retry on session activate",
                    path, e
                );
            }
            forge.backend.udev().pending_paths.push(path);
        },
    }
}

pub struct Udev {
    scheduled_redraws: Vec<RegistrationToken>,
    event_loop: LoopHandle<'static, Forge>,
    pub output_device: Option<OutputDevice>,
    /// DRM device paths whose initial `add_device` failed — typically because
    /// the libseat session is not yet active and atomic modesetting requires
    /// master. Retried on every `SessionEvent::ActivateSession`.
    pending_paths: Vec<PathBuf>,
    /// `UdevEvent::Changed` device ids that arrived while the libseat session
    /// was inactive. `change_device` would have hit EACCES on the DRM ioctls,
    /// so we hold the id here and replay it on `ActivateSession`.
    pending_change: Vec<DeviceId>,
    /// `set_display_status` requests issued while paused. The value is the
    /// requested DPMS state (`true` = on); only the last one matters.
    pending_dpms: Option<bool>,
    session: LibSeatSession,
    /// Dedicated KMS-commit thread. Phase 2b.1: spawned but owns no outputs yet;
    /// commits still happen inline on the main thread.
    #[allow(dead_code)]
    kms: KmsHandle,
}

impl Udev {
    fn new(event_loop: LoopHandle<'static, Forge>) -> Self {
        let (session, notifier) = match LibSeatSession::new() {
            Ok(s) => s,
            Err(e) => {
                error!(
                    "Unable to start libseat session: {}. Ensure logind/seatd service is \
                     running with no active session",
                    e
                );
                process::exit(1);
            },
        };

        let mut context = Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(
            session.clone().into(),
        );
        let seat_name = session.seat();
        let mut input_assigned = if session.is_active() {
            context.udev_assign_seat(&seat_name).expect("assign seat");
            true
        } else {
            false
        };

        let input_backend = LibinputInputBackend::new(context.clone());
        event_loop
            .insert_source(input_backend, |event, _, forge| {
                forge.handle_input(event);
            })
            .expect("insert input source");

        event_loop
            .insert_source(notifier, move |event, _, forge| match event {
                SessionEvent::PauseSession => {
                    if input_assigned {
                        context.suspend();
                    }
                    if let Some(dev) = &mut forge.backend.udev().output_device {
                        // Stop the KMS thread flipping BEFORE we drop DRM master,
                        // or its in-flight page_flips hit EACCES and corrupt KMS
                        // state (which then fails the modeset on resume).
                        crate::kms::send_on(&dev.kms, KmsCmd::Pause);
                        dev.drm.pause();
                    }
                },
                SessionEvent::ActivateSession => {
                    if input_assigned {
                        if let Err(e) = context.resume() {
                            error!("Failed to resume libinput: {e:?}");
                        }
                    } else if context.udev_assign_seat(&seat_name).is_ok() {
                        input_assigned = true;
                    } else {
                        error!("libinput udev_assign_seat failed");
                    }

                    if let Some(dev) = &mut forge.backend.udev().output_device {
                        if let Err(e) = dev.drm.activate(true) {
                            error!("Failed to re-activate DRM device after VT switch: {e:?}");
                        }
                        // DRM master is back: let the KMS thread flip again, then
                        // have it re-sync each surface (modeset on the next flip).
                        // The main-thread swapchains drop their buffer ages so the
                        // next render full-repaints in step with the forced modeset.
                        crate::kms::send_on(&dev.kms, KmsCmd::Resume);
                        dev.presenter.reset_buffers();
                        crate::kms::send_on(&dev.kms, KmsCmd::ResetState { crtc: dev.presenter.crtc() });
                        for mirror in dev.mirrors.iter_mut() {
                            mirror.presenter.reset_buffers();
                            mirror.pending_frame = false;
                            crate::kms::send_on(&dev.kms, KmsCmd::ResetState { crtc: mirror.presenter.crtc() });
                        }
                    }

                    // Retry devices whose initial setup failed (typically due to
                    // not holding DRM master yet). Now that the session is active
                    // we should be master and the modeset / atomic init can succeed.
                    let pending: Vec<PathBuf> =
                        forge.backend.udev().pending_paths.drain(..).collect();
                    for path in pending {
                        add_device(forge, path);
                    }

                    // Replay hotplug events that arrived while we were paused.
                    // Last-write-wins is fine — the kernel's connector state is
                    // already up to date, we just need to rescan it.
                    let pending_changes: Vec<DeviceId> =
                        forge.backend.udev().pending_change.drain(..).collect();
                    for device_id in pending_changes {
                        let display = forge.display_handle.clone();
                        trace_error!(forge.backend.udev().change_device(
                            &display,
                            &mut forge.windows,
                            device_id,
                        ));
                    }

                    // Replay any DPMS request that landed while paused.
                    if let Some(on) = forge.backend.udev().pending_dpms.take() {
                        forge.set_display_status(on);
                    }

                    forge.invalidate_keyboard_focus();
                    forge.force_redraw(true);
                },
            })
            .expect("insert notifier source");

        let (kms, kms_events) = crate::kms::spawn();
        event_loop
            .insert_source(kms_events, move |event, _, forge| {
                let calloop::channel::Event::Msg(event) = event else { return };
                match event {
                    crate::kms::KmsEvent::Presented { crtc, metadata } => {
                        // Irrefutable without the `winit` feature (Backend is
                        // then Udev-only); the else arm matters once winit adds
                        // a second variant.
                        #[allow(irrefutable_let_patterns)]
                        let Backend::Udev(ref mut udev) = forge.backend else { return };
                        let Some(dev) = &mut udev.output_device else { return };

                        if dev.mirrors.iter().any(|m| m.presenter.crtc() == crtc) {
                            // Mirror vblank: re-mirror the primary's latest frame on
                            // this output's OWN vblank, decoupled from the primary's
                            // `create_frame`. When the mirror paced only on the
                            // primary's frame, its `pending_frame` was usually still
                            // true at that moment (the two vblanks land almost in
                            // phase and the mirror's arrives just after), so it
                            // flipped one frame in two — a hard 30fps on the external
                            // output. Blitting the primary's last composed buffer
                            // here (no scene recompose) keeps it at full rate.
                            dev.repaint_mirror(crtc);
                            return;
                        } else if dev.presenter.crtc() != crtc {
                            return;
                        } else {
                            #[cfg(feature = "profiling")]
                            GlobalProfiler::lock().new_frame();
                            forge.windows.mark_presented(&dev.last_render_states, &metadata);
                        }

                        let frame_interval = forge.windows.canvas().frame_interval();
                        match forge.frame_pacer.predict().filter(|p| p < &frame_interval) {
                            Some(prediction) => {
                                forge.backend.schedule_redraw(frame_interval - prediction);
                            },
                            None => forge.create_frame(),
                        }
                    },
                    crate::kms::KmsEvent::FlipFailed { crtc } => {
                        // Irrefutable without the `winit` feature (Backend is
                        // then Udev-only); the else arm matters once winit adds
                        // a second variant.
                        #[allow(irrefutable_let_patterns)]
                        let Backend::Udev(ref mut udev) = forge.backend else { return };
                        let Some(dev) = &mut udev.output_device else { return };
                        if let Some(index) =
                            dev.mirrors.iter().position(|m| m.presenter.crtc() == crtc)
                        {
                            dev.mirrors[index].pending_frame = false;
                            dev.mirrors[index].presenter.flip_failed();
                            return;
                        }
                        if dev.presenter.crtc() == crtc {
                            dev.presenter.flip_failed();
                            forge.request_redraw();
                        }
                    },
                }
            })
            .expect("insert kms event source");

        Self {
            event_loop,
            session,
            scheduled_redraws: Vec::new(),
            output_device: None,
            pending_paths: Vec::new(),
            pending_change: Vec::new(),
            pending_dpms: None,
            kms,
        }
    }

    pub fn seat_name(&self) -> String {
        self.session.seat()
    }

    pub fn change_vt(&mut self, vt: i32) {
        trace_error!(self.session.change_vt(vt));
    }

    pub fn set_display_status(&mut self, on: bool) {
        // `set_enabled` updates the DRM `ACTIVE` connector property — an ioctl
        // that requires master. If we're paused (libseat dropped master for a
        // VT switch, suspend, etc.) calling it would return EACCES and the
        // get_drm_property path would log the error twice without recovering.
        // Stash the latest desired state instead and replay it on resume.
        if !self.session.is_active() {
            self.pending_dpms = Some(on);
            return;
        }
        let Some(dev) = &mut self.output_device else { return };
        dev.set_enabled(on);

        if on {
            self.schedule_redraw(Duration::ZERO);
        }
    }

    /// True when at least one external (non-panel) monitor is connected.
    pub fn has_external_display(&self) -> bool {
        self.output_device.as_ref().is_some_and(|d| d.has_external)
    }

    fn is_external_connector(info: &ConnectorInfo) -> bool {
        info.state() == ConnectorState::Connected
            && !matches!(
                info.interface(),
                Interface::EmbeddedDisplayPort | Interface::LVDS | Interface::DSI
            )
    }

    pub fn render(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        text_renderer: &mut TextRenderer,
        mpris: &mut crate::mpris::MprisState,
        polkit: &mut crate::polkit::PolkitAgent,
        power_menu: &mut crate::powermenu::PowerMenu,
        osd: &mut crate::osd::Osd,
        topbar: &mut crate::topbar::Topbar,
        sys_state: &crate::sysmon::SystemState,
        app_switcher: &mut crate::app_switcher::AppSwitcher,
    ) -> RenderedFrame {
        let Some(dev) = &mut self.output_device else {
            return RenderedFrame { rendered: false, gpu_fence: None };
        };

        match dev.render(&self.event_loop, windows, cursor_position, text_renderer, mpris, polkit, power_menu, osd, topbar, sys_state, app_switcher) {
            Ok(frame) => frame,
            Err(e) => {
                if !dev.drm.is_active() || is_transient_render_error(&*e) {
                    return RenderedFrame { rendered: false, gpu_fence: None };
                }
                error!("Render error: {e}");
                let device_id = dev.id;
                let timer = Timer::from_duration(DRM_RETRY_DELAY);
                let _ = self.event_loop.insert_source(timer, move |_, _, forge| {
                    info!("Reconnecting DRM device after render failure");
                    trace_error!(forge.backend.udev().change_device(
                        &forge.display_handle,
                        &mut forge.windows,
                        device_id,
                    ));
                    TimeoutAction::Drop
                });
                RenderedFrame { rendered: false, gpu_fence: None }
            }
        }
    }

    pub fn renderer(&mut self) -> Option<&mut GlesRenderer> {
        self.output_device.as_mut().map(|d| &mut d.gles)
    }

    pub fn reset_buffer_ages(&mut self) {
        if let Some(dev) = &mut self.output_device {
            dev.presenter.reset_buffer_ages();
            for mirror in &mut dev.mirrors {
                mirror.presenter.reset_buffer_ages();
            }
        }
    }

    pub fn schedule_redraw(&mut self, duration: Duration) {
        let token = self
            .event_loop
            .insert_source(Timer::from_duration(duration), |_, _, forge| {
                forge.create_frame();
                TimeoutAction::Drop
            })
            .expect("insert render timer");
        self.scheduled_redraws.push(token);
    }

    pub fn cancel_scheduled_redraws(&mut self) {
        for token in self.scheduled_redraws.drain(..) {
            self.event_loop.remove(token);
        }
    }

    pub fn request_screencopy(&mut self, screencopy: Screencopy) {
        if let Some(dev) = &mut self.output_device {
            dev.screencopy = Some(screencopy);
        }
    }

    pub fn request_export_dmabuf(&mut self, capture: ExportDmabufCapture) {
        if let Some(dev) = &mut self.output_device {
            dev.export_dmabuf = Some(capture);
        }
    }

    pub fn request_image_copy(&mut self, capture: ImageCopyCapture) {
        if let Some(dev) = &mut self.output_device {
            dev.image_copy = Some(capture);
        }
    }

    fn default_dmabuf_feedback(
        &self,
        backend: &UdevBackend,
    ) -> Result<DmabufFeedback, Box<dyn Error>> {
        self.output_device
            .as_ref()
            .ok_or_else(|| "no output device available".into())
            .and_then(|d| d.default_dmabuf_feedback(backend))
    }

    fn has_connected_display(drm: &DrmDevice) -> bool {
        let Ok(resources) = drm.resource_handles() else {
            return false;
        };

        resources.connectors().iter().any(|conn| {
            drm.get_connector(*conn, true)
                .map(|c| c.state() == ConnectorState::Connected)
                .unwrap_or(false)
        })
    }

    fn add_device(
        &mut self,
        display: &DisplayHandle,
        windows: &mut Windows,
        path: &Path,
        retries_left: u32,
    ) -> Result<bool, Box<dyn Error>> {
        if self.output_device.is_some() {
            debug!("Output device already configured, skipping {:?}", path);
            return Ok(false);
        }

        let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY;
        let device_fd = match self.session.open(path, open_flags) {
            Ok(fd) => DrmDeviceFd::new(DeviceFd::from(fd)),
            Err(e) => {
                if !self.session.is_active() {
                    return Err(e.into());
                }
                if retries_left > 0 {
                    info!(
                        "Device {:?} busy, scheduling retry ({} left)...",
                        path,
                        retries_left - 1
                    );
                    let timer = Timer::from_duration(DRM_RETRY_DELAY);
                    let retry_path = path.to_path_buf();
                    let remaining = retries_left - 1;
                    self.event_loop
                        .insert_source(timer, move |_, _, forge| {
                            trace_error!(forge.backend.udev().add_device(
                                &forge.display_handle,
                                &mut forge.windows,
                                &retry_path,
                                remaining,
                            ));
                            TimeoutAction::Drop
                        })
                        .expect("drm retry");
                } else {
                    error!(
                        "Failed to open device {:?} after all retries: {}. \
                         Ensure no other compositor is running and the session has seat access.",
                        path, e
                    );
                }
                return Err(e.into());
            },
        };

        let (mut drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;

        if !Self::has_connected_display(&drm) {
            debug!("Device {:?} has no connected display, skipping", path);
            return Ok(false);
        }

        info!("Initializing display device: {:?}", path);

        let gbm = GbmDevice::new(device_fd)?;
        let egl_display = unsafe { EGLDisplay::new(gbm.clone())? };
        let context = EGLContext::new(&egl_display)?;
        let gles = unsafe { GlesRenderer::new(context).expect("create renderer") };

        let (presenter, mirrors, primary_connector, committers, has_external) = self
            .create_drm_compositor(display, windows, &gles, &mut drm, &gbm)
            .ok_or("failed to create drm compositor - no valid connector/crtc")?;

        let device_id = drm.device_id();

        // The vblank notifier and every output's commit half move to the KMS
        // thread. From here the main thread never page-flips or reads vblanks; it
        // dispatches `Flip` commands and reacts to `Presented`/`FlipFailed`.
        self.kms.send(KmsCmd::SetDevice {
            notifier: drm_notifier,
            outputs: committers,
        });
        let kms = self.kms.sender();

        let graphics = Graphics::new();
        let last_render_states = RenderElementStates { states: HashMap::new() };

        self.output_device = Some(OutputDevice {
            last_render_states,
            presenter,
            mirrors,
            primary_connector,
            graphics,
            gles,
            kms,
            gbm,
            drm,
            id: device_id,
            screencopy: None,
            export_dmabuf: None,
            image_copy: None,
            primary_flipped_last: true,
            has_external,
        });

        Ok(true)
    }

    fn remove_device(&mut self, device_id: DeviceId) {
        if let Some(dev) = self.output_device.take().filter(|d| d.id == device_id) {
            // Drop the device's notifier and committers on the KMS thread before
            // the `DrmDevice` itself goes away with `dev`.
            self.kms.send(KmsCmd::ClearDevice);
            drop(dev);
            info!("Removed display device (id: {})", device_id);
        }
    }

    fn change_device(
        &mut self,
        _display: &DisplayHandle,
        _windows: &mut Windows,
        device_id: DeviceId,
    ) -> Result<(), Box<dyn Error>> {
        let Some(dev) = self.output_device.as_mut().filter(|d| d.id == device_id) else {
            return Ok(());
        };

        let resources = dev.drm.resource_handles()?;
        let mut connected: HashMap<ConnectorHandle, ConnectorInfo> = HashMap::new();
        for handle in resources.connectors() {
            if let Ok(info) = dev.drm.get_connector(*handle, true) {
                if info.state() == ConnectorState::Connected {
                    connected.insert(*handle, info);
                }
            }
        }
        dev.has_external = connected.values().any(Self::is_external_connector);

        let dropped_crtcs: Vec<crtc::Handle> = dev
            .mirrors
            .iter()
            .filter(|m| !connected.contains_key(&m.connector))
            .map(|m| m.presenter.crtc())
            .collect();
        let before = dev.mirrors.len();
        dev.mirrors.retain(|m| connected.contains_key(&m.connector));
        let dropped = before - dev.mirrors.len();
        if dropped > 0 {
            info!("Hotplug: dropped {dropped} mirror(s) for disconnected connector(s)");
        }
        for crtc in dropped_crtcs {
            crate::kms::send_on(&dev.kms, KmsCmd::RemoveOutput { crtc });
        }

        let formats = match Bind::<Dmabuf>::supported_formats(&dev.gles) {
            Some(f) => f,
            None => return Ok(()),
        };

        let already: std::collections::HashSet<ConnectorHandle> =
            dev.mirrors.iter().map(|m| m.connector).collect();

        for (handle, info) in connected.iter() {
            if *handle == dev.primary_connector { continue; }
            if already.contains(handle) { continue; }

            let Some(mode) = info.modes()
                .iter()
                .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
                .or_else(|| info.modes().first())
                .copied()
            else { continue };

            let mut used_crtcs: Vec<crtc::Handle> = vec![dev.presenter.crtc()];
            for m in &dev.mirrors {
                used_crtcs.push(m.presenter.crtc());
            }

            let Some(surface) =
                Self::create_surface(&mut dev.drm, &resources, info, mode, &used_crtcs)
            else { continue };

            let (mw, mh) = mode.size();
            let size = Size::<i32, Physical>::from((mw as i32, mh as i32));
            let code = Fourcc::Argb8888;
            let modifiers = Self::pick_modifiers(&surface, &formats, code);
            let allocator = GbmAllocator::new(
                dev.gbm.clone(),
                GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
            );

            let (presenter, committer) = Presenter::new(
                surface,
                allocator,
                GbmFramebufferExporter::new(dev.gbm.clone(), NodeFilter::All),
                code,
                modifiers,
                size,
                1.0,
            );
            let crtc = presenter.crtc();
            info!("Hotplug: added mirror presenter for {:?}", info.interface());
            dev.mirrors.push(MirrorHead {
                presenter,
                pending_frame: false,
                connector: *handle,
            });
            crate::kms::send_on(&dev.kms, KmsCmd::AddOutput { crtc, committer });
        }

        Ok(())
    }

    fn create_drm_compositor(
        &mut self,
        display: &DisplayHandle,
        windows: &mut Windows,
        gles: &GlesRenderer,
        drm: &mut DrmDevice,
        gbm: &GbmDevice<DrmDeviceFd>,
    ) -> Option<(Presenter, Vec<MirrorHead>, ConnectorHandle, Vec<(crtc::Handle, Committer)>, bool)> {
        let formats = Bind::<Dmabuf>::supported_formats(gles)?;
        let resources = drm.resource_handles().ok()?;

        // Find all connected connectors
        let connected: Vec<ConnectorInfo> = resources
            .connectors()
            .iter()
            .filter_map(|conn| {
                drm.get_connector(*conn, true)
                    .ok()
                    .filter(|c| c.state() == ConnectorState::Connected)
            })
            .collect();
        let has_external = connected.iter().any(Self::is_external_connector);

        let primary = connected.first()?;

        for conn in &connected {
            info!("Connected output: {:?} ({:?})", conn.interface(), conn.state());
        }

        // Use primary connector's preferred mode
        let primary_mode = {
            let modes = primary.modes();
            *modes
                .iter()
                .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
                .unwrap_or(modes.first()?)
        };

        let (w, h) = primary_mode.size();
        info!("Primary display mode: {}x{}@{}Hz", w, h, primary_mode.vrefresh());

        let mode = Mode {
            size: (w as i32, h as i32).into(),
            refresh: primary_mode.vrefresh() as i32 * 1000,
        };

        let (pw, ph) = primary.size().unwrap_or((0, 0));
        let output_name = format!("{:?}", primary.interface());

        windows.set_output(Output::new(
            display,
            output_name,
            mode,
            PhysicalProperties {
                size: (pw as i32, ph as i32).into(),
                subpixel: Subpixel::Unknown,
                serial_number: "Unknown".into(),
                model: "Generic DRM".into(),
                make: "Forge".into(),
            },
        ));

        let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;

        // Prefer a hardware clone: one CRTC driving every connected output from a
        // single framebuffer (one flip, one vblank). Driving a second CRTC's
        // commit every frame starves the event loop, so cloning removes it. On
        // any failure, including hardware that can't clone these connectors, fall
        // back to per-output mirror compositors; the clone path never returns
        // early, so a rejected clone can't leave the screen without a compositor.
        let size = Size::<i32, Physical>::from((w as i32, h as i32));
        let scale = windows.canvas().scale();
        let code = Fourcc::Argb8888;

        // Primary output presenter. The hardware clone is not used here: i915
        // cannot clone eDP+DP from one CRTC, and the per-output mirror path
        // handles additional connectors below.
        let cloned = false;
        let surface = Self::create_surface(drm, &resources, primary, primary_mode, &[])?;
        let modifiers = Self::pick_modifiers(&surface, &formats, code);
        let (primary_compositor, primary_committer) = Presenter::new(
            surface,
            GbmAllocator::new(gbm.clone(), gbm_flags),
            GbmFramebufferExporter::new(gbm.clone(), NodeFilter::All),
            code,
            modifiers,
            size,
            scale,
        );
        let primary_crtc = primary_compositor.crtc();
        let mut committers = vec![(primary_crtc, primary_committer)];

        // Each additional connector gets its own per-output mirror compositor
        // driven by its own vblank, decoupled from the primary (skipped when a
        // hardware clone already drives every output from the primary CRTC).
        let mut mirrors = Vec::new();
        let mut used_crtcs = vec![primary_crtc];

        for conn in connected.iter().skip(1) {
            if cloned { break; }

            let conn_mode = {
                let modes = conn.modes();
                *modes
                    .iter()
                    .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
                    .unwrap_or(match modes.first() {
                        Some(m) => m,
                        None => {
                            info!("No modes for connector {:?}, skipping", conn.interface());
                            continue;
                        }
                    })
            };

            let (mw, mh) = conn_mode.size();
            info!(
                "Mirror output {:?}: {}x{}@{}Hz",
                conn.interface(), mw, mh, conn_mode.vrefresh()
            );

            let surface = match Self::create_surface(drm, &resources, conn, conn_mode, &used_crtcs)
            {
                Some(s) => s,
                None => {
                    info!("Could not create surface for mirror {:?}", conn.interface());
                    continue;
                }
            };

            let mirror_crtc = surface.crtc();
            used_crtcs.push(mirror_crtc);

            let allocator = GbmAllocator::new(
                gbm.clone(),
                GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
            );
            let mirror_size = Size::<i32, Physical>::from((mw as i32, mh as i32));
            let mirror_modifiers = Self::pick_modifiers(&surface, &formats, code);

            let (presenter, committer) = Presenter::new(
                surface,
                allocator,
                GbmFramebufferExporter::new(gbm.clone(), NodeFilter::All),
                code,
                mirror_modifiers,
                mirror_size,
                1.0,
            );
            info!("Created mirror presenter for {:?}", conn.interface());
            mirrors.push(MirrorHead {
                presenter,
                pending_frame: false,
                connector: conn.handle(),
            });
            committers.push((mirror_crtc, committer));
        }

        if !mirrors.is_empty() {
            info!("Multi-output: 1 primary + {} mirror(s)", mirrors.len());
        }

        Some((primary_compositor, mirrors, primary.handle(), committers, has_external))
    }

    /// Modifiers usable for `code` on both the primary plane and the renderer,
    /// mirroring smithay's `DrmCompositor` format selection. Falls back to the
    /// implicit modifier when the intersection is empty.
    fn pick_modifiers(surface: &DrmSurface, renderer_formats: &FormatSet, code: Fourcc) -> Vec<DrmModifier> {
        use std::collections::HashSet;
        let plane: HashSet<DrmModifier> = surface
            .plane_info()
            .formats
            .iter()
            .filter(|f| f.code == code)
            .map(|f| f.modifier)
            .collect();
        let rend: HashSet<DrmModifier> = renderer_formats
            .iter()
            .filter(|f| f.code == code)
            .map(|f| f.modifier)
            .collect();
        let inter: Vec<DrmModifier> = plane.intersection(&rend).copied().collect();
        if inter.is_empty() {
            vec![DrmModifier::Invalid]
        } else {
            inter
        }
    }

    /// Create a DRM surface for a single connector, avoiding CRTCs in `excluded`.
    fn create_surface(
        drm: &mut DrmDevice,
        resources: &ResourceHandles,
        connector: &ConnectorInfo,
        mode: DrmMode,
        excluded: &[crtc::Handle],
    ) -> Option<DrmSurface> {
        for encoder in connector.encoders() {
            let Ok(encoder) = drm.get_encoder(*encoder) else { continue };

            let mut crtcs = resources.filter_crtcs(encoder.possible_crtcs());
            crtcs.sort_by_cached_key(|crtc| {
                drm.planes(crtc).map_or(0, |p| -(p.overlay.len() as isize))
            });

            for crtc in crtcs {
                if excluded.contains(&crtc) { continue; }
                if let Ok(surface) = drm.create_surface(crtc, mode, &[connector.handle()]) {
                    info!("Created DRM surface with CRTC {:?}", crtc);
                    return Some(surface);
                }
            }
        }

        error!("Could not create DRM surface - no valid encoder/crtc combination");
        None
    }

}

struct MirrorHead {
    presenter: Presenter,
    pending_frame: bool,
    connector: ConnectorHandle,
}

pub struct OutputDevice {
    last_render_states: RenderElementStates,
    screencopy: Option<Screencopy>,
    export_dmabuf: Option<ExportDmabufCapture>,
    image_copy: Option<ImageCopyCapture>,
    presenter: Presenter,
    mirrors: Vec<MirrorHead>,
    primary_connector: ConnectorHandle,
    gbm: GbmDevice<DrmDeviceFd>,
    gles: GlesRenderer,
    graphics: Graphics,
    drm: DrmDevice,
    id: DeviceId,
    /// Flip-command sender to the KMS thread, one clone per device. The vblank
    /// notifier and the `Committer`s live on that thread; this is how `render`
    /// dispatches page-flips for this device's outputs.
    kms: Sender<KmsCmd>,
    /// Whether the primary queued a flip on the last `render`. When `false` the
    /// primary will produce no vblank (e.g. its panel is off because the laptop
    /// lid is closed with an external monitor), so a mirror's vblank must drive
    /// frame pacing instead — otherwise the loop stalls while the external output
    /// is the only live one.
    primary_flipped_last: bool,
    /// Cached "an external (non-panel) monitor is connected", recomputed only
    /// when connectors are enumerated (startup / hotplug). The lid-close logic
    /// reads this every status tick; force-probing the connector there blocked
    /// the render thread for tens of ms on DP-MST sideband round-trips.
    has_external: bool,
}

impl OutputDevice {
    /// Re-mirror the primary's latest composed frame onto one mirror output,
    /// driven by that mirror's own vblank. Blits the primary's `last_dmabuf` into
    /// a fresh swapchain slot of the mirror and flips it — no scene recompose, so
    /// it runs independently of the primary's `create_frame` and keeps the mirror
    /// at full refresh.
    fn repaint_mirror(&mut self, crtc: crtc::Handle) {
        let Some(index) = self.mirrors.iter().position(|m| m.presenter.crtc() == crtc) else {
            return;
        };
        self.mirrors[index].pending_frame = false;

        // The primary's last composed buffer is what every mirror shows. If the
        // primary hasn't composed anything yet there is nothing to mirror.
        let Some(src) = self.presenter.last_dmabuf().cloned() else { return };

        let Self { mirrors, gles, kms, .. } = self;
        let mirror = &mut mirrors[index];
        if let Some((fb, slot)) = mirror.presenter.blit_flip(gles, &src) {
            crate::kms::send_on(kms, KmsCmd::Flip { crtc, fb, slot });
            mirror.pending_frame = true;
        }
    }

    fn get_drm_property(&self, name: &str) -> Result<PropertyHandle, io::Error> {
        let crtc = self.presenter.crtc();
        let properties = self.drm.get_properties(crtc)?;
        let (handles, _) = properties.as_props_and_values();

        for handle in handles {
            let info = self.drm.get_property(*handle)?;
            let prop_name = info
                .name()
                .to_str()
                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;

            if prop_name == name {
                return Ok(*handle);
            }
        }

        Err(io::Error::new(io::ErrorKind::NotFound, "missing drm property"))
    }

    fn set_enabled(&mut self, enabled: bool) {
        let Ok(property) = self.get_drm_property("ACTIVE") else {
            error!("Could not get DRM property `ACTIVE`");
            return;
        };

        let crtc = self.presenter.crtc();
        trace_error!(self.drm.set_property(crtc, property, PropertyValue::Boolean(enabled).into()));

        for mirror in &self.mirrors {
            let crtc = mirror.presenter.crtc();
            trace_error!(self.drm.set_property(crtc, property, PropertyValue::Boolean(enabled).into()));
        }
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn render(
        &mut self,
        event_loop: &LoopHandle<'static, Forge>,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        text_renderer: &mut TextRenderer,
        mpris: &mut crate::mpris::MprisState,
        polkit: &mut crate::polkit::PolkitAgent,
        power_menu: &mut crate::powermenu::PowerMenu,
        osd: &mut crate::osd::Osd,
        topbar: &mut crate::topbar::Topbar,
        sys_state: &crate::sysmon::SystemState,
        app_switcher: &mut crate::app_switcher::AppSwitcher,
    ) -> Result<RenderedFrame, Box<dyn Error>> {
        let scale = windows.canvas().scale();
        let output_size = windows.canvas().physical_resolution();

        let textures =
            windows.textures(&mut self.gles, &mut self.graphics, cursor_position, text_renderer, mpris, polkit, power_menu, osd, topbar, sys_state, app_switcher);

        // Primary output: compose onto the presenter's swapchain (GLES render on
        // the main thread). The commit (page_flip) runs right after the mirrors,
        // before the capture paths, so a capture error can never sit between
        // render and commit and desync the damage tracker.
        let mut primary_rendered = self.presenter.render(&mut self.gles, textures);

        // Purge dead dmabuf imports and drain the deferred-destruction queue;
        // otherwise imported client buffers' fds pile up until EMFILE.
        let _ = self.gles.cleanup_texture_cache();

        // Carry the render states to the vblank handler for client frame
        // callbacks (`mark_presented`), and the GPU fence to the frame pacer.
        let mut gpu_fence = None;
        if let Some(rf) = primary_rendered.as_mut() {
            self.last_render_states = std::mem::take(&mut rf.states);
            gpu_fence = rf.gpu_fence.take();
        }

        // Kick each idle mirror once off the primary's frame by blitting the
        // primary's just-composed buffer (no scene recompose — a mirror just
        // mirrors). After this first flip each mirror sustains itself on its own
        // vblank via `repaint_mirror`, so it runs at full refresh regardless of
        // the primary's cadence. A mirror with a frame still in flight is skipped.
        // Mirroring needs the primary's `last_dmabuf`; if the primary composed
        // nothing this frame the mirrors keep their last image until it does.
        let mut any_mirror_flipped = false;
        if let Some(src) = self.presenter.last_dmabuf().cloned() {
            let Self { mirrors, gles, kms, .. } = self;
            for mirror in mirrors.iter_mut() {
                if mirror.pending_frame {
                    continue;
                }
                let crtc = mirror.presenter.crtc();
                if let Some((fb, slot)) = mirror.presenter.blit_flip(gles, &src) {
                    crate::kms::send_on(kms, KmsCmd::Flip { crtc, fb, slot });
                    mirror.pending_frame = true;
                    any_mirror_flipped = true;
                }
            }
        }

        // Dispatch the primary's page-flip to the KMS thread. This must stay
        // before the capture paths: `prepare_flip` advances the swapchain in
        // lockstep with the damage tracker, so nothing may sit between render and
        // this point. `flipped` is whether a flip was dispatched; a damage-free
        // frame skips it, so no vblank will come and the caller reschedules by
        // timer instead of waiting. The flip runs async; a failure comes back as
        // `KmsEvent::FlipFailed`.
        let mut flipped = false;
        if let Some(rf) = primary_rendered.take() {
            let crtc = self.presenter.crtc();
            if let Some((fb, slot)) = self.presenter.prepare_flip(rf) {
                crate::kms::send_on(&self.kms, KmsCmd::Flip { crtc, fb, slot });
                flipped = true;
            }
        }
        // Record whether the primary will produce a vblank this cycle, so the
        // `Presented` handler knows whether a mirror's vblank should drive pacing.
        self.primary_flipped_last = flipped;

        // --- DMA-BUF captures: blit from the primary's just-composed buffer ---
        let capture_src = self.presenter.last_dmabuf().cloned();

        // Screencopy: DMA-BUF path
        let mut screencopy_shm: Option<(Screencopy, Rectangle<i32, Physical>, WlBuffer)> = None;
        if let Some(mut screencopy) = self.screencopy.take() {
            let region = screencopy.region();
            screencopy.damage(&[Rectangle::from_size(region.size)]);

            let buffer = screencopy.buffer();
            if let (Ok(dmabuf), Some(src)) = (dmabuf::get_dmabuf(buffer), capture_src.as_ref()) {
                let sync_point = Self::copy_framebuffer_dma(
                    &mut self.gles,
                    scale,
                    src,
                    region,
                    &mut dmabuf.clone(),
                )?;

                match sync_point.export() {
                    Some(sync_fd) => {
                        let mut screencopy = Some(screencopy);
                        let source = Generic::new(sync_fd, Interest::READ, TriggerMode::OneShot);
                        let _ = event_loop.insert_source(source, move |_, _, _| {
                            screencopy.take().unwrap().submit();
                            Ok(PostAction::Remove)
                        });
                    },
                    None => screencopy.submit(),
                }
            } else {
                let buffer_type = renderer::buffer_type(buffer);
                if !matches!(buffer_type, Some(BufferType::Shm)) {
                    return Err(format!("unsupported buffer format: {buffer_type:?}").into());
                }
                let buf_clone = buffer.clone();
                screencopy_shm = Some((screencopy, region, buf_clone));
            }
        }

        // Export-dmabuf capture
        if let Some(capture) = self.export_dmabuf.take() {
            let w = output_size.w as u32;
            let h = output_size.h as u32;

            let result: Result<(), Box<dyn Error>> = (|| {
                let Some(src) = capture_src.as_ref() else { return Ok(()) };
                let bo = self.gbm.create_buffer_object::<()>(
                    w, h,
                    gbm::Format::Argb8888,
                    GbmBufferFlags::RENDERING,
                )?;

                let stride = bo.stride();
                let modifier: u64 = bo.modifier().into();
                let fd = bo.fd()?;
                let bo_size = stride * h;

                let gbm_buf = GbmBuffer::from_bo(bo, false);
                let mut dmabuf = gbm_buf.export()?;
                let region = Rectangle::from_size(output_size);

                let sync_point = Self::copy_framebuffer_dma(
                    &mut self.gles,
                    scale,
                    src,
                    region,
                    &mut dmabuf,
                )?;

                // Defer the submit until the GPU has finished the blit — submitting the
                // fd before the blit completes can hand the consumer a torn/partial frame.
                let fmt = Fourcc::Argb8888 as u32;
                match sync_point.export() {
                    Some(sync_fd) => {
                        let mut capture = Some(capture);
                        let mut fd = Some(fd);
                        let source = Generic::new(sync_fd, Interest::READ, TriggerMode::OneShot);
                        let _ = event_loop.insert_source(source, move |_, _, _| {
                            capture.take().unwrap().submit(
                                w, h, fmt, modifier, fd.take().unwrap(), stride, bo_size,
                            );
                            Ok(PostAction::Remove)
                        });
                    }
                    None => capture.submit(w, h, fmt, modifier, fd, stride, bo_size),
                }
                Ok(())
            })();

            if let Err(e) = result {
                debug!("export-dmabuf capture failed: {e}");
            }
        }

        // Image-copy-capture: DMA-BUF path
        let mut image_copy_shm: Option<(ImageCopyCapture, WlBuffer)> = None;
        if let Some(capture) = self.image_copy.take() {
            let buffer = capture.buffer().clone();
            let region = Rectangle::from_size(output_size);

            if let (Ok(dmabuf), Some(src)) = (dmabuf::get_dmabuf(&buffer), capture_src.as_ref()) {
                match Self::copy_framebuffer_dma(
                    &mut self.gles,
                    scale,
                    src,
                    region,
                    &mut dmabuf.clone(),
                ) {
                    Ok(sync_point) => {
                        match sync_point.export() {
                            Some(sync_fd) => {
                                let mut capture = Some(capture);
                                let source = Generic::new(sync_fd, Interest::READ, TriggerMode::OneShot);
                                let _ = event_loop.insert_source(source, move |_, _, _| {
                                    capture.take().unwrap().submit();
                                    Ok(PostAction::Remove)
                                });
                            }
                            None => capture.submit(),
                        }
                    }
                    Err(e) => {
                        debug!("image-copy-capture DMA-BUF failed: {e}");
                        capture.fail();
                    }
                }
            } else {
                image_copy_shm = Some((capture, buffer));
            }
        }

        // --- SHM captures (need windows for re-render) ---

        if let Some((screencopy, region, buffer)) = screencopy_shm {
            let sync_point = self.copy_framebuffer_shm(windows, cursor_position, text_renderer, mpris, polkit, power_menu, osd, topbar, sys_state, app_switcher, region, &buffer)?;

            match sync_point.export() {
                Some(sync_fd) => {
                    let mut screencopy = Some(screencopy);
                    let source = Generic::new(sync_fd, Interest::READ, TriggerMode::OneShot);
                    let _ = event_loop.insert_source(source, move |_, _, _| {
                        screencopy.take().unwrap().submit();
                        Ok(PostAction::Remove)
                    });
                },
                None => screencopy.submit(),
            }
        }

        if let Some((capture, buffer)) = image_copy_shm {
            let region = Rectangle::from_size(output_size);
            let buffer_type = renderer::buffer_type(&buffer);
            if matches!(buffer_type, Some(BufferType::Shm)) {
                match self.copy_framebuffer_shm(windows, cursor_position, text_renderer, mpris, polkit, power_menu, osd, topbar, sys_state, app_switcher, region, &buffer) {
                    Ok(sync_point) => {
                        match sync_point.export() {
                            Some(sync_fd) => {
                                let mut capture = Some(capture);
                                let source = Generic::new(sync_fd, Interest::READ, TriggerMode::OneShot);
                                let _ = event_loop.insert_source(source, move |_, _, _| {
                                    capture.take().unwrap().submit();
                                    Ok(PostAction::Remove)
                                });
                            }
                            None => capture.submit(),
                        }
                    }
                    Err(e) => {
                        debug!("image-copy-capture SHM failed: {e}");
                        capture.fail();
                    }
                }
            } else {
                debug!("image-copy-capture: unsupported buffer format: {buffer_type:?}");
                capture.fail();
            }
        }

        // `rendered` means "a flip was queued, so a vblank will come to drive the
        // next frame". True if the primary OR any mirror flipped — otherwise the
        // pacing would stall whenever the primary is down but a mirror is live.
        Ok(RenderedFrame { rendered: flipped || any_mirror_flipped, gpu_fence })
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn copy_framebuffer_dma(
        gles: &mut GlesRenderer,
        _scale: f64,
        src: &Dmabuf,
        region: Rectangle<i32, Physical>,
        buffer: &mut Dmabuf,
    ) -> Result<SyncPoint, Box<dyn Error>> {
        use smithay::backend::renderer::{Blit, TextureFilter};
        let mut src = src.clone();
        let src_fb = gles.bind(&mut src)?;
        let mut dst_fb = gles.bind(buffer)?;
        let rect = Rectangle::from_size(region.size);
        let sync_point = gles.blit(&src_fb, &mut dst_fb, rect, rect, TextureFilter::Linear)?;
        Ok(sync_point)
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn copy_framebuffer_shm(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        text_renderer: &mut TextRenderer,
        mpris: &mut crate::mpris::MprisState,
        polkit: &mut crate::polkit::PolkitAgent,
        power_menu: &mut crate::powermenu::PowerMenu,
        osd: &mut crate::osd::Osd,
        topbar: &mut crate::topbar::Topbar,
        sys_state: &crate::sysmon::SystemState,
        app_switcher: &mut crate::app_switcher::AppSwitcher,
        region: Rectangle<i32, Physical>,
        buffer: &WlBuffer,
    ) -> Result<SyncPoint, Box<dyn Error>> {
        let buffer_dimensions = renderer::buffer_dimensions(buffer).unwrap();
        let mut offscreen_buffer: GlesRenderbuffer =
            self.gles.create_buffer(Fourcc::Abgr8888, buffer_dimensions)?;
        let mut framebuffer = self.gles.bind(&mut offscreen_buffer)?;

        let canvas = windows.canvas();
        let scale = canvas.scale();
        let output_size = canvas.physical_resolution();
        let transform = canvas.orientation().output_transform();

        let damage = transform.transform_rect_in(region, &output_size);
        let textures =
            windows.textures(&mut self.gles, &mut self.graphics, cursor_position, text_renderer, mpris, polkit, power_menu, osd, topbar, sys_state, app_switcher);

        let mut frame = self.gles.render(&mut framebuffer, output_size, transform)?;
        frame.clear(CLEAR_COLOR.into(), &[damage])?;
        utils::draw_render_elements(&mut frame, scale, textures, &[damage])?;
        let sync_point = frame.finish()?;

        shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
            if buffer_data.format != wl_shm::Format::Argb8888
                || buffer_data.stride != region.size.w * 4
                || buffer_data.height != region.size.h
                || shm_len as i32 != buffer_data.stride * buffer_data.height
            {
                return Err::<_, Box<dyn Error>>("Invalid buffer format".into());
            }

            self.gles.with_context(|gl| unsafe {
                gl.ReadPixels(
                    region.loc.x,
                    region.loc.y,
                    region.size.w,
                    region.size.h,
                    ffi::RGBA,
                    ffi::UNSIGNED_BYTE,
                    shm_buffer.cast(),
                );
            })?;

            for i in 0..(region.size.w * region.size.h) as usize {
                unsafe {
                    let src = shm_buffer.offset(i as isize * 4);
                    let dst = shm_buffer.offset(i as isize * 4 + 2);
                    ptr::swap(src, dst);
                }
            }

            Ok(sync_point)
        })?
    }

    fn default_dmabuf_feedback(
        &self,
        backend: &UdevBackend,
    ) -> Result<DmabufFeedback, Box<dyn Error>> {
        let surface = self.presenter.surface();
        let planes = surface.planes();

        let dmabuf_fmts = self.gles.dmabuf_formats();
        let dmabuf_formats = dmabuf_fmts.indexset();

        let primary_formats: IndexSet<_> =
            planes.primary.iter().flat_map(|p| p.formats.iter()).copied().collect();
        let primary_formats = primary_formats.intersection(&dmabuf_formats).copied();

        let any_overlay_formats: IndexSet<_> =
            planes.overlay.iter().flat_map(|p| p.formats.iter()).copied().collect();
        let any_overlay_formats = any_overlay_formats.intersection(&dmabuf_formats).copied();

        let mut all_overlay_formats = dmabuf_formats.clone();
        all_overlay_formats.retain(|f| planes.overlay.iter().all(|p| p.formats.contains(f)));

        let mut gbm_id = self.gbm.dev_id()?;
        if !DrmNode::from_dev_id(gbm_id)?.has_render() {
            let drm_node = backend
                .device_list()
                .filter_map(|(_, path)| DrmNode::from_path(path).ok())
                .find(DrmNode::has_render)
                .ok_or("no drm device with render node")?;

            debug!(
                "{:?} has no render node, using {:?} for dmabuf feedback",
                self.gbm.dev_path().unwrap_or_default(),
                drm_node.dev_path().unwrap_or_default(),
            );

            gbm_id = drm_node.dev_id();
        }

        let feedback_builder = DmabufFeedbackBuilder::new(gbm_id, dmabuf_formats.iter().copied());
        let surface_id = surface.device_fd().dev_id()?;
        let flags = Some(TrancheFlags::Scanout);

        let feedback = feedback_builder
            .add_preference_tranche(surface_id, flags, all_overlay_formats)
            .add_preference_tranche(surface_id, flags, any_overlay_formats)
            .add_preference_tranche(surface_id, flags, primary_formats)
            .build()?;

        Ok(feedback)
    }
}

pub struct RenderedFrame {
    pub gpu_fence: Option<OwnedFd>,
    pub rendered: bool,
}

// EACCES / EBUSY / EWOULDBLOCK from a page-flip race a pending PauseSession;
// the resume path will recover, a reopen would just lose to logind.
fn is_transient_render_error(err: &(dyn Error + 'static)) -> bool {
    let mut cur: Option<&(dyn Error + 'static)> = Some(err);
    while let Some(e) = cur {
        if let Some(io) = e.downcast_ref::<io::Error>() {
            return matches!(
                io.kind(),
                io::ErrorKind::PermissionDenied
                    | io::ErrorKind::WouldBlock
                    | io::ErrorKind::Interrupted
            ) || io.raw_os_error() == Some(libc::EBUSY);
        }
        cur = e.source();
    }
    false
}

fn is_panfrost_gpu() -> bool {
    let Ok(entries) = std::fs::read_dir("/sys/bus/platform/drivers/panfrost") else {
        return false;
    };
    entries
        .filter_map(Result::ok)
        .any(|e| e.file_name().to_string_lossy().contains("gpu"))
}

