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::{GlesRenderbuffer, GlesRenderer};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{self, Bind, BufferType, ImportDma, 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::backend::input::InputEvent;
use smithay::input::keyboard::LedState;
use smithay::reexports::input::{Device as InputDevice, DeviceCapability, 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, Logical, Physical, Point, 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::render_context::RenderContext;
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");

    // Try the boot GPU first. The first device that reports a connected
    // output becomes *the* output device, and udev's enumeration order
    // is not ours to choose: on a hybrid-graphics laptop the discrete
    // card often enumerates as card0 while the panel hangs off the
    // integrated one, and a discrete card with nothing plugged in can
    // still claim a connector (nouveau's analog load detection reports
    // phantom VGA/TV outputs). Picking it means compositing into a
    // display nobody is looking at. `boot_vga` is the firmware's own
    // answer to "which GPU drives the screen".
    let mut devices: Vec<(DeviceId, PathBuf)> = backend
        .device_list()
        .map(|(id, path)| (id, path.to_path_buf()))
        .collect();
    let preferred = std::env::var("FORGE_DRM_DEVICE").ok();
    devices.sort_by_key(|(_, path)| {
        let explicit = preferred.as_deref().is_some_and(|p| path.as_os_str() == p);
        // false sorts before true: explicit override, then boot GPU.
        (!explicit, !is_boot_gpu(path))
    });

    for (device_id, path) in devices {
        info!(
            "Found DRM device: {:?} (id: {}, boot GPU: {})",
            path,
            device_id,
            is_boot_gpu(&path)
        );
        forge.backend.udev().known_paths.insert(device_id, path.clone());
        add_device(&mut forge, path);
    }

    // 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();

    // On an inactive VT there is no device yet and this fails; the
    // ActivateSession path re-runs it once the real device lands, so
    // clients that connect later still get default feedback.
    create_dmabuf_global(&mut forge);

    event_loop
        .handle()
        .insert_source(backend, move |event, _, forge| match event {
            UdevEvent::Added { device_id, path } => {
                forge.backend.udev().known_paths.insert(device_id, path.clone());
                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() {
                    change_or_add_device(forge, device_id);
                } else {
                    let pending = &mut forge.backend.udev().pending_change;
                    if !pending.contains(&device_id) {
                        pending.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 retry_add_device(forge: &mut Forge, path: &Path, retries_left: u32) {
    // Same post-init kick as the add_device wrapper: a device brought up from
    // a retry timer still needs its first frame scheduled or the modeset
    // never reaches the hardware.
    match forge.backend.udev().add_device(&forge.display_handle, &mut forge.windows, path, retries_left) {
        Ok(true) => {
            info!("Display device initialized: {:?}", path);
            forge.run_init_if_needed();
            forge.create_frame();
        },
        Ok(false) => {},
        Err(e) => error!("Device retry for {:?} failed: {}", path, e),
    }
}

/// True when the kernel marks this DRM node's PCI device as the one
/// the firmware initialised for output — `/sys/class/drm/cardN/device/
/// boot_vga`. Absent (non-PCI, or a kernel that does not export it)
/// reads as false, which just leaves the enumeration order alone.
fn is_boot_gpu(path: &Path) -> bool {
    let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
        return false;
    };
    std::fs::read_to_string(format!("/sys/class/drm/{name}/device/boot_vga"))
        .map(|s| s.trim() == "1")
        .unwrap_or(false)
}

/// Re-run connector detection on every DRM device we know about.
/// Called on resume: a suspend can swallow the hotplug events for a
/// dock attached or detached while the machine was down, so the
/// pre-suspend output state cannot be trusted.
pub fn reprobe_all(forge: &mut Forge) {
    let known: Vec<DeviceId> = forge.backend.udev().known_paths.keys().copied().collect();
    for device_id in known {
        change_or_add_device(forge, device_id);
    }
}

fn change_or_add_device(forge: &mut Forge, device_id: DeviceId) {
    // A connector change on a device that was skipped at add time (no display
    // connected then, e.g. booting with only a DP-alt dock whose connector
    // appears once the Type-C mode is negotiated) is the cue to retry the
    // full init instead of rescanning a device we never brought up.
    if forge.backend.udev().output_device.is_none() {
        if let Some(path) = forge.backend.udev().known_paths.get(&device_id).cloned() {
            info!("Connector change on uninitialized device {device_id}, retrying init");
            add_device(forge, path);
            return;
        }
    }

    trace_error!(forge.backend.udev().change_device(
        &forge.display_handle,
        &mut forge.windows,
        device_id,
    ));
}

/// Publish the default-feedback dmabuf global. Idempotent by way of
/// `dmabuf_global_done`: creating it twice would hand clients two
/// globals for the same protocol.
fn create_dmabuf_global(forge: &mut Forge) {
    if forge.backend.udev().dmabuf_global_done {
        return;
    }
    if let Ok(feedback) = forge.backend.udev().default_dmabuf_feedback() {
        let (dmabuf_state, dh) = forge.dmabuf_global_target();
        dmabuf_state.create_global_with_default_feedback::<Forge>(dh, &feedback);
        forge.backend.udev().dmabuf_global_done = true;
        info!("dmabuf: default feedback global published");
    }
}

/// Queue a device for the next `ActivateSession`, at most once: a
/// pause/activate cycle that keeps failing used to stack duplicates of
/// the same path and re-run its (failing) init once per copy.
fn queue_pending_path(forge: &mut Forge, path: PathBuf) {
    let pending = &mut forge.backend.udev().pending_paths;
    if !pending.contains(&path) {
        pending.push(path);
    }
}

fn add_device(forge: &mut Forge, path: PathBuf) {
    if !forge.backend.udev().session.is_active() {
        debug!(
            "Session inactive — queued {:?} for retry on session activate",
            path
        );
        queue_pending_path(forge, path);
        return;
    }

    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) => debug!("Skipped render-only device: {:?}", path),
        Err(e) => {
            warn!(
                "Could not add device {:?} now: {} — queued for retry on session activate",
                path, e
            );
            queue_pending_path(forge, path);
        },
    }
}

mod output_device;
mod connectors;
mod device_setup;
pub use output_device::{OutputDevice, OutputInfo, RenderedFrame};
use output_device::{is_panfrost_gpu, is_transient_render_error, HeadKind, SecondaryHead};

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>,
    /// Every DRM device path seen on `UdevEvent::Added`, kept so a later
    /// connector change can retry the full init of a device that was skipped
    /// for having no connected display at add time.
    known_paths: HashMap<DeviceId, PathBuf>,
    /// `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>,
    /// The default-feedback dmabuf global is published once, whenever
    /// the first real device shows up — which on an inactive-VT boot is
    /// after the session activates, not at startup.
    dmabuf_global_done: bool,
    /// DPMS state actually pushed to the CRTC. Diverges from the
    /// compositor's desired `display_on` while a request is queued for
    /// a paused session.
    applied_dpms: bool,
    session: LibSeatSession,
    /// Keyboard devices, tracked so their lock LEDs can follow the xkb state.
    keyboards: Vec<InputDevice>,
    /// Dedicated KMS-commit thread: owns the vblank notifier and every
    /// output's `Committer`.
    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, |mut event, _, forge| {
                match &mut event {
                    InputEvent::DeviceAdded { device }
                        if device.has_capability(DeviceCapability::Keyboard) =>
                    {
                        if let Some(kb) = forge.seat.get_keyboard() {
                            device.led_update(kb.led_state().into());
                        }
                        forge.backend.udev().keyboards.push(device.clone());
                    },
                    InputEvent::DeviceRemoved { device }
                        if device.has_capability(DeviceCapability::Keyboard) =>
                    {
                        forge.backend.udev().keyboards.retain(|d| d != &*device);
                    },
                    _ => {},
                }
                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 {
                        // libseat has already revoked the seat by the time this
                        // runs; waiting for the KMS thread keeps a commit from
                        // racing the master release in `drm.pause()`.
                        crate::kms::pause_on(&dev.kms);
                        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.force_modeset_all();
                    }

                    // The activation is the user's doing; the daemon's idle clock has run since boot.
                    crate::powermanager_dbus::activity();
                    if forge.session.power_key_inhibitor.is_none() {
                        crate::logind_dbus::inhibit_power_key();
                    }

                    // 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 {
                        change_or_add_device(forge, device_id);
                    }

                    // The first real device may have only just arrived
                    // (inactive-VT boot): publish the dmabuf global now
                    // if startup could not.
                    create_dmabuf_global(forge);

                    // Replay any DPMS request that landed while paused.
                    if let Some(on) = forge.backend.udev().pending_dpms.take() {
                        info!("session resumed: replaying queued dpms on={on}");
                        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, cursor_only } => {
                        // 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 };

                        // A pointer move composed nothing: the render states are
                        // an earlier frame's, so acknowledging it would send that
                        // frame's callbacks twice, and pacing off it would make
                        // every mouse move schedule a repaint — the exact cost
                        // the cursor plane exists to avoid.
                        if cursor_only {
                            return;
                        }

                        if let Some(idx) =
                            dev.heads.iter().position(|h| h.presenter.crtc() == crtc)
                        {
                            let kind = dev.heads[idx].kind;
                            match kind {
                                // 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.
                                HeadKind::Mirror => dev.repaint_mirror(crtc),
                                HeadKind::Extend => dev.heads[idx].pending_frame = false,
                            }
                            // Ride an extend head's own vblank while the scene is
                            // still dirty, so a fast external is not capped at the
                            // primary's cadence: the recompose is damage-gated per
                            // head, and a clean scene produces no flip — no loop.
                            if kind == HeadKind::Extend && forge.windows.damaged() {
                                forge.create_frame();
                            }
                            return;
                        } else if dev.presenter.crtc() != crtc {
                            return;
                        } else {
                            dev.primary_pending = false;
                            #[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 };
                        // Paused sessions fail every flip with EACCES; the
                        // resume path is what recovers them, not a retry.
                        let active = udev.session.is_active();
                        let Some(dev) = &mut udev.output_device else { return };
                        if let Some(index) =
                            dev.heads.iter().position(|m| m.presenter.crtc() == crtc)
                        {
                            dev.heads[index].pending_frame = false;
                            dev.heads[index].presenter.flip_failed();
                            return;
                        }
                        if dev.presenter.crtc() == crtc {
                            dev.primary_pending = false;
                            dev.presenter.flip_failed();
                            // EBUSY means the previous commit has not drained
                            // yet, and recomposing in the same breath fails
                            // the same way. Retry a frame later instead.
                            if active {
                                let interval = forge.windows.canvas().frame_interval();
                                forge.windows.set_dirty();
                                forge.backend.schedule_redraw(interval);
                            }
                        }
                    },
                }
            })
            .expect("insert kms event source");

        Self {
            event_loop,
            session,
            scheduled_redraws: Vec::new(),
            output_device: None,
            pending_paths: Vec::new(),
            pending_change: Vec::new(),
            known_paths: HashMap::new(),
            pending_dpms: None,
            applied_dpms: true,
            dmabuf_global_done: false,
            keyboards: Vec::new(),
            kms,
        }
    }

    /// Push the keyboard lock LED state (num/caps/scroll) to every keyboard.
    pub fn update_keyboard_leds(&mut self, led_state: LedState) {
        for keyboard in &mut self.keyboards {
            keyboard.led_update(led_state.into());
        }
    }

    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, soft: bool) {
        // Internal DSI panel: blank the backlight only and keep the CRTC and the
        // nwl-dsi bridge enabled. Disabling the pipeline for a blank and rebuilding
        // it around a suspend is what wedges the panel on resume; an always-active
        // pipeline is resumed cleanly by the kernel. The backlight is a separate
        // device needing no DRM master, so this works while the session is paused.
        //
        // Only when a backlight actually exists: a panel with no sysfs backlight
        // (e.g. the Rubik Pi) would be "soft-blanked" to nothing — the screen
        // stays lit while the compositor stops drawing. There, fall through to
        // real DPMS, the only thing that darkens it.
        if soft && crate::backlight::available() {
            crate::backlight::set_power(on);
            self.applied_dpms = on;
            if on && self.session.is_active() {
                self.schedule_redraw(Duration::ZERO);
            }
            return;
        }

        // External / desktop output: real DPMS. `set_enabled` updates the DRM
        // `ACTIVE` property, an ioctl that requires master; if paused, stash the
        // request and replay it on resume.
        if !self.session.is_active() {
            info!("set_display_status: session paused, queueing dpms on={on}");
            self.pending_dpms = Some(on);
            return;
        }
        let Some(dev) = &mut self.output_device else {
            warn!("set_display_status: no output device, dropping dpms on={on}");
            return;
        };
        info!("set_display_status: applying dpms on={on} to output device");
        dev.set_enabled(on);
        self.applied_dpms = on;

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

    /// DPMS state the hardware is actually in, which is not the
    /// requested one while a request sits in `pending_dpms`.
    pub fn applied_display_status(&self) -> bool {
        self.applied_dpms
    }

    /// 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)
    }

    /// Connector inventory of the current output device, empty without one.
    pub fn output_infos(&self) -> Vec<OutputInfo> {
        self.output_device
            .as_ref()
            .map(|d| d.output_infos.clone())
            .unwrap_or_default()
    }

    pub fn render(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        ctx: &mut RenderContext,
        pipewire: Option<&mut crate::pw::PipeWire>,
    ) -> RenderedFrame {
        let Some(dev) = &mut self.output_device else {
            return RenderedFrame { rendered: false, gpu_fence: None };
        };

        // Mirrors blit the primary's composed buffer, so only the primary
        // presenter tracks damage against the output scale and orientation.
        dev.presenter.set_scale(windows.output_scale());
        dev.presenter.set_transform(windows.canvas().orientation().output_transform());

        match dev.render(&self.event_loop, windows, cursor_position, ctx, pipewire) {
            Ok(frame) => frame,
            Err(e) => {
                if !dev.drm.is_active() {
                    return RenderedFrame { rendered: false, gpu_fence: None };
                }
                if is_transient_render_error(&*e) {
                    warn!("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.heads {
                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) -> Result<DmabufFeedback, Box<dyn Error>> {
        // The device list comes from `known_paths` rather than the udev
        // backend so this can also run from the session-activate path,
        // where the backend has been moved into its event source.
        let paths: Vec<PathBuf> = self.known_paths.values().cloned().collect();
        self.output_device
            .as_ref()
            .ok_or_else(|| "no output device available".into())
            .and_then(|d| d.default_dmabuf_feedback(&paths))
    }

    /// Tear down every secondary head and rebuild them from the current
    /// config — the live path for mirror↔extend switches, per-connector
    /// mode changes and enable/disable. The primary presenter is never
    /// rebuilt, so the main screen does not blank.
    pub fn reconfigure_outputs(&mut self, display: &DisplayHandle, windows: &mut Windows) {
        let Some(dev) = &mut self.output_device else { return };
        let device_id = dev.id;
        for head in dev.heads.drain(..) {
            crate::kms::send_on(&dev.kms, KmsCmd::RemoveOutput { crtc: head.presenter.crtc() });
        }
        windows.clear_extra_outputs();
        trace_error!(self.change_device(display, windows, device_id));
        // Re-fit every window against the new topology; windows whose screen
        // vanished (mirror switch, disabled output) evacuate to the primary
        // through the owner-area clamp.
        //
        // But NOT while mirror is forced for the lock: that mirror is
        // transient and the desktop is hidden behind the lock surface, so
        // re-fitting here would clamp the extend screens' windows onto the
        // primary and strand them there when the lock clears and extend
        // returns. Skipping it keeps their global positions, so they reappear
        // on their own screen — the resize_all on unlock (mirror no longer
        // forced) then runs against the full, restored topology.
        if !crate::output::mirror_forced() {
            windows.resize_all();
        }
    }

    /// Retune an extend head's damage tracker after a live scale change.
    pub fn set_head_scale(&mut self, name: &str, scale: f64) {
        if let Some(dev) = &mut self.output_device {
            if let Some(head) = dev.heads.iter_mut().find(|h| h.name == name) {
                head.presenter.set_scale(scale);
            }
        }
    }

}
