//! A single DRM output device on the udev backend: the `OutputDevice`, its
//! primary and secondary heads, per-frame render/flip and framebuffer capture.
//! The `Udev` backend in the parent module owns and drives these.

use super::*;

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum HeadKind {
    /// Blits the primary's composed buffer — every screen shows the same.
    Mirror,
    /// Composes its own region of the desktop at its own scale.
    Extend,
}

pub(super) struct SecondaryHead {
    pub(super) presenter: Presenter,
    pub(super) pending_frame: bool,
    pub(super) connector: ConnectorHandle,
    pub(super) kind: HeadKind,
    /// Connector name, matching the `Output` registered in `Windows` for
    /// extend heads.
    pub(super) name: String,
}

/// Snapshot of one connected connector, refreshed on startup and hotplug.
/// Serves the `list_outputs` IPC so Settings can enumerate screens.
#[derive(Clone, serde::Serialize)]
pub struct OutputInfo {
    pub connector: String,
    pub identity: Option<String>,
    pub make: String,
    pub model: String,
    pub serial: String,
    pub external: bool,
    pub primary: bool,
    pub physical_mm: (u32, u32),
    pub auto_scale: f64,
    pub current_mode: String,
    pub modes: Vec<String>,
}

pub struct OutputDevice {
    pub(super) last_render_states: RenderElementStates,
    pub(super) screencopy: Option<Screencopy>,
    pub(super) export_dmabuf: Option<ExportDmabufCapture>,
    pub(super) image_copy: Option<ImageCopyCapture>,
    pub(super) presenter: Presenter,
    pub(super) heads: Vec<SecondaryHead>,
    pub(super) primary_connector: ConnectorHandle,
    pub(super) gbm: GbmDevice<DrmDeviceFd>,
    pub(super) gles: GlesRenderer,
    pub(super) graphics: Graphics,
    pub(super) drm: DrmDevice,
    pub(super) 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.
    pub(super) 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.
    pub(super) 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.
    pub(super) has_external: bool,
    /// Connector inventory refreshed alongside `has_external`.
    pub(super) output_infos: Vec<OutputInfo>,
    /// A composed primary flip is in flight. The kernel allows one pending
    /// non-blocking commit per CRTC, and with extend heads waking the render
    /// loop on their own vblanks a second primary commit could otherwise be
    /// dispatched mid-interval — an EBUSY storm. Cleared by the primary's
    /// vblank or its flip failure.
    pub(super) primary_pending: bool,
}

impl OutputDevice {
    /// Drop every surface's buffer ages, clear any in-flight mirror flip, and
    /// force a modeset on the next flip of each CRTC. Shared by the VT-switch
    /// resume and DPMS wake paths.
    pub(super) fn force_modeset_all(&mut self) {
        self.presenter.reset_buffers();
        self.primary_pending = false;
        crate::kms::send_on(&self.kms, KmsCmd::ResetState { crtc: self.presenter.crtc() });
        for mirror in self.heads.iter_mut() {
            mirror.presenter.reset_buffers();
            mirror.pending_frame = false;
            crate::kms::send_on(&self.kms, KmsCmd::ResetState { crtc: mirror.presenter.crtc() });
        }
    }

    /// 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.
    pub(super) fn repaint_mirror(&mut self, crtc: crtc::Handle) {
        let Some(index) = self.heads.iter().position(|m| m.presenter.crtc() == crtc) else {
            return;
        };
        self.heads[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 { heads, gles, kms, .. } = self;
        let mirror = &mut heads[index];
        if let Some((fb, slot)) = mirror.presenter.blit_flip(gles, &src) {
            let plan = crate::present::FlipPlan { primary: Some((fb, slot)), cursor: None };
            crate::kms::send_on(kms, KmsCmd::Flip { crtc, plan });
            mirror.pending_frame = true;
        }
    }

    pub(super) 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"))
    }

    pub(super) 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.heads {
            let crtc = mirror.presenter.crtc();
            trace_error!(self.drm.set_property(crtc, property, PropertyValue::Boolean(enabled).into()));
        }

        if enabled {
            self.force_modeset_all();
        }
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(super) fn render(
        &mut self,
        event_loop: &LoopHandle<'static, Forge>,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        ctx: &mut RenderContext,
        pipewire: Option<&mut crate::pw::PipeWire>,
    ) -> 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.clone(), ctx);

        // 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.
        // The cursor plane stays off while mirrors are attached — they copy
        // this output's composed buffer, which lacks the plane's pixels — and
        // while a screencast is active, so the video carries the pointer. The
        // per-CRTC gate on the KMS thread serialises the plane's cursor-only
        // commits with the composed flips (a second non-blocking commit in the
        // same vblank is EBUSY, which used to stall the primary with extend
        // heads driving extra `create_frame`s).
        let cast_active = pipewire.as_ref().is_some_and(|pw| pw.cast.is_some());
        let any_mirror = self.heads.iter().any(|h| h.kind == HeadKind::Mirror);
        self.presenter
            .set_cursor_plane_enabled(!any_mirror && !cast_active);
        // One composed commit per CRTC: while the previous primary flip is
        // in flight, skip composing — the vblank (or failure) that clears
        // `primary_pending` re-runs `create_frame` and picks the damage up.
        let primary_skipped = self.primary_pending;
        let mut primary_rendered = if primary_skipped {
            // `textures()` cleared the dirty flag; keep the vblank's `create_frame` recomposing.
            windows.set_dirty();
            None
        } else {
            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 { heads, gles, kms, .. } = self;
            for mirror in heads.iter_mut() {
                if mirror.kind != HeadKind::Mirror || mirror.pending_frame {
                    continue;
                }
                let crtc = mirror.presenter.crtc();
                if let Some((fb, slot)) = mirror.presenter.blit_flip(gles, &src) {
                    let plan =
                        crate::present::FlipPlan { primary: Some((fb, slot)), cursor: None };
                    crate::kms::send_on(kms, KmsCmd::Flip { crtc, plan });
                    mirror.pending_frame = true;
                    any_mirror_flipped = true;
                }
            }
        }

        // Extend heads compose their own region of the desktop at their own
        // scale. A head with a frame in flight keeps its last image until
        // its vblank frees the slot; an undamaged head composes nothing.
        {
            let Self { heads, gles, graphics, kms, .. } = self;
            for head in heads.iter_mut() {
                if head.kind != HeadKind::Extend || head.pending_frame {
                    continue;
                }
                head.presenter.set_cursor_plane_enabled(!cast_active);
                let elements = windows.textures_for_extra_output(
                    gles,
                    graphics,
                    &head.name,
                    cursor_position.clone(),
                    ctx,
                );
                if let Some(rendered) = head.presenter.render(gles, &elements) {
                    let crtc = head.presenter.crtc();
                    if let Some(plan) = head.presenter.prepare_flip(rendered) {
                        // A cursor-only commit consumes no swapchain slot and
                        // its vblank is ignored, so it must not mark a frame
                        // in flight — but it must still reach the CRTC.
                        let composed = plan.primary.is_some();
                        crate::kms::send_on(kms, KmsCmd::Flip { crtc, plan });
                        if composed {
                            head.pending_frame = 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(plan) = self.presenter.prepare_flip(rf) {
                // A cursor-only commit produces a vblank the main thread ignores,
                // so it does not count as a composed frame for pacing.
                flipped = plan.primary.is_some();
                crate::kms::send_on(&self.kms, KmsCmd::Flip { crtc, plan });
                if flipped {
                    self.primary_pending = 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;

        // Screenshot to clipboard: re-render the scene into an isolated
        // offscreen buffer and read that. Binding the live scanout buffer
        // instead (one of the swapchain's own buffers) desyncs its
        // buffer-age / damage tracking, and the screen then trembles until the
        // session is reset — the fence and flips stay healthy, so it is the
        // buffer contents, not the pacer.
        if let Some((region, sink)) = windows.pending_screenshot.take() {
            let result = self.read_screenshot_offscreen(windows, cursor_position.clone(), ctx, region);
            windows.deliver_screenshot(result, sink);
        }

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

            // The requested wl_output's just-composed buffer: an extend
            // head's own, or the primary's.
            let src = match screencopy.source() {
                Some(name) => self
                    .heads
                    .iter()
                    .find(|h| h.name == name)
                    .and_then(|h| h.presenter.last_dmabuf().cloned()),
                None => capture_src.clone(),
            };

            let buffer = screencopy.buffer();
            if let (Ok(dmabuf), Some(src)) = (dmabuf::get_dmabuf(buffer), 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 source = screencopy.source().map(str::to_string);
            let sync_point = self.copy_framebuffer_shm(windows, cursor_position.clone(), ctx, source.as_deref(), 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, ctx, None, 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();
            }
        }

        // Screencast: feed the cast from its source's just-composed buffer.
        if let Some(pw) = pipewire {
            self.feed_screencast(windows, pw, &capture_src, output_size, scale);
        }

        // `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)]
    /// Read a screenshot region (global logical coords) out of whichever
    /// output's just-composed buffer holds it — the primary or an extend head —
    /// so a capture of a window on a secondary monitor is not clamped to empty.
    pub fn gbm_device(&self) -> GbmDevice<DrmDeviceFd> {
        self.gbm.clone()
    }

    /// Feed the active screencast from its source's just-composed buffer — an
    /// extend head's own or the primary's (`capture_src`) — with a dmabuf blit
    /// for zero-copy consumers or a CPU readback for MemPtr ones. A stream in
    /// the error state is torn down here; a source size change renegotiates.
    fn feed_screencast(
        &mut self,
        windows: &Windows,
        pw: &mut crate::pw::PipeWire,
        capture_src: &Option<Dmabuf>,
        output_size: Size<i32, Physical>,
        scale: f64,
    ) {
        if pw.cast.as_ref().is_some_and(|c| c.is_dead()) {
            pw.stop_cast();
        }
        let Some(cast) = pw.cast.as_mut() else { return };

        // The cast's source: an extend head's own buffer or the primary's.
        let source = match cast.output.clone() {
            None => capture_src.clone().map(|b| (b, output_size, scale)),
            Some(name) => {
                let buf = self
                    .heads
                    .iter()
                    .find(|h| h.name == name)
                    .and_then(|h| h.presenter.last_dmabuf().cloned());
                let canvas = windows
                    .extra_outputs()
                    .iter()
                    .find(|o| o.smithay_output().name() == name)
                    .map(|o| *o.canvas());
                buf.zip(canvas).map(|(b, c)| (b, c.physical_resolution(), c.scale()))
            }
        };
        let Some((src, size, src_scale)) = source else { return };

        if cast.width as i32 != size.w || cast.height as i32 != size.h {
            cast.renegotiate(size.w as u32, size.h as u32);
        } else if let Some(frame) = cast.dma_frame() {
            let region = Rectangle::from_size(size);
            let mut dst = frame.dmabuf.clone();
            match Self::copy_framebuffer_dma(&mut self.gles, src_scale, &src, region, &mut dst) {
                Ok(sync) => {
                    // The fd is queued to the consumer right away, so the
                    // blit must have landed by then.
                    let _ = sync.wait();
                    frame.finish();
                }
                Err(e) => {
                    warn!("screencast blit: {e}");
                    frame.discard();
                }
            }
        } else if cast.wants_shm() {
            let mut src = src.clone();
            let read = self
                .gles
                .bind(&mut src)
                .map_err(|e| format!("bind dmabuf: {e:?}"))
                .and_then(|target| {
                    crate::screenshot::read_frame_rgba(&mut self.gles, &target, size)
                });
            match read {
                Ok(pixels) => cast.feed(&pixels, size.w as usize * 4),
                Err(e) => warn!("screencast readback: {e}"),
            }
        }
    }

    /// Read a screenshot region by re-rendering the primary scene into an
    /// isolated offscreen buffer and reading that. The earlier approach bound
    /// the primary's live scanout buffer (a swapchain buffer) as a target,
    /// which desynced the swapchain's buffer-age / damage tracking and left the
    /// screen trembling until the session was reset. Re-rendering never touches
    /// the swapchain, the same way the screencopy SHM path already does.
    fn read_screenshot_offscreen(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        ctx: &mut RenderContext,
        region: Rectangle<i32, Logical>,
    ) -> Result<Vec<u8>, String> {
        let canvas = *windows.canvas();
        let scale = canvas.scale();
        let output_size = canvas.physical_resolution();
        let origin = canvas.logical_rect().loc;
        let local = Rectangle::new(region.loc - origin, region.size);
        let region_phys = local.to_physical_precise_round(scale);
        let textures = windows.textures(&mut self.gles, &mut self.graphics, cursor_position, ctx);
        crate::screenshot::encode_region_png(&mut self.gles, output_size, scale, textures, region_phys)
    }

    pub(super) 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)]
    pub(super) fn copy_framebuffer_shm(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        ctx: &mut RenderContext,
        source: Option<&str>,
        region: Rectangle<i32, Physical>,
        buffer: &WlBuffer,
    ) -> Result<SyncPoint, Box<dyn Error>> {
        let buffer_dimensions = renderer::buffer_dimensions(buffer).unwrap();

        // An extend head's capture re-renders that head's own scene at its
        // canvas; the primary re-renders the full one.
        let canvas = source
            .and_then(|name| {
                windows
                    .extra_outputs()
                    .iter()
                    .find(|o| o.smithay_output().name() == name)
                    .map(|o| *o.canvas())
            })
            .unwrap_or(*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 head_elements;
        let textures: &[crate::drawing::ForgeElement] = match source {
            Some(name) => {
                head_elements = windows.textures_for_extra_output(
                    &mut self.gles,
                    &mut self.graphics,
                    name,
                    cursor_position,
                    ctx,
                );
                &head_elements
            }
            None => windows.textures(&mut self.gles, &mut self.graphics, cursor_position, ctx),
        };

        let (mut offscreen_buffer, sync_point): (GlesRenderbuffer, _) =
            crate::drawing::render_elements_offscreen(
                &mut self.gles, Fourcc::Abgr8888, buffer_dimensions, output_size, scale,
                transform, CLEAR_COLOR, textures, &[damage],
            )?;

        let target = self.gles.bind(&mut offscreen_buffer)?;
        // The offscreen render comes out top-down, matching what screencopy
        // delivers: read the region as-is and keep the row order.
        let read_region = Rectangle::<i32, smithay::utils::Buffer>::new(
            Point::from((region.loc.x, region.loc.y)),
            Size::from((region.size.w, region.size.h)),
        );
        let pixels = crate::screenshot::read_target_rgba(&mut self.gles, &target, read_region)
            .map_err(|e| -> Box<dyn Error> { e.into() })?;

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

            let row = region.size.w as usize * 4;
            let rows = region.size.h as usize;
            if pixels.len() < row * rows {
                return Err("short readback".into());
            }
            unsafe {
                ptr::copy_nonoverlapping(pixels.as_ptr(), shm_buffer, row * rows);
            }

            // Abgr (RGBA bytes) -> the announced Argb8888 (BGRA bytes).
            for i in 0..(region.size.w * region.size.h) as usize {
                unsafe {
                    let a = shm_buffer.add(i * 4);
                    let b = shm_buffer.add(i * 4 + 2);
                    ptr::swap(a, b);
                }
            }

            Ok(sync_point)
        })?
    }

    pub(super) fn default_dmabuf_feedback(
        &self,
        device_paths: &[PathBuf],
    ) -> 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 = device_paths
                .iter()
                .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.
pub(super) 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
}

pub(super) 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"))
}
